Coverage for python/lsst/ip/isr/isrTask.py: 18%

765 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-11-15 10:24 +0000

1# This file is part of ip_isr. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

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

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

7# for details of code ownership. 

8# 

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

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

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

12# (at your option) any later version. 

13# 

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

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

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

17# GNU General Public License for more details. 

18# 

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

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

21 

22__all__ = ["IsrTask", "IsrTaskConfig"] 

23 

24import math 

25import numpy 

26 

27import lsst.geom 

28import lsst.afw.image as afwImage 

29import lsst.afw.math as afwMath 

30import lsst.pex.config as pexConfig 

31import lsst.pipe.base as pipeBase 

32import lsst.pipe.base.connectionTypes as cT 

33 

34from contextlib import contextmanager 

35from lsstDebug import getDebugFrame 

36 

37from lsst.afw.cameraGeom import NullLinearityType 

38from lsst.afw.display import getDisplay 

39from lsst.meas.algorithms.detection import SourceDetectionTask 

40from lsst.utils.timer import timeMethod 

41 

42from . import isrFunctions 

43from . import isrQa 

44from . import linearize 

45from .defects import Defects 

46 

47from .assembleCcdTask import AssembleCcdTask 

48from .crosstalk import CrosstalkTask, CrosstalkCalib 

49from .fringe import FringeTask 

50from .isr import maskNans 

51from .masking import MaskingTask 

52from .overscan import OverscanCorrectionTask 

53from .straylight import StrayLightTask 

54from .vignette import VignetteTask 

55from .ampOffset import AmpOffsetTask 

56from .deferredCharge import DeferredChargeTask 

57from .isrStatistics import IsrStatisticsTask 

58from lsst.daf.butler import DimensionGraph 

59 

60 

61def crosstalkSourceLookup(datasetType, registry, quantumDataId, collections): 

62 """Lookup function to identify crosstalkSource entries. 

63 

64 This should return an empty list under most circumstances. Only 

65 when inter-chip crosstalk has been identified should this be 

66 populated. 

67 

68 Parameters 

69 ---------- 

70 datasetType : `str` 

71 Dataset to lookup. 

72 registry : `lsst.daf.butler.Registry` 

73 Butler registry to query. 

74 quantumDataId : `lsst.daf.butler.ExpandedDataCoordinate` 

75 Data id to transform to identify crosstalkSources. The 

76 ``detector`` entry will be stripped. 

77 collections : `lsst.daf.butler.CollectionSearch` 

78 Collections to search through. 

79 

80 Returns 

81 ------- 

82 results : `list` [`lsst.daf.butler.DatasetRef`] 

83 List of datasets that match the query that will be used as 

84 crosstalkSources. 

85 """ 

86 newDataId = quantumDataId.subset(DimensionGraph(registry.dimensions, names=["instrument", "exposure"])) 

87 results = set(registry.queryDatasets(datasetType, collections=collections, dataId=newDataId, 

88 findFirst=True)) 

89 # In some contexts, calling `.expanded()` to expand all data IDs in the 

90 # query results can be a lot faster because it vectorizes lookups. But in 

91 # this case, expandDataId shouldn't need to hit the database at all in the 

92 # steady state, because only the detector record is unknown and those are 

93 # cached in the registry. 

94 return [ref.expanded(registry.expandDataId(ref.dataId, records=newDataId.records)) for ref in results] 

95 

96 

97class IsrTaskConnections(pipeBase.PipelineTaskConnections, 

98 dimensions={"instrument", "exposure", "detector"}, 

99 defaultTemplates={}): 

100 ccdExposure = cT.Input( 

101 name="raw", 

102 doc="Input exposure to process.", 

103 storageClass="Exposure", 

104 dimensions=["instrument", "exposure", "detector"], 

105 ) 

106 camera = cT.PrerequisiteInput( 

107 name="camera", 

108 storageClass="Camera", 

109 doc="Input camera to construct complete exposures.", 

110 dimensions=["instrument"], 

111 isCalibration=True, 

112 ) 

113 

114 crosstalk = cT.PrerequisiteInput( 

115 name="crosstalk", 

116 doc="Input crosstalk object", 

117 storageClass="CrosstalkCalib", 

118 dimensions=["instrument", "detector"], 

119 isCalibration=True, 

120 minimum=0, # can fall back to cameraGeom 

121 ) 

122 crosstalkSources = cT.PrerequisiteInput( 

123 name="isrOverscanCorrected", 

124 doc="Overscan corrected input images.", 

125 storageClass="Exposure", 

126 dimensions=["instrument", "exposure", "detector"], 

127 deferLoad=True, 

128 multiple=True, 

129 lookupFunction=crosstalkSourceLookup, 

130 minimum=0, # not needed for all instruments, no config to control this 

131 ) 

132 bias = cT.PrerequisiteInput( 

133 name="bias", 

134 doc="Input bias calibration.", 

135 storageClass="ExposureF", 

136 dimensions=["instrument", "detector"], 

137 isCalibration=True, 

138 ) 

139 dark = cT.PrerequisiteInput( 

140 name='dark', 

141 doc="Input dark calibration.", 

142 storageClass="ExposureF", 

143 dimensions=["instrument", "detector"], 

144 isCalibration=True, 

145 ) 

146 flat = cT.PrerequisiteInput( 

147 name="flat", 

148 doc="Input flat calibration.", 

149 storageClass="ExposureF", 

150 dimensions=["instrument", "physical_filter", "detector"], 

151 isCalibration=True, 

152 ) 

153 ptc = cT.PrerequisiteInput( 

154 name="ptc", 

155 doc="Input Photon Transfer Curve dataset", 

156 storageClass="PhotonTransferCurveDataset", 

157 dimensions=["instrument", "detector"], 

158 isCalibration=True, 

159 ) 

160 fringes = cT.PrerequisiteInput( 

161 name="fringe", 

162 doc="Input fringe calibration.", 

163 storageClass="ExposureF", 

164 dimensions=["instrument", "physical_filter", "detector"], 

165 isCalibration=True, 

166 minimum=0, # only needed for some bands, even when enabled 

167 ) 

168 strayLightData = cT.PrerequisiteInput( 

169 name='yBackground', 

170 doc="Input stray light calibration.", 

171 storageClass="StrayLightData", 

172 dimensions=["instrument", "physical_filter", "detector"], 

173 deferLoad=True, 

174 isCalibration=True, 

175 minimum=0, # only needed for some bands, even when enabled 

176 ) 

177 bfKernel = cT.PrerequisiteInput( 

178 name='bfKernel', 

179 doc="Input brighter-fatter kernel.", 

180 storageClass="NumpyArray", 

181 dimensions=["instrument"], 

182 isCalibration=True, 

183 minimum=0, # can use either bfKernel or newBFKernel 

184 ) 

185 newBFKernel = cT.PrerequisiteInput( 

186 name='brighterFatterKernel', 

187 doc="Newer complete kernel + gain solutions.", 

188 storageClass="BrighterFatterKernel", 

189 dimensions=["instrument", "detector"], 

190 isCalibration=True, 

191 minimum=0, # can use either bfKernel or newBFKernel 

192 ) 

193 defects = cT.PrerequisiteInput( 

194 name='defects', 

195 doc="Input defect tables.", 

196 storageClass="Defects", 

197 dimensions=["instrument", "detector"], 

198 isCalibration=True, 

199 ) 

200 linearizer = cT.PrerequisiteInput( 

201 name='linearizer', 

202 storageClass="Linearizer", 

203 doc="Linearity correction calibration.", 

204 dimensions=["instrument", "detector"], 

205 isCalibration=True, 

206 minimum=0, # can fall back to cameraGeom 

207 ) 

208 opticsTransmission = cT.PrerequisiteInput( 

209 name="transmission_optics", 

210 storageClass="TransmissionCurve", 

211 doc="Transmission curve due to the optics.", 

212 dimensions=["instrument"], 

213 isCalibration=True, 

214 ) 

215 filterTransmission = cT.PrerequisiteInput( 

216 name="transmission_filter", 

217 storageClass="TransmissionCurve", 

218 doc="Transmission curve due to the filter.", 

219 dimensions=["instrument", "physical_filter"], 

220 isCalibration=True, 

221 ) 

222 sensorTransmission = cT.PrerequisiteInput( 

223 name="transmission_sensor", 

224 storageClass="TransmissionCurve", 

225 doc="Transmission curve due to the sensor.", 

226 dimensions=["instrument", "detector"], 

227 isCalibration=True, 

228 ) 

229 atmosphereTransmission = cT.PrerequisiteInput( 

230 name="transmission_atmosphere", 

231 storageClass="TransmissionCurve", 

232 doc="Transmission curve due to the atmosphere.", 

233 dimensions=["instrument"], 

234 isCalibration=True, 

235 ) 

236 illumMaskedImage = cT.PrerequisiteInput( 

237 name="illum", 

238 doc="Input illumination correction.", 

239 storageClass="MaskedImageF", 

240 dimensions=["instrument", "physical_filter", "detector"], 

241 isCalibration=True, 

242 ) 

243 deferredChargeCalib = cT.PrerequisiteInput( 

244 name="cpCtiCalib", 

245 doc="Deferred charge/CTI correction dataset.", 

246 storageClass="IsrCalib", 

247 dimensions=["instrument", "detector"], 

248 isCalibration=True, 

249 ) 

250 

251 outputExposure = cT.Output( 

252 name='postISRCCD', 

253 doc="Output ISR processed exposure.", 

254 storageClass="Exposure", 

255 dimensions=["instrument", "exposure", "detector"], 

256 ) 

257 preInterpExposure = cT.Output( 

258 name='preInterpISRCCD', 

259 doc="Output ISR processed exposure, with pixels left uninterpolated.", 

260 storageClass="ExposureF", 

261 dimensions=["instrument", "exposure", "detector"], 

262 ) 

263 outputOssThumbnail = cT.Output( 

264 name="OssThumb", 

265 doc="Output Overscan-subtracted thumbnail image.", 

266 storageClass="Thumbnail", 

267 dimensions=["instrument", "exposure", "detector"], 

268 ) 

269 outputFlattenedThumbnail = cT.Output( 

270 name="FlattenedThumb", 

271 doc="Output flat-corrected thumbnail image.", 

272 storageClass="Thumbnail", 

273 dimensions=["instrument", "exposure", "detector"], 

274 ) 

275 outputStatistics = cT.Output( 

276 name="isrStatistics", 

277 doc="Output of additional statistics table.", 

278 storageClass="StructuredDataDict", 

279 dimensions=["instrument", "exposure", "detector"], 

280 ) 

281 

282 def __init__(self, *, config=None): 

283 super().__init__(config=config) 

284 

285 if config.doBias is not True: 

286 self.prerequisiteInputs.remove("bias") 

287 if config.doLinearize is not True: 

288 self.prerequisiteInputs.remove("linearizer") 

289 if config.doCrosstalk is not True: 

290 self.prerequisiteInputs.remove("crosstalkSources") 

291 self.prerequisiteInputs.remove("crosstalk") 

292 if config.doBrighterFatter is not True: 

293 self.prerequisiteInputs.remove("bfKernel") 

294 self.prerequisiteInputs.remove("newBFKernel") 

295 if config.doDefect is not True: 

296 self.prerequisiteInputs.remove("defects") 

297 if config.doDark is not True: 

298 self.prerequisiteInputs.remove("dark") 

299 if config.doFlat is not True: 

300 self.prerequisiteInputs.remove("flat") 

301 if config.doFringe is not True: 

302 self.prerequisiteInputs.remove("fringes") 

303 if config.doStrayLight is not True: 

304 self.prerequisiteInputs.remove("strayLightData") 

305 if config.usePtcGains is not True and config.usePtcReadNoise is not True: 

306 self.prerequisiteInputs.remove("ptc") 

307 if config.doAttachTransmissionCurve is not True: 

308 self.prerequisiteInputs.remove("opticsTransmission") 

309 self.prerequisiteInputs.remove("filterTransmission") 

310 self.prerequisiteInputs.remove("sensorTransmission") 

311 self.prerequisiteInputs.remove("atmosphereTransmission") 

312 else: 

313 if config.doUseOpticsTransmission is not True: 

314 self.prerequisiteInputs.remove("opticsTransmission") 

315 if config.doUseFilterTransmission is not True: 

316 self.prerequisiteInputs.remove("filterTransmission") 

317 if config.doUseSensorTransmission is not True: 

318 self.prerequisiteInputs.remove("sensorTransmission") 

319 if config.doUseAtmosphereTransmission is not True: 

320 self.prerequisiteInputs.remove("atmosphereTransmission") 

321 if config.doIlluminationCorrection is not True: 

322 self.prerequisiteInputs.remove("illumMaskedImage") 

323 if config.doDeferredCharge is not True: 

324 self.prerequisiteInputs.remove("deferredChargeCalib") 

325 

326 if config.doWrite is not True: 

327 self.outputs.remove("outputExposure") 

328 self.outputs.remove("preInterpExposure") 

329 self.outputs.remove("outputFlattenedThumbnail") 

330 self.outputs.remove("outputOssThumbnail") 

331 self.outputs.remove("outputStatistics") 

332 

333 if config.doSaveInterpPixels is not True: 

334 self.outputs.remove("preInterpExposure") 

335 if config.qa.doThumbnailOss is not True: 

336 self.outputs.remove("outputOssThumbnail") 

337 if config.qa.doThumbnailFlattened is not True: 

338 self.outputs.remove("outputFlattenedThumbnail") 

339 if config.doCalculateStatistics is not True: 

340 self.outputs.remove("outputStatistics") 

341 

342 

343class IsrTaskConfig(pipeBase.PipelineTaskConfig, 

344 pipelineConnections=IsrTaskConnections): 

345 """Configuration parameters for IsrTask. 

346 

347 Items are grouped in the order in which they are executed by the task. 

348 """ 

349 datasetType = pexConfig.Field( 

350 dtype=str, 

351 doc="Dataset type for input data; users will typically leave this alone, " 

352 "but camera-specific ISR tasks will override it", 

353 default="raw", 

354 ) 

355 

356 fallbackFilterName = pexConfig.Field( 

357 dtype=str, 

358 doc="Fallback default filter name for calibrations.", 

359 optional=True 

360 ) 

361 useFallbackDate = pexConfig.Field( 

362 dtype=bool, 

363 doc="Pass observation date when using fallback filter.", 

364 default=False, 

365 ) 

366 expectWcs = pexConfig.Field( 

367 dtype=bool, 

368 default=True, 

369 doc="Expect input science images to have a WCS (set False for e.g. spectrographs)." 

370 ) 

371 fwhm = pexConfig.Field( 

372 dtype=float, 

373 doc="FWHM of PSF in arcseconds.", 

374 default=1.0, 

375 ) 

376 qa = pexConfig.ConfigField( 

377 dtype=isrQa.IsrQaConfig, 

378 doc="QA related configuration options.", 

379 ) 

380 

381 # Image conversion configuration 

382 doConvertIntToFloat = pexConfig.Field( 

383 dtype=bool, 

384 doc="Convert integer raw images to floating point values?", 

385 default=True, 

386 ) 

387 

388 # Saturated pixel handling. 

389 doSaturation = pexConfig.Field( 

390 dtype=bool, 

391 doc="Mask saturated pixels? NB: this is totally independent of the" 

392 " interpolation option - this is ONLY setting the bits in the mask." 

393 " To have them interpolated make sure doSaturationInterpolation=True", 

394 default=True, 

395 ) 

396 saturatedMaskName = pexConfig.Field( 

397 dtype=str, 

398 doc="Name of mask plane to use in saturation detection and interpolation", 

399 default="SAT", 

400 ) 

401 saturation = pexConfig.Field( 

402 dtype=float, 

403 doc="The saturation level to use if no Detector is present in the Exposure (ignored if NaN)", 

404 default=float("NaN"), 

405 ) 

406 growSaturationFootprintSize = pexConfig.Field( 

407 dtype=int, 

408 doc="Number of pixels by which to grow the saturation footprints", 

409 default=1, 

410 ) 

411 

412 # Suspect pixel handling. 

413 doSuspect = pexConfig.Field( 

414 dtype=bool, 

415 doc="Mask suspect pixels?", 

416 default=False, 

417 ) 

418 suspectMaskName = pexConfig.Field( 

419 dtype=str, 

420 doc="Name of mask plane to use for suspect pixels", 

421 default="SUSPECT", 

422 ) 

423 numEdgeSuspect = pexConfig.Field( 

424 dtype=int, 

425 doc="Number of edge pixels to be flagged as untrustworthy.", 

426 default=0, 

427 ) 

428 edgeMaskLevel = pexConfig.ChoiceField( 

429 dtype=str, 

430 doc="Mask edge pixels in which coordinate frame: DETECTOR or AMP?", 

431 default="DETECTOR", 

432 allowed={ 

433 'DETECTOR': 'Mask only the edges of the full detector.', 

434 'AMP': 'Mask edges of each amplifier.', 

435 }, 

436 ) 

437 

438 # Initial masking options. 

439 doSetBadRegions = pexConfig.Field( 

440 dtype=bool, 

441 doc="Should we set the level of all BAD patches of the chip to the chip's average value?", 

442 default=True, 

443 ) 

444 badStatistic = pexConfig.ChoiceField( 

445 dtype=str, 

446 doc="How to estimate the average value for BAD regions.", 

447 default='MEANCLIP', 

448 allowed={ 

449 "MEANCLIP": "Correct using the (clipped) mean of good data", 

450 "MEDIAN": "Correct using the median of the good data", 

451 }, 

452 ) 

453 

454 # Overscan subtraction configuration. 

455 doOverscan = pexConfig.Field( 

456 dtype=bool, 

457 doc="Do overscan subtraction?", 

458 default=True, 

459 ) 

460 overscan = pexConfig.ConfigurableField( 

461 target=OverscanCorrectionTask, 

462 doc="Overscan subtraction task for image segments.", 

463 ) 

464 

465 # Amplifier to CCD assembly configuration 

466 doAssembleCcd = pexConfig.Field( 

467 dtype=bool, 

468 default=True, 

469 doc="Assemble amp-level exposures into a ccd-level exposure?" 

470 ) 

471 assembleCcd = pexConfig.ConfigurableField( 

472 target=AssembleCcdTask, 

473 doc="CCD assembly task", 

474 ) 

475 

476 # General calibration configuration. 

477 doAssembleIsrExposures = pexConfig.Field( 

478 dtype=bool, 

479 default=False, 

480 doc="Assemble amp-level calibration exposures into ccd-level exposure?" 

481 ) 

482 doTrimToMatchCalib = pexConfig.Field( 

483 dtype=bool, 

484 default=False, 

485 doc="Trim raw data to match calibration bounding boxes?" 

486 ) 

487 

488 # Bias subtraction. 

489 doBias = pexConfig.Field( 

490 dtype=bool, 

491 doc="Apply bias frame correction?", 

492 default=True, 

493 ) 

494 biasDataProductName = pexConfig.Field( 

495 dtype=str, 

496 doc="Name of the bias data product", 

497 default="bias", 

498 ) 

499 doBiasBeforeOverscan = pexConfig.Field( 

500 dtype=bool, 

501 doc="Reverse order of overscan and bias correction.", 

502 default=False 

503 ) 

504 

505 # Deferred charge correction. 

506 doDeferredCharge = pexConfig.Field( 

507 dtype=bool, 

508 doc="Apply deferred charge correction?", 

509 default=False, 

510 ) 

511 deferredChargeCorrection = pexConfig.ConfigurableField( 

512 target=DeferredChargeTask, 

513 doc="Deferred charge correction task.", 

514 ) 

515 

516 # Variance construction 

517 doVariance = pexConfig.Field( 

518 dtype=bool, 

519 doc="Calculate variance?", 

520 default=True 

521 ) 

522 gain = pexConfig.Field( 

523 dtype=float, 

524 doc="The gain to use if no Detector is present in the Exposure (ignored if NaN)", 

525 default=float("NaN"), 

526 ) 

527 readNoise = pexConfig.Field( 

528 dtype=float, 

529 doc="The read noise to use if no Detector is present in the Exposure", 

530 default=0.0, 

531 ) 

532 doEmpiricalReadNoise = pexConfig.Field( 

533 dtype=bool, 

534 default=False, 

535 doc="Calculate empirical read noise instead of value from AmpInfo data?" 

536 ) 

537 usePtcReadNoise = pexConfig.Field( 

538 dtype=bool, 

539 default=False, 

540 doc="Use readnoise values from the Photon Transfer Curve?" 

541 ) 

542 maskNegativeVariance = pexConfig.Field( 

543 dtype=bool, 

544 default=True, 

545 doc="Mask pixels that claim a negative variance? This likely indicates a failure " 

546 "in the measurement of the overscan at an edge due to the data falling off faster " 

547 "than the overscan model can account for it." 

548 ) 

549 negativeVarianceMaskName = pexConfig.Field( 

550 dtype=str, 

551 default="BAD", 

552 doc="Mask plane to use to mark pixels with negative variance, if `maskNegativeVariance` is True.", 

553 ) 

554 # Linearization. 

555 doLinearize = pexConfig.Field( 

556 dtype=bool, 

557 doc="Correct for nonlinearity of the detector's response?", 

558 default=True, 

559 ) 

560 

561 # Crosstalk. 

562 doCrosstalk = pexConfig.Field( 

563 dtype=bool, 

564 doc="Apply intra-CCD crosstalk correction?", 

565 default=False, 

566 ) 

567 doCrosstalkBeforeAssemble = pexConfig.Field( 

568 dtype=bool, 

569 doc="Apply crosstalk correction before CCD assembly, and before trimming?", 

570 default=False, 

571 ) 

572 crosstalk = pexConfig.ConfigurableField( 

573 target=CrosstalkTask, 

574 doc="Intra-CCD crosstalk correction", 

575 ) 

576 

577 # Masking options. 

578 doDefect = pexConfig.Field( 

579 dtype=bool, 

580 doc="Apply correction for CCD defects, e.g. hot pixels?", 

581 default=True, 

582 ) 

583 doNanMasking = pexConfig.Field( 

584 dtype=bool, 

585 doc="Mask non-finite (NAN, inf) pixels?", 

586 default=True, 

587 ) 

588 doWidenSaturationTrails = pexConfig.Field( 

589 dtype=bool, 

590 doc="Widen bleed trails based on their width?", 

591 default=True 

592 ) 

593 

594 # Brighter-Fatter correction. 

595 doBrighterFatter = pexConfig.Field( 

596 dtype=bool, 

597 default=False, 

598 doc="Apply the brighter-fatter correction?" 

599 ) 

600 brighterFatterLevel = pexConfig.ChoiceField( 

601 dtype=str, 

602 default="DETECTOR", 

603 doc="The level at which to correct for brighter-fatter.", 

604 allowed={ 

605 "AMP": "Every amplifier treated separately.", 

606 "DETECTOR": "One kernel per detector", 

607 } 

608 ) 

609 brighterFatterMaxIter = pexConfig.Field( 

610 dtype=int, 

611 default=10, 

612 doc="Maximum number of iterations for the brighter-fatter correction" 

613 ) 

614 brighterFatterThreshold = pexConfig.Field( 

615 dtype=float, 

616 default=1000, 

617 doc="Threshold used to stop iterating the brighter-fatter correction. It is the " 

618 "absolute value of the difference between the current corrected image and the one " 

619 "from the previous iteration summed over all the pixels." 

620 ) 

621 brighterFatterApplyGain = pexConfig.Field( 

622 dtype=bool, 

623 default=True, 

624 doc="Should the gain be applied when applying the brighter-fatter correction?" 

625 ) 

626 brighterFatterMaskListToInterpolate = pexConfig.ListField( 

627 dtype=str, 

628 doc="List of mask planes that should be interpolated over when applying the brighter-fatter " 

629 "correction.", 

630 default=["SAT", "BAD", "NO_DATA", "UNMASKEDNAN"], 

631 ) 

632 brighterFatterMaskGrowSize = pexConfig.Field( 

633 dtype=int, 

634 default=0, 

635 doc="Number of pixels to grow the masks listed in config.brighterFatterMaskListToInterpolate " 

636 "when brighter-fatter correction is applied." 

637 ) 

638 

639 # Dark subtraction. 

640 doDark = pexConfig.Field( 

641 dtype=bool, 

642 doc="Apply dark frame correction?", 

643 default=True, 

644 ) 

645 darkDataProductName = pexConfig.Field( 

646 dtype=str, 

647 doc="Name of the dark data product", 

648 default="dark", 

649 ) 

650 

651 # Camera-specific stray light removal. 

652 doStrayLight = pexConfig.Field( 

653 dtype=bool, 

654 doc="Subtract stray light in the y-band (due to encoder LEDs)?", 

655 default=False, 

656 ) 

657 strayLight = pexConfig.ConfigurableField( 

658 target=StrayLightTask, 

659 doc="y-band stray light correction" 

660 ) 

661 

662 # Flat correction. 

663 doFlat = pexConfig.Field( 

664 dtype=bool, 

665 doc="Apply flat field correction?", 

666 default=True, 

667 ) 

668 flatDataProductName = pexConfig.Field( 

669 dtype=str, 

670 doc="Name of the flat data product", 

671 default="flat", 

672 ) 

673 flatScalingType = pexConfig.ChoiceField( 

674 dtype=str, 

675 doc="The method for scaling the flat on the fly.", 

676 default='USER', 

677 allowed={ 

678 "USER": "Scale by flatUserScale", 

679 "MEAN": "Scale by the inverse of the mean", 

680 "MEDIAN": "Scale by the inverse of the median", 

681 }, 

682 ) 

683 flatUserScale = pexConfig.Field( 

684 dtype=float, 

685 doc="If flatScalingType is 'USER' then scale flat by this amount; ignored otherwise", 

686 default=1.0, 

687 ) 

688 doTweakFlat = pexConfig.Field( 

689 dtype=bool, 

690 doc="Tweak flats to match observed amplifier ratios?", 

691 default=False 

692 ) 

693 

694 # Amplifier normalization based on gains instead of using flats 

695 # configuration. 

696 doApplyGains = pexConfig.Field( 

697 dtype=bool, 

698 doc="Correct the amplifiers for their gains instead of applying flat correction", 

699 default=False, 

700 ) 

701 usePtcGains = pexConfig.Field( 

702 dtype=bool, 

703 doc="Use the gain values from the Photon Transfer Curve?", 

704 default=False, 

705 ) 

706 normalizeGains = pexConfig.Field( 

707 dtype=bool, 

708 doc="Normalize all the amplifiers in each CCD to have the same median value.", 

709 default=False, 

710 ) 

711 

712 # Fringe correction. 

713 doFringe = pexConfig.Field( 

714 dtype=bool, 

715 doc="Apply fringe correction?", 

716 default=True, 

717 ) 

718 fringe = pexConfig.ConfigurableField( 

719 target=FringeTask, 

720 doc="Fringe subtraction task", 

721 ) 

722 fringeAfterFlat = pexConfig.Field( 

723 dtype=bool, 

724 doc="Do fringe subtraction after flat-fielding?", 

725 default=True, 

726 ) 

727 

728 # Amp offset correction. 

729 doAmpOffset = pexConfig.Field( 

730 doc="Calculate and apply amp offset corrections?", 

731 dtype=bool, 

732 default=False, 

733 ) 

734 ampOffset = pexConfig.ConfigurableField( 

735 doc="Amp offset correction task.", 

736 target=AmpOffsetTask, 

737 ) 

738 

739 # Initial CCD-level background statistics options. 

740 doMeasureBackground = pexConfig.Field( 

741 dtype=bool, 

742 doc="Measure the background level on the reduced image?", 

743 default=False, 

744 ) 

745 

746 # Camera-specific masking configuration. 

747 doCameraSpecificMasking = pexConfig.Field( 

748 dtype=bool, 

749 doc="Mask camera-specific bad regions?", 

750 default=False, 

751 ) 

752 masking = pexConfig.ConfigurableField( 

753 target=MaskingTask, 

754 doc="Masking task." 

755 ) 

756 

757 # Interpolation options. 

758 doInterpolate = pexConfig.Field( 

759 dtype=bool, 

760 doc="Interpolate masked pixels?", 

761 default=True, 

762 ) 

763 doSaturationInterpolation = pexConfig.Field( 

764 dtype=bool, 

765 doc="Perform interpolation over pixels masked as saturated?" 

766 " NB: This is independent of doSaturation; if that is False this plane" 

767 " will likely be blank, resulting in a no-op here.", 

768 default=True, 

769 ) 

770 doNanInterpolation = pexConfig.Field( 

771 dtype=bool, 

772 doc="Perform interpolation over pixels masked as NaN?" 

773 " NB: This is independent of doNanMasking; if that is False this plane" 

774 " will likely be blank, resulting in a no-op here.", 

775 default=True, 

776 ) 

777 doNanInterpAfterFlat = pexConfig.Field( 

778 dtype=bool, 

779 doc=("If True, ensure we interpolate NaNs after flat-fielding, even if we " 

780 "also have to interpolate them before flat-fielding."), 

781 default=False, 

782 ) 

783 maskListToInterpolate = pexConfig.ListField( 

784 dtype=str, 

785 doc="List of mask planes that should be interpolated.", 

786 default=['SAT', 'BAD'], 

787 ) 

788 doSaveInterpPixels = pexConfig.Field( 

789 dtype=bool, 

790 doc="Save a copy of the pre-interpolated pixel values?", 

791 default=False, 

792 ) 

793 

794 # Default photometric calibration options. 

795 fluxMag0T1 = pexConfig.DictField( 

796 keytype=str, 

797 itemtype=float, 

798 doc="The approximate flux of a zero-magnitude object in a one-second exposure, per filter.", 

799 default=dict((f, pow(10.0, 0.4*m)) for f, m in (("Unknown", 28.0), 

800 )) 

801 ) 

802 defaultFluxMag0T1 = pexConfig.Field( 

803 dtype=float, 

804 doc="Default value for fluxMag0T1 (for an unrecognized filter).", 

805 default=pow(10.0, 0.4*28.0) 

806 ) 

807 

808 # Vignette correction configuration. 

809 doVignette = pexConfig.Field( 

810 dtype=bool, 

811 doc=("Compute and attach the validPolygon defining the unvignetted region to the exposure " 

812 "according to vignetting parameters?"), 

813 default=False, 

814 ) 

815 doMaskVignettePolygon = pexConfig.Field( 

816 dtype=bool, 

817 doc=("Add a mask bit for pixels within the vignetted region. Ignored if doVignette " 

818 "is False"), 

819 default=True, 

820 ) 

821 vignetteValue = pexConfig.Field( 

822 dtype=float, 

823 doc="Value to replace image array pixels with in the vignetted region? Ignored if None.", 

824 optional=True, 

825 default=None, 

826 ) 

827 vignette = pexConfig.ConfigurableField( 

828 target=VignetteTask, 

829 doc="Vignetting task.", 

830 ) 

831 

832 # Transmission curve configuration. 

833 doAttachTransmissionCurve = pexConfig.Field( 

834 dtype=bool, 

835 default=False, 

836 doc="Construct and attach a wavelength-dependent throughput curve for this CCD image?" 

837 ) 

838 doUseOpticsTransmission = pexConfig.Field( 

839 dtype=bool, 

840 default=True, 

841 doc="Load and use transmission_optics (if doAttachTransmissionCurve is True)?" 

842 ) 

843 doUseFilterTransmission = pexConfig.Field( 

844 dtype=bool, 

845 default=True, 

846 doc="Load and use transmission_filter (if doAttachTransmissionCurve is True)?" 

847 ) 

848 doUseSensorTransmission = pexConfig.Field( 

849 dtype=bool, 

850 default=True, 

851 doc="Load and use transmission_sensor (if doAttachTransmissionCurve is True)?" 

852 ) 

853 doUseAtmosphereTransmission = pexConfig.Field( 

854 dtype=bool, 

855 default=True, 

856 doc="Load and use transmission_atmosphere (if doAttachTransmissionCurve is True)?" 

857 ) 

858 

859 # Illumination correction. 

860 doIlluminationCorrection = pexConfig.Field( 

861 dtype=bool, 

862 default=False, 

863 doc="Perform illumination correction?" 

864 ) 

865 illuminationCorrectionDataProductName = pexConfig.Field( 

866 dtype=str, 

867 doc="Name of the illumination correction data product.", 

868 default="illumcor", 

869 ) 

870 illumScale = pexConfig.Field( 

871 dtype=float, 

872 doc="Scale factor for the illumination correction.", 

873 default=1.0, 

874 ) 

875 illumFilters = pexConfig.ListField( 

876 dtype=str, 

877 default=[], 

878 doc="Only perform illumination correction for these filters." 

879 ) 

880 

881 # Calculate additional statistics? 

882 doCalculateStatistics = pexConfig.Field( 

883 dtype=bool, 

884 doc="Should additional ISR statistics be calculated?", 

885 default=False, 

886 ) 

887 isrStats = pexConfig.ConfigurableField( 

888 target=IsrStatisticsTask, 

889 doc="Task to calculate additional statistics.", 

890 ) 

891 

892 # Write the outputs to disk. If ISR is run as a subtask, this may not 

893 # be needed. 

894 doWrite = pexConfig.Field( 

895 dtype=bool, 

896 doc="Persist postISRCCD?", 

897 default=True, 

898 ) 

899 

900 def validate(self): 

901 super().validate() 

902 if self.doFlat and self.doApplyGains: 

903 raise ValueError("You may not specify both doFlat and doApplyGains") 

904 if self.doBiasBeforeOverscan and self.doTrimToMatchCalib: 

905 raise ValueError("You may not specify both doBiasBeforeOverscan and doTrimToMatchCalib") 

906 if self.doSaturationInterpolation and self.saturatedMaskName not in self.maskListToInterpolate: 

907 self.maskListToInterpolate.append(self.saturatedMaskName) 

908 if not self.doSaturationInterpolation and self.saturatedMaskName in self.maskListToInterpolate: 

909 self.maskListToInterpolate.remove(self.saturatedMaskName) 

910 if self.doNanInterpolation and "UNMASKEDNAN" not in self.maskListToInterpolate: 

911 self.maskListToInterpolate.append("UNMASKEDNAN") 

912 

913 

914class IsrTask(pipeBase.PipelineTask): 

915 """Apply common instrument signature correction algorithms to a raw frame. 

916 

917 The process for correcting imaging data is very similar from 

918 camera to camera. This task provides a vanilla implementation of 

919 doing these corrections, including the ability to turn certain 

920 corrections off if they are not needed. The inputs to the primary 

921 method, `run()`, are a raw exposure to be corrected and the 

922 calibration data products. The raw input is a single chip sized 

923 mosaic of all amps including overscans and other non-science 

924 pixels. 

925 

926 The __init__ method sets up the subtasks for ISR processing, using 

927 the defaults from `lsst.ip.isr`. 

928 

929 Parameters 

930 ---------- 

931 args : `list` 

932 Positional arguments passed to the Task constructor. 

933 None used at this time. 

934 kwargs : `dict`, optional 

935 Keyword arguments passed on to the Task constructor. 

936 None used at this time. 

937 """ 

938 ConfigClass = IsrTaskConfig 

939 _DefaultName = "isr" 

940 

941 def __init__(self, **kwargs): 

942 super().__init__(**kwargs) 

943 self.makeSubtask("assembleCcd") 

944 self.makeSubtask("crosstalk") 

945 self.makeSubtask("strayLight") 

946 self.makeSubtask("fringe") 

947 self.makeSubtask("masking") 

948 self.makeSubtask("overscan") 

949 self.makeSubtask("vignette") 

950 self.makeSubtask("ampOffset") 

951 self.makeSubtask("deferredChargeCorrection") 

952 self.makeSubtask("isrStats") 

953 

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

955 inputs = butlerQC.get(inputRefs) 

956 

957 try: 

958 inputs['detectorNum'] = inputRefs.ccdExposure.dataId['detector'] 

959 except Exception as e: 

960 raise ValueError("Failure to find valid detectorNum value for Dataset %s: %s." % 

961 (inputRefs, e)) 

962 

963 detector = inputs['ccdExposure'].getDetector() 

964 

965 if self.config.doCrosstalk is True: 

966 # Crosstalk sources need to be defined by the pipeline 

967 # yaml if they exist. 

968 if 'crosstalk' in inputs and inputs['crosstalk'] is not None: 

969 if not isinstance(inputs['crosstalk'], CrosstalkCalib): 

970 inputs['crosstalk'] = CrosstalkCalib.fromTable(inputs['crosstalk']) 

971 else: 

972 coeffVector = (self.config.crosstalk.crosstalkValues 

973 if self.config.crosstalk.useConfigCoefficients else None) 

974 crosstalkCalib = CrosstalkCalib().fromDetector(detector, coeffVector=coeffVector) 

975 inputs['crosstalk'] = crosstalkCalib 

976 if inputs['crosstalk'].interChip and len(inputs['crosstalk'].interChip) > 0: 

977 if 'crosstalkSources' not in inputs: 

978 self.log.warning("No crosstalkSources found for chip with interChip terms!") 

979 

980 if self.doLinearize(detector) is True: 

981 if 'linearizer' in inputs: 

982 if isinstance(inputs['linearizer'], dict): 

983 linearizer = linearize.Linearizer(detector=detector, log=self.log) 

984 linearizer.fromYaml(inputs['linearizer']) 

985 self.log.warning("Dictionary linearizers will be deprecated in DM-28741.") 

986 elif isinstance(inputs['linearizer'], numpy.ndarray): 

987 linearizer = linearize.Linearizer(table=inputs.get('linearizer', None), 

988 detector=detector, 

989 log=self.log) 

990 self.log.warning("Bare lookup table linearizers will be deprecated in DM-28741.") 

991 else: 

992 linearizer = inputs['linearizer'] 

993 linearizer.log = self.log 

994 inputs['linearizer'] = linearizer 

995 else: 

996 inputs['linearizer'] = linearize.Linearizer(detector=detector, log=self.log) 

997 self.log.warning("Constructing linearizer from cameraGeom information.") 

998 

999 if self.config.doDefect is True: 

1000 if "defects" in inputs and inputs['defects'] is not None: 

1001 # defects is loaded as a BaseCatalog with columns 

1002 # x0, y0, width, height. Masking expects a list of defects 

1003 # defined by their bounding box 

1004 if not isinstance(inputs["defects"], Defects): 

1005 inputs["defects"] = Defects.fromTable(inputs["defects"]) 

1006 

1007 # Load the correct style of brighter-fatter kernel, and repack 

1008 # the information as a numpy array. 

1009 if self.config.doBrighterFatter: 

1010 brighterFatterKernel = inputs.pop('newBFKernel', None) 

1011 if brighterFatterKernel is None: 

1012 brighterFatterKernel = inputs.get('bfKernel', None) 

1013 

1014 if brighterFatterKernel is not None and not isinstance(brighterFatterKernel, numpy.ndarray): 

1015 # This is a ISR calib kernel 

1016 detName = detector.getName() 

1017 level = brighterFatterKernel.level 

1018 

1019 # This is expected to be a dictionary of amp-wise gains. 

1020 inputs['bfGains'] = brighterFatterKernel.gain 

1021 if self.config.brighterFatterLevel == 'DETECTOR': 

1022 if level == 'DETECTOR': 

1023 if detName in brighterFatterKernel.detKernels: 

1024 inputs['bfKernel'] = brighterFatterKernel.detKernels[detName] 

1025 else: 

1026 raise RuntimeError("Failed to extract kernel from new-style BF kernel.") 

1027 elif level == 'AMP': 

1028 self.log.warning("Making DETECTOR level kernel from AMP based brighter " 

1029 "fatter kernels.") 

1030 brighterFatterKernel.makeDetectorKernelFromAmpwiseKernels(detName) 

1031 inputs['bfKernel'] = brighterFatterKernel.detKernels[detName] 

1032 elif self.config.brighterFatterLevel == 'AMP': 

1033 raise NotImplementedError("Per-amplifier brighter-fatter correction not implemented") 

1034 

1035 if self.config.doFringe is True and self.fringe.checkFilter(inputs['ccdExposure']): 

1036 expId = inputs['ccdExposure'].info.id 

1037 inputs['fringes'] = self.fringe.loadFringes(inputs['fringes'], 

1038 expId=expId, 

1039 assembler=self.assembleCcd 

1040 if self.config.doAssembleIsrExposures else None) 

1041 else: 

1042 inputs['fringes'] = pipeBase.Struct(fringes=None) 

1043 

1044 if self.config.doStrayLight is True and self.strayLight.checkFilter(inputs['ccdExposure']): 

1045 if 'strayLightData' not in inputs: 

1046 inputs['strayLightData'] = None 

1047 

1048 outputs = self.run(**inputs) 

1049 butlerQC.put(outputs, outputRefs) 

1050 

1051 @timeMethod 

1052 def run(self, ccdExposure, *, camera=None, bias=None, linearizer=None, 

1053 crosstalk=None, crosstalkSources=None, 

1054 dark=None, flat=None, ptc=None, bfKernel=None, bfGains=None, defects=None, 

1055 fringes=pipeBase.Struct(fringes=None), opticsTransmission=None, filterTransmission=None, 

1056 sensorTransmission=None, atmosphereTransmission=None, 

1057 detectorNum=None, strayLightData=None, illumMaskedImage=None, 

1058 deferredChargeCalib=None, 

1059 ): 

1060 """Perform instrument signature removal on an exposure. 

1061 

1062 Steps included in the ISR processing, in order performed, are: 

1063 

1064 - saturation and suspect pixel masking 

1065 - overscan subtraction 

1066 - CCD assembly of individual amplifiers 

1067 - bias subtraction 

1068 - variance image construction 

1069 - linearization of non-linear response 

1070 - crosstalk masking 

1071 - brighter-fatter correction 

1072 - dark subtraction 

1073 - fringe correction 

1074 - stray light subtraction 

1075 - flat correction 

1076 - masking of known defects and camera specific features 

1077 - vignette calculation 

1078 - appending transmission curve and distortion model 

1079 

1080 Parameters 

1081 ---------- 

1082 ccdExposure : `lsst.afw.image.Exposure` 

1083 The raw exposure that is to be run through ISR. The 

1084 exposure is modified by this method. 

1085 camera : `lsst.afw.cameraGeom.Camera`, optional 

1086 The camera geometry for this exposure. Required if 

1087 one or more of ``ccdExposure``, ``bias``, ``dark``, or 

1088 ``flat`` does not have an associated detector. 

1089 bias : `lsst.afw.image.Exposure`, optional 

1090 Bias calibration frame. 

1091 linearizer : `lsst.ip.isr.linearize.LinearizeBase`, optional 

1092 Functor for linearization. 

1093 crosstalk : `lsst.ip.isr.crosstalk.CrosstalkCalib`, optional 

1094 Calibration for crosstalk. 

1095 crosstalkSources : `list`, optional 

1096 List of possible crosstalk sources. 

1097 dark : `lsst.afw.image.Exposure`, optional 

1098 Dark calibration frame. 

1099 flat : `lsst.afw.image.Exposure`, optional 

1100 Flat calibration frame. 

1101 ptc : `lsst.ip.isr.PhotonTransferCurveDataset`, optional 

1102 Photon transfer curve dataset, with, e.g., gains 

1103 and read noise. 

1104 bfKernel : `numpy.ndarray`, optional 

1105 Brighter-fatter kernel. 

1106 bfGains : `dict` of `float`, optional 

1107 Gains used to override the detector's nominal gains for the 

1108 brighter-fatter correction. A dict keyed by amplifier name for 

1109 the detector in question. 

1110 defects : `lsst.ip.isr.Defects`, optional 

1111 List of defects. 

1112 fringes : `lsst.pipe.base.Struct`, optional 

1113 Struct containing the fringe correction data, with 

1114 elements: 

1115 

1116 ``fringes`` 

1117 fringe calibration frame (`lsst.afw.image.Exposure`) 

1118 ``seed`` 

1119 random seed derived from the ``ccdExposureId`` for random 

1120 number generator (`numpy.uint32`) 

1121 opticsTransmission: `lsst.afw.image.TransmissionCurve`, optional 

1122 A ``TransmissionCurve`` that represents the throughput of the, 

1123 optics, to be evaluated in focal-plane coordinates. 

1124 filterTransmission : `lsst.afw.image.TransmissionCurve` 

1125 A ``TransmissionCurve`` that represents the throughput of the 

1126 filter itself, to be evaluated in focal-plane coordinates. 

1127 sensorTransmission : `lsst.afw.image.TransmissionCurve` 

1128 A ``TransmissionCurve`` that represents the throughput of the 

1129 sensor itself, to be evaluated in post-assembly trimmed detector 

1130 coordinates. 

1131 atmosphereTransmission : `lsst.afw.image.TransmissionCurve` 

1132 A ``TransmissionCurve`` that represents the throughput of the 

1133 atmosphere, assumed to be spatially constant. 

1134 detectorNum : `int`, optional 

1135 The integer number for the detector to process. 

1136 strayLightData : `object`, optional 

1137 Opaque object containing calibration information for stray-light 

1138 correction. If `None`, no correction will be performed. 

1139 illumMaskedImage : `lsst.afw.image.MaskedImage`, optional 

1140 Illumination correction image. 

1141 

1142 Returns 

1143 ------- 

1144 result : `lsst.pipe.base.Struct` 

1145 Result struct with component: 

1146 

1147 ``exposure`` 

1148 The fully ISR corrected exposure. 

1149 (`lsst.afw.image.Exposure`) 

1150 ``outputExposure`` 

1151 An alias for ``exposure``. (`lsst.afw.image.Exposure`) 

1152 ``ossThumb`` 

1153 Thumbnail image of the exposure after overscan subtraction. 

1154 (`numpy.ndarray`) 

1155 ``flattenedThumb`` 

1156 Thumbnail image of the exposure after flat-field correction. 

1157 (`numpy.ndarray`) 

1158 ``outputStatistics`` 

1159 Values of the additional statistics calculated. 

1160 

1161 Raises 

1162 ------ 

1163 RuntimeError 

1164 Raised if a configuration option is set to `True`, but the 

1165 required calibration data has not been specified. 

1166 

1167 Notes 

1168 ----- 

1169 The current processed exposure can be viewed by setting the 

1170 appropriate `lsstDebug` entries in the ``debug.display`` 

1171 dictionary. The names of these entries correspond to some of 

1172 the `IsrTaskConfig` Boolean options, with the value denoting the 

1173 frame to use. The exposure is shown inside the matching 

1174 option check and after the processing of that step has 

1175 finished. The steps with debug points are: 

1176 

1177 * doAssembleCcd 

1178 * doBias 

1179 * doCrosstalk 

1180 * doBrighterFatter 

1181 * doDark 

1182 * doFringe 

1183 * doStrayLight 

1184 * doFlat 

1185 

1186 In addition, setting the ``postISRCCD`` entry displays the 

1187 exposure after all ISR processing has finished. 

1188 """ 

1189 

1190 ccdExposure = self.ensureExposure(ccdExposure, camera, detectorNum) 

1191 bias = self.ensureExposure(bias, camera, detectorNum) 

1192 dark = self.ensureExposure(dark, camera, detectorNum) 

1193 flat = self.ensureExposure(flat, camera, detectorNum) 

1194 

1195 ccd = ccdExposure.getDetector() 

1196 filterLabel = ccdExposure.getFilter() 

1197 physicalFilter = isrFunctions.getPhysicalFilter(filterLabel, self.log) 

1198 

1199 if not ccd: 

1200 assert not self.config.doAssembleCcd, "You need a Detector to run assembleCcd." 

1201 ccd = [FakeAmp(ccdExposure, self.config)] 

1202 

1203 # Validate Input 

1204 if self.config.doBias and bias is None: 

1205 raise RuntimeError("Must supply a bias exposure if config.doBias=True.") 

1206 if self.doLinearize(ccd) and linearizer is None: 

1207 raise RuntimeError("Must supply a linearizer if config.doLinearize=True for this detector.") 

1208 if self.config.doBrighterFatter and bfKernel is None: 

1209 raise RuntimeError("Must supply a kernel if config.doBrighterFatter=True.") 

1210 if self.config.doDark and dark is None: 

1211 raise RuntimeError("Must supply a dark exposure if config.doDark=True.") 

1212 if self.config.doFlat and flat is None: 

1213 raise RuntimeError("Must supply a flat exposure if config.doFlat=True.") 

1214 if self.config.doDefect and defects is None: 

1215 raise RuntimeError("Must supply defects if config.doDefect=True.") 

1216 if (self.config.doFringe and physicalFilter in self.fringe.config.filters 

1217 and fringes.fringes is None): 

1218 # The `fringes` object needs to be a pipeBase.Struct, as 

1219 # we use it as a `dict` for the parameters of 

1220 # `FringeTask.run()`. The `fringes.fringes` `list` may 

1221 # not be `None` if `doFringe=True`. Otherwise, raise. 

1222 raise RuntimeError("Must supply fringe exposure as a pipeBase.Struct.") 

1223 if (self.config.doIlluminationCorrection and physicalFilter in self.config.illumFilters 

1224 and illumMaskedImage is None): 

1225 raise RuntimeError("Must supply an illumcor if config.doIlluminationCorrection=True.") 

1226 if (self.config.doDeferredCharge and deferredChargeCalib is None): 

1227 raise RuntimeError("Must supply a deferred charge calibration if config.doDeferredCharge=True.") 

1228 

1229 # Begin ISR processing. 

1230 if self.config.doConvertIntToFloat: 

1231 self.log.info("Converting exposure to floating point values.") 

1232 ccdExposure = self.convertIntToFloat(ccdExposure) 

1233 

1234 if self.config.doBias and self.config.doBiasBeforeOverscan: 

1235 self.log.info("Applying bias correction.") 

1236 isrFunctions.biasCorrection(ccdExposure.getMaskedImage(), bias.getMaskedImage(), 

1237 trimToFit=self.config.doTrimToMatchCalib) 

1238 self.debugView(ccdExposure, "doBias") 

1239 

1240 # Amplifier level processing. 

1241 overscans = [] 

1242 for amp in ccd: 

1243 # if ccdExposure is one amp, 

1244 # check for coverage to prevent performing ops multiple times 

1245 if ccdExposure.getBBox().contains(amp.getBBox()): 

1246 # Check for fully masked bad amplifiers, 

1247 # and generate masks for SUSPECT and SATURATED values. 

1248 badAmp = self.maskAmplifier(ccdExposure, amp, defects) 

1249 

1250 if self.config.doOverscan and not badAmp: 

1251 # Overscan correction on amp-by-amp basis. 

1252 overscanResults = self.overscanCorrection(ccdExposure, amp) 

1253 self.log.debug("Corrected overscan for amplifier %s.", amp.getName()) 

1254 if overscanResults is not None and \ 

1255 self.config.qa is not None and self.config.qa.saveStats is True: 

1256 

1257 self.metadata[f"FIT MEDIAN {amp.getName()}"] = overscanResults.overscanMean 

1258 self.metadata[f"FIT STDEV {amp.getName()}"] = overscanResults.overscanSigma 

1259 self.log.debug(" Overscan stats for amplifer %s: %f +/- %f", 

1260 amp.getName(), overscanResults.overscanMean, 

1261 overscanResults.overscanSigma) 

1262 

1263 self.metadata[f"RESIDUAL MEDIAN {amp.getName()}"] = overscanResults.residualMean 

1264 self.metadata[f"RESIDUAL STDEV {amp.getName()}"] = overscanResults.residualSigma 

1265 self.log.debug(" Overscan stats for amplifer %s after correction: %f +/- %f", 

1266 amp.getName(), overscanResults.residualMean, 

1267 overscanResults.residualSigma) 

1268 

1269 ccdExposure.getMetadata().set('OVERSCAN', "Overscan corrected") 

1270 else: 

1271 if badAmp: 

1272 self.log.warning("Amplifier %s is bad.", amp.getName()) 

1273 overscanResults = None 

1274 

1275 overscans.append(overscanResults if overscanResults is not None else None) 

1276 else: 

1277 self.log.info("Skipped OSCAN for %s.", amp.getName()) 

1278 

1279 if self.config.doDeferredCharge: 

1280 self.log.info("Applying deferred charge/CTI correction.") 

1281 self.deferredChargeCorrection.run(ccdExposure, deferredChargeCalib) 

1282 self.debugView(ccdExposure, "doDeferredCharge") 

1283 

1284 if self.config.doCrosstalk and self.config.doCrosstalkBeforeAssemble: 

1285 self.log.info("Applying crosstalk correction.") 

1286 self.crosstalk.run(ccdExposure, crosstalk=crosstalk, 

1287 crosstalkSources=crosstalkSources, camera=camera) 

1288 self.debugView(ccdExposure, "doCrosstalk") 

1289 

1290 if self.config.doAssembleCcd: 

1291 self.log.info("Assembling CCD from amplifiers.") 

1292 ccdExposure = self.assembleCcd.assembleCcd(ccdExposure) 

1293 

1294 if self.config.expectWcs and not ccdExposure.getWcs(): 

1295 self.log.warning("No WCS found in input exposure.") 

1296 self.debugView(ccdExposure, "doAssembleCcd") 

1297 

1298 ossThumb = None 

1299 if self.config.qa.doThumbnailOss: 

1300 ossThumb = isrQa.makeThumbnail(ccdExposure, isrQaConfig=self.config.qa) 

1301 

1302 if self.config.doBias and not self.config.doBiasBeforeOverscan: 

1303 self.log.info("Applying bias correction.") 

1304 isrFunctions.biasCorrection(ccdExposure.getMaskedImage(), bias.getMaskedImage(), 

1305 trimToFit=self.config.doTrimToMatchCalib) 

1306 self.debugView(ccdExposure, "doBias") 

1307 

1308 if self.config.doVariance: 

1309 for amp, overscanResults in zip(ccd, overscans): 

1310 if ccdExposure.getBBox().contains(amp.getBBox()): 

1311 self.log.debug("Constructing variance map for amplifer %s.", amp.getName()) 

1312 ampExposure = ccdExposure.Factory(ccdExposure, amp.getBBox()) 

1313 if overscanResults is not None: 

1314 self.updateVariance(ampExposure, amp, 

1315 overscanImage=overscanResults.overscanImage, 

1316 ptcDataset=ptc) 

1317 else: 

1318 self.updateVariance(ampExposure, amp, 

1319 overscanImage=None, 

1320 ptcDataset=ptc) 

1321 if self.config.qa is not None and self.config.qa.saveStats is True: 

1322 qaStats = afwMath.makeStatistics(ampExposure.getVariance(), 

1323 afwMath.MEDIAN | afwMath.STDEVCLIP) 

1324 self.metadata[f"ISR VARIANCE {amp.getName()} MEDIAN"] = \ 

1325 qaStats.getValue(afwMath.MEDIAN) 

1326 self.metadata[f"ISR VARIANCE {amp.getName()} STDEV"] = \ 

1327 qaStats.getValue(afwMath.STDEVCLIP) 

1328 self.log.debug(" Variance stats for amplifer %s: %f +/- %f.", 

1329 amp.getName(), qaStats.getValue(afwMath.MEDIAN), 

1330 qaStats.getValue(afwMath.STDEVCLIP)) 

1331 if self.config.maskNegativeVariance: 

1332 self.maskNegativeVariance(ccdExposure) 

1333 

1334 if self.doLinearize(ccd): 

1335 self.log.info("Applying linearizer.") 

1336 linearizer.applyLinearity(image=ccdExposure.getMaskedImage().getImage(), 

1337 detector=ccd, log=self.log) 

1338 

1339 if self.config.doCrosstalk and not self.config.doCrosstalkBeforeAssemble: 

1340 self.log.info("Applying crosstalk correction.") 

1341 self.crosstalk.run(ccdExposure, crosstalk=crosstalk, 

1342 crosstalkSources=crosstalkSources, isTrimmed=True) 

1343 self.debugView(ccdExposure, "doCrosstalk") 

1344 

1345 # Masking block. Optionally mask known defects, NAN/inf pixels, 

1346 # widen trails, and do anything else the camera needs. Saturated and 

1347 # suspect pixels have already been masked. 

1348 if self.config.doDefect: 

1349 self.log.info("Masking defects.") 

1350 self.maskDefect(ccdExposure, defects) 

1351 

1352 if self.config.numEdgeSuspect > 0: 

1353 self.log.info("Masking edges as SUSPECT.") 

1354 self.maskEdges(ccdExposure, numEdgePixels=self.config.numEdgeSuspect, 

1355 maskPlane="SUSPECT", level=self.config.edgeMaskLevel) 

1356 

1357 if self.config.doNanMasking: 

1358 self.log.info("Masking non-finite (NAN, inf) value pixels.") 

1359 self.maskNan(ccdExposure) 

1360 

1361 if self.config.doWidenSaturationTrails: 

1362 self.log.info("Widening saturation trails.") 

1363 isrFunctions.widenSaturationTrails(ccdExposure.getMaskedImage().getMask()) 

1364 

1365 if self.config.doCameraSpecificMasking: 

1366 self.log.info("Masking regions for camera specific reasons.") 

1367 self.masking.run(ccdExposure) 

1368 

1369 if self.config.doBrighterFatter: 

1370 # We need to apply flats and darks before we can interpolate, and 

1371 # we need to interpolate before we do B-F, but we do B-F without 

1372 # the flats and darks applied so we can work in units of electrons 

1373 # or holes. This context manager applies and then removes the darks 

1374 # and flats. 

1375 # 

1376 # We also do not want to interpolate values here, so operate on 

1377 # temporary images so we can apply only the BF-correction and roll 

1378 # back the interpolation. 

1379 interpExp = ccdExposure.clone() 

1380 with self.flatContext(interpExp, flat, dark): 

1381 isrFunctions.interpolateFromMask( 

1382 maskedImage=interpExp.getMaskedImage(), 

1383 fwhm=self.config.fwhm, 

1384 growSaturatedFootprints=self.config.growSaturationFootprintSize, 

1385 maskNameList=list(self.config.brighterFatterMaskListToInterpolate) 

1386 ) 

1387 bfExp = interpExp.clone() 

1388 

1389 self.log.info("Applying brighter-fatter correction using kernel type %s / gains %s.", 

1390 type(bfKernel), type(bfGains)) 

1391 bfResults = isrFunctions.brighterFatterCorrection(bfExp, bfKernel, 

1392 self.config.brighterFatterMaxIter, 

1393 self.config.brighterFatterThreshold, 

1394 self.config.brighterFatterApplyGain, 

1395 bfGains) 

1396 if bfResults[1] == self.config.brighterFatterMaxIter: 

1397 self.log.warning("Brighter-fatter correction did not converge, final difference %f.", 

1398 bfResults[0]) 

1399 else: 

1400 self.log.info("Finished brighter-fatter correction in %d iterations.", 

1401 bfResults[1]) 

1402 image = ccdExposure.getMaskedImage().getImage() 

1403 bfCorr = bfExp.getMaskedImage().getImage() 

1404 bfCorr -= interpExp.getMaskedImage().getImage() 

1405 image += bfCorr 

1406 

1407 # Applying the brighter-fatter correction applies a 

1408 # convolution to the science image. At the edges this 

1409 # convolution may not have sufficient valid pixels to 

1410 # produce a valid correction. Mark pixels within the size 

1411 # of the brighter-fatter kernel as EDGE to warn of this 

1412 # fact. 

1413 self.log.info("Ensuring image edges are masked as EDGE to the brighter-fatter kernel size.") 

1414 self.maskEdges(ccdExposure, numEdgePixels=numpy.max(bfKernel.shape) // 2, 

1415 maskPlane="EDGE") 

1416 

1417 if self.config.brighterFatterMaskGrowSize > 0: 

1418 self.log.info("Growing masks to account for brighter-fatter kernel convolution.") 

1419 for maskPlane in self.config.brighterFatterMaskListToInterpolate: 

1420 isrFunctions.growMasks(ccdExposure.getMask(), 

1421 radius=self.config.brighterFatterMaskGrowSize, 

1422 maskNameList=maskPlane, 

1423 maskValue=maskPlane) 

1424 

1425 self.debugView(ccdExposure, "doBrighterFatter") 

1426 

1427 if self.config.doDark: 

1428 self.log.info("Applying dark correction.") 

1429 self.darkCorrection(ccdExposure, dark) 

1430 self.debugView(ccdExposure, "doDark") 

1431 

1432 if self.config.doFringe and not self.config.fringeAfterFlat: 

1433 self.log.info("Applying fringe correction before flat.") 

1434 self.fringe.run(ccdExposure, **fringes.getDict()) 

1435 self.debugView(ccdExposure, "doFringe") 

1436 

1437 if self.config.doStrayLight and self.strayLight.check(ccdExposure): 

1438 self.log.info("Checking strayLight correction.") 

1439 self.strayLight.run(ccdExposure, strayLightData) 

1440 self.debugView(ccdExposure, "doStrayLight") 

1441 

1442 if self.config.doFlat: 

1443 self.log.info("Applying flat correction.") 

1444 self.flatCorrection(ccdExposure, flat) 

1445 self.debugView(ccdExposure, "doFlat") 

1446 

1447 if self.config.doApplyGains: 

1448 self.log.info("Applying gain correction instead of flat.") 

1449 if self.config.usePtcGains: 

1450 self.log.info("Using gains from the Photon Transfer Curve.") 

1451 isrFunctions.applyGains(ccdExposure, self.config.normalizeGains, 

1452 ptcGains=ptc.gain) 

1453 else: 

1454 isrFunctions.applyGains(ccdExposure, self.config.normalizeGains) 

1455 

1456 if self.config.doFringe and self.config.fringeAfterFlat: 

1457 self.log.info("Applying fringe correction after flat.") 

1458 self.fringe.run(ccdExposure, **fringes.getDict()) 

1459 

1460 if self.config.doVignette: 

1461 if self.config.doMaskVignettePolygon: 

1462 self.log.info("Constructing, attaching, and masking vignette polygon.") 

1463 else: 

1464 self.log.info("Constructing and attaching vignette polygon.") 

1465 self.vignettePolygon = self.vignette.run( 

1466 exposure=ccdExposure, doUpdateMask=self.config.doMaskVignettePolygon, 

1467 vignetteValue=self.config.vignetteValue, log=self.log) 

1468 

1469 if self.config.doAttachTransmissionCurve: 

1470 self.log.info("Adding transmission curves.") 

1471 isrFunctions.attachTransmissionCurve(ccdExposure, opticsTransmission=opticsTransmission, 

1472 filterTransmission=filterTransmission, 

1473 sensorTransmission=sensorTransmission, 

1474 atmosphereTransmission=atmosphereTransmission) 

1475 

1476 flattenedThumb = None 

1477 if self.config.qa.doThumbnailFlattened: 

1478 flattenedThumb = isrQa.makeThumbnail(ccdExposure, isrQaConfig=self.config.qa) 

1479 

1480 if self.config.doIlluminationCorrection and physicalFilter in self.config.illumFilters: 

1481 self.log.info("Performing illumination correction.") 

1482 isrFunctions.illuminationCorrection(ccdExposure.getMaskedImage(), 

1483 illumMaskedImage, illumScale=self.config.illumScale, 

1484 trimToFit=self.config.doTrimToMatchCalib) 

1485 

1486 preInterpExp = None 

1487 if self.config.doSaveInterpPixels: 

1488 preInterpExp = ccdExposure.clone() 

1489 

1490 # Reset and interpolate bad pixels. 

1491 # 

1492 # Large contiguous bad regions (which should have the BAD mask 

1493 # bit set) should have their values set to the image median. 

1494 # This group should include defects and bad amplifiers. As the 

1495 # area covered by these defects are large, there's little 

1496 # reason to expect that interpolation would provide a more 

1497 # useful value. 

1498 # 

1499 # Smaller defects can be safely interpolated after the larger 

1500 # regions have had their pixel values reset. This ensures 

1501 # that the remaining defects adjacent to bad amplifiers (as an 

1502 # example) do not attempt to interpolate extreme values. 

1503 if self.config.doSetBadRegions: 

1504 badPixelCount, badPixelValue = isrFunctions.setBadRegions(ccdExposure) 

1505 if badPixelCount > 0: 

1506 self.log.info("Set %d BAD pixels to %f.", badPixelCount, badPixelValue) 

1507 

1508 if self.config.doInterpolate: 

1509 self.log.info("Interpolating masked pixels.") 

1510 isrFunctions.interpolateFromMask( 

1511 maskedImage=ccdExposure.getMaskedImage(), 

1512 fwhm=self.config.fwhm, 

1513 growSaturatedFootprints=self.config.growSaturationFootprintSize, 

1514 maskNameList=list(self.config.maskListToInterpolate) 

1515 ) 

1516 

1517 self.roughZeroPoint(ccdExposure) 

1518 

1519 # correct for amp offsets within the CCD 

1520 if self.config.doAmpOffset: 

1521 self.log.info("Correcting amp offsets.") 

1522 self.ampOffset.run(ccdExposure) 

1523 

1524 if self.config.doMeasureBackground: 

1525 self.log.info("Measuring background level.") 

1526 self.measureBackground(ccdExposure, self.config.qa) 

1527 

1528 if self.config.qa is not None and self.config.qa.saveStats is True: 

1529 for amp in ccd: 

1530 ampExposure = ccdExposure.Factory(ccdExposure, amp.getBBox()) 

1531 qaStats = afwMath.makeStatistics(ampExposure.getImage(), 

1532 afwMath.MEDIAN | afwMath.STDEVCLIP) 

1533 self.metadata[f"ISR BACKGROUND {amp.getName()} MEDIAN"] = qaStats.getValue(afwMath.MEDIAN) 

1534 self.metadata[f"ISR BACKGROUND {amp.getName()} STDEV"] = \ 

1535 qaStats.getValue(afwMath.STDEVCLIP) 

1536 self.log.debug(" Background stats for amplifer %s: %f +/- %f", 

1537 amp.getName(), qaStats.getValue(afwMath.MEDIAN), 

1538 qaStats.getValue(afwMath.STDEVCLIP)) 

1539 

1540 # calculate additional statistics. 

1541 outputStatistics = None 

1542 if self.config.doCalculateStatistics: 

1543 outputStatistics = self.isrStats.run(ccdExposure, overscanResults=overscans, 

1544 ptc=ptc).results 

1545 

1546 self.debugView(ccdExposure, "postISRCCD") 

1547 

1548 return pipeBase.Struct( 

1549 exposure=ccdExposure, 

1550 ossThumb=ossThumb, 

1551 flattenedThumb=flattenedThumb, 

1552 

1553 preInterpExposure=preInterpExp, 

1554 outputExposure=ccdExposure, 

1555 outputOssThumbnail=ossThumb, 

1556 outputFlattenedThumbnail=flattenedThumb, 

1557 outputStatistics=outputStatistics, 

1558 ) 

1559 

1560 def ensureExposure(self, inputExp, camera=None, detectorNum=None): 

1561 """Ensure that the data returned by Butler is a fully constructed exp. 

1562 

1563 ISR requires exposure-level image data for historical reasons, so if we 

1564 did not recieve that from Butler, construct it from what we have, 

1565 modifying the input in place. 

1566 

1567 Parameters 

1568 ---------- 

1569 inputExp : `lsst.afw.image` image-type. 

1570 The input data structure obtained from Butler. 

1571 Can be `lsst.afw.image.Exposure`, 

1572 `lsst.afw.image.DecoratedImageU`, 

1573 or `lsst.afw.image.ImageF` 

1574 camera : `lsst.afw.cameraGeom.camera`, optional 

1575 The camera associated with the image. Used to find the appropriate 

1576 detector if detector is not already set. 

1577 detectorNum : `int`, optional 

1578 The detector in the camera to attach, if the detector is not 

1579 already set. 

1580 

1581 Returns 

1582 ------- 

1583 inputExp : `lsst.afw.image.Exposure` 

1584 The re-constructed exposure, with appropriate detector parameters. 

1585 

1586 Raises 

1587 ------ 

1588 TypeError 

1589 Raised if the input data cannot be used to construct an exposure. 

1590 """ 

1591 if isinstance(inputExp, afwImage.DecoratedImageU): 

1592 inputExp = afwImage.makeExposure(afwImage.makeMaskedImage(inputExp)) 

1593 elif isinstance(inputExp, afwImage.ImageF): 

1594 inputExp = afwImage.makeExposure(afwImage.makeMaskedImage(inputExp)) 

1595 elif isinstance(inputExp, afwImage.MaskedImageF): 

1596 inputExp = afwImage.makeExposure(inputExp) 

1597 elif isinstance(inputExp, afwImage.Exposure): 

1598 pass 

1599 elif inputExp is None: 

1600 # Assume this will be caught by the setup if it is a problem. 

1601 return inputExp 

1602 else: 

1603 raise TypeError("Input Exposure is not known type in isrTask.ensureExposure: %s." % 

1604 (type(inputExp), )) 

1605 

1606 if inputExp.getDetector() is None: 

1607 if camera is None or detectorNum is None: 

1608 raise RuntimeError('Must supply both a camera and detector number when using exposures ' 

1609 'without a detector set.') 

1610 inputExp.setDetector(camera[detectorNum]) 

1611 

1612 return inputExp 

1613 

1614 def convertIntToFloat(self, exposure): 

1615 """Convert exposure image from uint16 to float. 

1616 

1617 If the exposure does not need to be converted, the input is 

1618 immediately returned. For exposures that are converted to use 

1619 floating point pixels, the variance is set to unity and the 

1620 mask to zero. 

1621 

1622 Parameters 

1623 ---------- 

1624 exposure : `lsst.afw.image.Exposure` 

1625 The raw exposure to be converted. 

1626 

1627 Returns 

1628 ------- 

1629 newexposure : `lsst.afw.image.Exposure` 

1630 The input ``exposure``, converted to floating point pixels. 

1631 

1632 Raises 

1633 ------ 

1634 RuntimeError 

1635 Raised if the exposure type cannot be converted to float. 

1636 

1637 """ 

1638 if isinstance(exposure, afwImage.ExposureF): 

1639 # Nothing to be done 

1640 self.log.debug("Exposure already of type float.") 

1641 return exposure 

1642 if not hasattr(exposure, "convertF"): 

1643 raise RuntimeError("Unable to convert exposure (%s) to float." % type(exposure)) 

1644 

1645 newexposure = exposure.convertF() 

1646 newexposure.variance[:] = 1 

1647 newexposure.mask[:] = 0x0 

1648 

1649 return newexposure 

1650 

1651 def maskAmplifier(self, ccdExposure, amp, defects): 

1652 """Identify bad amplifiers, saturated and suspect pixels. 

1653 

1654 Parameters 

1655 ---------- 

1656 ccdExposure : `lsst.afw.image.Exposure` 

1657 Input exposure to be masked. 

1658 amp : `lsst.afw.cameraGeom.Amplifier` 

1659 Catalog of parameters defining the amplifier on this 

1660 exposure to mask. 

1661 defects : `lsst.ip.isr.Defects` 

1662 List of defects. Used to determine if the entire 

1663 amplifier is bad. 

1664 

1665 Returns 

1666 ------- 

1667 badAmp : `Bool` 

1668 If this is true, the entire amplifier area is covered by 

1669 defects and unusable. 

1670 

1671 """ 

1672 maskedImage = ccdExposure.getMaskedImage() 

1673 

1674 badAmp = False 

1675 

1676 # Check if entire amp region is defined as a defect 

1677 # NB: need to use amp.getBBox() for correct comparison with current 

1678 # defects definition. 

1679 if defects is not None: 

1680 badAmp = bool(sum([v.getBBox().contains(amp.getBBox()) for v in defects])) 

1681 

1682 # In the case of a bad amp, we will set mask to "BAD" 

1683 # (here use amp.getRawBBox() for correct association with pixels in 

1684 # current ccdExposure). 

1685 if badAmp: 

1686 dataView = afwImage.MaskedImageF(maskedImage, amp.getRawBBox(), 

1687 afwImage.PARENT) 

1688 maskView = dataView.getMask() 

1689 maskView |= maskView.getPlaneBitMask("BAD") 

1690 del maskView 

1691 return badAmp 

1692 

1693 # Mask remaining defects after assembleCcd() to allow for defects that 

1694 # cross amplifier boundaries. Saturation and suspect pixels can be 

1695 # masked now, though. 

1696 limits = dict() 

1697 if self.config.doSaturation and not badAmp: 

1698 limits.update({self.config.saturatedMaskName: amp.getSaturation()}) 

1699 if self.config.doSuspect and not badAmp: 

1700 limits.update({self.config.suspectMaskName: amp.getSuspectLevel()}) 

1701 if math.isfinite(self.config.saturation): 

1702 limits.update({self.config.saturatedMaskName: self.config.saturation}) 

1703 

1704 for maskName, maskThreshold in limits.items(): 

1705 if not math.isnan(maskThreshold): 

1706 dataView = maskedImage.Factory(maskedImage, amp.getRawBBox()) 

1707 isrFunctions.makeThresholdMask( 

1708 maskedImage=dataView, 

1709 threshold=maskThreshold, 

1710 growFootprints=0, 

1711 maskName=maskName 

1712 ) 

1713 

1714 # Determine if we've fully masked this amplifier with SUSPECT and 

1715 # SAT pixels. 

1716 maskView = afwImage.Mask(maskedImage.getMask(), amp.getRawDataBBox(), 

1717 afwImage.PARENT) 

1718 maskVal = maskView.getPlaneBitMask([self.config.saturatedMaskName, 

1719 self.config.suspectMaskName]) 

1720 if numpy.all(maskView.getArray() & maskVal > 0): 

1721 badAmp = True 

1722 maskView |= maskView.getPlaneBitMask("BAD") 

1723 

1724 return badAmp 

1725 

1726 def overscanCorrection(self, ccdExposure, amp): 

1727 """Apply overscan correction in place. 

1728 

1729 This method does initial pixel rejection of the overscan 

1730 region. The overscan can also be optionally segmented to 

1731 allow for discontinuous overscan responses to be fit 

1732 separately. The actual overscan subtraction is performed by 

1733 the `lsst.ip.isr.overscan.OverscanTask`, which is called here 

1734 after the amplifier is preprocessed. 

1735 

1736 Parameters 

1737 ---------- 

1738 ccdExposure : `lsst.afw.image.Exposure` 

1739 Exposure to have overscan correction performed. 

1740 amp : `lsst.afw.cameraGeom.Amplifer` 

1741 The amplifier to consider while correcting the overscan. 

1742 

1743 Returns 

1744 ------- 

1745 overscanResults : `lsst.pipe.base.Struct` 

1746 Result struct with components: 

1747 

1748 ``imageFit`` 

1749 Value or fit subtracted from the amplifier image data. 

1750 (scalar or `lsst.afw.image.Image`) 

1751 ``overscanFit`` 

1752 Value or fit subtracted from the overscan image data. 

1753 (scalar or `lsst.afw.image.Image`) 

1754 ``overscanImage`` 

1755 Image of the overscan region with the overscan 

1756 correction applied. This quantity is used to estimate 

1757 the amplifier read noise empirically. 

1758 (`lsst.afw.image.Image`) 

1759 ``edgeMask`` 

1760 Mask of the suspect pixels. (`lsst.afw.image.Mask`) 

1761 ``overscanMean`` 

1762 Median overscan fit value. (`float`) 

1763 ``overscanSigma`` 

1764 Clipped standard deviation of the overscan after 

1765 correction. (`float`) 

1766 

1767 Raises 

1768 ------ 

1769 RuntimeError 

1770 Raised if the ``amp`` does not contain raw pixel information. 

1771 

1772 See Also 

1773 -------- 

1774 lsst.ip.isr.overscan.OverscanTask 

1775 

1776 """ 

1777 if amp.getRawHorizontalOverscanBBox().isEmpty(): 

1778 self.log.info("ISR_OSCAN: No overscan region. Not performing overscan correction.") 

1779 return None 

1780 

1781 # Perform overscan correction on subregions. 

1782 overscanResults = self.overscan.run(ccdExposure, amp) 

1783 

1784 metadata = ccdExposure.getMetadata() 

1785 ampNum = amp.getName() 

1786 metadata[f"ISR_OSCAN_LEVEL{ampNum}"] = overscanResults.overscanMean 

1787 metadata[f"ISR_OSCAN_SIGMA{ampNum}"] = overscanResults.overscanSigma 

1788 

1789 return overscanResults 

1790 

1791 def updateVariance(self, ampExposure, amp, overscanImage=None, ptcDataset=None): 

1792 """Set the variance plane using the gain and read noise 

1793 

1794 The read noise is calculated from the ``overscanImage`` if the 

1795 ``doEmpiricalReadNoise`` option is set in the configuration; otherwise 

1796 the value from the amplifier data is used. 

1797 

1798 Parameters 

1799 ---------- 

1800 ampExposure : `lsst.afw.image.Exposure` 

1801 Exposure to process. 

1802 amp : `lsst.afw.cameraGeom.Amplifier` or `FakeAmp` 

1803 Amplifier detector data. 

1804 overscanImage : `lsst.afw.image.MaskedImage`, optional. 

1805 Image of overscan, required only for empirical read noise. 

1806 ptcDataset : `lsst.ip.isr.PhotonTransferCurveDataset`, optional 

1807 PTC dataset containing the gains and read noise. 

1808 

1809 Raises 

1810 ------ 

1811 RuntimeError 

1812 Raised if either ``usePtcGains`` of ``usePtcReadNoise`` 

1813 are ``True``, but ptcDataset is not provided. 

1814 

1815 Raised if ```doEmpiricalReadNoise`` is ``True`` but 

1816 ``overscanImage`` is ``None``. 

1817 

1818 See also 

1819 -------- 

1820 lsst.ip.isr.isrFunctions.updateVariance 

1821 """ 

1822 maskPlanes = [self.config.saturatedMaskName, self.config.suspectMaskName] 

1823 if self.config.usePtcGains: 

1824 if ptcDataset is None: 

1825 raise RuntimeError("No ptcDataset provided to use PTC gains.") 

1826 else: 

1827 gain = ptcDataset.gain[amp.getName()] 

1828 self.log.info("Using gain from Photon Transfer Curve.") 

1829 else: 

1830 gain = amp.getGain() 

1831 

1832 if math.isnan(gain): 

1833 gain = 1.0 

1834 self.log.warning("Gain set to NAN! Updating to 1.0 to generate Poisson variance.") 

1835 elif gain <= 0: 

1836 patchedGain = 1.0 

1837 self.log.warning("Gain for amp %s == %g <= 0; setting to %f.", 

1838 amp.getName(), gain, patchedGain) 

1839 gain = patchedGain 

1840 

1841 if self.config.doEmpiricalReadNoise and overscanImage is None: 

1842 badPixels = isrFunctions.countMaskedPixels(ampExposure.getMaskedImage(), 

1843 [self.config.saturatedMaskName, 

1844 self.config.suspectMaskName, 

1845 "BAD", "NO_DATA"]) 

1846 allPixels = ampExposure.getWidth() * ampExposure.getHeight() 

1847 if allPixels == badPixels: 

1848 # If the image is bad, do not raise. 

1849 self.log.info("Skipping empirical read noise for amp %s. No good pixels.", 

1850 amp.getName()) 

1851 else: 

1852 raise RuntimeError("Overscan is none for EmpiricalReadNoise.") 

1853 

1854 if self.config.doEmpiricalReadNoise and overscanImage is not None: 

1855 stats = afwMath.StatisticsControl() 

1856 stats.setAndMask(overscanImage.mask.getPlaneBitMask(maskPlanes)) 

1857 readNoise = afwMath.makeStatistics(overscanImage.getImage(), 

1858 afwMath.STDEVCLIP, stats).getValue() 

1859 self.log.info("Calculated empirical read noise for amp %s: %f.", 

1860 amp.getName(), readNoise) 

1861 elif self.config.usePtcReadNoise: 

1862 if ptcDataset is None: 

1863 raise RuntimeError("No ptcDataset provided to use PTC readnoise.") 

1864 else: 

1865 readNoise = ptcDataset.noise[amp.getName()] 

1866 self.log.info("Using read noise from Photon Transfer Curve.") 

1867 else: 

1868 readNoise = amp.getReadNoise() 

1869 

1870 isrFunctions.updateVariance( 

1871 maskedImage=ampExposure.getMaskedImage(), 

1872 gain=gain, 

1873 readNoise=readNoise, 

1874 ) 

1875 

1876 def maskNegativeVariance(self, exposure): 

1877 """Identify and mask pixels with negative variance values. 

1878 

1879 Parameters 

1880 ---------- 

1881 exposure : `lsst.afw.image.Exposure` 

1882 Exposure to process. 

1883 

1884 See Also 

1885 -------- 

1886 lsst.ip.isr.isrFunctions.updateVariance 

1887 """ 

1888 maskPlane = exposure.getMask().getPlaneBitMask(self.config.negativeVarianceMaskName) 

1889 bad = numpy.where(exposure.getVariance().getArray() <= 0.0) 

1890 exposure.mask.array[bad] |= maskPlane 

1891 

1892 def darkCorrection(self, exposure, darkExposure, invert=False): 

1893 """Apply dark correction in place. 

1894 

1895 Parameters 

1896 ---------- 

1897 exposure : `lsst.afw.image.Exposure` 

1898 Exposure to process. 

1899 darkExposure : `lsst.afw.image.Exposure` 

1900 Dark exposure of the same size as ``exposure``. 

1901 invert : `Bool`, optional 

1902 If True, re-add the dark to an already corrected image. 

1903 

1904 Raises 

1905 ------ 

1906 RuntimeError 

1907 Raised if either ``exposure`` or ``darkExposure`` do not 

1908 have their dark time defined. 

1909 

1910 See Also 

1911 -------- 

1912 lsst.ip.isr.isrFunctions.darkCorrection 

1913 """ 

1914 expScale = exposure.getInfo().getVisitInfo().getDarkTime() 

1915 if math.isnan(expScale): 

1916 raise RuntimeError("Exposure darktime is NAN.") 

1917 if darkExposure.getInfo().getVisitInfo() is not None \ 

1918 and not math.isnan(darkExposure.getInfo().getVisitInfo().getDarkTime()): 

1919 darkScale = darkExposure.getInfo().getVisitInfo().getDarkTime() 

1920 else: 

1921 # DM-17444: darkExposure.getInfo.getVisitInfo() is None 

1922 # so getDarkTime() does not exist. 

1923 self.log.warning("darkExposure.getInfo().getVisitInfo() does not exist. Using darkScale = 1.0.") 

1924 darkScale = 1.0 

1925 

1926 isrFunctions.darkCorrection( 

1927 maskedImage=exposure.getMaskedImage(), 

1928 darkMaskedImage=darkExposure.getMaskedImage(), 

1929 expScale=expScale, 

1930 darkScale=darkScale, 

1931 invert=invert, 

1932 trimToFit=self.config.doTrimToMatchCalib 

1933 ) 

1934 

1935 def doLinearize(self, detector): 

1936 """Check if linearization is needed for the detector cameraGeom. 

1937 

1938 Checks config.doLinearize and the linearity type of the first 

1939 amplifier. 

1940 

1941 Parameters 

1942 ---------- 

1943 detector : `lsst.afw.cameraGeom.Detector` 

1944 Detector to get linearity type from. 

1945 

1946 Returns 

1947 ------- 

1948 doLinearize : `Bool` 

1949 If True, linearization should be performed. 

1950 """ 

1951 return self.config.doLinearize and \ 

1952 detector.getAmplifiers()[0].getLinearityType() != NullLinearityType 

1953 

1954 def flatCorrection(self, exposure, flatExposure, invert=False): 

1955 """Apply flat correction in place. 

1956 

1957 Parameters 

1958 ---------- 

1959 exposure : `lsst.afw.image.Exposure` 

1960 Exposure to process. 

1961 flatExposure : `lsst.afw.image.Exposure` 

1962 Flat exposure of the same size as ``exposure``. 

1963 invert : `Bool`, optional 

1964 If True, unflatten an already flattened image. 

1965 

1966 See Also 

1967 -------- 

1968 lsst.ip.isr.isrFunctions.flatCorrection 

1969 """ 

1970 isrFunctions.flatCorrection( 

1971 maskedImage=exposure.getMaskedImage(), 

1972 flatMaskedImage=flatExposure.getMaskedImage(), 

1973 scalingType=self.config.flatScalingType, 

1974 userScale=self.config.flatUserScale, 

1975 invert=invert, 

1976 trimToFit=self.config.doTrimToMatchCalib 

1977 ) 

1978 

1979 def saturationDetection(self, exposure, amp): 

1980 """Detect and mask saturated pixels in config.saturatedMaskName. 

1981 

1982 Parameters 

1983 ---------- 

1984 exposure : `lsst.afw.image.Exposure` 

1985 Exposure to process. Only the amplifier DataSec is processed. 

1986 amp : `lsst.afw.cameraGeom.Amplifier` 

1987 Amplifier detector data. 

1988 

1989 See Also 

1990 -------- 

1991 lsst.ip.isr.isrFunctions.makeThresholdMask 

1992 """ 

1993 if not math.isnan(amp.getSaturation()): 

1994 maskedImage = exposure.getMaskedImage() 

1995 dataView = maskedImage.Factory(maskedImage, amp.getRawBBox()) 

1996 isrFunctions.makeThresholdMask( 

1997 maskedImage=dataView, 

1998 threshold=amp.getSaturation(), 

1999 growFootprints=0, 

2000 maskName=self.config.saturatedMaskName, 

2001 ) 

2002 

2003 def saturationInterpolation(self, exposure): 

2004 """Interpolate over saturated pixels, in place. 

2005 

2006 This method should be called after `saturationDetection`, to 

2007 ensure that the saturated pixels have been identified in the 

2008 SAT mask. It should also be called after `assembleCcd`, since 

2009 saturated regions may cross amplifier boundaries. 

2010 

2011 Parameters 

2012 ---------- 

2013 exposure : `lsst.afw.image.Exposure` 

2014 Exposure to process. 

2015 

2016 See Also 

2017 -------- 

2018 lsst.ip.isr.isrTask.saturationDetection 

2019 lsst.ip.isr.isrFunctions.interpolateFromMask 

2020 """ 

2021 isrFunctions.interpolateFromMask( 

2022 maskedImage=exposure.getMaskedImage(), 

2023 fwhm=self.config.fwhm, 

2024 growSaturatedFootprints=self.config.growSaturationFootprintSize, 

2025 maskNameList=list(self.config.saturatedMaskName), 

2026 ) 

2027 

2028 def suspectDetection(self, exposure, amp): 

2029 """Detect and mask suspect pixels in config.suspectMaskName. 

2030 

2031 Parameters 

2032 ---------- 

2033 exposure : `lsst.afw.image.Exposure` 

2034 Exposure to process. Only the amplifier DataSec is processed. 

2035 amp : `lsst.afw.cameraGeom.Amplifier` 

2036 Amplifier detector data. 

2037 

2038 See Also 

2039 -------- 

2040 lsst.ip.isr.isrFunctions.makeThresholdMask 

2041 

2042 Notes 

2043 ----- 

2044 Suspect pixels are pixels whose value is greater than 

2045 amp.getSuspectLevel(). This is intended to indicate pixels that may be 

2046 affected by unknown systematics; for example if non-linearity 

2047 corrections above a certain level are unstable then that would be a 

2048 useful value for suspectLevel. A value of `nan` indicates that no such 

2049 level exists and no pixels are to be masked as suspicious. 

2050 """ 

2051 suspectLevel = amp.getSuspectLevel() 

2052 if math.isnan(suspectLevel): 

2053 return 

2054 

2055 maskedImage = exposure.getMaskedImage() 

2056 dataView = maskedImage.Factory(maskedImage, amp.getRawBBox()) 

2057 isrFunctions.makeThresholdMask( 

2058 maskedImage=dataView, 

2059 threshold=suspectLevel, 

2060 growFootprints=0, 

2061 maskName=self.config.suspectMaskName, 

2062 ) 

2063 

2064 def maskDefect(self, exposure, defectBaseList): 

2065 """Mask defects using mask plane "BAD", in place. 

2066 

2067 Parameters 

2068 ---------- 

2069 exposure : `lsst.afw.image.Exposure` 

2070 Exposure to process. 

2071 defectBaseList : defect-type 

2072 List of defects to mask. Can be of type `lsst.ip.isr.Defects` 

2073 or `list` of `lsst.afw.image.DefectBase`. 

2074 

2075 Notes 

2076 ----- 

2077 Call this after CCD assembly, since defects may cross amplifier 

2078 boundaries. 

2079 """ 

2080 maskedImage = exposure.getMaskedImage() 

2081 if not isinstance(defectBaseList, Defects): 

2082 # Promotes DefectBase to Defect 

2083 defectList = Defects(defectBaseList) 

2084 else: 

2085 defectList = defectBaseList 

2086 defectList.maskPixels(maskedImage, maskName="BAD") 

2087 

2088 def maskEdges(self, exposure, numEdgePixels=0, maskPlane="SUSPECT", level='DETECTOR'): 

2089 """Mask edge pixels with applicable mask plane. 

2090 

2091 Parameters 

2092 ---------- 

2093 exposure : `lsst.afw.image.Exposure` 

2094 Exposure to process. 

2095 numEdgePixels : `int`, optional 

2096 Number of edge pixels to mask. 

2097 maskPlane : `str`, optional 

2098 Mask plane name to use. 

2099 level : `str`, optional 

2100 Level at which to mask edges. 

2101 """ 

2102 maskedImage = exposure.getMaskedImage() 

2103 maskBitMask = maskedImage.getMask().getPlaneBitMask(maskPlane) 

2104 

2105 if numEdgePixels > 0: 

2106 if level == 'DETECTOR': 

2107 boxes = [maskedImage.getBBox()] 

2108 elif level == 'AMP': 

2109 boxes = [amp.getBBox() for amp in exposure.getDetector()] 

2110 

2111 for box in boxes: 

2112 # This makes a bbox numEdgeSuspect pixels smaller than the 

2113 # image on each side 

2114 subImage = maskedImage[box] 

2115 box.grow(-numEdgePixels) 

2116 # Mask pixels outside box 

2117 SourceDetectionTask.setEdgeBits( 

2118 subImage, 

2119 box, 

2120 maskBitMask) 

2121 

2122 def maskAndInterpolateDefects(self, exposure, defectBaseList): 

2123 """Mask and interpolate defects using mask plane "BAD", in place. 

2124 

2125 Parameters 

2126 ---------- 

2127 exposure : `lsst.afw.image.Exposure` 

2128 Exposure to process. 

2129 defectBaseList : defects-like 

2130 List of defects to mask and interpolate. Can be 

2131 `lsst.ip.isr.Defects` or `list` of `lsst.afw.image.DefectBase`. 

2132 

2133 See Also 

2134 -------- 

2135 lsst.ip.isr.isrTask.maskDefect 

2136 """ 

2137 self.maskDefect(exposure, defectBaseList) 

2138 self.maskEdges(exposure, numEdgePixels=self.config.numEdgeSuspect, 

2139 maskPlane="SUSPECT", level=self.config.edgeMaskLevel) 

2140 isrFunctions.interpolateFromMask( 

2141 maskedImage=exposure.getMaskedImage(), 

2142 fwhm=self.config.fwhm, 

2143 growSaturatedFootprints=0, 

2144 maskNameList=["BAD"], 

2145 ) 

2146 

2147 def maskNan(self, exposure): 

2148 """Mask NaNs using mask plane "UNMASKEDNAN", in place. 

2149 

2150 Parameters 

2151 ---------- 

2152 exposure : `lsst.afw.image.Exposure` 

2153 Exposure to process. 

2154 

2155 Notes 

2156 ----- 

2157 We mask over all non-finite values (NaN, inf), including those 

2158 that are masked with other bits (because those may or may not be 

2159 interpolated over later, and we want to remove all NaN/infs). 

2160 Despite this behaviour, the "UNMASKEDNAN" mask plane is used to 

2161 preserve the historical name. 

2162 """ 

2163 maskedImage = exposure.getMaskedImage() 

2164 

2165 # Find and mask NaNs 

2166 maskedImage.getMask().addMaskPlane("UNMASKEDNAN") 

2167 maskVal = maskedImage.getMask().getPlaneBitMask("UNMASKEDNAN") 

2168 numNans = maskNans(maskedImage, maskVal) 

2169 self.metadata["NUMNANS"] = numNans 

2170 if numNans > 0: 

2171 self.log.warning("There were %d unmasked NaNs.", numNans) 

2172 

2173 def maskAndInterpolateNan(self, exposure): 

2174 """"Mask and interpolate NaN/infs using mask plane "UNMASKEDNAN", 

2175 in place. 

2176 

2177 Parameters 

2178 ---------- 

2179 exposure : `lsst.afw.image.Exposure` 

2180 Exposure to process. 

2181 

2182 See Also 

2183 -------- 

2184 lsst.ip.isr.isrTask.maskNan 

2185 """ 

2186 self.maskNan(exposure) 

2187 isrFunctions.interpolateFromMask( 

2188 maskedImage=exposure.getMaskedImage(), 

2189 fwhm=self.config.fwhm, 

2190 growSaturatedFootprints=0, 

2191 maskNameList=["UNMASKEDNAN"], 

2192 ) 

2193 

2194 def measureBackground(self, exposure, IsrQaConfig=None): 

2195 """Measure the image background in subgrids, for quality control. 

2196 

2197 Parameters 

2198 ---------- 

2199 exposure : `lsst.afw.image.Exposure` 

2200 Exposure to process. 

2201 IsrQaConfig : `lsst.ip.isr.isrQa.IsrQaConfig` 

2202 Configuration object containing parameters on which background 

2203 statistics and subgrids to use. 

2204 """ 

2205 if IsrQaConfig is not None: 

2206 statsControl = afwMath.StatisticsControl(IsrQaConfig.flatness.clipSigma, 

2207 IsrQaConfig.flatness.nIter) 

2208 maskVal = exposure.getMaskedImage().getMask().getPlaneBitMask(["BAD", "SAT", "DETECTED"]) 

2209 statsControl.setAndMask(maskVal) 

2210 maskedImage = exposure.getMaskedImage() 

2211 stats = afwMath.makeStatistics(maskedImage, afwMath.MEDIAN | afwMath.STDEVCLIP, statsControl) 

2212 skyLevel = stats.getValue(afwMath.MEDIAN) 

2213 skySigma = stats.getValue(afwMath.STDEVCLIP) 

2214 self.log.info("Flattened sky level: %f +/- %f.", skyLevel, skySigma) 

2215 metadata = exposure.getMetadata() 

2216 metadata["SKYLEVEL"] = skyLevel 

2217 metadata["SKYSIGMA"] = skySigma 

2218 

2219 # calcluating flatlevel over the subgrids 

2220 stat = afwMath.MEANCLIP if IsrQaConfig.flatness.doClip else afwMath.MEAN 

2221 meshXHalf = int(IsrQaConfig.flatness.meshX/2.) 

2222 meshYHalf = int(IsrQaConfig.flatness.meshY/2.) 

2223 nX = int((exposure.getWidth() + meshXHalf) / IsrQaConfig.flatness.meshX) 

2224 nY = int((exposure.getHeight() + meshYHalf) / IsrQaConfig.flatness.meshY) 

2225 skyLevels = numpy.zeros((nX, nY)) 

2226 

2227 for j in range(nY): 

2228 yc = meshYHalf + j * IsrQaConfig.flatness.meshY 

2229 for i in range(nX): 

2230 xc = meshXHalf + i * IsrQaConfig.flatness.meshX 

2231 

2232 xLLC = xc - meshXHalf 

2233 yLLC = yc - meshYHalf 

2234 xURC = xc + meshXHalf - 1 

2235 yURC = yc + meshYHalf - 1 

2236 

2237 bbox = lsst.geom.Box2I(lsst.geom.Point2I(xLLC, yLLC), lsst.geom.Point2I(xURC, yURC)) 

2238 miMesh = maskedImage.Factory(exposure.getMaskedImage(), bbox, afwImage.LOCAL) 

2239 

2240 skyLevels[i, j] = afwMath.makeStatistics(miMesh, stat, statsControl).getValue() 

2241 

2242 good = numpy.where(numpy.isfinite(skyLevels)) 

2243 skyMedian = numpy.median(skyLevels[good]) 

2244 flatness = (skyLevels[good] - skyMedian) / skyMedian 

2245 flatness_rms = numpy.std(flatness) 

2246 flatness_pp = flatness.max() - flatness.min() if len(flatness) > 0 else numpy.nan 

2247 

2248 self.log.info("Measuring sky levels in %dx%d grids: %f.", nX, nY, skyMedian) 

2249 self.log.info("Sky flatness in %dx%d grids - pp: %f rms: %f.", 

2250 nX, nY, flatness_pp, flatness_rms) 

2251 

2252 metadata["FLATNESS_PP"] = float(flatness_pp) 

2253 metadata["FLATNESS_RMS"] = float(flatness_rms) 

2254 metadata["FLATNESS_NGRIDS"] = '%dx%d' % (nX, nY) 

2255 metadata["FLATNESS_MESHX"] = IsrQaConfig.flatness.meshX 

2256 metadata["FLATNESS_MESHY"] = IsrQaConfig.flatness.meshY 

2257 

2258 def roughZeroPoint(self, exposure): 

2259 """Set an approximate magnitude zero point for the exposure. 

2260 

2261 Parameters 

2262 ---------- 

2263 exposure : `lsst.afw.image.Exposure` 

2264 Exposure to process. 

2265 """ 

2266 filterLabel = exposure.getFilter() 

2267 physicalFilter = isrFunctions.getPhysicalFilter(filterLabel, self.log) 

2268 

2269 if physicalFilter in self.config.fluxMag0T1: 

2270 fluxMag0 = self.config.fluxMag0T1[physicalFilter] 

2271 else: 

2272 self.log.warning("No rough magnitude zero point defined for filter %s.", physicalFilter) 

2273 fluxMag0 = self.config.defaultFluxMag0T1 

2274 

2275 expTime = exposure.getInfo().getVisitInfo().getExposureTime() 

2276 if not expTime > 0: # handle NaN as well as <= 0 

2277 self.log.warning("Non-positive exposure time; skipping rough zero point.") 

2278 return 

2279 

2280 self.log.info("Setting rough magnitude zero point for filter %s: %f", 

2281 physicalFilter, 2.5*math.log10(fluxMag0*expTime)) 

2282 exposure.setPhotoCalib(afwImage.makePhotoCalibFromCalibZeroPoint(fluxMag0*expTime, 0.0)) 

2283 

2284 @contextmanager 

2285 def flatContext(self, exp, flat, dark=None): 

2286 """Context manager that applies and removes flats and darks, 

2287 if the task is configured to apply them. 

2288 

2289 Parameters 

2290 ---------- 

2291 exp : `lsst.afw.image.Exposure` 

2292 Exposure to process. 

2293 flat : `lsst.afw.image.Exposure` 

2294 Flat exposure the same size as ``exp``. 

2295 dark : `lsst.afw.image.Exposure`, optional 

2296 Dark exposure the same size as ``exp``. 

2297 

2298 Yields 

2299 ------ 

2300 exp : `lsst.afw.image.Exposure` 

2301 The flat and dark corrected exposure. 

2302 """ 

2303 if self.config.doDark and dark is not None: 

2304 self.darkCorrection(exp, dark) 

2305 if self.config.doFlat: 

2306 self.flatCorrection(exp, flat) 

2307 try: 

2308 yield exp 

2309 finally: 

2310 if self.config.doFlat: 

2311 self.flatCorrection(exp, flat, invert=True) 

2312 if self.config.doDark and dark is not None: 

2313 self.darkCorrection(exp, dark, invert=True) 

2314 

2315 def debugView(self, exposure, stepname): 

2316 """Utility function to examine ISR exposure at different stages. 

2317 

2318 Parameters 

2319 ---------- 

2320 exposure : `lsst.afw.image.Exposure` 

2321 Exposure to view. 

2322 stepname : `str` 

2323 State of processing to view. 

2324 """ 

2325 frame = getDebugFrame(self._display, stepname) 

2326 if frame: 

2327 display = getDisplay(frame) 

2328 display.scale('asinh', 'zscale') 

2329 display.mtv(exposure) 

2330 prompt = "Press Enter to continue [c]... " 

2331 while True: 

2332 ans = input(prompt).lower() 

2333 if ans in ("", "c",): 

2334 break 

2335 

2336 

2337class FakeAmp(object): 

2338 """A Detector-like object that supports returning gain and saturation level 

2339 

2340 This is used when the input exposure does not have a detector. 

2341 

2342 Parameters 

2343 ---------- 

2344 exposure : `lsst.afw.image.Exposure` 

2345 Exposure to generate a fake amplifier for. 

2346 config : `lsst.ip.isr.isrTaskConfig` 

2347 Configuration to apply to the fake amplifier. 

2348 """ 

2349 

2350 def __init__(self, exposure, config): 

2351 self._bbox = exposure.getBBox(afwImage.LOCAL) 

2352 self._RawHorizontalOverscanBBox = lsst.geom.Box2I() 

2353 self._gain = config.gain 

2354 self._readNoise = config.readNoise 

2355 self._saturation = config.saturation 

2356 

2357 def getBBox(self): 

2358 return self._bbox 

2359 

2360 def getRawBBox(self): 

2361 return self._bbox 

2362 

2363 def getRawHorizontalOverscanBBox(self): 

2364 return self._RawHorizontalOverscanBBox 

2365 

2366 def getGain(self): 

2367 return self._gain 

2368 

2369 def getReadNoise(self): 

2370 return self._readNoise 

2371 

2372 def getSaturation(self): 

2373 return self._saturation 

2374 

2375 def getSuspectLevel(self): 

2376 return float("NaN")