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 lsst.afw.image import Filter 

47from .utilities import lookupStaticCalibrations 

48 

49import fgcm 

50 

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

52 'FgcmMakeLutRunner'] 

53 

54 

55class FgcmMakeLutConnections(pipeBase.PipelineTaskConnections, 

56 dimensions=('instrument',), 

57 defaultTemplates={}): 

58 camera = connectionTypes.PrerequisiteInput( 

59 doc="Camera instrument", 

60 name="camera", 

61 storageClass="Camera", 

62 dimensions=("instrument",), 

63 lookupFunction=lookupStaticCalibrations, 

64 isCalibration=True, 

65 ) 

66 

67 transmission_optics = connectionTypes.PrerequisiteInput( 

68 doc="Optics transmission curve information", 

69 name="transmission_optics", 

70 storageClass="TransmissionCurve", 

71 dimensions=("instrument",), 

72 lookupFunction=lookupStaticCalibrations, 

73 isCalibration=True, 

74 deferLoad=True, 

75 ) 

76 

77 transmission_sensor = connectionTypes.PrerequisiteInput( 

78 doc="Sensor transmission curve information", 

79 name="transmission_sensor", 

80 storageClass="TransmissionCurve", 

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

82 lookupFunction=lookupStaticCalibrations, 

83 isCalibration=True, 

84 deferLoad=True, 

85 multiple=True, 

86 ) 

87 

88 transmission_filter = connectionTypes.PrerequisiteInput( 

89 doc="Filter transmission curve information", 

90 name="transmission_filter", 

91 storageClass="TransmissionCurve", 

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

93 lookupFunction=lookupStaticCalibrations, 

94 isCalibration=True, 

95 deferLoad=True, 

96 multiple=True, 

97 ) 

98 

99 fgcmLookUpTable = connectionTypes.Output( 

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

101 "chromatic corrections."), 

102 name="fgcmLookUpTable", 

103 storageClass="Catalog", 

104 dimensions=("instrument",), 

105 ) 

106 

107 

108class FgcmMakeLutParametersConfig(pexConfig.Config): 

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

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

111 # telescope elevation directly from the camera. 

112 elevation = pexConfig.Field( 

113 doc="Telescope elevation (m)", 

114 dtype=float, 

115 default=None, 

116 ) 

117 pmbRange = pexConfig.ListField( 

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

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

120 dtype=float, 

121 default=None, 

122 ) 

123 pmbSteps = pexConfig.Field( 

124 doc="Barometric Pressure number of steps", 

125 dtype=int, 

126 default=5, 

127 ) 

128 pwvRange = pexConfig.ListField( 

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

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

131 dtype=float, 

132 default=None, 

133 ) 

134 pwvSteps = pexConfig.Field( 

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

136 dtype=int, 

137 default=15, 

138 ) 

139 o3Range = pexConfig.ListField( 

140 doc="Ozone range (dob)", 

141 dtype=float, 

142 default=[220.0, 310.0], 

143 ) 

144 o3Steps = pexConfig.Field( 

145 doc="Ozone number of steps", 

146 dtype=int, 

147 default=3, 

148 ) 

149 tauRange = pexConfig.ListField( 

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

151 dtype=float, 

152 default=[0.002, 0.35], 

153 ) 

154 tauSteps = pexConfig.Field( 

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

156 dtype=int, 

157 default=11, 

158 ) 

159 alphaRange = pexConfig.ListField( 

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

161 dtype=float, 

162 default=[0.0, 2.0], 

163 ) 

164 alphaSteps = pexConfig.Field( 

165 doc="Aerosol alpha number of steps", 

166 dtype=int, 

167 default=9, 

168 ) 

169 zenithRange = pexConfig.ListField( 

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

171 dtype=float, 

172 default=[0.0, 70.0], 

173 ) 

174 zenithSteps = pexConfig.Field( 

175 doc="Zenith angle number of steps", 

176 dtype=int, 

177 default=21, 

178 ) 

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

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

181 pmbStd = pexConfig.Field( 

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

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

184 dtype=float, 

185 default=None, 

186 ) 

187 pwvStd = pexConfig.Field( 

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

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

190 dtype=float, 

191 default=None, 

192 ) 

193 o3Std = pexConfig.Field( 

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

195 dtype=float, 

196 default=263.0, 

197 ) 

198 tauStd = pexConfig.Field( 

199 doc="Standard Atmosphere aerosol optical depth", 

200 dtype=float, 

201 default=0.03, 

202 ) 

203 alphaStd = pexConfig.Field( 

204 doc="Standard Atmosphere aerosol alpha", 

205 dtype=float, 

206 default=1.0, 

207 ) 

208 airmassStd = pexConfig.Field( 

209 doc=("Standard Atmosphere airmass; " 

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

211 dtype=float, 

212 default=None, 

213 ) 

214 lambdaNorm = pexConfig.Field( 

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

216 dtype=float, 

217 default=7750.0, 

218 ) 

219 lambdaStep = pexConfig.Field( 

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

221 dtype=float, 

222 default=0.5, 

223 ) 

224 lambdaRange = pexConfig.ListField( 

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

226 dtype=float, 

227 default=[3000.0, 11000.0], 

228 ) 

229 

230 

231class FgcmMakeLutConfig(pipeBase.PipelineTaskConfig, 

232 pipelineConnections=FgcmMakeLutConnections): 

233 """Config for FgcmMakeLutTask""" 

234 

235 filterNames = pexConfig.ListField( 

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

237 dtype=str, 

238 default=None, 

239 ) 

240 stdFilterNames = pexConfig.ListField( 

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

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

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

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

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

246 "properly cross-calibrated."), 

247 dtype=str, 

248 default=None, 

249 ) 

250 atmosphereTableName = pexConfig.Field( 

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

252 dtype=str, 

253 default=None, 

254 optional=True, 

255 ) 

256 parameters = pexConfig.ConfigField( 

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

258 dtype=FgcmMakeLutParametersConfig, 

259 default=None, 

260 check=None) 

261 

262 def validate(self): 

263 """ 

264 Validate the config parameters. 

265 

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

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

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

269 directly from the specified atmosphereTableName. 

270 """ 

271 # check that filterNames and stdFilterNames are okay 

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

273 self._fields['stdFilterNames'].validate(self) 

274 

275 if self.atmosphereTableName is None: 

276 # Validate the parameters 

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

278 

279 

280class FgcmMakeLutRunner(pipeBase.ButlerInitializedTaskRunner): 

281 """Subclass of TaskRunner for fgcmMakeLutTask 

282 

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

284 does not run on any data in the repository. 

285 This runner does not use any parallelization. 

286 """ 

287 

288 @staticmethod 

289 def getTargetList(parsedCmd): 

290 """ 

291 Return a list with one element, the butler. 

292 """ 

293 return [parsedCmd.butler] 

294 

295 def __call__(self, butler): 

296 """ 

297 Parameters 

298 ---------- 

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

300 

301 Returns 

302 ------- 

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

304 exitStatus (0: success; 1: failure) 

305 """ 

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

307 

308 exitStatus = 0 

309 if self.doRaise: 

310 task.runDataRef(butler) 

311 else: 

312 try: 

313 task.runDataRef(butler) 

314 except Exception as e: 

315 exitStatus = 1 

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

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

318 traceback.print_exc(file=sys.stderr) 

319 

320 task.writeMetadata(butler) 

321 

322 # The task does not return any results: 

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

324 

325 def run(self, parsedCmd): 

326 """ 

327 Run the task, with no multiprocessing 

328 

329 Parameters 

330 ---------- 

331 parsedCmd: ArgumentParser parsed command line 

332 """ 

333 

334 resultList = [] 

335 

336 if self.precall(parsedCmd): 

337 targetList = self.getTargetList(parsedCmd) 

338 # make sure that we only get 1 

339 resultList = self(targetList[0]) 

340 

341 return resultList 

342 

343 

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

345 """ 

346 Make Look-Up Table for FGCM. 

347 

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

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

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

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

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

353 

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

355 atmosphere table packaged with fgcm. 

356 """ 

357 

358 ConfigClass = FgcmMakeLutConfig 

359 RunnerClass = FgcmMakeLutRunner 

360 _DefaultName = "fgcmMakeLut" 

361 

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

363 super().__init__(**kwargs) 

364 

365 # no saving of metadata for now 

366 def _getMetadataName(self): 

367 return None 

368 

369 @pipeBase.timeMethod 

370 def runDataRef(self, butler): 

371 """ 

372 Make a Look-Up Table for FGCM 

373 

374 Parameters 

375 ---------- 

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

377 

378 Raises 

379 ------ 

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

381 available filter transmission curves. 

382 """ 

383 camera = butler.get('camera') 

384 opticsDataRef = butler.dataRef('transmission_optics') 

385 

386 sensorDataRefDict = {} 

387 for detector in camera: 

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

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

390 

391 filterDataRefDict = {} 

392 for filterName in self.config.filterNames: 

393 f = Filter(filterName) 

394 foundTrans = False 

395 # Get all possible aliases and also try the short filterName 

396 aliases = f.getAliases() 

397 aliases.extend(filterName) 

398 for alias in f.getAliases(): 

399 dataRef = butler.dataRef('transmission_filter', filter=alias) 

400 if dataRef.datasetExists(): 

401 foundTrans = True 

402 filterDataRefDict[alias] = dataRef 

403 break 

404 if not foundTrans: 

405 raise ValueError("Cound not find transmission for filter %s via any alias." % 

406 (filterName)) 

407 

408 lutCat = self._fgcmMakeLut(camera, 

409 opticsDataRef, 

410 sensorDataRefDict, 

411 filterDataRefDict) 

412 butler.put(lutCat, 'fgcmLookUpTable') 

413 

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

415 camera = butlerQC.get(inputRefs.camera) 

416 

417 # Instantiate the instrument to load filter information 

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

419 butlerQC.registry) 

420 opticsDataRef = butlerQC.get(inputRefs.transmission_optics) 

421 

422 sensorRefs = butlerQC.get(inputRefs.transmission_sensor) 

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

424 sensorRef in sensorRefs} 

425 

426 filterRefs = butlerQC.get(inputRefs.transmission_filter) 

427 filterDataRefDict = {filterRef.dataId.byName()['physical_filter']: filterRef for 

428 filterRef in filterRefs} 

429 

430 lutCat = self._fgcmMakeLut(camera, 

431 opticsDataRef, 

432 sensorDataRefDict, 

433 filterDataRefDict) 

434 butlerQC.put(lutCat, outputRefs.fgcmLookUpTable) 

435 

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

437 filterDataRefDict): 

438 """ 

439 Make a FGCM Look-up Table 

440 

441 Parameters 

442 ---------- 

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

444 Camera from the butler. 

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

446 `lsst.daf.butler.DeferredDatasetHandle` 

447 Reference to optics transmission curve. 

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

449 `lsst.daf.butler.DeferredDatasetHandle`] 

450 Dictionary of references to sensor transmission curves. Key will 

451 be detector id. 

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

453 `lsst.daf.butler.DeferredDatasetHandle`] 

454 Dictionary of references to filter transmission curves. Key will 

455 be physical filter name. 

456 

457 Returns 

458 ------- 

459 fgcmLookUpTable : `BaseCatalog` 

460 The FGCM look-up table. 

461 """ 

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

463 nCcd = len(camera) 

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

465 

466 # Load in optics, etc. 

467 self._loadThroughputs(camera, 

468 opticsDataRef, 

469 sensorDataRefDict, 

470 filterDataRefDict) 

471 

472 lutConfig = self._createLutConfig(nCcd) 

473 

474 # make the lut object 

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

476 self.fgcmLutMaker = fgcm.FgcmLUTMaker(lutConfig) 

477 

478 # generate the throughput dictionary. 

479 

480 # these will be in Angstroms 

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

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

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

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

485 self.fgcmLutMaker.lambdaStep*10.) 

486 

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

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

489 throughputLambda[1] - throughputLambda[0])) 

490 

491 throughputDict = {} 

492 for i, filterName in enumerate(self.config.filterNames): 

493 tDict = {} 

494 tDict['LAMBDA'] = throughputLambda 

495 for ccdIndex, detector in enumerate(camera): 

496 tDict[ccdIndex] = self._getThroughputDetector(detector, filterName, throughputLambda) 

497 throughputDict[filterName] = tDict 

498 

499 # set the throughputs 

500 self.fgcmLutMaker.setThroughputs(throughputDict) 

501 

502 # make the LUT 

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

504 self.fgcmLutMaker.makeLUT() 

505 

506 # and save the LUT 

507 

508 # build the index values 

509 comma = ',' 

510 filterNameString = comma.join(self.config.filterNames) 

511 stdFilterNameString = comma.join(self.config.stdFilterNames) 

512 

513 atmosphereTableName = 'NoTableWasUsed' 

514 if self.config.atmosphereTableName is not None: 

515 atmosphereTableName = self.config.atmosphereTableName 

516 

517 lutSchema = self._makeLutSchema(filterNameString, stdFilterNameString, 

518 atmosphereTableName) 

519 

520 lutCat = self._makeLutCat(lutSchema, filterNameString, 

521 stdFilterNameString, atmosphereTableName) 

522 return lutCat 

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.filterNames 

538 lutConfig['stdFilterNames'] = self.config.stdFilterNames 

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 name. 

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 filterName in self.config.filterNames: 

603 f = Filter(filterName) 

604 foundTrans = False 

605 # Get all possible aliases, and also try the short filterName 

606 aliases = f.getAliases() 

607 aliases.extend(filterName) 

608 for alias in f.getAliases(): 

609 if alias in filterDataRefDict: 

610 self._filtersTransmission[filterName] = filterDataRefDict[alias].get() 

611 foundTrans = True 

612 break 

613 if not foundTrans: 

614 raise ValueError("Could not find transmission for filter %s via any alias." % 

615 (filterName)) 

616 

617 def _getThroughputDetector(self, detector, filterName, throughputLambda): 

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

619 

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

621 

622 Parameters 

623 ---------- 

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

625 Detector on camera 

626 filterName: `str` 

627 Short name for filter 

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

629 Wavelength steps (Angstrom) 

630 

631 Returns 

632 ------- 

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

634 Throughput (max 1.0) at throughputLambda 

635 """ 

636 

637 c = detector.getCenter(afwCameraGeom.FOCAL_PLANE) 

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

639 

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

641 wavelengths=throughputLambda) 

642 

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

644 wavelengths=throughputLambda) 

645 

646 throughput *= self._filtersTransmission[filterName].sampleAt(position=c, 

647 wavelengths=throughputLambda) 

648 

649 # Clip the throughput from 0 to 1 

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

651 

652 return throughput 

653 

654 def _makeLutSchema(self, filterNameString, stdFilterNameString, 

655 atmosphereTableName): 

656 """ 

657 Make the LUT schema 

658 

659 Parameters 

660 ---------- 

661 filterNameString: `str` 

662 Combined string of all the filterNames 

663 stdFilterNameString: `str` 

664 Combined string of all the standard filterNames 

665 atmosphereTableName: `str` 

666 Name of the atmosphere table used to generate LUT 

667 

668 Returns 

669 ------- 

670 lutSchema: `afwTable.schema` 

671 """ 

672 

673 lutSchema = afwTable.Schema() 

674 

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

676 size=len(atmosphereTableName)) 

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

678 lutSchema.addField('filterNames', type=str, doc='filterNames in LUT', 

679 size=len(filterNameString)) 

680 lutSchema.addField('stdFilterNames', type=str, doc='Standard filterNames in LUT', 

681 size=len(stdFilterNameString)) 

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

683 size=self.fgcmLutMaker.pmb.size) 

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

685 size=self.fgcmLutMaker.pmb.size) 

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

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

688 size=self.fgcmLutMaker.pwv.size) 

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

690 size=self.fgcmLutMaker.o3.size) 

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

692 size=self.fgcmLutMaker.tau.size) 

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

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

695 size=self.fgcmLutMaker.alpha.size) 

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

697 size=self.fgcmLutMaker.zenith.size) 

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

699 

700 # and the standard values 

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

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

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

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

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

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

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

708 size=2) 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

725 size=self.fgcmLutMaker.atmLambda.size) 

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

727 size=self.fgcmLutMaker.atmStdTrans.size) 

728 

729 # and the look-up-tables 

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

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

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

733 

734 return lutSchema 

735 

736 def _makeLutCat(self, lutSchema, filterNameString, stdFilterNameString, 

737 atmosphereTableName): 

738 """ 

739 Make the LUT schema 

740 

741 Parameters 

742 ---------- 

743 lutSchema: `afwTable.schema` 

744 Lut catalog schema 

745 filterNameString: `str` 

746 Combined string of all the filterNames 

747 stdFilterNameString: `str` 

748 Combined string of all the standard filterNames 

749 atmosphereTableName: `str` 

750 Name of the atmosphere table used to generate LUT 

751 

752 Returns 

753 ------- 

754 lutCat: `afwTable.BaseCatalog` 

755 Lut catalog for persistence 

756 """ 

757 

758 # The somewhat strange format is to make sure that 

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

760 # (see DM-11419) 

761 

762 lutCat = afwTable.BaseCatalog(lutSchema) 

763 lutCat.table.preallocate(14) 

764 

765 # first fill the first index 

766 rec = lutCat.addNew() 

767 

768 rec['tablename'] = atmosphereTableName 

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

770 rec['filterNames'] = filterNameString 

771 rec['stdFilterNames'] = stdFilterNameString 

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

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

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

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

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

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

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

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

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

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

782 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

800 

801 rec['luttype'] = 'I0' 

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

803 

804 # and add the rest 

805 rec = lutCat.addNew() 

806 rec['luttype'] = 'I1' 

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

808 

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

810 'D_PMB_I1', 'D_LNPWV_I1', 'D_O3_I1', 'D_LNTAU_I1', 'D_ALPHA_I1', 

811 'D_SECZENITH_I1'] 

812 for derivType in derivTypes: 

813 rec = lutCat.addNew() 

814 rec['luttype'] = derivType 

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

816 

817 return lutCat