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

267 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 .utilities import lookupStaticCalibrations 

46 

47import fgcm 

48 

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

50 'FgcmMakeLutRunner'] 

51 

52 

53class FgcmMakeLutConnections(pipeBase.PipelineTaskConnections, 

54 dimensions=('instrument',), 

55 defaultTemplates={}): 

56 camera = connectionTypes.PrerequisiteInput( 

57 doc="Camera instrument", 

58 name="camera", 

59 storageClass="Camera", 

60 dimensions=("instrument",), 

61 lookupFunction=lookupStaticCalibrations, 

62 isCalibration=True, 

63 ) 

64 

65 transmission_optics = connectionTypes.PrerequisiteInput( 

66 doc="Optics transmission curve information", 

67 name="transmission_optics", 

68 storageClass="TransmissionCurve", 

69 dimensions=("instrument",), 

70 lookupFunction=lookupStaticCalibrations, 

71 isCalibration=True, 

72 deferLoad=True, 

73 ) 

74 

75 transmission_sensor = connectionTypes.PrerequisiteInput( 

76 doc="Sensor transmission curve information", 

77 name="transmission_sensor", 

78 storageClass="TransmissionCurve", 

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

80 lookupFunction=lookupStaticCalibrations, 

81 isCalibration=True, 

82 deferLoad=True, 

83 multiple=True, 

84 ) 

85 

86 transmission_filter = connectionTypes.PrerequisiteInput( 

87 doc="Filter transmission curve information", 

88 name="transmission_filter", 

89 storageClass="TransmissionCurve", 

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

91 lookupFunction=lookupStaticCalibrations, 

92 isCalibration=True, 

93 deferLoad=True, 

94 multiple=True, 

95 ) 

96 

97 fgcmLookUpTable = connectionTypes.Output( 

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

99 "chromatic corrections."), 

100 name="fgcmLookUpTable", 

101 storageClass="Catalog", 

102 dimensions=("instrument",), 

103 ) 

104 

105 

106class FgcmMakeLutParametersConfig(pexConfig.Config): 

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

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

109 # telescope elevation directly from the camera. 

110 elevation = pexConfig.Field( 

111 doc="Telescope elevation (m)", 

112 dtype=float, 

113 default=None, 

114 ) 

115 pmbRange = pexConfig.ListField( 

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

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

118 dtype=float, 

119 default=None, 

120 ) 

121 pmbSteps = pexConfig.Field( 

122 doc="Barometric Pressure number of steps", 

123 dtype=int, 

124 default=5, 

125 ) 

126 pwvRange = pexConfig.ListField( 

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

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

129 dtype=float, 

130 default=None, 

131 ) 

132 pwvSteps = pexConfig.Field( 

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

134 dtype=int, 

135 default=15, 

136 ) 

137 o3Range = pexConfig.ListField( 

138 doc="Ozone range (dob)", 

139 dtype=float, 

140 default=[220.0, 310.0], 

141 ) 

142 o3Steps = pexConfig.Field( 

143 doc="Ozone number of steps", 

144 dtype=int, 

145 default=3, 

146 ) 

147 tauRange = pexConfig.ListField( 

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

149 dtype=float, 

150 default=[0.002, 0.35], 

151 ) 

152 tauSteps = pexConfig.Field( 

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

154 dtype=int, 

155 default=11, 

156 ) 

157 alphaRange = pexConfig.ListField( 

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

159 dtype=float, 

160 default=[0.0, 2.0], 

161 ) 

162 alphaSteps = pexConfig.Field( 

163 doc="Aerosol alpha number of steps", 

164 dtype=int, 

165 default=9, 

166 ) 

167 zenithRange = pexConfig.ListField( 

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

169 dtype=float, 

170 default=[0.0, 70.0], 

171 ) 

172 zenithSteps = pexConfig.Field( 

173 doc="Zenith angle number of steps", 

174 dtype=int, 

175 default=21, 

176 ) 

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

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

179 pmbStd = pexConfig.Field( 

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

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

182 dtype=float, 

183 default=None, 

184 ) 

185 pwvStd = pexConfig.Field( 

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

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

188 dtype=float, 

189 default=None, 

190 ) 

191 o3Std = pexConfig.Field( 

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

193 dtype=float, 

194 default=263.0, 

195 ) 

196 tauStd = pexConfig.Field( 

197 doc="Standard Atmosphere aerosol optical depth", 

198 dtype=float, 

199 default=0.03, 

200 ) 

201 alphaStd = pexConfig.Field( 

202 doc="Standard Atmosphere aerosol alpha", 

203 dtype=float, 

204 default=1.0, 

205 ) 

206 airmassStd = pexConfig.Field( 

207 doc=("Standard Atmosphere airmass; " 

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

209 dtype=float, 

210 default=None, 

211 ) 

212 lambdaNorm = pexConfig.Field( 

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

214 dtype=float, 

215 default=7750.0, 

216 ) 

217 lambdaStep = pexConfig.Field( 

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

219 dtype=float, 

220 default=0.5, 

221 ) 

222 lambdaRange = pexConfig.ListField( 

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

224 dtype=float, 

225 default=[3000.0, 11000.0], 

226 ) 

227 

228 

229class FgcmMakeLutConfig(pipeBase.PipelineTaskConfig, 

230 pipelineConnections=FgcmMakeLutConnections): 

231 """Config for FgcmMakeLutTask""" 

232 physicalFilters = pexConfig.ListField( 

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

234 dtype=str, 

235 default=[], 

236 ) 

237 stdPhysicalFilterOverrideMap = pexConfig.DictField( 

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

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

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

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

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

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

244 keytype=str, 

245 itemtype=str, 

246 default={}, 

247 ) 

248 atmosphereTableName = pexConfig.Field( 

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

250 dtype=str, 

251 default=None, 

252 optional=True, 

253 ) 

254 parameters = pexConfig.ConfigField( 

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

256 dtype=FgcmMakeLutParametersConfig, 

257 default=None, 

258 check=None) 

259 

260 def validate(self): 

261 """ 

262 Validate the config parameters. 

263 

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

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

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

267 directly from the specified atmosphereTableName. 

268 """ 

269 # check that filterNames and stdFilterNames are okay 

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

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

272 

273 if self.atmosphereTableName is None: 

274 # Validate the parameters 

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

276 

277 

278class FgcmMakeLutRunner(pipeBase.ButlerInitializedTaskRunner): 

279 """Subclass of TaskRunner for fgcmMakeLutTask 

280 

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

282 does not run on any data in the repository. 

283 This runner does not use any parallelization. 

284 """ 

285 

286 @staticmethod 

287 def getTargetList(parsedCmd): 

288 """ 

289 Return a list with one element, the butler. 

290 """ 

291 return [parsedCmd.butler] 

292 

293 def __call__(self, butler): 

294 """ 

295 Parameters 

296 ---------- 

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

298 

299 Returns 

300 ------- 

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

302 exitStatus (0: success; 1: failure) 

303 """ 

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

305 

306 exitStatus = 0 

307 if self.doRaise: 

308 task.runDataRef(butler) 

309 else: 

310 try: 

311 task.runDataRef(butler) 

312 except Exception as e: 

313 exitStatus = 1 

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

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

316 traceback.print_exc(file=sys.stderr) 

317 

318 task.writeMetadata(butler) 

319 

320 # The task does not return any results: 

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

322 

323 def run(self, parsedCmd): 

324 """ 

325 Run the task, with no multiprocessing 

326 

327 Parameters 

328 ---------- 

329 parsedCmd: ArgumentParser parsed command line 

330 """ 

331 

332 resultList = [] 

333 

334 if self.precall(parsedCmd): 

335 targetList = self.getTargetList(parsedCmd) 

336 # make sure that we only get 1 

337 resultList = self(targetList[0]) 

338 

339 return resultList 

340 

341 

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

343 """ 

344 Make Look-Up Table for FGCM. 

345 

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

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

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

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

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

351 

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

353 atmosphere table packaged with fgcm. 

354 """ 

355 

356 ConfigClass = FgcmMakeLutConfig 

357 RunnerClass = FgcmMakeLutRunner 

358 _DefaultName = "fgcmMakeLut" 

359 

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

361 super().__init__(**kwargs) 

362 

363 # no saving of metadata for now 

364 def _getMetadataName(self): 

365 return None 

366 

367 @pipeBase.timeMethod 

368 def runDataRef(self, butler): 

369 """ 

370 Make a Look-Up Table for FGCM 

371 

372 Parameters 

373 ---------- 

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

375 

376 Raises 

377 ------ 

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

379 available filter transmission curves. 

380 """ 

381 camera = butler.get('camera') 

382 opticsDataRef = butler.dataRef('transmission_optics') 

383 

384 sensorDataRefDict = {} 

385 for detector in camera: 

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

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

388 

389 filterDataRefDict = {} 

390 for physicalFilter in self.config.physicalFilters: 

391 # The physical filters map directly to dataId filter names 

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

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

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

395 if not dataRef.datasetExists(): 

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

397 filterDataRefDict[physicalFilter] = dataRef 

398 

399 lutCat = self._fgcmMakeLut(camera, 

400 opticsDataRef, 

401 sensorDataRefDict, 

402 filterDataRefDict) 

403 butler.put(lutCat, 'fgcmLookUpTable') 

404 

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

406 camera = butlerQC.get(inputRefs.camera) 

407 

408 opticsDataRef = butlerQC.get(inputRefs.transmission_optics) 

409 

410 sensorRefs = butlerQC.get(inputRefs.transmission_sensor) 

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

412 sensorRef in sensorRefs} 

413 

414 filterRefs = butlerQC.get(inputRefs.transmission_filter) 

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

416 filterRef in filterRefs} 

417 

418 lutCat = self._fgcmMakeLut(camera, 

419 opticsDataRef, 

420 sensorDataRefDict, 

421 filterDataRefDict) 

422 butlerQC.put(lutCat, outputRefs.fgcmLookUpTable) 

423 

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

425 filterDataRefDict): 

426 """ 

427 Make a FGCM Look-up Table 

428 

429 Parameters 

430 ---------- 

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

432 Camera from the butler. 

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

434 `lsst.daf.butler.DeferredDatasetHandle` 

435 Reference to optics transmission curve. 

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

437 `lsst.daf.butler.DeferredDatasetHandle`] 

438 Dictionary of references to sensor transmission curves. Key will 

439 be detector id. 

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

441 `lsst.daf.butler.DeferredDatasetHandle`] 

442 Dictionary of references to filter transmission curves. Key will 

443 be physical filter label. 

444 

445 Returns 

446 ------- 

447 fgcmLookUpTable : `BaseCatalog` 

448 The FGCM look-up table. 

449 """ 

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

451 nCcd = len(camera) 

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

453 

454 # Load in optics, etc. 

455 self._loadThroughputs(camera, 

456 opticsDataRef, 

457 sensorDataRefDict, 

458 filterDataRefDict) 

459 

460 lutConfig = self._createLutConfig(nCcd) 

461 

462 # make the lut object 

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

464 self.fgcmLutMaker = fgcm.FgcmLUTMaker(lutConfig) 

465 

466 # generate the throughput dictionary. 

467 

468 # these will be in Angstroms 

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

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

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

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

473 self.fgcmLutMaker.lambdaStep*10.) 

474 

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

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

477 throughputLambda[1] - throughputLambda[0])) 

478 

479 throughputDict = {} 

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

481 tDict = {} 

482 tDict['LAMBDA'] = throughputLambda 

483 for ccdIndex, detector in enumerate(camera): 

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

485 throughputDict[physicalFilter] = tDict 

486 

487 # set the throughputs 

488 self.fgcmLutMaker.setThroughputs(throughputDict) 

489 

490 # make the LUT 

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

492 self.fgcmLutMaker.makeLUT() 

493 

494 # and save the LUT 

495 

496 # build the index values 

497 comma = ',' 

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

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

500 

501 atmosphereTableName = 'NoTableWasUsed' 

502 if self.config.atmosphereTableName is not None: 

503 atmosphereTableName = self.config.atmosphereTableName 

504 

505 lutSchema = self._makeLutSchema(physicalFilterString, stdPhysicalFilterString, 

506 atmosphereTableName) 

507 

508 lutCat = self._makeLutCat(lutSchema, physicalFilterString, 

509 stdPhysicalFilterString, atmosphereTableName) 

510 return lutCat 

511 

512 def _getStdPhysicalFilterList(self): 

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

514 and config.stdPhysicalFilterOverrideMap 

515 

516 Returns 

517 ------- 

518 stdPhysicalFilters : `list` 

519 """ 

520 override = self.config.stdPhysicalFilterOverrideMap 

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

522 physicalFilter in self.config.physicalFilters] 

523 

524 def _createLutConfig(self, nCcd): 

525 """ 

526 Create the fgcmLut config dictionary 

527 

528 Parameters 

529 ---------- 

530 nCcd: `int` 

531 Number of CCDs in the camera 

532 """ 

533 

534 # create the common stub of the lutConfig 

535 lutConfig = {} 

536 lutConfig['logger'] = self.log 

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

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

539 lutConfig['nCCD'] = nCcd 

540 

541 # atmosphereTable already validated if available 

542 if self.config.atmosphereTableName is not None: 

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

544 else: 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

568 

569 return lutConfig 

570 

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

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

573 

574 Parameters 

575 ---------- 

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

577 Camera from the butler 

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

579 `lsst.daf.butler.DeferredDatasetHandle` 

580 Reference to optics transmission curve. 

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

582 `lsst.daf.butler.DeferredDatasetHandle`] 

583 Dictionary of references to sensor transmission curves. Key will 

584 be detector id. 

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

586 `lsst.daf.butler.DeferredDatasetHandle`] 

587 Dictionary of references to filter transmission curves. Key will 

588 be physical filter label. 

589 

590 Raises 

591 ------ 

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

593 available filter transmission curves. 

594 """ 

595 self._opticsTransmission = opticsDataRef.get() 

596 

597 self._sensorsTransmission = {} 

598 for detector in camera: 

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

600 

601 self._filtersTransmission = {} 

602 for physicalFilter in self.config.physicalFilters: 

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

604 

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

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

607 

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

609 

610 Parameters 

611 ---------- 

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

613 Detector on camera 

614 physicalFilter: `str` 

615 Physical filter label 

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

617 Wavelength steps (Angstrom) 

618 

619 Returns 

620 ------- 

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

622 Throughput (max 1.0) at throughputLambda 

623 """ 

624 

625 c = detector.getCenter(afwCameraGeom.FOCAL_PLANE) 

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

627 

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

629 wavelengths=throughputLambda) 

630 

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

632 wavelengths=throughputLambda) 

633 

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

635 wavelengths=throughputLambda) 

636 

637 # Clip the throughput from 0 to 1 

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

639 

640 return throughput 

641 

642 def _makeLutSchema(self, physicalFilterString, stdPhysicalFilterString, 

643 atmosphereTableName): 

644 """ 

645 Make the LUT schema 

646 

647 Parameters 

648 ---------- 

649 physicalFilterString: `str` 

650 Combined string of all the physicalFilters 

651 stdPhysicalFilterString: `str` 

652 Combined string of all the standard physicalFilters 

653 atmosphereTableName: `str` 

654 Name of the atmosphere table used to generate LUT 

655 

656 Returns 

657 ------- 

658 lutSchema: `afwTable.schema` 

659 """ 

660 

661 lutSchema = afwTable.Schema() 

662 

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

664 size=len(atmosphereTableName)) 

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

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

667 size=len(physicalFilterString)) 

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

669 size=len(stdPhysicalFilterString)) 

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

671 size=self.fgcmLutMaker.pmb.size) 

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

673 size=self.fgcmLutMaker.pmb.size) 

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

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

676 size=self.fgcmLutMaker.pwv.size) 

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

678 size=self.fgcmLutMaker.o3.size) 

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

680 size=self.fgcmLutMaker.tau.size) 

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

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

683 size=self.fgcmLutMaker.alpha.size) 

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

685 size=self.fgcmLutMaker.zenith.size) 

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

687 

688 # and the standard values 

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

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

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

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

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

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

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

696 size=2) 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

713 size=self.fgcmLutMaker.atmLambda.size) 

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

715 size=self.fgcmLutMaker.atmStdTrans.size) 

716 

717 # and the look-up-tables 

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

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

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

721 

722 return lutSchema 

723 

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

725 atmosphereTableName): 

726 """ 

727 Make the LUT schema 

728 

729 Parameters 

730 ---------- 

731 lutSchema: `afwTable.schema` 

732 Lut catalog schema 

733 physicalFilterString: `str` 

734 Combined string of all the physicalFilters 

735 stdPhysicalFilterString: `str` 

736 Combined string of all the standard physicalFilters 

737 atmosphereTableName: `str` 

738 Name of the atmosphere table used to generate LUT 

739 

740 Returns 

741 ------- 

742 lutCat: `afwTable.BaseCatalog` 

743 Lut catalog for persistence 

744 """ 

745 

746 # The somewhat strange format is to make sure that 

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

748 # (see DM-11419) 

749 

750 lutCat = afwTable.BaseCatalog(lutSchema) 

751 lutCat.table.preallocate(14) 

752 

753 # first fill the first index 

754 rec = lutCat.addNew() 

755 

756 rec['tablename'] = atmosphereTableName 

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

758 rec['physicalFilters'] = physicalFilterString 

759 rec['stdPhysicalFilters'] = stdPhysicalFilterString 

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

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

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

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

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

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

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

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

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

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

770 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

788 

789 rec['luttype'] = 'I0' 

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

791 

792 # and add the rest 

793 rec = lutCat.addNew() 

794 rec['luttype'] = 'I1' 

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

796 

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

798 'D_PMB_I1', 'D_LNPWV_I1', 'D_O3_I1', 'D_LNTAU_I1', 'D_ALPHA_I1', 

799 'D_SECZENITH_I1'] 

800 for derivType in derivTypes: 

801 rec = lutCat.addNew() 

802 rec['luttype'] = derivType 

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

804 

805 return lutCat