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

910 statements  

« prev     ^ index     » next       coverage.py v6.4.1, created at 2022-07-09 06:37 -0700

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 

22import math 

23import numpy 

24 

25import lsst.geom 

26import lsst.afw.image as afwImage 

27import lsst.afw.math as afwMath 

28import lsst.pex.config as pexConfig 

29import lsst.pipe.base as pipeBase 

30import lsst.pipe.base.connectionTypes as cT 

31 

32from contextlib import contextmanager 

33from lsstDebug import getDebugFrame 

34 

35from lsst.afw.cameraGeom import NullLinearityType, ReadoutCorner 

36from lsst.afw.display import getDisplay 

37from lsst.daf.persistence import ButlerDataRef 

38from lsst.daf.persistence.butler import NoResults 

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 lsst.daf.butler import DimensionGraph 

57 

58 

59__all__ = ["IsrTask", "IsrTaskConfig", "RunIsrTask", "RunIsrConfig"] 

60 

61 

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

63 """Lookup function to identify crosstalkSource entries. 

64 

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

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

67 populated. 

68 

69 Parameters 

70 ---------- 

71 datasetType : `str` 

72 Dataset to lookup. 

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

74 Butler registry to query. 

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

76 Data id to transform to identify crosstalkSources. The 

77 ``detector`` entry will be stripped. 

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

79 Collections to search through. 

80 

81 Returns 

82 ------- 

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

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

85 crosstalkSources. 

86 """ 

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

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

89 findFirst=True)) 

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

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

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

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

94 # cached in the registry. 

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

96 

97 

98class IsrTaskConnections(pipeBase.PipelineTaskConnections, 

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

100 defaultTemplates={}): 

101 ccdExposure = cT.Input( 

102 name="raw", 

103 doc="Input exposure to process.", 

104 storageClass="Exposure", 

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

106 ) 

107 camera = cT.PrerequisiteInput( 

108 name="camera", 

109 storageClass="Camera", 

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

111 dimensions=["instrument"], 

112 isCalibration=True, 

113 ) 

114 

115 crosstalk = cT.PrerequisiteInput( 

116 name="crosstalk", 

117 doc="Input crosstalk object", 

118 storageClass="CrosstalkCalib", 

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

120 isCalibration=True, 

121 minimum=0, # can fall back to cameraGeom 

122 ) 

123 crosstalkSources = cT.PrerequisiteInput( 

124 name="isrOverscanCorrected", 

125 doc="Overscan corrected input images.", 

126 storageClass="Exposure", 

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

128 deferLoad=True, 

129 multiple=True, 

130 lookupFunction=crosstalkSourceLookup, 

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

132 ) 

133 bias = cT.PrerequisiteInput( 

134 name="bias", 

135 doc="Input bias calibration.", 

136 storageClass="ExposureF", 

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

138 isCalibration=True, 

139 ) 

140 dark = cT.PrerequisiteInput( 

141 name='dark', 

142 doc="Input dark calibration.", 

143 storageClass="ExposureF", 

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

145 isCalibration=True, 

146 ) 

147 flat = cT.PrerequisiteInput( 

148 name="flat", 

149 doc="Input flat calibration.", 

150 storageClass="ExposureF", 

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

152 isCalibration=True, 

153 ) 

154 ptc = cT.PrerequisiteInput( 

155 name="ptc", 

156 doc="Input Photon Transfer Curve dataset", 

157 storageClass="PhotonTransferCurveDataset", 

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

159 isCalibration=True, 

160 ) 

161 fringes = cT.PrerequisiteInput( 

162 name="fringe", 

163 doc="Input fringe calibration.", 

164 storageClass="ExposureF", 

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

166 isCalibration=True, 

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

168 ) 

169 strayLightData = cT.PrerequisiteInput( 

170 name='yBackground', 

171 doc="Input stray light calibration.", 

172 storageClass="StrayLightData", 

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

174 deferLoad=True, 

175 isCalibration=True, 

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

177 ) 

178 bfKernel = cT.PrerequisiteInput( 

179 name='bfKernel', 

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

181 storageClass="NumpyArray", 

182 dimensions=["instrument"], 

183 isCalibration=True, 

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

185 ) 

186 newBFKernel = cT.PrerequisiteInput( 

187 name='brighterFatterKernel', 

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

189 storageClass="BrighterFatterKernel", 

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

191 isCalibration=True, 

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

193 ) 

194 defects = cT.PrerequisiteInput( 

195 name='defects', 

196 doc="Input defect tables.", 

197 storageClass="Defects", 

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

199 isCalibration=True, 

200 ) 

201 linearizer = cT.PrerequisiteInput( 

202 name='linearizer', 

203 storageClass="Linearizer", 

204 doc="Linearity correction calibration.", 

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

206 isCalibration=True, 

207 minimum=0, # can fall back to cameraGeom 

208 ) 

209 opticsTransmission = cT.PrerequisiteInput( 

210 name="transmission_optics", 

211 storageClass="TransmissionCurve", 

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

213 dimensions=["instrument"], 

214 isCalibration=True, 

215 ) 

216 filterTransmission = cT.PrerequisiteInput( 

217 name="transmission_filter", 

218 storageClass="TransmissionCurve", 

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

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

221 isCalibration=True, 

222 ) 

223 sensorTransmission = cT.PrerequisiteInput( 

224 name="transmission_sensor", 

225 storageClass="TransmissionCurve", 

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

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

228 isCalibration=True, 

229 ) 

230 atmosphereTransmission = cT.PrerequisiteInput( 

231 name="transmission_atmosphere", 

232 storageClass="TransmissionCurve", 

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

234 dimensions=["instrument"], 

235 isCalibration=True, 

236 ) 

237 illumMaskedImage = cT.PrerequisiteInput( 

238 name="illum", 

239 doc="Input illumination correction.", 

240 storageClass="MaskedImageF", 

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

242 isCalibration=True, 

243 ) 

244 

245 outputExposure = cT.Output( 

246 name='postISRCCD', 

247 doc="Output ISR processed exposure.", 

248 storageClass="Exposure", 

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

250 ) 

251 preInterpExposure = cT.Output( 

252 name='preInterpISRCCD', 

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

254 storageClass="ExposureF", 

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

256 ) 

257 outputOssThumbnail = cT.Output( 

258 name="OssThumb", 

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

260 storageClass="Thumbnail", 

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

262 ) 

263 outputFlattenedThumbnail = cT.Output( 

264 name="FlattenedThumb", 

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

266 storageClass="Thumbnail", 

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

268 ) 

269 

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

271 super().__init__(config=config) 

272 

273 if config.doBias is not True: 

274 self.prerequisiteInputs.remove("bias") 

275 if config.doLinearize is not True: 

276 self.prerequisiteInputs.remove("linearizer") 

277 if config.doCrosstalk is not True: 

278 self.prerequisiteInputs.remove("crosstalkSources") 

279 self.prerequisiteInputs.remove("crosstalk") 

280 if config.doBrighterFatter is not True: 

281 self.prerequisiteInputs.remove("bfKernel") 

282 self.prerequisiteInputs.remove("newBFKernel") 

283 if config.doDefect is not True: 

284 self.prerequisiteInputs.remove("defects") 

285 if config.doDark is not True: 

286 self.prerequisiteInputs.remove("dark") 

287 if config.doFlat is not True: 

288 self.prerequisiteInputs.remove("flat") 

289 if config.doFringe is not True: 

290 self.prerequisiteInputs.remove("fringes") 

291 if config.doStrayLight is not True: 

292 self.prerequisiteInputs.remove("strayLightData") 

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

294 self.prerequisiteInputs.remove("ptc") 

295 if config.doAttachTransmissionCurve is not True: 

296 self.prerequisiteInputs.remove("opticsTransmission") 

297 self.prerequisiteInputs.remove("filterTransmission") 

298 self.prerequisiteInputs.remove("sensorTransmission") 

299 self.prerequisiteInputs.remove("atmosphereTransmission") 

300 else: 

301 if config.doUseOpticsTransmission is not True: 

302 self.prerequisiteInputs.remove("opticsTransmission") 

303 if config.doUseFilterTransmission is not True: 

304 self.prerequisiteInputs.remove("filterTransmission") 

305 if config.doUseSensorTransmission is not True: 

306 self.prerequisiteInputs.remove("sensorTransmission") 

307 if config.doUseAtmosphereTransmission is not True: 

308 self.prerequisiteInputs.remove("atmosphereTransmission") 

309 if config.doIlluminationCorrection is not True: 

310 self.prerequisiteInputs.remove("illumMaskedImage") 

311 

312 if config.doWrite is not True: 

313 self.outputs.remove("outputExposure") 

314 self.outputs.remove("preInterpExposure") 

315 self.outputs.remove("outputFlattenedThumbnail") 

316 self.outputs.remove("outputOssThumbnail") 

317 if config.doSaveInterpPixels is not True: 

318 self.outputs.remove("preInterpExposure") 

319 if config.qa.doThumbnailOss is not True: 

320 self.outputs.remove("outputOssThumbnail") 

321 if config.qa.doThumbnailFlattened is not True: 

322 self.outputs.remove("outputFlattenedThumbnail") 

323 

324 

325class IsrTaskConfig(pipeBase.PipelineTaskConfig, 

326 pipelineConnections=IsrTaskConnections): 

327 """Configuration parameters for IsrTask. 

328 

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

330 """ 

331 datasetType = pexConfig.Field( 

332 dtype=str, 

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

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

335 default="raw", 

336 ) 

337 

338 fallbackFilterName = pexConfig.Field( 

339 dtype=str, 

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

341 optional=True 

342 ) 

343 useFallbackDate = pexConfig.Field( 

344 dtype=bool, 

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

346 default=False, 

347 ) 

348 expectWcs = pexConfig.Field( 

349 dtype=bool, 

350 default=True, 

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

352 ) 

353 fwhm = pexConfig.Field( 

354 dtype=float, 

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

356 default=1.0, 

357 ) 

358 qa = pexConfig.ConfigField( 

359 dtype=isrQa.IsrQaConfig, 

360 doc="QA related configuration options.", 

361 ) 

362 

363 # Image conversion configuration 

364 doConvertIntToFloat = pexConfig.Field( 

365 dtype=bool, 

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

367 default=True, 

368 ) 

369 

370 # Saturated pixel handling. 

371 doSaturation = pexConfig.Field( 

372 dtype=bool, 

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

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

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

376 default=True, 

377 ) 

378 saturatedMaskName = pexConfig.Field( 

379 dtype=str, 

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

381 default="SAT", 

382 ) 

383 saturation = pexConfig.Field( 

384 dtype=float, 

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

386 default=float("NaN"), 

387 ) 

388 growSaturationFootprintSize = pexConfig.Field( 

389 dtype=int, 

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

391 default=1, 

392 ) 

393 

394 # Suspect pixel handling. 

395 doSuspect = pexConfig.Field( 

396 dtype=bool, 

397 doc="Mask suspect pixels?", 

398 default=False, 

399 ) 

400 suspectMaskName = pexConfig.Field( 

401 dtype=str, 

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

403 default="SUSPECT", 

404 ) 

405 numEdgeSuspect = pexConfig.Field( 

406 dtype=int, 

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

408 default=0, 

409 ) 

410 edgeMaskLevel = pexConfig.ChoiceField( 

411 dtype=str, 

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

413 default="DETECTOR", 

414 allowed={ 

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

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

417 }, 

418 ) 

419 

420 # Initial masking options. 

421 doSetBadRegions = pexConfig.Field( 

422 dtype=bool, 

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

424 default=True, 

425 ) 

426 badStatistic = pexConfig.ChoiceField( 

427 dtype=str, 

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

429 default='MEANCLIP', 

430 allowed={ 

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

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

433 }, 

434 ) 

435 

436 # Overscan subtraction configuration. 

437 doOverscan = pexConfig.Field( 

438 dtype=bool, 

439 doc="Do overscan subtraction?", 

440 default=True, 

441 ) 

442 overscan = pexConfig.ConfigurableField( 

443 target=OverscanCorrectionTask, 

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

445 ) 

446 overscanFitType = pexConfig.ChoiceField( 

447 dtype=str, 

448 doc="The method for fitting the overscan bias level.", 

449 default='MEDIAN', 

450 allowed={ 

451 "POLY": "Fit ordinary polynomial to the longest axis of the overscan region", 

452 "CHEB": "Fit Chebyshev polynomial to the longest axis of the overscan region", 

453 "LEG": "Fit Legendre polynomial to the longest axis of the overscan region", 

454 "NATURAL_SPLINE": "Fit natural spline to the longest axis of the overscan region", 

455 "CUBIC_SPLINE": "Fit cubic spline to the longest axis of the overscan region", 

456 "AKIMA_SPLINE": "Fit Akima spline to the longest axis of the overscan region", 

457 "MEAN": "Correct using the mean of the overscan region", 

458 "MEANCLIP": "Correct using a clipped mean of the overscan region", 

459 "MEDIAN": "Correct using the median of the overscan region", 

460 "MEDIAN_PER_ROW": "Correct using the median per row of the overscan region", 

461 }, 

462 deprecated=("Please configure overscan via the OverscanCorrectionConfig interface." 

463 " This option will no longer be used, and will be removed after v20.") 

464 ) 

465 overscanOrder = pexConfig.Field( 

466 dtype=int, 

467 doc=("Order of polynomial or to fit if overscan fit type is a polynomial, " 

468 "or number of spline knots if overscan fit type is a spline."), 

469 default=1, 

470 deprecated=("Please configure overscan via the OverscanCorrectionConfig interface." 

471 " This option will no longer be used, and will be removed after v20.") 

472 ) 

473 overscanNumSigmaClip = pexConfig.Field( 

474 dtype=float, 

475 doc="Rejection threshold (sigma) for collapsing overscan before fit", 

476 default=3.0, 

477 deprecated=("Please configure overscan via the OverscanCorrectionConfig interface." 

478 " This option will no longer be used, and will be removed after v20.") 

479 ) 

480 overscanIsInt = pexConfig.Field( 

481 dtype=bool, 

482 doc="Treat overscan as an integer image for purposes of overscan.FitType=MEDIAN" 

483 " and overscan.FitType=MEDIAN_PER_ROW.", 

484 default=True, 

485 deprecated=("Please configure overscan via the OverscanCorrectionConfig interface." 

486 " This option will no longer be used, and will be removed after v20.") 

487 ) 

488 # These options do not get deprecated, as they define how we slice up the 

489 # image data. 

490 overscanNumLeadingColumnsToSkip = pexConfig.Field( 

491 dtype=int, 

492 doc="Number of columns to skip in overscan, i.e. those closest to amplifier", 

493 default=0, 

494 ) 

495 overscanNumTrailingColumnsToSkip = pexConfig.Field( 

496 dtype=int, 

497 doc="Number of columns to skip in overscan, i.e. those farthest from amplifier", 

498 default=0, 

499 ) 

500 overscanMaxDev = pexConfig.Field( 500 ↛ exitline 500 didn't jump to the function exit

501 dtype=float, 

502 doc="Maximum deviation from the median for overscan", 

503 default=1000.0, check=lambda x: x > 0 

504 ) 

505 overscanBiasJump = pexConfig.Field( 

506 dtype=bool, 

507 doc="Fit the overscan in a piecewise-fashion to correct for bias jumps?", 

508 default=False, 

509 ) 

510 overscanBiasJumpKeyword = pexConfig.Field( 

511 dtype=str, 

512 doc="Header keyword containing information about devices.", 

513 default="NO_SUCH_KEY", 

514 ) 

515 overscanBiasJumpDevices = pexConfig.ListField( 

516 dtype=str, 

517 doc="List of devices that need piecewise overscan correction.", 

518 default=(), 

519 ) 

520 overscanBiasJumpLocation = pexConfig.Field( 

521 dtype=int, 

522 doc="Location of bias jump along y-axis.", 

523 default=0, 

524 ) 

525 

526 # Amplifier to CCD assembly configuration 

527 doAssembleCcd = pexConfig.Field( 

528 dtype=bool, 

529 default=True, 

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

531 ) 

532 assembleCcd = pexConfig.ConfigurableField( 

533 target=AssembleCcdTask, 

534 doc="CCD assembly task", 

535 ) 

536 

537 # General calibration configuration. 

538 doAssembleIsrExposures = pexConfig.Field( 

539 dtype=bool, 

540 default=False, 

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

542 ) 

543 doTrimToMatchCalib = pexConfig.Field( 

544 dtype=bool, 

545 default=False, 

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

547 ) 

548 

549 # Bias subtraction. 

550 doBias = pexConfig.Field( 

551 dtype=bool, 

552 doc="Apply bias frame correction?", 

553 default=True, 

554 ) 

555 biasDataProductName = pexConfig.Field( 

556 dtype=str, 

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

558 default="bias", 

559 ) 

560 doBiasBeforeOverscan = pexConfig.Field( 

561 dtype=bool, 

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

563 default=False 

564 ) 

565 

566 # Variance construction 

567 doVariance = pexConfig.Field( 

568 dtype=bool, 

569 doc="Calculate variance?", 

570 default=True 

571 ) 

572 gain = pexConfig.Field( 

573 dtype=float, 

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

575 default=float("NaN"), 

576 ) 

577 readNoise = pexConfig.Field( 

578 dtype=float, 

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

580 default=0.0, 

581 ) 

582 doEmpiricalReadNoise = pexConfig.Field( 

583 dtype=bool, 

584 default=False, 

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

586 ) 

587 usePtcReadNoise = pexConfig.Field( 

588 dtype=bool, 

589 default=False, 

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

591 ) 

592 maskNegativeVariance = pexConfig.Field( 

593 dtype=bool, 

594 default=True, 

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

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

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

598 ) 

599 negativeVarianceMaskName = pexConfig.Field( 

600 dtype=str, 

601 default="BAD", 

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

603 ) 

604 # Linearization. 

605 doLinearize = pexConfig.Field( 

606 dtype=bool, 

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

608 default=True, 

609 ) 

610 

611 # Crosstalk. 

612 doCrosstalk = pexConfig.Field( 

613 dtype=bool, 

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

615 default=False, 

616 ) 

617 doCrosstalkBeforeAssemble = pexConfig.Field( 

618 dtype=bool, 

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

620 default=False, 

621 ) 

622 crosstalk = pexConfig.ConfigurableField( 

623 target=CrosstalkTask, 

624 doc="Intra-CCD crosstalk correction", 

625 ) 

626 

627 # Masking options. 

628 doDefect = pexConfig.Field( 

629 dtype=bool, 

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

631 default=True, 

632 ) 

633 doNanMasking = pexConfig.Field( 

634 dtype=bool, 

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

636 default=True, 

637 ) 

638 doWidenSaturationTrails = pexConfig.Field( 

639 dtype=bool, 

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

641 default=True 

642 ) 

643 

644 # Brighter-Fatter correction. 

645 doBrighterFatter = pexConfig.Field( 

646 dtype=bool, 

647 default=False, 

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

649 ) 

650 brighterFatterLevel = pexConfig.ChoiceField( 

651 dtype=str, 

652 default="DETECTOR", 

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

654 allowed={ 

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

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

657 } 

658 ) 

659 brighterFatterMaxIter = pexConfig.Field( 

660 dtype=int, 

661 default=10, 

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

663 ) 

664 brighterFatterThreshold = pexConfig.Field( 

665 dtype=float, 

666 default=1000, 

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

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

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

670 ) 

671 brighterFatterApplyGain = pexConfig.Field( 

672 dtype=bool, 

673 default=True, 

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

675 ) 

676 brighterFatterMaskListToInterpolate = pexConfig.ListField( 

677 dtype=str, 

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

679 "correction.", 

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

681 ) 

682 brighterFatterMaskGrowSize = pexConfig.Field( 

683 dtype=int, 

684 default=0, 

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

686 "when brighter-fatter correction is applied." 

687 ) 

688 

689 # Dark subtraction. 

690 doDark = pexConfig.Field( 

691 dtype=bool, 

692 doc="Apply dark frame correction?", 

693 default=True, 

694 ) 

695 darkDataProductName = pexConfig.Field( 

696 dtype=str, 

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

698 default="dark", 

699 ) 

700 

701 # Camera-specific stray light removal. 

702 doStrayLight = pexConfig.Field( 

703 dtype=bool, 

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

705 default=False, 

706 ) 

707 strayLight = pexConfig.ConfigurableField( 

708 target=StrayLightTask, 

709 doc="y-band stray light correction" 

710 ) 

711 

712 # Flat correction. 

713 doFlat = pexConfig.Field( 

714 dtype=bool, 

715 doc="Apply flat field correction?", 

716 default=True, 

717 ) 

718 flatDataProductName = pexConfig.Field( 

719 dtype=str, 

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

721 default="flat", 

722 ) 

723 flatScalingType = pexConfig.ChoiceField( 

724 dtype=str, 

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

726 default='USER', 

727 allowed={ 

728 "USER": "Scale by flatUserScale", 

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

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

731 }, 

732 ) 

733 flatUserScale = pexConfig.Field( 

734 dtype=float, 

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

736 default=1.0, 

737 ) 

738 doTweakFlat = pexConfig.Field( 

739 dtype=bool, 

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

741 default=False 

742 ) 

743 

744 # Amplifier normalization based on gains instead of using flats 

745 # configuration. 

746 doApplyGains = pexConfig.Field( 

747 dtype=bool, 

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

749 default=False, 

750 ) 

751 usePtcGains = pexConfig.Field( 

752 dtype=bool, 

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

754 default=False, 

755 ) 

756 normalizeGains = pexConfig.Field( 

757 dtype=bool, 

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

759 default=False, 

760 ) 

761 

762 # Fringe correction. 

763 doFringe = pexConfig.Field( 

764 dtype=bool, 

765 doc="Apply fringe correction?", 

766 default=True, 

767 ) 

768 fringe = pexConfig.ConfigurableField( 

769 target=FringeTask, 

770 doc="Fringe subtraction task", 

771 ) 

772 fringeAfterFlat = pexConfig.Field( 

773 dtype=bool, 

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

775 default=True, 

776 ) 

777 

778 # Amp offset correction. 

779 doAmpOffset = pexConfig.Field( 

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

781 dtype=bool, 

782 default=False, 

783 ) 

784 ampOffset = pexConfig.ConfigurableField( 

785 doc="Amp offset correction task.", 

786 target=AmpOffsetTask, 

787 ) 

788 

789 # Initial CCD-level background statistics options. 

790 doMeasureBackground = pexConfig.Field( 

791 dtype=bool, 

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

793 default=False, 

794 ) 

795 

796 # Camera-specific masking configuration. 

797 doCameraSpecificMasking = pexConfig.Field( 

798 dtype=bool, 

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

800 default=False, 

801 ) 

802 masking = pexConfig.ConfigurableField( 

803 target=MaskingTask, 

804 doc="Masking task." 

805 ) 

806 

807 # Interpolation options. 

808 doInterpolate = pexConfig.Field( 

809 dtype=bool, 

810 doc="Interpolate masked pixels?", 

811 default=True, 

812 ) 

813 doSaturationInterpolation = pexConfig.Field( 

814 dtype=bool, 

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

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

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

818 default=True, 

819 ) 

820 doNanInterpolation = pexConfig.Field( 

821 dtype=bool, 

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

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

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

825 default=True, 

826 ) 

827 doNanInterpAfterFlat = pexConfig.Field( 

828 dtype=bool, 

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

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

831 default=False, 

832 ) 

833 maskListToInterpolate = pexConfig.ListField( 

834 dtype=str, 

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

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

837 ) 

838 doSaveInterpPixels = pexConfig.Field( 

839 dtype=bool, 

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

841 default=False, 

842 ) 

843 

844 # Default photometric calibration options. 

845 fluxMag0T1 = pexConfig.DictField( 

846 keytype=str, 

847 itemtype=float, 

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

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

850 )) 

851 ) 

852 defaultFluxMag0T1 = pexConfig.Field( 

853 dtype=float, 

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

855 default=pow(10.0, 0.4*28.0) 

856 ) 

857 

858 # Vignette correction configuration. 

859 doVignette = pexConfig.Field( 

860 dtype=bool, 

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

862 "according to vignetting parameters?"), 

863 default=False, 

864 ) 

865 doMaskVignettePolygon = pexConfig.Field( 

866 dtype=bool, 

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

868 "is False"), 

869 default=True, 

870 ) 

871 vignetteValue = pexConfig.Field( 

872 dtype=float, 

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

874 optional=True, 

875 default=None, 

876 ) 

877 vignette = pexConfig.ConfigurableField( 

878 target=VignetteTask, 

879 doc="Vignetting task.", 

880 ) 

881 

882 # Transmission curve configuration. 

883 doAttachTransmissionCurve = pexConfig.Field( 

884 dtype=bool, 

885 default=False, 

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

887 ) 

888 doUseOpticsTransmission = pexConfig.Field( 

889 dtype=bool, 

890 default=True, 

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

892 ) 

893 doUseFilterTransmission = pexConfig.Field( 

894 dtype=bool, 

895 default=True, 

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

897 ) 

898 doUseSensorTransmission = pexConfig.Field( 

899 dtype=bool, 

900 default=True, 

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

902 ) 

903 doUseAtmosphereTransmission = pexConfig.Field( 

904 dtype=bool, 

905 default=True, 

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

907 ) 

908 

909 # Illumination correction. 

910 doIlluminationCorrection = pexConfig.Field( 

911 dtype=bool, 

912 default=False, 

913 doc="Perform illumination correction?" 

914 ) 

915 illuminationCorrectionDataProductName = pexConfig.Field( 

916 dtype=str, 

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

918 default="illumcor", 

919 ) 

920 illumScale = pexConfig.Field( 

921 dtype=float, 

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

923 default=1.0, 

924 ) 

925 illumFilters = pexConfig.ListField( 

926 dtype=str, 

927 default=[], 

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

929 ) 

930 

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

932 # be needed. 

933 doWrite = pexConfig.Field( 

934 dtype=bool, 

935 doc="Persist postISRCCD?", 

936 default=True, 

937 ) 

938 

939 def validate(self): 

940 super().validate() 

941 if self.doFlat and self.doApplyGains: 

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

943 if self.doBiasBeforeOverscan and self.doTrimToMatchCalib: 

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

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

946 self.maskListToInterpolate.append(self.saturatedMaskName) 

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

948 self.maskListToInterpolate.remove(self.saturatedMaskName) 

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

950 self.maskListToInterpolate.append("UNMASKEDNAN") 

951 

952 

953class IsrTask(pipeBase.PipelineTask, pipeBase.CmdLineTask): 

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

955 

956 The process for correcting imaging data is very similar from 

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

958 doing these corrections, including the ability to turn certain 

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

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

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

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

963 pixels. The method `runDataRef()` identifies and defines the 

964 calibration data products, and is intended for use by a 

965 `lsst.pipe.base.cmdLineTask.CmdLineTask` and takes as input only a 

966 `daf.persistence.butlerSubset.ButlerDataRef`. This task may be 

967 subclassed for different camera, although the most camera specific 

968 methods have been split into subtasks that can be redirected 

969 appropriately. 

970 

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

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

973 

974 Parameters 

975 ---------- 

976 args : `list` 

977 Positional arguments passed to the Task constructor. 

978 None used at this time. 

979 kwargs : `dict`, optional 

980 Keyword arguments passed on to the Task constructor. 

981 None used at this time. 

982 """ 

983 ConfigClass = IsrTaskConfig 

984 _DefaultName = "isr" 

985 

986 def __init__(self, **kwargs): 

987 super().__init__(**kwargs) 

988 self.makeSubtask("assembleCcd") 

989 self.makeSubtask("crosstalk") 

990 self.makeSubtask("strayLight") 

991 self.makeSubtask("fringe") 

992 self.makeSubtask("masking") 

993 self.makeSubtask("overscan") 

994 self.makeSubtask("vignette") 

995 self.makeSubtask("ampOffset") 

996 

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

998 inputs = butlerQC.get(inputRefs) 

999 

1000 try: 

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

1002 except Exception as e: 

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

1004 (inputRefs, e)) 

1005 

1006 inputs['isGen3'] = True 

1007 

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

1009 

1010 if self.config.doCrosstalk is True: 

1011 # Crosstalk sources need to be defined by the pipeline 

1012 # yaml if they exist. 

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

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

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

1016 else: 

1017 coeffVector = (self.config.crosstalk.crosstalkValues 

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

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

1020 inputs['crosstalk'] = crosstalkCalib 

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

1022 if 'crosstalkSources' not in inputs: 

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

1024 

1025 if self.doLinearize(detector) is True: 

1026 if 'linearizer' in inputs: 

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

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

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

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

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

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

1033 detector=detector, 

1034 log=self.log) 

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

1036 else: 

1037 linearizer = inputs['linearizer'] 

1038 linearizer.log = self.log 

1039 inputs['linearizer'] = linearizer 

1040 else: 

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

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

1043 

1044 if self.config.doDefect is True: 

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

1046 # defects is loaded as a BaseCatalog with columns 

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

1048 # defined by their bounding box 

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

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

1051 

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

1053 # the information as a numpy array. 

1054 if self.config.doBrighterFatter: 

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

1056 if brighterFatterKernel is None: 

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

1058 

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

1060 # This is a ISR calib kernel 

1061 detName = detector.getName() 

1062 level = brighterFatterKernel.level 

1063 

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

1065 inputs['bfGains'] = brighterFatterKernel.gain 

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

1067 if level == 'DETECTOR': 

1068 if detName in brighterFatterKernel.detKernels: 

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

1070 else: 

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

1072 elif level == 'AMP': 

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

1074 "fatter kernels.") 

1075 brighterFatterKernel.makeDetectorKernelFromAmpwiseKernels(detName) 

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

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

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

1079 

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

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

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

1083 expId=expId, 

1084 assembler=self.assembleCcd 

1085 if self.config.doAssembleIsrExposures else None) 

1086 else: 

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

1088 

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

1090 if 'strayLightData' not in inputs: 

1091 inputs['strayLightData'] = None 

1092 

1093 outputs = self.run(**inputs) 

1094 butlerQC.put(outputs, outputRefs) 

1095 

1096 def readIsrData(self, dataRef, rawExposure): 

1097 """Retrieve necessary frames for instrument signature removal. 

1098 

1099 Pre-fetching all required ISR data products limits the IO 

1100 required by the ISR. Any conflict between the calibration data 

1101 available and that needed for ISR is also detected prior to 

1102 doing processing, allowing it to fail quickly. 

1103 

1104 Parameters 

1105 ---------- 

1106 dataRef : `daf.persistence.butlerSubset.ButlerDataRef` 

1107 Butler reference of the detector data to be processed 

1108 rawExposure : `afw.image.Exposure` 

1109 The raw exposure that will later be corrected with the 

1110 retrieved calibration data; should not be modified in this 

1111 method. 

1112 

1113 Returns 

1114 ------- 

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

1116 Result struct with components (which may be `None`): 

1117 - ``bias``: bias calibration frame (`afw.image.Exposure`) 

1118 - ``linearizer``: functor for linearization 

1119 (`ip.isr.linearize.LinearizeBase`) 

1120 - ``crosstalkSources``: list of possible crosstalk sources (`list`) 

1121 - ``dark``: dark calibration frame (`afw.image.Exposure`) 

1122 - ``flat``: flat calibration frame (`afw.image.Exposure`) 

1123 - ``bfKernel``: Brighter-Fatter kernel (`numpy.ndarray`) 

1124 - ``defects``: list of defects (`lsst.ip.isr.Defects`) 

1125 - ``fringes``: `lsst.pipe.base.Struct` with components: 

1126 - ``fringes``: fringe calibration frame (`afw.image.Exposure`) 

1127 - ``seed``: random seed derived from the ccdExposureId for random 

1128 number generator (`uint32`). 

1129 - ``opticsTransmission``: `lsst.afw.image.TransmissionCurve` 

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

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

1132 - ``filterTransmission`` : `lsst.afw.image.TransmissionCurve` 

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

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

1135 - ``sensorTransmission`` : `lsst.afw.image.TransmissionCurve` 

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

1137 sensor itself, to be evaluated in post-assembly trimmed 

1138 detector coordinates. 

1139 - ``atmosphereTransmission`` : `lsst.afw.image.TransmissionCurve` 

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

1141 atmosphere, assumed to be spatially constant. 

1142 - ``strayLightData`` : `object` 

1143 An opaque object containing calibration information for 

1144 stray-light correction. If `None`, no correction will be 

1145 performed. 

1146 - ``illumMaskedImage`` : illumination correction image 

1147 (`lsst.afw.image.MaskedImage`) 

1148 

1149 Raises 

1150 ------ 

1151 NotImplementedError : 

1152 Raised if a per-amplifier brighter-fatter kernel is requested by 

1153 the configuration. 

1154 """ 

1155 try: 

1156 dateObs = rawExposure.getInfo().getVisitInfo().getDate() 

1157 dateObs = dateObs.toPython().isoformat() 

1158 except RuntimeError: 

1159 self.log.warning("Unable to identify dateObs for rawExposure.") 

1160 dateObs = None 

1161 

1162 ccd = rawExposure.getDetector() 

1163 filterLabel = rawExposure.getFilter() 

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

1165 rawExposure.mask.addMaskPlane("UNMASKEDNAN") # needed to match pre DM-15862 processing. 

1166 biasExposure = (self.getIsrExposure(dataRef, self.config.biasDataProductName) 

1167 if self.config.doBias else None) 

1168 # immediate=True required for functors and linearizers are functors 

1169 # see ticket DM-6515 

1170 linearizer = (dataRef.get("linearizer", immediate=True) 

1171 if self.doLinearize(ccd) else None) 

1172 if linearizer is not None and not isinstance(linearizer, numpy.ndarray): 

1173 linearizer.log = self.log 

1174 if isinstance(linearizer, numpy.ndarray): 

1175 linearizer = linearize.Linearizer(table=linearizer, detector=ccd) 

1176 

1177 crosstalkCalib = None 

1178 if self.config.doCrosstalk: 

1179 try: 

1180 crosstalkCalib = dataRef.get("crosstalk", immediate=True) 

1181 except NoResults: 

1182 coeffVector = (self.config.crosstalk.crosstalkValues 

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

1184 crosstalkCalib = CrosstalkCalib().fromDetector(ccd, coeffVector=coeffVector) 

1185 crosstalkSources = (self.crosstalk.prepCrosstalk(dataRef, crosstalkCalib) 

1186 if self.config.doCrosstalk else None) 

1187 

1188 darkExposure = (self.getIsrExposure(dataRef, self.config.darkDataProductName) 

1189 if self.config.doDark else None) 

1190 flatExposure = (self.getIsrExposure(dataRef, self.config.flatDataProductName, 

1191 dateObs=dateObs) 

1192 if self.config.doFlat else None) 

1193 

1194 brighterFatterKernel = None 

1195 brighterFatterGains = None 

1196 if self.config.doBrighterFatter is True: 

1197 try: 

1198 # Use the new-style cp_pipe version of the kernel if it exists 

1199 # If using a new-style kernel, always use the self-consistent 

1200 # gains, i.e. the ones inside the kernel object itself 

1201 brighterFatterKernel = dataRef.get("brighterFatterKernel") 

1202 brighterFatterGains = brighterFatterKernel.gain 

1203 self.log.info("New style brighter-fatter kernel (brighterFatterKernel) loaded") 

1204 except NoResults: 

1205 try: # Fall back to the old-style numpy-ndarray style kernel if necessary. 

1206 brighterFatterKernel = dataRef.get("bfKernel") 

1207 self.log.info("Old style brighter-fatter kernel (bfKernel) loaded") 

1208 except NoResults: 

1209 brighterFatterKernel = None 

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

1211 # If the kernel is not an ndarray, it's the cp_pipe version 

1212 # so extract the kernel for this detector, or raise an error 

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

1214 if brighterFatterKernel.detKernels: 

1215 brighterFatterKernel = brighterFatterKernel.detKernels[ccd.getName()] 

1216 else: 

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

1218 else: 

1219 # TODO DM-15631 for implementing this 

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

1221 

1222 defectList = (dataRef.get("defects") 

1223 if self.config.doDefect else None) 

1224 expId = rawExposure.info.id 

1225 fringeStruct = (self.fringe.readFringes(dataRef, expId=expId, assembler=self.assembleCcd 

1226 if self.config.doAssembleIsrExposures else None) 

1227 if self.config.doFringe and self.fringe.checkFilter(rawExposure) 

1228 else pipeBase.Struct(fringes=None)) 

1229 

1230 if self.config.doAttachTransmissionCurve: 

1231 opticsTransmission = (dataRef.get("transmission_optics") 

1232 if self.config.doUseOpticsTransmission else None) 

1233 filterTransmission = (dataRef.get("transmission_filter") 

1234 if self.config.doUseFilterTransmission else None) 

1235 sensorTransmission = (dataRef.get("transmission_sensor") 

1236 if self.config.doUseSensorTransmission else None) 

1237 atmosphereTransmission = (dataRef.get("transmission_atmosphere") 

1238 if self.config.doUseAtmosphereTransmission else None) 

1239 else: 

1240 opticsTransmission = None 

1241 filterTransmission = None 

1242 sensorTransmission = None 

1243 atmosphereTransmission = None 

1244 

1245 if self.config.doStrayLight: 

1246 strayLightData = self.strayLight.readIsrData(dataRef, rawExposure) 

1247 else: 

1248 strayLightData = None 

1249 

1250 illumMaskedImage = (self.getIsrExposure(dataRef, 

1251 self.config.illuminationCorrectionDataProductName).getMaskedImage() 

1252 if (self.config.doIlluminationCorrection 

1253 and physicalFilter in self.config.illumFilters) 

1254 else None) 

1255 

1256 # Struct should include only kwargs to run() 

1257 return pipeBase.Struct(bias=biasExposure, 

1258 linearizer=linearizer, 

1259 crosstalk=crosstalkCalib, 

1260 crosstalkSources=crosstalkSources, 

1261 dark=darkExposure, 

1262 flat=flatExposure, 

1263 bfKernel=brighterFatterKernel, 

1264 bfGains=brighterFatterGains, 

1265 defects=defectList, 

1266 fringes=fringeStruct, 

1267 opticsTransmission=opticsTransmission, 

1268 filterTransmission=filterTransmission, 

1269 sensorTransmission=sensorTransmission, 

1270 atmosphereTransmission=atmosphereTransmission, 

1271 strayLightData=strayLightData, 

1272 illumMaskedImage=illumMaskedImage 

1273 ) 

1274 

1275 @timeMethod 

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

1277 crosstalk=None, crosstalkSources=None, 

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

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

1280 sensorTransmission=None, atmosphereTransmission=None, 

1281 detectorNum=None, strayLightData=None, illumMaskedImage=None, 

1282 isGen3=False, 

1283 ): 

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

1285 

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

1287 - saturation and suspect pixel masking 

1288 - overscan subtraction 

1289 - CCD assembly of individual amplifiers 

1290 - bias subtraction 

1291 - variance image construction 

1292 - linearization of non-linear response 

1293 - crosstalk masking 

1294 - brighter-fatter correction 

1295 - dark subtraction 

1296 - fringe correction 

1297 - stray light subtraction 

1298 - flat correction 

1299 - masking of known defects and camera specific features 

1300 - vignette calculation 

1301 - appending transmission curve and distortion model 

1302 

1303 Parameters 

1304 ---------- 

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

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

1307 exposure is modified by this method. 

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

1309 The camera geometry for this exposure. Required if 

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

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

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

1313 Bias calibration frame. 

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

1315 Functor for linearization. 

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

1317 Calibration for crosstalk. 

1318 crosstalkSources : `list`, optional 

1319 List of possible crosstalk sources. 

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

1321 Dark calibration frame. 

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

1323 Flat calibration frame. 

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

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

1326 and read noise. 

1327 bfKernel : `numpy.ndarray`, optional 

1328 Brighter-fatter kernel. 

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

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

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

1332 the detector in question. 

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

1334 List of defects. 

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

1336 Struct containing the fringe correction data, with 

1337 elements: 

1338 - ``fringes``: fringe calibration frame (`afw.image.Exposure`) 

1339 - ``seed``: random seed derived from the ccdExposureId for random 

1340 number generator (`uint32`) 

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

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

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

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

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

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

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

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

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

1350 coordinates. 

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

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

1353 atmosphere, assumed to be spatially constant. 

1354 detectorNum : `int`, optional 

1355 The integer number for the detector to process. 

1356 isGen3 : bool, optional 

1357 Flag this call to run() as using the Gen3 butler environment. 

1358 strayLightData : `object`, optional 

1359 Opaque object containing calibration information for stray-light 

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

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

1362 Illumination correction image. 

1363 

1364 Returns 

1365 ------- 

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

1367 Result struct with component: 

1368 - ``exposure`` : `afw.image.Exposure` 

1369 The fully ISR corrected exposure. 

1370 - ``outputExposure`` : `afw.image.Exposure` 

1371 An alias for `exposure` 

1372 - ``ossThumb`` : `numpy.ndarray` 

1373 Thumbnail image of the exposure after overscan subtraction. 

1374 - ``flattenedThumb`` : `numpy.ndarray` 

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

1376 

1377 Raises 

1378 ------ 

1379 RuntimeError 

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

1381 required calibration data has not been specified. 

1382 

1383 Notes 

1384 ----- 

1385 The current processed exposure can be viewed by setting the 

1386 appropriate lsstDebug entries in the `debug.display` 

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

1388 the IsrTaskConfig Boolean options, with the value denoting the 

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

1390 option check and after the processing of that step has 

1391 finished. The steps with debug points are: 

1392 

1393 doAssembleCcd 

1394 doBias 

1395 doCrosstalk 

1396 doBrighterFatter 

1397 doDark 

1398 doFringe 

1399 doStrayLight 

1400 doFlat 

1401 

1402 In addition, setting the "postISRCCD" entry displays the 

1403 exposure after all ISR processing has finished. 

1404 

1405 """ 

1406 

1407 if isGen3 is True: 

1408 # Gen3 currently cannot automatically do configuration overrides. 

1409 # DM-15257 looks to discuss this issue. 

1410 # Configure input exposures; 

1411 

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

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

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

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

1416 else: 

1417 if isinstance(ccdExposure, ButlerDataRef): 

1418 return self.runDataRef(ccdExposure) 

1419 

1420 ccd = ccdExposure.getDetector() 

1421 filterLabel = ccdExposure.getFilter() 

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

1423 

1424 if not ccd: 

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

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

1427 

1428 # Validate Input 

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

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

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

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

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

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

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

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

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

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

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

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

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

1442 and fringes.fringes is None): 

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

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

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

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

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

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

1449 and illumMaskedImage is None): 

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

1451 

1452 # Begin ISR processing. 

1453 if self.config.doConvertIntToFloat: 

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

1455 ccdExposure = self.convertIntToFloat(ccdExposure) 

1456 

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

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

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

1460 trimToFit=self.config.doTrimToMatchCalib) 

1461 self.debugView(ccdExposure, "doBias") 

1462 

1463 # Amplifier level processing. 

1464 overscans = [] 

1465 for amp in ccd: 

1466 # if ccdExposure is one amp, 

1467 # check for coverage to prevent performing ops multiple times 

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

1469 # Check for fully masked bad amplifiers, 

1470 # and generate masks for SUSPECT and SATURATED values. 

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

1472 

1473 if self.config.doOverscan and not badAmp: 

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

1475 overscanResults = self.overscanCorrection(ccdExposure, amp) 

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

1477 if overscanResults is not None and \ 

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

1479 if isinstance(overscanResults.overscanFit, float): 

1480 qaMedian = overscanResults.overscanFit 

1481 qaStdev = float("NaN") 

1482 else: 

1483 qaStats = afwMath.makeStatistics(overscanResults.overscanFit, 

1484 afwMath.MEDIAN | afwMath.STDEVCLIP) 

1485 qaMedian = qaStats.getValue(afwMath.MEDIAN) 

1486 qaStdev = qaStats.getValue(afwMath.STDEVCLIP) 

1487 

1488 self.metadata[f"FIT MEDIAN {amp.getName()}"] = qaMedian 

1489 self.metadata[f"FIT STDEV {amp.getName()}"] = qaStdev 

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

1491 amp.getName(), qaMedian, qaStdev) 

1492 

1493 # Residuals after overscan correction 

1494 qaStatsAfter = afwMath.makeStatistics(overscanResults.overscanImage, 

1495 afwMath.MEDIAN | afwMath.STDEVCLIP) 

1496 qaMedianAfter = qaStatsAfter.getValue(afwMath.MEDIAN) 

1497 qaStdevAfter = qaStatsAfter.getValue(afwMath.STDEVCLIP) 

1498 

1499 self.metadata[f"RESIDUAL MEDIAN {amp.getName()}"] = qaMedianAfter 

1500 self.metadata[f"RESIDUAL STDEV {amp.getName()}"] = qaStdevAfter 

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

1502 amp.getName(), qaMedianAfter, qaStdevAfter) 

1503 

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

1505 else: 

1506 if badAmp: 

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

1508 overscanResults = None 

1509 

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

1511 else: 

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

1513 

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

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

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

1517 crosstalkSources=crosstalkSources, camera=camera) 

1518 self.debugView(ccdExposure, "doCrosstalk") 

1519 

1520 if self.config.doAssembleCcd: 

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

1522 ccdExposure = self.assembleCcd.assembleCcd(ccdExposure) 

1523 

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

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

1526 self.debugView(ccdExposure, "doAssembleCcd") 

1527 

1528 ossThumb = None 

1529 if self.config.qa.doThumbnailOss: 

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

1531 

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

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

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

1535 trimToFit=self.config.doTrimToMatchCalib) 

1536 self.debugView(ccdExposure, "doBias") 

1537 

1538 if self.config.doVariance: 

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

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

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

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

1543 if overscanResults is not None: 

1544 self.updateVariance(ampExposure, amp, 

1545 overscanImage=overscanResults.overscanImage, 

1546 ptcDataset=ptc) 

1547 else: 

1548 self.updateVariance(ampExposure, amp, 

1549 overscanImage=None, 

1550 ptcDataset=ptc) 

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

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

1553 afwMath.MEDIAN | afwMath.STDEVCLIP) 

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

1555 qaStats.getValue(afwMath.MEDIAN) 

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

1557 qaStats.getValue(afwMath.STDEVCLIP) 

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

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

1560 qaStats.getValue(afwMath.STDEVCLIP)) 

1561 if self.config.maskNegativeVariance: 

1562 self.maskNegativeVariance(ccdExposure) 

1563 

1564 if self.doLinearize(ccd): 

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

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

1567 detector=ccd, log=self.log) 

1568 

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

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

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

1572 crosstalkSources=crosstalkSources, isTrimmed=True) 

1573 self.debugView(ccdExposure, "doCrosstalk") 

1574 

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

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

1577 # suspect pixels have already been masked. 

1578 if self.config.doDefect: 

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

1580 self.maskDefect(ccdExposure, defects) 

1581 

1582 if self.config.numEdgeSuspect > 0: 

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

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

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

1586 

1587 if self.config.doNanMasking: 

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

1589 self.maskNan(ccdExposure) 

1590 

1591 if self.config.doWidenSaturationTrails: 

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

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

1594 

1595 if self.config.doCameraSpecificMasking: 

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

1597 self.masking.run(ccdExposure) 

1598 

1599 if self.config.doBrighterFatter: 

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

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

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

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

1604 # and flats. 

1605 # 

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

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

1608 # back the interpolation. 

1609 interpExp = ccdExposure.clone() 

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

1611 isrFunctions.interpolateFromMask( 

1612 maskedImage=interpExp.getMaskedImage(), 

1613 fwhm=self.config.fwhm, 

1614 growSaturatedFootprints=self.config.growSaturationFootprintSize, 

1615 maskNameList=list(self.config.brighterFatterMaskListToInterpolate) 

1616 ) 

1617 bfExp = interpExp.clone() 

1618 

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

1620 type(bfKernel), type(bfGains)) 

1621 bfResults = isrFunctions.brighterFatterCorrection(bfExp, bfKernel, 

1622 self.config.brighterFatterMaxIter, 

1623 self.config.brighterFatterThreshold, 

1624 self.config.brighterFatterApplyGain, 

1625 bfGains) 

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

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

1628 bfResults[0]) 

1629 else: 

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

1631 bfResults[1]) 

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

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

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

1635 image += bfCorr 

1636 

1637 # Applying the brighter-fatter correction applies a 

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

1639 # convolution may not have sufficient valid pixels to 

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

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

1642 # fact. 

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

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

1645 maskPlane="EDGE") 

1646 

1647 if self.config.brighterFatterMaskGrowSize > 0: 

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

1649 for maskPlane in self.config.brighterFatterMaskListToInterpolate: 

1650 isrFunctions.growMasks(ccdExposure.getMask(), 

1651 radius=self.config.brighterFatterMaskGrowSize, 

1652 maskNameList=maskPlane, 

1653 maskValue=maskPlane) 

1654 

1655 self.debugView(ccdExposure, "doBrighterFatter") 

1656 

1657 if self.config.doDark: 

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

1659 self.darkCorrection(ccdExposure, dark) 

1660 self.debugView(ccdExposure, "doDark") 

1661 

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

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

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

1665 self.debugView(ccdExposure, "doFringe") 

1666 

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

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

1669 self.strayLight.run(ccdExposure, strayLightData) 

1670 self.debugView(ccdExposure, "doStrayLight") 

1671 

1672 if self.config.doFlat: 

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

1674 self.flatCorrection(ccdExposure, flat) 

1675 self.debugView(ccdExposure, "doFlat") 

1676 

1677 if self.config.doApplyGains: 

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

1679 if self.config.usePtcGains: 

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

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

1682 ptcGains=ptc.gain) 

1683 else: 

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

1685 

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

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

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

1689 

1690 if self.config.doVignette: 

1691 if self.config.doMaskVignettePolygon: 

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

1693 else: 

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

1695 self.vignettePolygon = self.vignette.run( 

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

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

1698 

1699 if self.config.doAttachTransmissionCurve: 

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

1701 isrFunctions.attachTransmissionCurve(ccdExposure, opticsTransmission=opticsTransmission, 

1702 filterTransmission=filterTransmission, 

1703 sensorTransmission=sensorTransmission, 

1704 atmosphereTransmission=atmosphereTransmission) 

1705 

1706 flattenedThumb = None 

1707 if self.config.qa.doThumbnailFlattened: 

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

1709 

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

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

1712 isrFunctions.illuminationCorrection(ccdExposure.getMaskedImage(), 

1713 illumMaskedImage, illumScale=self.config.illumScale, 

1714 trimToFit=self.config.doTrimToMatchCalib) 

1715 

1716 preInterpExp = None 

1717 if self.config.doSaveInterpPixels: 

1718 preInterpExp = ccdExposure.clone() 

1719 

1720 # Reset and interpolate bad pixels. 

1721 # 

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

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

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

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

1726 # reason to expect that interpolation would provide a more 

1727 # useful value. 

1728 # 

1729 # Smaller defects can be safely interpolated after the larger 

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

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

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

1733 if self.config.doSetBadRegions: 

1734 badPixelCount, badPixelValue = isrFunctions.setBadRegions(ccdExposure) 

1735 if badPixelCount > 0: 

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

1737 

1738 if self.config.doInterpolate: 

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

1740 isrFunctions.interpolateFromMask( 

1741 maskedImage=ccdExposure.getMaskedImage(), 

1742 fwhm=self.config.fwhm, 

1743 growSaturatedFootprints=self.config.growSaturationFootprintSize, 

1744 maskNameList=list(self.config.maskListToInterpolate) 

1745 ) 

1746 

1747 self.roughZeroPoint(ccdExposure) 

1748 

1749 # correct for amp offsets within the CCD 

1750 if self.config.doAmpOffset: 

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

1752 self.ampOffset.run(ccdExposure) 

1753 

1754 if self.config.doMeasureBackground: 

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

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

1757 

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

1759 for amp in ccd: 

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

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

1762 afwMath.MEDIAN | afwMath.STDEVCLIP) 

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

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

1765 qaStats.getValue(afwMath.STDEVCLIP) 

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

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

1768 qaStats.getValue(afwMath.STDEVCLIP)) 

1769 

1770 self.debugView(ccdExposure, "postISRCCD") 

1771 

1772 return pipeBase.Struct( 

1773 exposure=ccdExposure, 

1774 ossThumb=ossThumb, 

1775 flattenedThumb=flattenedThumb, 

1776 

1777 preInterpExposure=preInterpExp, 

1778 outputExposure=ccdExposure, 

1779 outputOssThumbnail=ossThumb, 

1780 outputFlattenedThumbnail=flattenedThumb, 

1781 ) 

1782 

1783 @timeMethod 

1784 def runDataRef(self, sensorRef): 

1785 """Perform instrument signature removal on a ButlerDataRef of a Sensor. 

1786 

1787 This method contains the `CmdLineTask` interface to the ISR 

1788 processing. All IO is handled here, freeing the `run()` method 

1789 to manage only pixel-level calculations. The steps performed 

1790 are: 

1791 - Read in necessary detrending/isr/calibration data. 

1792 - Process raw exposure in `run()`. 

1793 - Persist the ISR-corrected exposure as "postISRCCD" if 

1794 config.doWrite=True. 

1795 

1796 Parameters 

1797 ---------- 

1798 sensorRef : `daf.persistence.butlerSubset.ButlerDataRef` 

1799 DataRef of the detector data to be processed 

1800 

1801 Returns 

1802 ------- 

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

1804 Result struct with component: 

1805 - ``exposure`` : `afw.image.Exposure` 

1806 The fully ISR corrected exposure. 

1807 

1808 Raises 

1809 ------ 

1810 RuntimeError 

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

1812 required calibration data does not exist. 

1813 

1814 """ 

1815 self.log.info("Performing ISR on sensor %s.", sensorRef.dataId) 

1816 

1817 ccdExposure = sensorRef.get(self.config.datasetType) 

1818 

1819 camera = sensorRef.get("camera") 

1820 isrData = self.readIsrData(sensorRef, ccdExposure) 

1821 

1822 result = self.run(ccdExposure, camera=camera, **isrData.getDict()) 

1823 

1824 if self.config.doWrite: 

1825 sensorRef.put(result.exposure, "postISRCCD") 

1826 if result.preInterpExposure is not None: 

1827 sensorRef.put(result.preInterpExposure, "postISRCCD_uninterpolated") 

1828 if result.ossThumb is not None: 

1829 isrQa.writeThumbnail(sensorRef, result.ossThumb, "ossThumb") 

1830 if result.flattenedThumb is not None: 

1831 isrQa.writeThumbnail(sensorRef, result.flattenedThumb, "flattenedThumb") 

1832 

1833 return result 

1834 

1835 def getIsrExposure(self, dataRef, datasetType, dateObs=None, immediate=True): 

1836 """Retrieve a calibration dataset for removing instrument signature. 

1837 

1838 Parameters 

1839 ---------- 

1840 

1841 dataRef : `daf.persistence.butlerSubset.ButlerDataRef` 

1842 DataRef of the detector data to find calibration datasets 

1843 for. 

1844 datasetType : `str` 

1845 Type of dataset to retrieve (e.g. 'bias', 'flat', etc). 

1846 dateObs : `str`, optional 

1847 Date of the observation. Used to correct butler failures 

1848 when using fallback filters. 

1849 immediate : `Bool` 

1850 If True, disable butler proxies to enable error handling 

1851 within this routine. 

1852 

1853 Returns 

1854 ------- 

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

1856 Requested calibration frame. 

1857 

1858 Raises 

1859 ------ 

1860 RuntimeError 

1861 Raised if no matching calibration frame can be found. 

1862 """ 

1863 try: 

1864 exp = dataRef.get(datasetType, immediate=immediate) 

1865 except Exception as exc1: 

1866 if not self.config.fallbackFilterName: 

1867 raise RuntimeError("Unable to retrieve %s for %s: %s." % (datasetType, dataRef.dataId, exc1)) 

1868 try: 

1869 if self.config.useFallbackDate and dateObs: 

1870 exp = dataRef.get(datasetType, filter=self.config.fallbackFilterName, 

1871 dateObs=dateObs, immediate=immediate) 

1872 else: 

1873 exp = dataRef.get(datasetType, filter=self.config.fallbackFilterName, immediate=immediate) 

1874 except Exception as exc2: 

1875 raise RuntimeError("Unable to retrieve %s for %s, even with fallback filter %s: %s AND %s." % 

1876 (datasetType, dataRef.dataId, self.config.fallbackFilterName, exc1, exc2)) 

1877 self.log.warning("Using fallback calibration from filter %s.", self.config.fallbackFilterName) 

1878 

1879 if self.config.doAssembleIsrExposures: 

1880 exp = self.assembleCcd.assembleCcd(exp) 

1881 return exp 

1882 

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

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

1885 

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

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

1888 modifying the input in place. 

1889 

1890 Parameters 

1891 ---------- 

1892 inputExp : `lsst.afw.image.Exposure`, `lsst.afw.image.DecoratedImageU`, 

1893 or `lsst.afw.image.ImageF` 

1894 The input data structure obtained from Butler. 

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

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

1897 detector if detector is not already set. 

1898 detectorNum : `int`, optional 

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

1900 already set. 

1901 

1902 Returns 

1903 ------- 

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

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

1906 

1907 Raises 

1908 ------ 

1909 TypeError 

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

1911 """ 

1912 if isinstance(inputExp, afwImage.DecoratedImageU): 

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

1914 elif isinstance(inputExp, afwImage.ImageF): 

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

1916 elif isinstance(inputExp, afwImage.MaskedImageF): 

1917 inputExp = afwImage.makeExposure(inputExp) 

1918 elif isinstance(inputExp, afwImage.Exposure): 

1919 pass 

1920 elif inputExp is None: 

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

1922 return inputExp 

1923 else: 

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

1925 (type(inputExp), )) 

1926 

1927 if inputExp.getDetector() is None: 

1928 if camera is None or detectorNum is None: 

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

1930 'without a detector set.') 

1931 inputExp.setDetector(camera[detectorNum]) 

1932 

1933 return inputExp 

1934 

1935 def convertIntToFloat(self, exposure): 

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

1937 

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

1939 immediately returned. For exposures that are converted to use 

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

1941 mask to zero. 

1942 

1943 Parameters 

1944 ---------- 

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

1946 The raw exposure to be converted. 

1947 

1948 Returns 

1949 ------- 

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

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

1952 

1953 Raises 

1954 ------ 

1955 RuntimeError 

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

1957 

1958 """ 

1959 if isinstance(exposure, afwImage.ExposureF): 

1960 # Nothing to be done 

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

1962 return exposure 

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

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

1965 

1966 newexposure = exposure.convertF() 

1967 newexposure.variance[:] = 1 

1968 newexposure.mask[:] = 0x0 

1969 

1970 return newexposure 

1971 

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

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

1974 

1975 Parameters 

1976 ---------- 

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

1978 Input exposure to be masked. 

1979 amp : `lsst.afw.table.AmpInfoCatalog` 

1980 Catalog of parameters defining the amplifier on this 

1981 exposure to mask. 

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

1983 List of defects. Used to determine if the entire 

1984 amplifier is bad. 

1985 

1986 Returns 

1987 ------- 

1988 badAmp : `Bool` 

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

1990 defects and unusable. 

1991 

1992 """ 

1993 maskedImage = ccdExposure.getMaskedImage() 

1994 

1995 badAmp = False 

1996 

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

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

1999 # defects definition. 

2000 if defects is not None: 

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

2002 

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

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

2005 # current ccdExposure). 

2006 if badAmp: 

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

2008 afwImage.PARENT) 

2009 maskView = dataView.getMask() 

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

2011 del maskView 

2012 return badAmp 

2013 

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

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

2016 # masked now, though. 

2017 limits = dict() 

2018 if self.config.doSaturation and not badAmp: 

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

2020 if self.config.doSuspect and not badAmp: 

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

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

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

2024 

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

2026 if not math.isnan(maskThreshold): 

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

2028 isrFunctions.makeThresholdMask( 

2029 maskedImage=dataView, 

2030 threshold=maskThreshold, 

2031 growFootprints=0, 

2032 maskName=maskName 

2033 ) 

2034 

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

2036 # SAT pixels. 

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

2038 afwImage.PARENT) 

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

2040 self.config.suspectMaskName]) 

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

2042 badAmp = True 

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

2044 

2045 return badAmp 

2046 

2047 def overscanCorrection(self, ccdExposure, amp): 

2048 """Apply overscan correction in place. 

2049 

2050 This method does initial pixel rejection of the overscan 

2051 region. The overscan can also be optionally segmented to 

2052 allow for discontinuous overscan responses to be fit 

2053 separately. The actual overscan subtraction is performed by 

2054 the `lsst.ip.isr.isrFunctions.overscanCorrection` function, 

2055 which is called here after the amplifier is preprocessed. 

2056 

2057 Parameters 

2058 ---------- 

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

2060 Exposure to have overscan correction performed. 

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

2062 The amplifier to consider while correcting the overscan. 

2063 

2064 Returns 

2065 ------- 

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

2067 Result struct with components: 

2068 - ``imageFit`` : scalar or `lsst.afw.image.Image` 

2069 Value or fit subtracted from the amplifier image data. 

2070 - ``overscanFit`` : scalar or `lsst.afw.image.Image` 

2071 Value or fit subtracted from the overscan image data. 

2072 - ``overscanImage`` : `lsst.afw.image.Image` 

2073 Image of the overscan region with the overscan 

2074 correction applied. This quantity is used to estimate 

2075 the amplifier read noise empirically. 

2076 

2077 Raises 

2078 ------ 

2079 RuntimeError 

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

2081 

2082 See Also 

2083 -------- 

2084 lsst.ip.isr.isrFunctions.overscanCorrection 

2085 """ 

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

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

2088 return None 

2089 

2090 statControl = afwMath.StatisticsControl() 

2091 statControl.setAndMask(ccdExposure.mask.getPlaneBitMask("SAT")) 

2092 

2093 # Determine the bounding boxes 

2094 dataBBox = amp.getRawDataBBox() 

2095 oscanBBox = amp.getRawHorizontalOverscanBBox() 

2096 dx0 = 0 

2097 dx1 = 0 

2098 

2099 prescanBBox = amp.getRawPrescanBBox() 

2100 if (oscanBBox.getBeginX() > prescanBBox.getBeginX()): # amp is at the right 

2101 dx0 += self.config.overscanNumLeadingColumnsToSkip 

2102 dx1 -= self.config.overscanNumTrailingColumnsToSkip 

2103 else: 

2104 dx0 += self.config.overscanNumTrailingColumnsToSkip 

2105 dx1 -= self.config.overscanNumLeadingColumnsToSkip 

2106 

2107 # Determine if we need to work on subregions of the amplifier 

2108 # and overscan. 

2109 imageBBoxes = [] 

2110 overscanBBoxes = [] 

2111 

2112 if ((self.config.overscanBiasJump 

2113 and self.config.overscanBiasJumpLocation) 

2114 and (ccdExposure.getMetadata().exists(self.config.overscanBiasJumpKeyword) 

2115 and ccdExposure.getMetadata().getScalar(self.config.overscanBiasJumpKeyword) in 

2116 self.config.overscanBiasJumpDevices)): 

2117 if amp.getReadoutCorner() in (ReadoutCorner.LL, ReadoutCorner.LR): 

2118 yLower = self.config.overscanBiasJumpLocation 

2119 yUpper = dataBBox.getHeight() - yLower 

2120 else: 

2121 yUpper = self.config.overscanBiasJumpLocation 

2122 yLower = dataBBox.getHeight() - yUpper 

2123 

2124 imageBBoxes.append(lsst.geom.Box2I(dataBBox.getBegin(), 

2125 lsst.geom.Extent2I(dataBBox.getWidth(), yLower))) 

2126 overscanBBoxes.append(lsst.geom.Box2I(oscanBBox.getBegin() + lsst.geom.Extent2I(dx0, 0), 

2127 lsst.geom.Extent2I(oscanBBox.getWidth() - dx0 + dx1, 

2128 yLower))) 

2129 

2130 imageBBoxes.append(lsst.geom.Box2I(dataBBox.getBegin() + lsst.geom.Extent2I(0, yLower), 

2131 lsst.geom.Extent2I(dataBBox.getWidth(), yUpper))) 

2132 overscanBBoxes.append(lsst.geom.Box2I(oscanBBox.getBegin() + lsst.geom.Extent2I(dx0, yLower), 

2133 lsst.geom.Extent2I(oscanBBox.getWidth() - dx0 + dx1, 

2134 yUpper))) 

2135 else: 

2136 imageBBoxes.append(lsst.geom.Box2I(dataBBox.getBegin(), 

2137 lsst.geom.Extent2I(dataBBox.getWidth(), dataBBox.getHeight()))) 

2138 overscanBBoxes.append(lsst.geom.Box2I(oscanBBox.getBegin() + lsst.geom.Extent2I(dx0, 0), 

2139 lsst.geom.Extent2I(oscanBBox.getWidth() - dx0 + dx1, 

2140 oscanBBox.getHeight()))) 

2141 

2142 # Perform overscan correction on subregions, ensuring saturated 

2143 # pixels are masked. 

2144 for imageBBox, overscanBBox in zip(imageBBoxes, overscanBBoxes): 

2145 ampImage = ccdExposure.maskedImage[imageBBox] 

2146 overscanImage = ccdExposure.maskedImage[overscanBBox] 

2147 

2148 overscanArray = overscanImage.image.array 

2149 median = numpy.ma.median(numpy.ma.masked_where(overscanImage.mask.array, overscanArray)) 

2150 bad = numpy.where(numpy.abs(overscanArray - median) > self.config.overscanMaxDev) 

2151 overscanImage.mask.array[bad] = overscanImage.mask.getPlaneBitMask("SAT") 

2152 

2153 statControl = afwMath.StatisticsControl() 

2154 statControl.setAndMask(ccdExposure.mask.getPlaneBitMask("SAT")) 

2155 

2156 overscanResults = self.overscan.run(ampImage.getImage(), overscanImage, amp) 

2157 

2158 # Measure average overscan levels and record them in the metadata. 

2159 levelStat = afwMath.MEDIAN 

2160 sigmaStat = afwMath.STDEVCLIP 

2161 

2162 sctrl = afwMath.StatisticsControl(self.config.qa.flatness.clipSigma, 

2163 self.config.qa.flatness.nIter) 

2164 metadata = ccdExposure.getMetadata() 

2165 ampNum = amp.getName() 

2166 # if self.config.overscanFitType in ("MEDIAN", "MEAN", "MEANCLIP"): 

2167 if isinstance(overscanResults.overscanFit, float): 

2168 metadata[f"ISR_OSCAN_LEVEL{ampNum}"] = overscanResults.overscanFit 

2169 metadata[f"ISR_OSCAN_SIGMA{ampNum}"] = 0.0 

2170 else: 

2171 stats = afwMath.makeStatistics(overscanResults.overscanFit, levelStat | sigmaStat, sctrl) 

2172 metadata[f"ISR_OSCAN_LEVEL{ampNum}"] = stats.getValue(levelStat) 

2173 metadata[f"ISR_OSCAN_SIGMA%{ampNum}"] = stats.getValue(sigmaStat) 

2174 

2175 return overscanResults 

2176 

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

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

2179 

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

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

2182 the value from the amplifier data is used. 

2183 

2184 Parameters 

2185 ---------- 

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

2187 Exposure to process. 

2188 amp : `lsst.afw.table.AmpInfoRecord` or `FakeAmp` 

2189 Amplifier detector data. 

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

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

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

2193 PTC dataset containing the gains and read noise. 

2194 

2195 

2196 Raises 

2197 ------ 

2198 RuntimeError 

2199 Raised if either ``usePtcGains`` of ``usePtcReadNoise`` 

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

2201 

2202 Raised if ```doEmpiricalReadNoise`` is ``True`` but 

2203 ``overscanImage`` is ``None``. 

2204 

2205 See also 

2206 -------- 

2207 lsst.ip.isr.isrFunctions.updateVariance 

2208 """ 

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

2210 if self.config.usePtcGains: 

2211 if ptcDataset is None: 

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

2213 else: 

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

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

2216 else: 

2217 gain = amp.getGain() 

2218 

2219 if math.isnan(gain): 

2220 gain = 1.0 

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

2222 elif gain <= 0: 

2223 patchedGain = 1.0 

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

2225 amp.getName(), gain, patchedGain) 

2226 gain = patchedGain 

2227 

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

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

2230 

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

2232 stats = afwMath.StatisticsControl() 

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

2234 readNoise = afwMath.makeStatistics(overscanImage, afwMath.STDEVCLIP, stats).getValue() 

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

2236 amp.getName(), readNoise) 

2237 elif self.config.usePtcReadNoise: 

2238 if ptcDataset is None: 

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

2240 else: 

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

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

2243 else: 

2244 readNoise = amp.getReadNoise() 

2245 

2246 isrFunctions.updateVariance( 

2247 maskedImage=ampExposure.getMaskedImage(), 

2248 gain=gain, 

2249 readNoise=readNoise, 

2250 ) 

2251 

2252 def maskNegativeVariance(self, exposure): 

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

2254 

2255 Parameters 

2256 ---------- 

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

2258 Exposure to process. 

2259 

2260 See Also 

2261 -------- 

2262 lsst.ip.isr.isrFunctions.updateVariance 

2263 """ 

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

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

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

2267 

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

2269 """Apply dark correction in place. 

2270 

2271 Parameters 

2272 ---------- 

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

2274 Exposure to process. 

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

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

2277 invert : `Bool`, optional 

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

2279 

2280 Raises 

2281 ------ 

2282 RuntimeError 

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

2284 have their dark time defined. 

2285 

2286 See Also 

2287 -------- 

2288 lsst.ip.isr.isrFunctions.darkCorrection 

2289 """ 

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

2291 if math.isnan(expScale): 

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

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

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

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

2296 else: 

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

2298 # so getDarkTime() does not exist. 

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

2300 darkScale = 1.0 

2301 

2302 isrFunctions.darkCorrection( 

2303 maskedImage=exposure.getMaskedImage(), 

2304 darkMaskedImage=darkExposure.getMaskedImage(), 

2305 expScale=expScale, 

2306 darkScale=darkScale, 

2307 invert=invert, 

2308 trimToFit=self.config.doTrimToMatchCalib 

2309 ) 

2310 

2311 def doLinearize(self, detector): 

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

2313 

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

2315 amplifier. 

2316 

2317 Parameters 

2318 ---------- 

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

2320 Detector to get linearity type from. 

2321 

2322 Returns 

2323 ------- 

2324 doLinearize : `Bool` 

2325 If True, linearization should be performed. 

2326 """ 

2327 return self.config.doLinearize and \ 

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

2329 

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

2331 """Apply flat correction in place. 

2332 

2333 Parameters 

2334 ---------- 

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

2336 Exposure to process. 

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

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

2339 invert : `Bool`, optional 

2340 If True, unflatten an already flattened image. 

2341 

2342 See Also 

2343 -------- 

2344 lsst.ip.isr.isrFunctions.flatCorrection 

2345 """ 

2346 isrFunctions.flatCorrection( 

2347 maskedImage=exposure.getMaskedImage(), 

2348 flatMaskedImage=flatExposure.getMaskedImage(), 

2349 scalingType=self.config.flatScalingType, 

2350 userScale=self.config.flatUserScale, 

2351 invert=invert, 

2352 trimToFit=self.config.doTrimToMatchCalib 

2353 ) 

2354 

2355 def saturationDetection(self, exposure, amp): 

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

2357 

2358 Parameters 

2359 ---------- 

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

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

2362 amp : `lsst.afw.table.AmpInfoCatalog` 

2363 Amplifier detector data. 

2364 

2365 See Also 

2366 -------- 

2367 lsst.ip.isr.isrFunctions.makeThresholdMask 

2368 """ 

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

2370 maskedImage = exposure.getMaskedImage() 

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

2372 isrFunctions.makeThresholdMask( 

2373 maskedImage=dataView, 

2374 threshold=amp.getSaturation(), 

2375 growFootprints=0, 

2376 maskName=self.config.saturatedMaskName, 

2377 ) 

2378 

2379 def saturationInterpolation(self, exposure): 

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

2381 

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

2383 ensure that the saturated pixels have been identified in the 

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

2385 saturated regions may cross amplifier boundaries. 

2386 

2387 Parameters 

2388 ---------- 

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

2390 Exposure to process. 

2391 

2392 See Also 

2393 -------- 

2394 lsst.ip.isr.isrTask.saturationDetection 

2395 lsst.ip.isr.isrFunctions.interpolateFromMask 

2396 """ 

2397 isrFunctions.interpolateFromMask( 

2398 maskedImage=exposure.getMaskedImage(), 

2399 fwhm=self.config.fwhm, 

2400 growSaturatedFootprints=self.config.growSaturationFootprintSize, 

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

2402 ) 

2403 

2404 def suspectDetection(self, exposure, amp): 

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

2406 

2407 Parameters 

2408 ---------- 

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

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

2411 amp : `lsst.afw.table.AmpInfoCatalog` 

2412 Amplifier detector data. 

2413 

2414 See Also 

2415 -------- 

2416 lsst.ip.isr.isrFunctions.makeThresholdMask 

2417 

2418 Notes 

2419 ----- 

2420 Suspect pixels are pixels whose value is greater than 

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

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

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

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

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

2426 """ 

2427 suspectLevel = amp.getSuspectLevel() 

2428 if math.isnan(suspectLevel): 

2429 return 

2430 

2431 maskedImage = exposure.getMaskedImage() 

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

2433 isrFunctions.makeThresholdMask( 

2434 maskedImage=dataView, 

2435 threshold=suspectLevel, 

2436 growFootprints=0, 

2437 maskName=self.config.suspectMaskName, 

2438 ) 

2439 

2440 def maskDefect(self, exposure, defectBaseList): 

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

2442 

2443 Parameters 

2444 ---------- 

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

2446 Exposure to process. 

2447 defectBaseList : `lsst.ip.isr.Defects` or `list` of 

2448 `lsst.afw.image.DefectBase`. 

2449 List of defects to mask. 

2450 

2451 Notes 

2452 ----- 

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

2454 boundaries. 

2455 """ 

2456 maskedImage = exposure.getMaskedImage() 

2457 if not isinstance(defectBaseList, Defects): 

2458 # Promotes DefectBase to Defect 

2459 defectList = Defects(defectBaseList) 

2460 else: 

2461 defectList = defectBaseList 

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

2463 

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

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

2466 

2467 Parameters 

2468 ---------- 

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

2470 Exposure to process. 

2471 numEdgePixels : `int`, optional 

2472 Number of edge pixels to mask. 

2473 maskPlane : `str`, optional 

2474 Mask plane name to use. 

2475 level : `str`, optional 

2476 Level at which to mask edges. 

2477 """ 

2478 maskedImage = exposure.getMaskedImage() 

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

2480 

2481 if numEdgePixels > 0: 

2482 if level == 'DETECTOR': 

2483 boxes = [maskedImage.getBBox()] 

2484 elif level == 'AMP': 

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

2486 

2487 for box in boxes: 

2488 # This makes a bbox numEdgeSuspect pixels smaller than the 

2489 # image on each side 

2490 subImage = maskedImage[box] 

2491 box.grow(-numEdgePixels) 

2492 # Mask pixels outside box 

2493 SourceDetectionTask.setEdgeBits( 

2494 subImage, 

2495 box, 

2496 maskBitMask) 

2497 

2498 def maskAndInterpolateDefects(self, exposure, defectBaseList): 

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

2500 

2501 Parameters 

2502 ---------- 

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

2504 Exposure to process. 

2505 defectBaseList : `lsst.ip.isr.Defects` or `list` of 

2506 `lsst.afw.image.DefectBase`. 

2507 List of defects to mask and interpolate. 

2508 

2509 See Also 

2510 -------- 

2511 lsst.ip.isr.isrTask.maskDefect 

2512 """ 

2513 self.maskDefect(exposure, defectBaseList) 

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

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

2516 isrFunctions.interpolateFromMask( 

2517 maskedImage=exposure.getMaskedImage(), 

2518 fwhm=self.config.fwhm, 

2519 growSaturatedFootprints=0, 

2520 maskNameList=["BAD"], 

2521 ) 

2522 

2523 def maskNan(self, exposure): 

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

2525 

2526 Parameters 

2527 ---------- 

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

2529 Exposure to process. 

2530 

2531 Notes 

2532 ----- 

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

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

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

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

2537 preserve the historical name. 

2538 """ 

2539 maskedImage = exposure.getMaskedImage() 

2540 

2541 # Find and mask NaNs 

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

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

2544 numNans = maskNans(maskedImage, maskVal) 

2545 self.metadata["NUMNANS"] = numNans 

2546 if numNans > 0: 

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

2548 

2549 def maskAndInterpolateNan(self, exposure): 

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

2551 in place. 

2552 

2553 Parameters 

2554 ---------- 

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

2556 Exposure to process. 

2557 

2558 See Also 

2559 -------- 

2560 lsst.ip.isr.isrTask.maskNan 

2561 """ 

2562 self.maskNan(exposure) 

2563 isrFunctions.interpolateFromMask( 

2564 maskedImage=exposure.getMaskedImage(), 

2565 fwhm=self.config.fwhm, 

2566 growSaturatedFootprints=0, 

2567 maskNameList=["UNMASKEDNAN"], 

2568 ) 

2569 

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

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

2572 

2573 Parameters 

2574 ---------- 

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

2576 Exposure to process. 

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

2578 Configuration object containing parameters on which background 

2579 statistics and subgrids to use. 

2580 """ 

2581 if IsrQaConfig is not None: 

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

2583 IsrQaConfig.flatness.nIter) 

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

2585 statsControl.setAndMask(maskVal) 

2586 maskedImage = exposure.getMaskedImage() 

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

2588 skyLevel = stats.getValue(afwMath.MEDIAN) 

2589 skySigma = stats.getValue(afwMath.STDEVCLIP) 

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

2591 metadata = exposure.getMetadata() 

2592 metadata["SKYLEVEL"] = skyLevel 

2593 metadata["SKYSIGMA"] = skySigma 

2594 

2595 # calcluating flatlevel over the subgrids 

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

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

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

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

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

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

2602 

2603 for j in range(nY): 

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

2605 for i in range(nX): 

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

2607 

2608 xLLC = xc - meshXHalf 

2609 yLLC = yc - meshYHalf 

2610 xURC = xc + meshXHalf - 1 

2611 yURC = yc + meshYHalf - 1 

2612 

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

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

2615 

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

2617 

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

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

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

2621 flatness_rms = numpy.std(flatness) 

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

2623 

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

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

2626 nX, nY, flatness_pp, flatness_rms) 

2627 

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

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

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

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

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

2633 

2634 def roughZeroPoint(self, exposure): 

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

2636 

2637 Parameters 

2638 ---------- 

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

2640 Exposure to process. 

2641 """ 

2642 filterLabel = exposure.getFilter() 

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

2644 

2645 if physicalFilter in self.config.fluxMag0T1: 

2646 fluxMag0 = self.config.fluxMag0T1[physicalFilter] 

2647 else: 

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

2649 fluxMag0 = self.config.defaultFluxMag0T1 

2650 

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

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

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

2654 return 

2655 

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

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

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

2659 

2660 @contextmanager 

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

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

2663 if the task is configured to apply them. 

2664 

2665 Parameters 

2666 ---------- 

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

2668 Exposure to process. 

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

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

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

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

2673 

2674 Yields 

2675 ------ 

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

2677 The flat and dark corrected exposure. 

2678 """ 

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

2680 self.darkCorrection(exp, dark) 

2681 if self.config.doFlat: 

2682 self.flatCorrection(exp, flat) 

2683 try: 

2684 yield exp 

2685 finally: 

2686 if self.config.doFlat: 

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

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

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

2690 

2691 def debugView(self, exposure, stepname): 

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

2693 

2694 Parameters 

2695 ---------- 

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

2697 Exposure to view. 

2698 stepname : `str` 

2699 State of processing to view. 

2700 """ 

2701 frame = getDebugFrame(self._display, stepname) 

2702 if frame: 

2703 display = getDisplay(frame) 

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

2705 display.mtv(exposure) 

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

2707 while True: 

2708 ans = input(prompt).lower() 

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

2710 break 

2711 

2712 

2713class FakeAmp(object): 

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

2715 

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

2717 

2718 Parameters 

2719 ---------- 

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

2721 Exposure to generate a fake amplifier for. 

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

2723 Configuration to apply to the fake amplifier. 

2724 """ 

2725 

2726 def __init__(self, exposure, config): 

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

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

2729 self._gain = config.gain 

2730 self._readNoise = config.readNoise 

2731 self._saturation = config.saturation 

2732 

2733 def getBBox(self): 

2734 return self._bbox 

2735 

2736 def getRawBBox(self): 

2737 return self._bbox 

2738 

2739 def getRawHorizontalOverscanBBox(self): 

2740 return self._RawHorizontalOverscanBBox 

2741 

2742 def getGain(self): 

2743 return self._gain 

2744 

2745 def getReadNoise(self): 

2746 return self._readNoise 

2747 

2748 def getSaturation(self): 

2749 return self._saturation 

2750 

2751 def getSuspectLevel(self): 

2752 return float("NaN") 

2753 

2754 

2755class RunIsrConfig(pexConfig.Config): 

2756 isr = pexConfig.ConfigurableField(target=IsrTask, doc="Instrument signature removal") 

2757 

2758 

2759class RunIsrTask(pipeBase.CmdLineTask): 

2760 """Task to wrap the default IsrTask to allow it to be retargeted. 

2761 

2762 The standard IsrTask can be called directly from a command line 

2763 program, but doing so removes the ability of the task to be 

2764 retargeted. As most cameras override some set of the IsrTask 

2765 methods, this would remove those data-specific methods in the 

2766 output post-ISR images. This wrapping class fixes the issue, 

2767 allowing identical post-ISR images to be generated by both the 

2768 processCcd and isrTask code. 

2769 """ 

2770 ConfigClass = RunIsrConfig 

2771 _DefaultName = "runIsr" 

2772 

2773 def __init__(self, *args, **kwargs): 

2774 super().__init__(*args, **kwargs) 

2775 self.makeSubtask("isr") 

2776 

2777 def runDataRef(self, dataRef): 

2778 """ 

2779 Parameters 

2780 ---------- 

2781 dataRef : `lsst.daf.persistence.ButlerDataRef` 

2782 data reference of the detector data to be processed 

2783 

2784 Returns 

2785 ------- 

2786 result : `pipeBase.Struct` 

2787 Result struct with component: 

2788 

2789 - exposure : `lsst.afw.image.Exposure` 

2790 Post-ISR processed exposure. 

2791 """ 

2792 return self.isr.runDataRef(dataRef)