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

1006 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-09-29 10:20 +0000

1# This file is part of ip_isr. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

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

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

7# for details of code ownership. 

8# 

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

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

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

12# (at your option) any later version. 

13# 

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

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

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

17# GNU General Public License for more details. 

18# 

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

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

21 

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

23 

24import math 

25import numpy 

26import numbers 

27 

28import lsst.geom 

29import lsst.afw.image as afwImage 

30import lsst.afw.math as afwMath 

31import lsst.pex.config as pexConfig 

32import lsst.pipe.base as pipeBase 

33import lsst.pipe.base.connectionTypes as cT 

34 

35from contextlib import contextmanager 

36from deprecated.sphinx import deprecated 

37from lsstDebug import getDebugFrame 

38 

39from lsst.afw.cameraGeom import NullLinearityType 

40from lsst.afw.display import getDisplay 

41from lsst.meas.algorithms.detection import SourceDetectionTask 

42from lsst.utils.timer import timeMethod 

43 

44from . import isrFunctions 

45from . import isrQa 

46from . import linearize 

47from .defects import Defects 

48 

49from .assembleCcdTask import AssembleCcdTask 

50from .binImageDataTask import BinImageDataTask 

51from .crosstalk import CrosstalkTask, CrosstalkCalib 

52from .fringe import FringeTask 

53from .isr import maskNans 

54from .masking import MaskingTask 

55from .overscan import OverscanCorrectionTask 

56from .straylight import StrayLightTask 

57from .vignette import VignetteTask 

58from .ampOffset import AmpOffsetTask 

59from .deferredCharge import DeferredChargeTask 

60from .isrStatistics import IsrStatisticsTask 

61from .ptcDataset import PhotonTransferCurveDataset 

62from .isrFunctions import compareCameraKeywords 

63from .brighterFatterKernel import (brighterFatterCorrection, 

64 fluxConservingBrighterFatterCorrection) 

65 

66 

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

68 """Lookup function to identify crosstalkSource entries. 

69 

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

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

72 populated. 

73 

74 Parameters 

75 ---------- 

76 datasetType : `str` 

77 Dataset to lookup. 

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

79 Butler registry to query. 

80 quantumDataId : `lsst.daf.butler.DataCoordinate` 

81 Expanded data id to transform to identify crosstalkSources. The 

82 ``detector`` entry will be stripped. 

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

84 Collections to search through. 

85 

86 Returns 

87 ------- 

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

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

90 crosstalkSources. 

91 """ 

92 newDataId = quantumDataId.subset(registry.dimensions.conform(["instrument", "exposure"])) 

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

94 findFirst=True)) 

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

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

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

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

99 # cached in the registry. 

100 records = {k: newDataId.records[k] for k in newDataId.dimensions.elements} 

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

102 

103 

104class IsrTaskConnections(pipeBase.PipelineTaskConnections, 

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

106 defaultTemplates={}): 

107 ccdExposure = cT.Input( 

108 name="raw", 

109 doc="Input exposure to process.", 

110 storageClass="Exposure", 

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

112 ) 

113 camera = cT.PrerequisiteInput( 

114 name="camera", 

115 storageClass="Camera", 

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

117 dimensions=["instrument"], 

118 isCalibration=True, 

119 ) 

120 

121 crosstalk = cT.PrerequisiteInput( 

122 name="crosstalk", 

123 doc="Input crosstalk object", 

124 storageClass="CrosstalkCalib", 

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

126 isCalibration=True, 

127 minimum=0, # can fall back to cameraGeom 

128 ) 

129 crosstalkSources = cT.PrerequisiteInput( 

130 name="isrOverscanCorrected", 

131 doc="Overscan corrected input images.", 

132 storageClass="Exposure", 

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

134 deferLoad=True, 

135 multiple=True, 

136 lookupFunction=crosstalkSourceLookup, 

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

138 ) 

139 bias = cT.PrerequisiteInput( 

140 name="bias", 

141 doc="Input bias calibration.", 

142 storageClass="ExposureF", 

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

144 isCalibration=True, 

145 ) 

146 dark = cT.PrerequisiteInput( 

147 name='dark', 

148 doc="Input dark calibration.", 

149 storageClass="ExposureF", 

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

151 isCalibration=True, 

152 ) 

153 flat = cT.PrerequisiteInput( 

154 name="flat", 

155 doc="Input flat calibration.", 

156 storageClass="ExposureF", 

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

158 isCalibration=True, 

159 ) 

160 ptc = cT.PrerequisiteInput( 

161 name="ptc", 

162 doc="Input Photon Transfer Curve dataset", 

163 storageClass="PhotonTransferCurveDataset", 

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

165 isCalibration=True, 

166 ) 

167 fringes = cT.PrerequisiteInput( 

168 name="fringe", 

169 doc="Input fringe calibration.", 

170 storageClass="ExposureF", 

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

172 isCalibration=True, 

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

174 ) 

175 strayLightData = cT.PrerequisiteInput( 

176 name='yBackground', 

177 doc="Input stray light calibration.", 

178 storageClass="StrayLightData", 

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

180 deferLoad=True, 

181 isCalibration=True, 

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

183 ) 

184 bfKernel = cT.PrerequisiteInput( 

185 name='bfKernel', 

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

187 storageClass="NumpyArray", 

188 dimensions=["instrument"], 

189 isCalibration=True, 

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

191 ) 

192 newBFKernel = cT.PrerequisiteInput( 

193 name='brighterFatterKernel', 

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

195 storageClass="BrighterFatterKernel", 

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

197 isCalibration=True, 

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

199 ) 

200 defects = cT.PrerequisiteInput( 

201 name='defects', 

202 doc="Input defect tables.", 

203 storageClass="Defects", 

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

205 isCalibration=True, 

206 ) 

207 linearizer = cT.PrerequisiteInput( 

208 name='linearizer', 

209 storageClass="Linearizer", 

210 doc="Linearity correction calibration.", 

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

212 isCalibration=True, 

213 minimum=0, # can fall back to cameraGeom 

214 ) 

215 opticsTransmission = cT.PrerequisiteInput( 

216 name="transmission_optics", 

217 storageClass="TransmissionCurve", 

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

219 dimensions=["instrument"], 

220 isCalibration=True, 

221 ) 

222 filterTransmission = cT.PrerequisiteInput( 

223 name="transmission_filter", 

224 storageClass="TransmissionCurve", 

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

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

227 isCalibration=True, 

228 ) 

229 sensorTransmission = cT.PrerequisiteInput( 

230 name="transmission_sensor", 

231 storageClass="TransmissionCurve", 

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

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

234 isCalibration=True, 

235 ) 

236 atmosphereTransmission = cT.PrerequisiteInput( 

237 name="transmission_atmosphere", 

238 storageClass="TransmissionCurve", 

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

240 dimensions=["instrument"], 

241 isCalibration=True, 

242 ) 

243 illumMaskedImage = cT.PrerequisiteInput( 

244 name="illum", 

245 doc="Input illumination correction.", 

246 storageClass="MaskedImageF", 

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

248 isCalibration=True, 

249 ) 

250 deferredChargeCalib = cT.PrerequisiteInput( 

251 name="cpCtiCalib", 

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

253 storageClass="IsrCalib", 

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

255 isCalibration=True, 

256 ) 

257 

258 outputExposure = cT.Output( 

259 name='postISRCCD', 

260 doc="Output ISR processed exposure.", 

261 storageClass="Exposure", 

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

263 ) 

264 preInterpExposure = cT.Output( 

265 name='preInterpISRCCD', 

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

267 storageClass="ExposureF", 

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

269 ) 

270 outputBin1Exposure = cT.Output( 

271 name="postIsrBin1", 

272 doc="First binned image.", 

273 storageClass="ExposureF", 

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

275 ) 

276 outputBin2Exposure = cT.Output( 

277 name="postIsrBin2", 

278 doc="Second binned image.", 

279 storageClass="ExposureF", 

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

281 ) 

282 

283 outputOssThumbnail = cT.Output( 

284 name="OssThumb", 

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

286 storageClass="Thumbnail", 

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

288 ) 

289 outputFlattenedThumbnail = cT.Output( 

290 name="FlattenedThumb", 

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

292 storageClass="Thumbnail", 

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

294 ) 

295 outputStatistics = cT.Output( 

296 name="isrStatistics", 

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

298 storageClass="StructuredDataDict", 

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

300 ) 

301 

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

303 super().__init__(config=config) 

304 

305 if config.doBias is not True: 

306 self.prerequisiteInputs.remove("bias") 

307 if config.doLinearize is not True: 

308 self.prerequisiteInputs.remove("linearizer") 

309 if config.doCrosstalk is not True: 

310 self.prerequisiteInputs.remove("crosstalkSources") 

311 self.prerequisiteInputs.remove("crosstalk") 

312 if config.doBrighterFatter is not True: 

313 self.prerequisiteInputs.remove("bfKernel") 

314 self.prerequisiteInputs.remove("newBFKernel") 

315 if config.doDefect is not True: 

316 self.prerequisiteInputs.remove("defects") 

317 if config.doDark is not True: 

318 self.prerequisiteInputs.remove("dark") 

319 if config.doFlat is not True: 

320 self.prerequisiteInputs.remove("flat") 

321 if config.doFringe is not True: 

322 self.prerequisiteInputs.remove("fringes") 

323 if config.doStrayLight is not True: 

324 self.prerequisiteInputs.remove("strayLightData") 

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

326 self.prerequisiteInputs.remove("ptc") 

327 if config.doAttachTransmissionCurve is not True: 

328 self.prerequisiteInputs.remove("opticsTransmission") 

329 self.prerequisiteInputs.remove("filterTransmission") 

330 self.prerequisiteInputs.remove("sensorTransmission") 

331 self.prerequisiteInputs.remove("atmosphereTransmission") 

332 else: 

333 if config.doUseOpticsTransmission is not True: 

334 self.prerequisiteInputs.remove("opticsTransmission") 

335 if config.doUseFilterTransmission is not True: 

336 self.prerequisiteInputs.remove("filterTransmission") 

337 if config.doUseSensorTransmission is not True: 

338 self.prerequisiteInputs.remove("sensorTransmission") 

339 if config.doUseAtmosphereTransmission is not True: 

340 self.prerequisiteInputs.remove("atmosphereTransmission") 

341 if config.doIlluminationCorrection is not True: 

342 self.prerequisiteInputs.remove("illumMaskedImage") 

343 if config.doDeferredCharge is not True: 

344 self.prerequisiteInputs.remove("deferredChargeCalib") 

345 

346 if config.doWrite is not True: 

347 self.outputs.remove("outputExposure") 

348 self.outputs.remove("preInterpExposure") 

349 self.outputs.remove("outputFlattenedThumbnail") 

350 self.outputs.remove("outputOssThumbnail") 

351 self.outputs.remove("outputStatistics") 

352 self.outputs.remove("outputBin1Exposure") 

353 self.outputs.remove("outputBin2Exposure") 

354 else: 

355 if config.doBinnedExposures is not True: 

356 self.outputs.remove("outputBin1Exposure") 

357 self.outputs.remove("outputBin2Exposure") 

358 if config.doSaveInterpPixels is not True: 

359 self.outputs.remove("preInterpExposure") 

360 if config.qa.doThumbnailOss is not True: 

361 self.outputs.remove("outputOssThumbnail") 

362 if config.qa.doThumbnailFlattened is not True: 

363 self.outputs.remove("outputFlattenedThumbnail") 

364 if config.doCalculateStatistics is not True: 

365 self.outputs.remove("outputStatistics") 

366 

367 

368class IsrTaskConfig(pipeBase.PipelineTaskConfig, 

369 pipelineConnections=IsrTaskConnections): 

370 """Configuration parameters for IsrTask. 

371 

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

373 """ 

374 datasetType = pexConfig.Field( 

375 dtype=str, 

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

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

378 default="raw", 

379 ) 

380 

381 fallbackFilterName = pexConfig.Field( 

382 dtype=str, 

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

384 optional=True 

385 ) 

386 useFallbackDate = pexConfig.Field( 

387 dtype=bool, 

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

389 default=False, 

390 ) 

391 expectWcs = pexConfig.Field( 

392 dtype=bool, 

393 default=True, 

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

395 ) 

396 fwhm = pexConfig.Field( 

397 dtype=float, 

398 doc="FWHM of PSF in arcseconds (currently unused).", 

399 default=1.0, 

400 ) 

401 qa = pexConfig.ConfigField( 

402 dtype=isrQa.IsrQaConfig, 

403 doc="QA related configuration options.", 

404 ) 

405 doHeaderProvenance = pexConfig.Field( 

406 dtype=bool, 

407 default=True, 

408 doc="Write calibration identifiers into output exposure header?", 

409 ) 

410 

411 # Calib checking configuration: 

412 doRaiseOnCalibMismatch = pexConfig.Field( 

413 dtype=bool, 

414 default=False, 

415 doc="Should IsrTask halt if exposure and calibration header values do not match?", 

416 ) 

417 cameraKeywordsToCompare = pexConfig.ListField( 

418 dtype=str, 

419 doc="List of header keywords to compare between exposure and calibrations.", 

420 default=[], 

421 ) 

422 

423 # Image conversion configuration 

424 doConvertIntToFloat = pexConfig.Field( 

425 dtype=bool, 

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

427 default=True, 

428 ) 

429 

430 # Saturated pixel handling. 

431 doSaturation = pexConfig.Field( 

432 dtype=bool, 

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

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

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

436 default=True, 

437 ) 

438 saturatedMaskName = pexConfig.Field( 

439 dtype=str, 

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

441 default="SAT", 

442 ) 

443 saturation = pexConfig.Field( 

444 dtype=float, 

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

446 default=float("NaN"), 

447 ) 

448 growSaturationFootprintSize = pexConfig.Field( 

449 dtype=int, 

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

451 default=1, 

452 ) 

453 

454 # Suspect pixel handling. 

455 doSuspect = pexConfig.Field( 

456 dtype=bool, 

457 doc="Mask suspect pixels?", 

458 default=False, 

459 ) 

460 suspectMaskName = pexConfig.Field( 

461 dtype=str, 

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

463 default="SUSPECT", 

464 ) 

465 numEdgeSuspect = pexConfig.Field( 

466 dtype=int, 

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

468 default=0, 

469 ) 

470 edgeMaskLevel = pexConfig.ChoiceField( 

471 dtype=str, 

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

473 default="DETECTOR", 

474 allowed={ 

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

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

477 }, 

478 ) 

479 

480 # Initial masking options. 

481 doSetBadRegions = pexConfig.Field( 

482 dtype=bool, 

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

484 default=True, 

485 ) 

486 badStatistic = pexConfig.ChoiceField( 

487 dtype=str, 

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

489 default='MEANCLIP', 

490 allowed={ 

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

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

493 }, 

494 ) 

495 

496 # Overscan subtraction configuration. 

497 doOverscan = pexConfig.Field( 

498 dtype=bool, 

499 doc="Do overscan subtraction?", 

500 default=True, 

501 ) 

502 overscan = pexConfig.ConfigurableField( 

503 target=OverscanCorrectionTask, 

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

505 ) 

506 

507 # Amplifier to CCD assembly configuration 

508 doAssembleCcd = pexConfig.Field( 

509 dtype=bool, 

510 default=True, 

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

512 ) 

513 assembleCcd = pexConfig.ConfigurableField( 

514 target=AssembleCcdTask, 

515 doc="CCD assembly task", 

516 ) 

517 

518 # General calibration configuration. 

519 doAssembleIsrExposures = pexConfig.Field( 

520 dtype=bool, 

521 default=False, 

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

523 ) 

524 doTrimToMatchCalib = pexConfig.Field( 

525 dtype=bool, 

526 default=False, 

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

528 ) 

529 

530 # Bias subtraction. 

531 doBias = pexConfig.Field( 

532 dtype=bool, 

533 doc="Apply bias frame correction?", 

534 default=True, 

535 ) 

536 biasDataProductName = pexConfig.Field( 

537 dtype=str, 

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

539 default="bias", 

540 ) 

541 doBiasBeforeOverscan = pexConfig.Field( 

542 dtype=bool, 

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

544 default=False 

545 ) 

546 

547 # Deferred charge correction. 

548 doDeferredCharge = pexConfig.Field( 

549 dtype=bool, 

550 doc="Apply deferred charge correction?", 

551 default=False, 

552 ) 

553 deferredChargeCorrection = pexConfig.ConfigurableField( 

554 target=DeferredChargeTask, 

555 doc="Deferred charge correction task.", 

556 ) 

557 

558 # Variance construction 

559 doVariance = pexConfig.Field( 

560 dtype=bool, 

561 doc="Calculate variance?", 

562 default=True 

563 ) 

564 gain = pexConfig.Field( 

565 dtype=float, 

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

567 default=float("NaN"), 

568 ) 

569 readNoise = pexConfig.Field( 

570 dtype=float, 

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

572 default=0.0, 

573 ) 

574 doEmpiricalReadNoise = pexConfig.Field( 

575 dtype=bool, 

576 default=False, 

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

578 ) 

579 usePtcReadNoise = pexConfig.Field( 

580 dtype=bool, 

581 default=False, 

582 doc="Use read noise values from the Photon Transfer Curve?" 

583 ) 

584 maskNegativeVariance = pexConfig.Field( 

585 dtype=bool, 

586 default=True, 

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

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

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

590 ) 

591 negativeVarianceMaskName = pexConfig.Field( 

592 dtype=str, 

593 default="BAD", 

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

595 ) 

596 # Linearization. 

597 doLinearize = pexConfig.Field( 

598 dtype=bool, 

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

600 default=True, 

601 ) 

602 

603 # Crosstalk. 

604 doCrosstalk = pexConfig.Field( 

605 dtype=bool, 

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

607 default=False, 

608 ) 

609 doCrosstalkBeforeAssemble = pexConfig.Field( 

610 dtype=bool, 

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

612 default=False, 

613 ) 

614 crosstalk = pexConfig.ConfigurableField( 

615 target=CrosstalkTask, 

616 doc="Intra-CCD crosstalk correction", 

617 ) 

618 

619 # Masking options. 

620 doDefect = pexConfig.Field( 

621 dtype=bool, 

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

623 default=True, 

624 ) 

625 doNanMasking = pexConfig.Field( 

626 dtype=bool, 

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

628 default=True, 

629 ) 

630 doWidenSaturationTrails = pexConfig.Field( 

631 dtype=bool, 

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

633 default=True 

634 ) 

635 

636 # Brighter-Fatter correction. 

637 doBrighterFatter = pexConfig.Field( 

638 dtype=bool, 

639 default=False, 

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

641 ) 

642 doFluxConservingBrighterFatterCorrection = pexConfig.Field( 

643 dtype=bool, 

644 default=False, 

645 doc="Apply the flux-conserving BFE correction by Miller et al.?" 

646 ) 

647 brighterFatterLevel = pexConfig.ChoiceField( 

648 dtype=str, 

649 default="DETECTOR", 

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

651 allowed={ 

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

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

654 } 

655 ) 

656 brighterFatterMaxIter = pexConfig.Field( 

657 dtype=int, 

658 default=10, 

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

660 ) 

661 brighterFatterThreshold = pexConfig.Field( 

662 dtype=float, 

663 default=1000, 

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

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

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

667 ) 

668 brighterFatterApplyGain = pexConfig.Field( 

669 dtype=bool, 

670 default=True, 

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

672 ) 

673 brighterFatterMaskListToInterpolate = pexConfig.ListField( 

674 dtype=str, 

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

676 "correction.", 

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

678 ) 

679 brighterFatterMaskGrowSize = pexConfig.Field( 

680 dtype=int, 

681 default=0, 

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

683 "when brighter-fatter correction is applied." 

684 ) 

685 

686 # Dark subtraction. 

687 doDark = pexConfig.Field( 

688 dtype=bool, 

689 doc="Apply dark frame correction?", 

690 default=True, 

691 ) 

692 darkDataProductName = pexConfig.Field( 

693 dtype=str, 

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

695 default="dark", 

696 ) 

697 

698 # Camera-specific stray light removal. 

699 doStrayLight = pexConfig.Field( 

700 dtype=bool, 

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

702 default=False, 

703 ) 

704 strayLight = pexConfig.ConfigurableField( 

705 target=StrayLightTask, 

706 doc="y-band stray light correction" 

707 ) 

708 

709 # Flat correction. 

710 doFlat = pexConfig.Field( 

711 dtype=bool, 

712 doc="Apply flat field correction?", 

713 default=True, 

714 ) 

715 flatDataProductName = pexConfig.Field( 

716 dtype=str, 

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

718 default="flat", 

719 ) 

720 flatScalingType = pexConfig.ChoiceField( 

721 dtype=str, 

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

723 default='USER', 

724 allowed={ 

725 "USER": "Scale by flatUserScale", 

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

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

728 }, 

729 ) 

730 flatUserScale = pexConfig.Field( 

731 dtype=float, 

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

733 default=1.0, 

734 ) 

735 doTweakFlat = pexConfig.Field( 

736 dtype=bool, 

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

738 default=False 

739 ) 

740 

741 # Amplifier normalization based on gains instead of using flats 

742 # configuration. 

743 doApplyGains = pexConfig.Field( 

744 dtype=bool, 

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

746 default=False, 

747 ) 

748 usePtcGains = pexConfig.Field( 

749 dtype=bool, 

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

751 default=False, 

752 ) 

753 normalizeGains = pexConfig.Field( 

754 dtype=bool, 

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

756 default=False, 

757 ) 

758 

759 # Fringe correction. 

760 doFringe = pexConfig.Field( 

761 dtype=bool, 

762 doc="Apply fringe correction?", 

763 default=True, 

764 ) 

765 fringe = pexConfig.ConfigurableField( 

766 target=FringeTask, 

767 doc="Fringe subtraction task", 

768 ) 

769 fringeAfterFlat = pexConfig.Field( 

770 dtype=bool, 

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

772 default=True, 

773 ) 

774 

775 # Amp offset correction. 

776 doAmpOffset = pexConfig.Field( 

777 doc="Calculate amp offset corrections?", 

778 dtype=bool, 

779 default=False, 

780 ) 

781 ampOffset = pexConfig.ConfigurableField( 

782 doc="Amp offset correction task.", 

783 target=AmpOffsetTask, 

784 ) 

785 

786 # Initial CCD-level background statistics options. 

787 doMeasureBackground = pexConfig.Field( 

788 dtype=bool, 

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

790 default=False, 

791 ) 

792 

793 # Camera-specific masking configuration. 

794 doCameraSpecificMasking = pexConfig.Field( 

795 dtype=bool, 

796 doc="Use camera-specific masking task to mask bad regions?", 

797 default=False, 

798 ) 

799 masking = pexConfig.ConfigurableField( 

800 target=MaskingTask, 

801 doc="Masking task. The default does nothing; retarget to a camera-specific " 

802 "task and set doCameraSpecificMasking.", 

803 ) 

804 

805 # Interpolation options. 

806 doInterpolate = pexConfig.Field( 

807 dtype=bool, 

808 doc="Interpolate masked pixels?", 

809 default=True, 

810 ) 

811 doSaturationInterpolation = pexConfig.Field( 

812 dtype=bool, 

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

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

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

816 default=True, 

817 ) 

818 doNanInterpolation = pexConfig.Field( 

819 dtype=bool, 

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

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

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

823 default=True, 

824 ) 

825 doNanInterpAfterFlat = pexConfig.Field( 

826 dtype=bool, 

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

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

829 default=False, 

830 ) 

831 maskListToInterpolate = pexConfig.ListField( 

832 dtype=str, 

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

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

835 ) 

836 doSaveInterpPixels = pexConfig.Field( 

837 dtype=bool, 

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

839 default=False, 

840 ) 

841 useLegacyInterp = pexConfig.Field( 

842 dtype=bool, 

843 doc="Use the legacy interpolation algorithm. If False use Gaussian Process.", 

844 default=True, 

845 ) 

846 

847 # Default photometric calibration options. 

848 fluxMag0T1 = pexConfig.DictField( 

849 keytype=str, 

850 itemtype=float, 

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

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

853 )) 

854 ) 

855 defaultFluxMag0T1 = pexConfig.Field( 

856 dtype=float, 

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

858 default=pow(10.0, 0.4*28.0) 

859 ) 

860 

861 # Vignette correction configuration. 

862 doVignette = pexConfig.Field( 

863 dtype=bool, 

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

865 "according to vignetting parameters?"), 

866 default=False, 

867 ) 

868 doMaskVignettePolygon = pexConfig.Field( 

869 dtype=bool, 

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

871 "is False"), 

872 default=True, 

873 ) 

874 vignetteValue = pexConfig.Field( 

875 dtype=float, 

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

877 optional=True, 

878 default=None, 

879 ) 

880 vignette = pexConfig.ConfigurableField( 

881 target=VignetteTask, 

882 doc="Vignetting task.", 

883 ) 

884 

885 # Transmission curve configuration. 

886 doAttachTransmissionCurve = pexConfig.Field( 

887 dtype=bool, 

888 default=False, 

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

890 ) 

891 doUseOpticsTransmission = pexConfig.Field( 

892 dtype=bool, 

893 default=True, 

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

895 ) 

896 doUseFilterTransmission = pexConfig.Field( 

897 dtype=bool, 

898 default=True, 

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

900 ) 

901 doUseSensorTransmission = pexConfig.Field( 

902 dtype=bool, 

903 default=True, 

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

905 ) 

906 doUseAtmosphereTransmission = pexConfig.Field( 

907 dtype=bool, 

908 default=True, 

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

910 ) 

911 

912 # Illumination correction. 

913 doIlluminationCorrection = pexConfig.Field( 

914 dtype=bool, 

915 default=False, 

916 doc="Perform illumination correction?" 

917 ) 

918 illuminationCorrectionDataProductName = pexConfig.Field( 

919 dtype=str, 

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

921 default="illumcor", 

922 ) 

923 illumScale = pexConfig.Field( 

924 dtype=float, 

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

926 default=1.0, 

927 ) 

928 illumFilters = pexConfig.ListField( 

929 dtype=str, 

930 default=[], 

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

932 ) 

933 

934 # Calculate image quality statistics? 

935 doStandardStatistics = pexConfig.Field( 

936 dtype=bool, 

937 doc="Should standard image quality statistics be calculated?", 

938 default=True, 

939 ) 

940 # Calculate additional statistics? 

941 doCalculateStatistics = pexConfig.Field( 

942 dtype=bool, 

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

944 default=False, 

945 ) 

946 isrStats = pexConfig.ConfigurableField( 

947 target=IsrStatisticsTask, 

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

949 ) 

950 

951 # Make binned images? 

952 doBinnedExposures = pexConfig.Field( 

953 dtype=bool, 

954 doc="Should binned exposures be calculated?", 

955 default=False, 

956 ) 

957 binning = pexConfig.ConfigurableField( 

958 target=BinImageDataTask, 

959 doc="Task to bin the exposure.", 

960 ) 

961 binFactor1 = pexConfig.Field( 

962 dtype=int, 

963 doc="Binning factor for first binned exposure. This is intended for a finely binned output.", 

964 default=8, 

965 check=lambda x: x > 1, 

966 ) 

967 binFactor2 = pexConfig.Field( 

968 dtype=int, 

969 doc="Binning factor for second binned exposure. This is intended for a coarsely binned output.", 

970 default=64, 

971 check=lambda x: x > 1, 

972 ) 

973 

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

975 # be needed. 

976 doWrite = pexConfig.Field( 

977 dtype=bool, 

978 doc="Persist postISRCCD?", 

979 default=True, 

980 ) 

981 

982 def validate(self): 

983 super().validate() 

984 if self.doFlat and self.doApplyGains: 984 ↛ 985line 984 didn't jump to line 985 because the condition on line 984 was never true

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

986 if self.doBiasBeforeOverscan and self.doTrimToMatchCalib: 986 ↛ 987line 986 didn't jump to line 987 because the condition on line 986 was never true

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

988 if self.doSaturationInterpolation and self.saturatedMaskName not in self.maskListToInterpolate: 988 ↛ 989line 988 didn't jump to line 989 because the condition on line 988 was never true

989 self.maskListToInterpolate.append(self.saturatedMaskName) 

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

991 self.maskListToInterpolate.remove(self.saturatedMaskName) 

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

993 self.maskListToInterpolate.append("UNMASKEDNAN") 

994 if self.ampOffset.doApplyAmpOffset and not self.doAmpOffset: 994 ↛ 995line 994 didn't jump to line 995 because the condition on line 994 was never true

995 raise ValueError("ampOffset.doApplyAmpOffset requires doAmpOffset to be True.") 

996 

997 

998class IsrTask(pipeBase.PipelineTask): 

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

1000 

1001 The process for correcting imaging data is very similar from 

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

1003 doing these corrections, including the ability to turn certain 

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

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

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

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

1008 pixels. 

1009 

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

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

1012 

1013 Parameters 

1014 ---------- 

1015 args : `list` 

1016 Positional arguments passed to the Task constructor. 

1017 None used at this time. 

1018 kwargs : `dict`, optional 

1019 Keyword arguments passed on to the Task constructor. 

1020 None used at this time. 

1021 """ 

1022 ConfigClass = IsrTaskConfig 

1023 _DefaultName = "isr" 

1024 

1025 def __init__(self, **kwargs): 

1026 super().__init__(**kwargs) 

1027 self.makeSubtask("assembleCcd") 

1028 self.makeSubtask("crosstalk") 

1029 self.makeSubtask("strayLight") 

1030 self.makeSubtask("fringe") 

1031 self.makeSubtask("masking") 

1032 self.makeSubtask("overscan") 

1033 self.makeSubtask("vignette") 

1034 self.makeSubtask("ampOffset") 

1035 self.makeSubtask("deferredChargeCorrection") 

1036 self.makeSubtask("isrStats") 

1037 self.makeSubtask("binning") 

1038 

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

1040 inputs = butlerQC.get(inputRefs) 

1041 

1042 try: 

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

1044 except Exception as e: 

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

1046 (inputRefs, e)) 

1047 

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

1049 

1050 # This is use for header provenance. 

1051 additionalInputDates = {} 

1052 

1053 if self.config.doCrosstalk is True: 

1054 # Crosstalk sources need to be defined by the pipeline 

1055 # yaml if they exist. 

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

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

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

1059 else: 

1060 coeffVector = (self.config.crosstalk.crosstalkValues 

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

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

1063 inputs['crosstalk'] = crosstalkCalib 

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

1065 if 'crosstalkSources' not in inputs: 

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

1067 

1068 if self.doLinearize(detector) is True: 

1069 if 'linearizer' in inputs: 

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

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

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

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

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

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

1076 detector=detector, 

1077 log=self.log) 

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

1079 else: 

1080 linearizer = inputs['linearizer'] 

1081 self.log.info("Loading linearizer from the Butler.") 

1082 linearizer.log = self.log 

1083 inputs['linearizer'] = linearizer 

1084 else: 

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

1086 self.log.info("Constructing linearizer from cameraGeom information.") 

1087 

1088 if self.config.doDefect is True: 

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

1090 # defects is loaded as a BaseCatalog with columns 

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

1092 # defined by their bounding box 

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

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

1095 

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

1097 # the information as a numpy array. 

1098 brighterFatterSource = None 

1099 if self.config.doBrighterFatter: 

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

1101 if brighterFatterKernel is None: 

1102 # This type of kernel must be in (y, x) index 

1103 # ordering, as it used directly as the .array 

1104 # component of the afwImage kernel. 

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

1106 brighterFatterSource = 'bfKernel' 

1107 additionalInputDates[brighterFatterSource] = self.extractCalibDate(brighterFatterKernel) 

1108 

1109 if brighterFatterKernel is None: 

1110 # This was requested by the config, but none were found. 

1111 raise RuntimeError("No brighter-fatter kernel was supplied.") 

1112 elif not isinstance(brighterFatterKernel, numpy.ndarray): 

1113 # This is a ISR calib kernel. These kernels are 

1114 # generated in (x, y) index ordering, and need to be 

1115 # transposed to be used directly as the .array 

1116 # component of the afwImage kernel. This is done 

1117 # explicitly below when setting the ``bfKernel`` 

1118 # input. 

1119 brighterFatterSource = 'newBFKernel' 

1120 additionalInputDates[brighterFatterSource] = self.extractCalibDate(brighterFatterKernel) 

1121 

1122 detName = detector.getName() 

1123 level = brighterFatterKernel.level 

1124 

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

1126 inputs['bfGains'] = brighterFatterKernel.gain 

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

1128 kernel = None 

1129 if level == 'DETECTOR': 

1130 if detName in brighterFatterKernel.detKernels: 

1131 kernel = brighterFatterKernel.detKernels[detName] 

1132 else: 

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

1134 elif level == 'AMP': 

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

1136 "fatter kernels.") 

1137 brighterFatterKernel.makeDetectorKernelFromAmpwiseKernels(detName) 

1138 kernel = brighterFatterKernel.detKernels[detName] 

1139 if kernel is None: 

1140 raise RuntimeError("Could not identify brighter-fatter kernel!") 

1141 # Do the one single transpose here so the kernel 

1142 # can be directly loaded into the afwImage .array 

1143 # component. 

1144 inputs['bfKernel'] = numpy.transpose(kernel) 

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

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

1147 

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

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

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

1151 expId=expId, 

1152 assembler=self.assembleCcd 

1153 if self.config.doAssembleIsrExposures else None) 

1154 else: 

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

1156 

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

1158 if 'strayLightData' not in inputs: 

1159 inputs['strayLightData'] = None 

1160 

1161 if self.config.doHeaderProvenance: 

1162 # Add calibration provenanace info to header. 

1163 exposureMetadata = inputs['ccdExposure'].getMetadata() 

1164 

1165 # These inputs change name during this step. These should 

1166 # have matching entries in the additionalInputDates dict. 

1167 additionalInputs = [] 

1168 if self.config.doBrighterFatter: 

1169 additionalInputs.append(brighterFatterSource) 

1170 

1171 for inputName in sorted(list(inputs.keys()) + additionalInputs): 

1172 reference = getattr(inputRefs, inputName, None) 

1173 if reference is not None and hasattr(reference, "run"): 

1174 runKey = f"LSST CALIB RUN {inputName.upper()}" 

1175 runValue = reference.run 

1176 idKey = f"LSST CALIB UUID {inputName.upper()}" 

1177 idValue = str(reference.id) 

1178 dateKey = f"LSST CALIB DATE {inputName.upper()}" 

1179 

1180 if inputName in additionalInputDates: 

1181 dateValue = additionalInputDates[inputName] 

1182 else: 

1183 dateValue = self.extractCalibDate(inputs[inputName]) 

1184 

1185 exposureMetadata[runKey] = runValue 

1186 exposureMetadata[idKey] = idValue 

1187 exposureMetadata[dateKey] = dateValue 

1188 

1189 outputs = self.run(**inputs) 

1190 butlerQC.put(outputs, outputRefs) 

1191 

1192 @timeMethod 

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

1194 crosstalk=None, crosstalkSources=None, 

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

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

1197 sensorTransmission=None, atmosphereTransmission=None, 

1198 detectorNum=None, strayLightData=None, illumMaskedImage=None, 

1199 deferredChargeCalib=None, 

1200 ): 

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

1202 

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

1204 

1205 - saturation and suspect pixel masking 

1206 - overscan subtraction 

1207 - CCD assembly of individual amplifiers 

1208 - bias subtraction 

1209 - variance image construction 

1210 - linearization of non-linear response 

1211 - crosstalk masking 

1212 - brighter-fatter correction 

1213 - dark subtraction 

1214 - fringe correction 

1215 - stray light subtraction 

1216 - flat correction 

1217 - masking of known defects and camera specific features 

1218 - vignette calculation 

1219 - appending transmission curve and distortion model 

1220 

1221 Parameters 

1222 ---------- 

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

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

1225 exposure is modified by this method. 

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

1227 The camera geometry for this exposure. Required if 

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

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

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

1231 Bias calibration frame. 

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

1233 Functor for linearization. 

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

1235 Calibration for crosstalk. 

1236 crosstalkSources : `list`, optional 

1237 List of possible crosstalk sources. 

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

1239 Dark calibration frame. 

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

1241 Flat calibration frame. 

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

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

1244 and read noise. 

1245 bfKernel : `numpy.ndarray`, optional 

1246 Brighter-fatter kernel. 

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

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

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

1250 the detector in question. 

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

1252 List of defects. 

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

1254 Struct containing the fringe correction data, with 

1255 elements: 

1256 

1257 ``fringes`` 

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

1259 ``seed`` 

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

1261 number generator (`numpy.uint32`) 

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

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

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

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

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

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

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

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

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

1271 coordinates. 

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

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

1274 atmosphere, assumed to be spatially constant. 

1275 detectorNum : `int`, optional 

1276 The integer number for the detector to process. 

1277 strayLightData : `object`, optional 

1278 Opaque object containing calibration information for stray-light 

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

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

1281 Illumination correction image. 

1282 

1283 Returns 

1284 ------- 

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

1286 Result struct with component: 

1287 

1288 ``exposure`` 

1289 The fully ISR corrected exposure. 

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

1291 ``outputExposure`` 

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

1293 ``ossThumb`` 

1294 Thumbnail image of the exposure after overscan subtraction. 

1295 (`numpy.ndarray`) 

1296 ``flattenedThumb`` 

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

1298 (`numpy.ndarray`) 

1299 ``outputStatistics`` 

1300 Values of the additional statistics calculated. 

1301 

1302 Raises 

1303 ------ 

1304 RuntimeError 

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

1306 required calibration data has not been specified. 

1307 

1308 Notes 

1309 ----- 

1310 The current processed exposure can be viewed by setting the 

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

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

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

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

1315 option check and after the processing of that step has 

1316 finished. The steps with debug points are: 

1317 

1318 * doAssembleCcd 

1319 * doBias 

1320 * doCrosstalk 

1321 * doBrighterFatter 

1322 * doDark 

1323 * doFringe 

1324 * doStrayLight 

1325 * doFlat 

1326 

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

1328 exposure after all ISR processing has finished. 

1329 """ 

1330 

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

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

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

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

1335 

1336 ccd = ccdExposure.getDetector() 

1337 filterLabel = ccdExposure.getFilter() 

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

1339 

1340 if not ccd: 1340 ↛ 1341line 1340 didn't jump to line 1341 because the condition on line 1340 was never true

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

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

1343 

1344 # Validate Input 

1345 if self.config.doBias and bias is None: 1345 ↛ 1346line 1345 didn't jump to line 1346 because the condition on line 1345 was never true

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

1347 if self.doLinearize(ccd) and linearizer is None: 1347 ↛ 1348line 1347 didn't jump to line 1348 because the condition on line 1347 was never true

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

1349 if self.config.doBrighterFatter and bfKernel is None: 1349 ↛ 1350line 1349 didn't jump to line 1350 because the condition on line 1349 was never true

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

1351 if self.config.doDark and dark is None: 1351 ↛ 1352line 1351 didn't jump to line 1352 because the condition on line 1351 was never true

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

1353 if self.config.doFlat and flat is None: 1353 ↛ 1354line 1353 didn't jump to line 1354 because the condition on line 1353 was never true

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

1355 if self.config.doDefect and defects is None: 1355 ↛ 1356line 1355 didn't jump to line 1356 because the condition on line 1355 was never true

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

1357 if (self.config.doFringe and physicalFilter in self.fringe.config.filters 1357 ↛ 1363line 1357 didn't jump to line 1363 because the condition on line 1357 was never true

1358 and fringes.fringes is None): 

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

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

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

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

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

1364 if (self.config.doIlluminationCorrection and physicalFilter in self.config.illumFilters 1364 ↛ 1366line 1364 didn't jump to line 1366 because the condition on line 1364 was never true

1365 and illumMaskedImage is None): 

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

1367 if (self.config.doDeferredCharge and deferredChargeCalib is None): 1367 ↛ 1368line 1367 didn't jump to line 1368 because the condition on line 1367 was never true

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

1369 if (self.config.usePtcGains and ptc is None): 1369 ↛ 1370line 1369 didn't jump to line 1370 because the condition on line 1369 was never true

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

1371 if (self.config.usePtcReadNoise and ptc is None): 1371 ↛ 1372line 1371 didn't jump to line 1372 because the condition on line 1371 was never true

1372 raise RuntimeError("No ptcDataset provided to use PTC read noise.") 

1373 

1374 # Validate that the inputs match the exposure configuration. 

1375 exposureMetadata = ccdExposure.getMetadata() 

1376 doRaise = self.config.doRaiseOnCalibMismatch 

1377 keywords = self.config.cameraKeywordsToCompare 

1378 if self.config.doBias: 

1379 compareCameraKeywords(doRaise, keywords, exposureMetadata, bias, "bias", log=self.log) 

1380 self.compareUnits(bias.metadata, "bias") 

1381 if self.config.doBrighterFatter: 

1382 compareCameraKeywords(doRaise, keywords, exposureMetadata, bfKernel, "bf", log=self.log) 

1383 if self.config.doCrosstalk: 

1384 compareCameraKeywords(doRaise, keywords, exposureMetadata, crosstalk, "crosstalk", log=self.log) 

1385 if self.config.doDark: 

1386 compareCameraKeywords(doRaise, keywords, exposureMetadata, dark, "dark", log=self.log) 

1387 self.compareUnits(dark.metadata, "dark") 

1388 if self.config.doDefect: 

1389 compareCameraKeywords(doRaise, keywords, exposureMetadata, defects, "defects", log=self.log) 

1390 if self.config.doDeferredCharge: 1390 ↛ 1391line 1390 didn't jump to line 1391 because the condition on line 1390 was never true

1391 compareCameraKeywords( 

1392 doRaise, 

1393 keywords, 

1394 exposureMetadata, 

1395 deferredChargeCalib, 

1396 "CTI", 

1397 log=self.log, 

1398 ) 

1399 if self.config.doFlat: 

1400 compareCameraKeywords(doRaise, keywords, exposureMetadata, flat, "flat", log=self.log) 

1401 self.compareUnits(flat.metadata, "flat") 

1402 if (self.config.doFringe and physicalFilter in self.fringe.config.filters): 1402 ↛ 1403line 1402 didn't jump to line 1403 because the condition on line 1402 was never true

1403 compareCameraKeywords( 

1404 doRaise, 

1405 keywords, 

1406 exposureMetadata, 

1407 fringes.fringes, 

1408 "fringe", 

1409 log=self.log, 

1410 ) 

1411 if (self.config.doIlluminationCorrection and physicalFilter in self.config.illumFilters): 1411 ↛ 1412line 1411 didn't jump to line 1412 because the condition on line 1411 was never true

1412 compareCameraKeywords( 

1413 doRaise, 

1414 keywords, 

1415 exposureMetadata, 

1416 illumMaskedImage, 

1417 "illumination", 

1418 log=self.log, 

1419 ) 

1420 if self.doLinearize(ccd): 1420 ↛ 1421line 1420 didn't jump to line 1421 because the condition on line 1420 was never true

1421 compareCameraKeywords(doRaise, keywords, exposureMetadata, linearizer, "linearizer", log=self.log) 

1422 if self.config.usePtcGains or self.config.usePtcReadNoise: 1422 ↛ 1423line 1422 didn't jump to line 1423 because the condition on line 1422 was never true

1423 compareCameraKeywords(doRaise, keywords, exposureMetadata, ptc, "PTC", log=self.log) 

1424 if self.config.doStrayLight: 

1425 compareCameraKeywords( 

1426 doRaise, 

1427 keywords, 

1428 exposureMetadata, 

1429 strayLightData, 

1430 "straylight", 

1431 log=self.log, 

1432 ) 

1433 

1434 # Start in adu. Update units to electrons when gain is applied: 

1435 # updateVariance, applyGains 

1436 # Check if needed during/after BFE correction, CTI correction. 

1437 exposureMetadata["LSST ISR UNITS"] = "adu" 

1438 exposureMetadata["LSST ISR CROSSTALK APPLIED"] = False 

1439 exposureMetadata["LSST ISR LINEARIZER APPLIED"] = False 

1440 exposureMetadata["LSST ISR CTI APPLIED"] = False 

1441 exposureMetadata["LSST ISR BIAS APPLIED"] = False 

1442 exposureMetadata["LSST ISR DARK APPLIED"] = False 

1443 exposureMetadata["LSST ISR BF APPLIED"] = False 

1444 exposureMetadata["LSST ISR FLAT APPLIED"] = False 

1445 exposureMetadata["LSST ISR DEFECTS APPLIED"] = False 

1446 

1447 # Begin ISR processing. 

1448 if self.config.doConvertIntToFloat: 

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

1450 ccdExposure = self.convertIntToFloat(ccdExposure) 

1451 

1452 if self.config.doBias and self.config.doBiasBeforeOverscan: 1452 ↛ 1453line 1452 didn't jump to line 1453 because the condition on line 1452 was never true

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

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

1455 trimToFit=self.config.doTrimToMatchCalib) 

1456 self.debugView(ccdExposure, "doBias") 

1457 ccdExposure.metadata["LSST ISR BIAS APPLIED"] = True 

1458 

1459 # Amplifier level processing. 

1460 overscans = [] 

1461 

1462 if self.config.doOverscan and self.config.overscan.doParallelOverscan: 

1463 # This will attempt to mask bleed pixels across all amplifiers. 

1464 self.overscan.maskParallelOverscan(ccdExposure, ccd) 

1465 

1466 for amp in ccd: 

1467 # if ccdExposure is one amp, 

1468 # check for coverage to prevent performing ops multiple times 

1469 if ccdExposure.getBBox().contains(amp.getBBox()): 1469 ↛ 1518line 1469 didn't jump to line 1518 because the condition on line 1469 was always true

1470 # Check for fully masked bad amplifiers, 

1471 # and generate masks for SUSPECT and SATURATED values. 

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

1473 

1474 if self.config.doOverscan and not badAmp: 

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

1476 overscanResults = self.overscanCorrection(ccdExposure, amp) 

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

1478 if overscanResults is not None and \ 1478 ↛ 1516line 1478 didn't jump to line 1516 because the condition on line 1478 was always true

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

1480 if isinstance(overscanResults.overscanMean, float): 

1481 # Only serial overscan was run 

1482 mean = overscanResults.overscanMean 

1483 median = overscanResults.overscanMedian 

1484 sigma = overscanResults.overscanSigma 

1485 residMean = overscanResults.residualMean 

1486 residMedian = overscanResults.residualMedian 

1487 residSigma = overscanResults.residualSigma 

1488 else: 

1489 # Both serial and parallel overscan were 

1490 # run. Only report serial here. 

1491 mean = overscanResults.overscanMean[0] 

1492 median = overscanResults.overscanMedian[0] 

1493 sigma = overscanResults.overscanSigma[0] 

1494 residMean = overscanResults.residualMean[0] 

1495 residMedian = overscanResults.residualMedian[0] 

1496 residSigma = overscanResults.residualSigma[0] 

1497 

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

1499 self.metadata[f"FIT MEAN {amp.getName()}"] = mean 

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

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

1502 amp.getName(), mean, sigma) 

1503 

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

1505 self.metadata[f"RESIDUAL MEAN {amp.getName()}"] = residMean 

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

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

1508 amp.getName(), residMean, residSigma) 

1509 

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

1511 else: 

1512 if badAmp: 1512 ↛ 1513line 1512 didn't jump to line 1513 because the condition on line 1512 was never true

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

1514 overscanResults = None 

1515 

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

1517 else: 

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

1519 

1520 # Define an effective PTC that will contain the gain and readout 

1521 # noise to be used throughout the ISR task. 

1522 ptc = self.defineEffectivePtc(ptc, ccd, bfGains, overscans, exposureMetadata) 

1523 

1524 if self.config.doDeferredCharge: 1524 ↛ 1525line 1524 didn't jump to line 1525 because the condition on line 1524 was never true

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

1526 self.deferredChargeCorrection.run( 

1527 ccdExposure, 

1528 deferredChargeCalib, 

1529 gains=ptc.gain, 

1530 ) 

1531 self.debugView(ccdExposure, "doDeferredCharge") 

1532 ccdExposure.metadata["LSST ISR CTI APPLIED"] = True 

1533 

1534 if self.config.doCrosstalk and self.config.doCrosstalkBeforeAssemble: 1534 ↛ 1535line 1534 didn't jump to line 1535 because the condition on line 1534 was never true

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

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

1537 crosstalkSources=crosstalkSources, camera=camera) 

1538 self.debugView(ccdExposure, "doCrosstalk") 

1539 ccdExposure.metadata["LSST ISR CROSSTALK APPLIED"] = True 

1540 

1541 if self.config.doAssembleCcd: 

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

1543 ccdExposure = self.assembleCcd.assembleCcd(ccdExposure) 

1544 

1545 if self.config.expectWcs and not ccdExposure.getWcs(): 1545 ↛ 1546line 1545 didn't jump to line 1546 because the condition on line 1545 was never true

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

1547 self.debugView(ccdExposure, "doAssembleCcd") 

1548 

1549 ossThumb = None 

1550 if self.config.qa.doThumbnailOss: 

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

1552 

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

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

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

1556 trimToFit=self.config.doTrimToMatchCalib) 

1557 self.debugView(ccdExposure, "doBias") 

1558 ccdExposure.metadata["LSST ISR BIAS APPLIED"] = True 

1559 

1560 if self.config.doVariance: 

1561 for amp in ccd: 

1562 if ccdExposure.getBBox().contains(amp.getBBox()): 1562 ↛ 1561line 1562 didn't jump to line 1561 because the condition on line 1562 was always true

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

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

1565 self.updateVariance(ampExposure, amp, ptc) 

1566 

1567 if self.config.qa is not None and self.config.qa.saveStats is True: 1567 ↛ 1561line 1567 didn't jump to line 1561 because the condition on line 1567 was always true

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

1569 afwMath.MEDIAN | afwMath.STDEVCLIP) 

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

1571 qaStats.getValue(afwMath.MEDIAN) 

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

1573 qaStats.getValue(afwMath.STDEVCLIP) 

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

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

1576 qaStats.getValue(afwMath.STDEVCLIP)) 

1577 if self.config.maskNegativeVariance: 

1578 self.maskNegativeVariance(ccdExposure) 

1579 

1580 if self.doLinearize(ccd): 1580 ↛ 1581line 1580 didn't jump to line 1581 because the condition on line 1580 was never true

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

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

1583 detector=ccd, log=self.log) 

1584 ccdExposure.metadata["LSST ISR LINEARIZER APPLIED"] = True 

1585 

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

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

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

1589 crosstalkSources=crosstalkSources) 

1590 self.debugView(ccdExposure, "doCrosstalk") 

1591 ccdExposure.metadata["LSST ISR CROSSTALK APPLIED"] = True 

1592 

1593 # Masking block. Optionally mask known defects,NaN/inf pixels, 

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

1595 # suspect pixels have already been masked. 

1596 if self.config.doDefect: 

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

1598 self.maskDefect(ccdExposure, defects) 

1599 ccdExposure.metadata["LSST ISR DEFECTS APPLIED"] = True 

1600 

1601 if self.config.numEdgeSuspect > 0: 

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

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

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

1605 

1606 if self.config.doNanMasking: 

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

1608 self.maskNan(ccdExposure) 

1609 

1610 if self.config.doWidenSaturationTrails: 

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

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

1613 

1614 if self.config.doCameraSpecificMasking: 

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

1616 self.masking.run(ccdExposure) 

1617 

1618 if self.config.doBrighterFatter: 

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

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

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

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

1623 # and flats. 

1624 # 

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

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

1627 # back the interpolation. 

1628 interpExp = ccdExposure.clone() 

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

1630 isrFunctions.interpolateFromMask( 

1631 maskedImage=interpExp.getMaskedImage(), 

1632 fwhm=self.config.fwhm, 

1633 growSaturatedFootprints=self.config.growSaturationFootprintSize, 

1634 maskNameList=list(self.config.brighterFatterMaskListToInterpolate), 

1635 useLegacyInterp=self.config.useLegacyInterp, 

1636 ) 

1637 bfExp = interpExp.clone() 

1638 

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

1640 type(bfKernel), type(bfGains)) 

1641 if self.config.doFluxConservingBrighterFatterCorrection: 1641 ↛ 1642line 1641 didn't jump to line 1642 because the condition on line 1641 was never true

1642 bfResults = fluxConservingBrighterFatterCorrection( 

1643 bfExp, 

1644 bfKernel, 

1645 self.config.brighterFatterMaxIter, 

1646 self.config.brighterFatterThreshold, 

1647 self.config.brighterFatterApplyGain, 

1648 bfGains 

1649 ) 

1650 else: 

1651 bfResults = brighterFatterCorrection( 

1652 bfExp, 

1653 bfKernel, 

1654 self.config.brighterFatterMaxIter, 

1655 self.config.brighterFatterThreshold, 

1656 self.config.brighterFatterApplyGain, 

1657 bfGains 

1658 ) 

1659 bfCorrIters = bfResults[1] 

1660 self.metadata["LSST ISR BF ITERS"] = bfCorrIters 

1661 if bfCorrIters == self.config.brighterFatterMaxIter - 1: 1661 ↛ 1665line 1661 didn't jump to line 1665 because the condition on line 1661 was always true

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

1663 bfResults[0]) 

1664 else: 

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

1666 bfResults[1]) 

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

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

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

1670 image += bfCorr 

1671 

1672 # Applying the brighter-fatter correction applies a 

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

1674 # convolution may not have sufficient valid pixels to 

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

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

1677 # fact. 

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

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

1680 maskPlane="EDGE") 

1681 

1682 if self.config.brighterFatterMaskGrowSize > 0: 1682 ↛ 1683line 1682 didn't jump to line 1683 because the condition on line 1682 was never true

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

1684 for maskPlane in self.config.brighterFatterMaskListToInterpolate: 

1685 isrFunctions.growMasks(ccdExposure.getMask(), 

1686 radius=self.config.brighterFatterMaskGrowSize, 

1687 maskNameList=maskPlane, 

1688 maskValue=maskPlane) 

1689 

1690 self.debugView(ccdExposure, "doBrighterFatter") 

1691 ccdExposure.metadata["LSST ISR BF APPLIED"] = True 

1692 

1693 if self.config.doDark: 

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

1695 self.darkCorrection(ccdExposure, dark) 

1696 self.debugView(ccdExposure, "doDark") 

1697 ccdExposure.metadata["LSST ISR DARK APPLIED"] = True 

1698 

1699 if self.config.doFringe and not self.config.fringeAfterFlat: 1699 ↛ 1700line 1699 didn't jump to line 1700 because the condition on line 1699 was never true

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

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

1702 self.debugView(ccdExposure, "doFringe") 

1703 

1704 if self.config.doStrayLight and self.strayLight.check(ccdExposure): 1704 ↛ 1705line 1704 didn't jump to line 1705 because the condition on line 1704 was never true

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

1706 self.strayLight.run(ccdExposure, strayLightData) 

1707 self.debugView(ccdExposure, "doStrayLight") 

1708 

1709 if self.config.doFlat: 

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

1711 self.flatCorrection(ccdExposure, flat) 

1712 self.debugView(ccdExposure, "doFlat") 

1713 ccdExposure.metadata["LSST ISR FLAT APPLIED"] = True 

1714 # TODO: DM-49159 

1715 # Add metadata re: type of flat. 

1716 

1717 if self.config.doApplyGains: 

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

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

1720 ptcGains=ptc.gain) 

1721 exposureMetadata["LSST ISR UNITS"] = "electron" 

1722 

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

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

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

1726 

1727 if self.config.doVignette: 

1728 if self.config.doMaskVignettePolygon: 1728 ↛ 1731line 1728 didn't jump to line 1731 because the condition on line 1728 was always true

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

1730 else: 

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

1732 self.vignettePolygon = self.vignette.run( 

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

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

1735 

1736 if self.config.doAttachTransmissionCurve: 

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

1738 isrFunctions.attachTransmissionCurve(ccdExposure, opticsTransmission=opticsTransmission, 

1739 filterTransmission=filterTransmission, 

1740 sensorTransmission=sensorTransmission, 

1741 atmosphereTransmission=atmosphereTransmission) 

1742 

1743 flattenedThumb = None 

1744 if self.config.qa.doThumbnailFlattened: 

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

1746 

1747 if self.config.doIlluminationCorrection and physicalFilter in self.config.illumFilters: 1747 ↛ 1748line 1747 didn't jump to line 1748 because the condition on line 1747 was never true

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

1749 isrFunctions.illuminationCorrection(ccdExposure.getMaskedImage(), 

1750 illumMaskedImage, illumScale=self.config.illumScale, 

1751 trimToFit=self.config.doTrimToMatchCalib) 

1752 

1753 preInterpExp = None 

1754 if self.config.doSaveInterpPixels: 1754 ↛ 1755line 1754 didn't jump to line 1755 because the condition on line 1754 was never true

1755 preInterpExp = ccdExposure.clone() 

1756 

1757 # Reset and interpolate bad pixels. 

1758 # 

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

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

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

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

1763 # reason to expect that interpolation would provide a more 

1764 # useful value. 

1765 # 

1766 # Smaller defects can be safely interpolated after the larger 

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

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

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

1770 if self.config.doSetBadRegions: 

1771 badPixelCount, badPixelValue = isrFunctions.setBadRegions(ccdExposure) 

1772 if badPixelCount > 0: 

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

1774 

1775 if self.config.doInterpolate: 

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

1777 isrFunctions.interpolateFromMask( 

1778 maskedImage=ccdExposure.getMaskedImage(), 

1779 fwhm=self.config.fwhm, 

1780 growSaturatedFootprints=self.config.growSaturationFootprintSize, 

1781 maskNameList=list(self.config.maskListToInterpolate), 

1782 useLegacyInterp=self.config.useLegacyInterp, 

1783 ) 

1784 

1785 self.roughZeroPoint(ccdExposure) 

1786 

1787 # Calculate amp offset corrections within the CCD. 

1788 if self.config.doAmpOffset: 1788 ↛ 1789line 1788 didn't jump to line 1789 because the condition on line 1788 was never true

1789 if self.config.ampOffset.doApplyAmpOffset: 

1790 self.log.info("Measuring and applying amp offset corrections.") 

1791 else: 

1792 self.log.info("Measuring amp offset corrections only, without applying them.") 

1793 self.ampOffset.run(ccdExposure) 

1794 

1795 if self.config.doMeasureBackground: 

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

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

1798 

1799 if self.config.qa is not None and self.config.qa.saveStats is True: 1799 ↛ 1812line 1799 didn't jump to line 1812 because the condition on line 1799 was always true

1800 for amp in ccd: 

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

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

1803 afwMath.MEDIAN | afwMath.STDEVCLIP) 

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

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

1806 qaStats.getValue(afwMath.STDEVCLIP) 

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

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

1809 qaStats.getValue(afwMath.STDEVCLIP)) 

1810 

1811 # Calculate standard image quality statistics 

1812 if self.config.doStandardStatistics: 1812 ↛ 1840line 1812 didn't jump to line 1840 because the condition on line 1812 was always true

1813 metadata = ccdExposure.getMetadata() 

1814 for amp in ccd: 

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

1816 ampName = amp.getName() 

1817 metadata[f"LSST ISR MASK SAT {ampName}"] = isrFunctions.countMaskedPixels( 

1818 ampExposure.getMaskedImage(), 

1819 [self.config.saturatedMaskName] 

1820 ) 

1821 metadata[f"LSST ISR MASK BAD {ampName}"] = isrFunctions.countMaskedPixels( 

1822 ampExposure.getMaskedImage(), 

1823 ["BAD"] 

1824 ) 

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

1826 afwMath.MEAN | afwMath.MEDIAN | afwMath.STDEVCLIP) 

1827 

1828 metadata[f"LSST ISR FINAL MEAN {ampName}"] = qaStats.getValue(afwMath.MEAN) 

1829 metadata[f"LSST ISR FINAL MEDIAN {ampName}"] = qaStats.getValue(afwMath.MEDIAN) 

1830 metadata[f"LSST ISR FINAL STDEV {ampName}"] = qaStats.getValue(afwMath.STDEVCLIP) 

1831 

1832 k1 = f"LSST ISR FINAL MEDIAN {ampName}" 

1833 k2 = f"LSST ISR OVERSCAN SERIAL MEDIAN {ampName}" 

1834 if self.config.doOverscan and k1 in metadata and k2 in metadata: 

1835 metadata[f"LSST ISR LEVEL {ampName}"] = metadata[k1] - metadata[k2] 

1836 else: 

1837 metadata[f"LSST ISR LEVEL {ampName}"] = numpy.nan 

1838 

1839 # calculate additional statistics. 

1840 outputStatistics = None 

1841 if self.config.doCalculateStatistics: 1841 ↛ 1842line 1841 didn't jump to line 1842 because the condition on line 1841 was never true

1842 outputStatistics = self.isrStats.run(ccdExposure, serialOverscanResults=overscans, 

1843 parallelOverscanResults=[None for _ in overscans], 

1844 bias=bias, dark=dark, flat=flat, ptc=ptc, 

1845 defects=defects, 

1846 doLegacyCtiStatistics=True).results 

1847 

1848 # do any binning. 

1849 outputBin1Exposure = None 

1850 outputBin2Exposure = None 

1851 if self.config.doBinnedExposures: 

1852 self.log.info("Creating binned exposures.") 

1853 outputBin1Exposure = self.binning.run( 

1854 ccdExposure, 

1855 binFactor=self.config.binFactor1, 

1856 ).outputData 

1857 outputBin2Exposure = self.binning.run( 

1858 ccdExposure, 

1859 binFactor=self.config.binFactor2, 

1860 ).outputData 

1861 

1862 self.debugView(ccdExposure, "postISRCCD") 

1863 

1864 return pipeBase.Struct( 

1865 exposure=ccdExposure, 

1866 ossThumb=ossThumb, 

1867 flattenedThumb=flattenedThumb, 

1868 

1869 outputBin1Exposure=outputBin1Exposure, 

1870 outputBin2Exposure=outputBin2Exposure, 

1871 

1872 preInterpExposure=preInterpExp, 

1873 outputExposure=ccdExposure, 

1874 outputOssThumbnail=ossThumb, 

1875 outputFlattenedThumbnail=flattenedThumb, 

1876 outputStatistics=outputStatistics, 

1877 ) 

1878 

1879 def defineEffectivePtc(self, ptcDataset, detector, bfGains, overScans, metadata): 

1880 """Define an effective Photon Transfer Curve dataset 

1881 with nominal gains and noise. 

1882 

1883 Parameters 

1884 ---------- 

1885 ptcDataset : `lsst.ip.isr.PhotonTransferCurveDataset` 

1886 Input Photon Transfer Curve dataset. 

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

1888 Detector object. 

1889 bfGains : `dict` 

1890 Gains from running the brighter-fatter code. 

1891 A dict keyed by amplifier name for the detector 

1892 in question. 

1893 overScans : `list` [`lsst.pipe.base.Struct`] 

1894 List of overscanResults structures 

1895 metadata : `lsst.daf.base.PropertyList` 

1896 Exposure metadata to update gain and read noise 

1897 provenance. 

1898 

1899 Returns 

1900 ------- 

1901 effectivePtc : `lsst.ip.isr.PhotonTransferCurveDataset` 

1902 PTC dataset containing gains and readout noise 

1903 values to be used throughout 

1904 Instrument Signature Removal. 

1905 """ 

1906 amps = detector.getAmplifiers() 

1907 ampNames = [amp.getName() for amp in amps] 

1908 detName = detector.getName() 

1909 effectivePtc = PhotonTransferCurveDataset(ampNames, 'EFFECTIVE_PTC', 1) 

1910 boolGainMismatch = False 

1911 doWarningPtcValidation = True 

1912 

1913 for amp, overscanResults in zip(amps, overScans): 

1914 ampName = amp.getName() 

1915 # Gain: 

1916 # Try first with the PTC gains. 

1917 gainProvenanceString = "amp" 

1918 if self.config.usePtcGains: 

1919 gain = ptcDataset.gain[ampName] 

1920 gainProvenanceString = "ptc" 

1921 self.log.debug("Using gain from Photon Transfer Curve.") 

1922 else: 

1923 # Try then with the amplifier gain. 

1924 # We already have a detector at this point. If there was no 

1925 # detector to begin with, one would have been created with 

1926 # self.config.gain and self.config.noise. Same comment 

1927 # applies for the noise block below. 

1928 gain = amp.getGain() 

1929 

1930 # Check if the gain up to this point differs from the 

1931 # gain in bfGains. If so, raise or warn, accordingly. 

1932 if not boolGainMismatch and bfGains is not None and ampName in bfGains: 

1933 bfGain = bfGains[ampName] 

1934 if not math.isclose(gain, bfGain, rel_tol=1e-4): 1934 ↛ 1948line 1934 didn't jump to line 1948 because the condition on line 1934 was always true

1935 if self.config.doRaiseOnCalibMismatch: 

1936 raise RuntimeError("Gain mismatch for det %s amp %s: " 

1937 "(gain (%s): %s, bfGain: %s)", 

1938 detName, ampName, gainProvenanceString, 

1939 gain, bfGain) 

1940 else: 

1941 self.log.warning("Gain mismatch for det %s amp %s: " 

1942 "(gain (%s): %s, bfGain: %s)", 

1943 detName, ampName, gainProvenanceString, 

1944 gain, bfGain) 

1945 boolGainMismatch = True 

1946 

1947 # Gain: 

1948 if math.isnan(gain): 1948 ↛ 1949line 1948 didn't jump to line 1949 because the condition on line 1948 was never true

1949 gain = 1.0 

1950 self.log.warning("Gain for amp %s set to NaN! Updating to" 

1951 " 1.0 to generate Poisson variance.", ampName) 

1952 elif gain <= 0: 1952 ↛ 1953line 1952 didn't jump to line 1953 because the condition on line 1952 was never true

1953 patchedGain = 1.0 

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

1955 ampName, gain, patchedGain) 

1956 gain = patchedGain 

1957 

1958 # Noise: 

1959 # Try first with the empirical noise from the overscan. 

1960 noiseProvenanceString = "amp" 

1961 if self.config.doEmpiricalReadNoise and overscanResults is not None: 

1962 noiseProvenanceString = "serial overscan" 

1963 if isinstance(overscanResults.residualSigma, float): 

1964 # Only serial overscan was run 

1965 noise = overscanResults.residualSigma 

1966 else: 

1967 # Both serial and parallel overscan were 

1968 # run. Only report noise from serial here. 

1969 noise = overscanResults.residualSigma[0] 

1970 

1971 # Overscan noise is always in adu; we must standardize 

1972 # the read noise attributes of all 

1973 # PhotonTransferCurveDataset objects to units of 

1974 # electrons. 

1975 noise *= gain 

1976 elif self.config.usePtcReadNoise: 

1977 # Try then with the PTC noise. 

1978 self.log.debug("Using noise from Photon Transfer Curve.") 

1979 noise = ptcDataset.noise[ampName] 

1980 noiseProvenanceString = "ptc" 

1981 else: 

1982 # Finally, try with the amplifier noise. 

1983 # We already have a detector at this point. If there was no 

1984 # detector to begin with, one would have been created with 

1985 # self.config.gain and self.config.noise. 

1986 # Amplifier object read noise is always in electron 

1987 noise = amp.getReadNoise() 

1988 

1989 # PTC Turnoff: 

1990 # Copy it over from the input PTC if it's positive. If it's a nan 

1991 # set it to a high value. 

1992 if ptcDataset is not None: 

1993 ptcTurnoff = ptcDataset.ptcTurnoff[ampName] 

1994 else: 

1995 ptcTurnoff = 2e19 

1996 

1997 if (isinstance(ptcTurnoff, numbers.Real) and ptcTurnoff > 0): 

1998 effectivePtc.ptcTurnoff[ampName] = ptcTurnoff 

1999 elif math.isnan(ptcTurnoff): 1999 ↛ 2002line 1999 didn't jump to line 2002 because the condition on line 1999 was always true

2000 effectivePtc.ptcTurnoff[ampName] = 2e19 

2001 

2002 effectivePtc.gain[ampName] = gain 

2003 effectivePtc.noise[ampName] = noise 

2004 # Make sure read noise, turnoff, and gain make sense 

2005 effectivePtc.validateGainNoiseTurnoffValues(ampName, doWarn=doWarningPtcValidation) 

2006 doWarningPtcValidation = False 

2007 

2008 # These keys are duplicated for compatability with isrTaskLSST 

2009 metadata[f"LSST GAIN {amp.getName()}"] = effectivePtc.gain[ampName] 

2010 metadata[f"LSST ISR GAIN {amp.getName()}"] = effectivePtc.gain[ampName] 

2011 metadata[f"LSST READNOISE {amp.getName()}"] = effectivePtc.noise[ampName] 

2012 metadata[f"LSST ISR READNOISE {amp.getName()}"] = effectivePtc.noise[ampName] 

2013 

2014 self.log.info("Det: %s - Noise provenance: %s, Gain provenance: %s", 

2015 detName, 

2016 noiseProvenanceString, 

2017 gainProvenanceString) 

2018 metadata["LSST ISR GAIN SOURCE"] = gainProvenanceString 

2019 metadata["LSST ISR NOISE SOURCE"] = noiseProvenanceString 

2020 metadata["LSST ISR READNOISE UNITS"] = "electron" 

2021 

2022 return effectivePtc 

2023 

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

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

2026 

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

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

2029 modifying the input in place. 

2030 

2031 Parameters 

2032 ---------- 

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

2034 The input data structure obtained from Butler. 

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

2036 `lsst.afw.image.DecoratedImageU`, 

2037 or `lsst.afw.image.ImageF` 

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

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

2040 detector if detector is not already set. 

2041 detectorNum : `int`, optional 

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

2043 already set. 

2044 

2045 Returns 

2046 ------- 

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

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

2049 

2050 Raises 

2051 ------ 

2052 TypeError 

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

2054 """ 

2055 if isinstance(inputExp, afwImage.DecoratedImageU): 2055 ↛ 2056line 2055 didn't jump to line 2056 because the condition on line 2055 was never true

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

2057 elif isinstance(inputExp, afwImage.ImageF): 2057 ↛ 2058line 2057 didn't jump to line 2058 because the condition on line 2057 was never true

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

2059 elif isinstance(inputExp, afwImage.MaskedImageF): 2059 ↛ 2060line 2059 didn't jump to line 2060 because the condition on line 2059 was never true

2060 inputExp = afwImage.makeExposure(inputExp) 

2061 elif isinstance(inputExp, afwImage.Exposure): 

2062 pass 

2063 elif inputExp is None: 2063 ↛ 2067line 2063 didn't jump to line 2067 because the condition on line 2063 was always true

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

2065 return inputExp 

2066 else: 

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

2068 (type(inputExp), )) 

2069 

2070 if inputExp.getDetector() is None: 2070 ↛ 2071line 2070 didn't jump to line 2071 because the condition on line 2070 was never true

2071 if camera is None or detectorNum is None: 

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

2073 'without a detector set.') 

2074 inputExp.setDetector(camera[detectorNum]) 

2075 

2076 return inputExp 

2077 

2078 @staticmethod 

2079 def extractCalibDate(calib): 

2080 """Extract common calibration metadata values that will be written to 

2081 output header. 

2082 

2083 Parameters 

2084 ---------- 

2085 calib : `lsst.afw.image.Exposure` or `lsst.ip.isr.IsrCalib` 

2086 Calibration to pull date information from. 

2087 

2088 Returns 

2089 ------- 

2090 dateString : `str` 

2091 Calibration creation date string to add to header. 

2092 """ 

2093 if hasattr(calib, "getMetadata"): 

2094 if 'CALIB_CREATION_DATE' in calib.getMetadata(): 

2095 return " ".join((calib.getMetadata().get("CALIB_CREATION_DATE", "Unknown"), 

2096 calib.getMetadata().get("CALIB_CREATION_TIME", "Unknown"))) 

2097 else: 

2098 return " ".join((calib.getMetadata().get("CALIB_CREATE_DATE", "Unknown"), 

2099 calib.getMetadata().get("CALIB_CREATE_TIME", "Unknown"))) 

2100 else: 

2101 return "Unknown Unknown" 

2102 

2103 def compareUnits(self, calibMetadata, calibName): 

2104 """Compare units from calibration to ISR units. 

2105 

2106 For the regular IsrTask this is used to confirm that calibs 

2107 suitable for IsrTaskLSST are not used with the old IsrTask. 

2108 

2109 Parameters 

2110 ---------- 

2111 calibMetadata : `lsst.daf.base.PropertyList` 

2112 Calibration metadata from header. 

2113 calibName : `str` 

2114 Calibration name for log message. 

2115 """ 

2116 calibUnits = calibMetadata.get("LSST ISR UNITS", "adu") 

2117 isrUnits = "adu" 

2118 if calibUnits != isrUnits: 2118 ↛ 2119line 2118 didn't jump to line 2119 because the condition on line 2118 was never true

2119 if self.config.doRaiseOnCalibMismatch: 

2120 raise RuntimeError( 

2121 "Unit mismatch: isr has %s units but %s has %s units", 

2122 isrUnits, 

2123 calibName, 

2124 calibUnits, 

2125 ) 

2126 else: 

2127 self.log.warning( 

2128 "Unit mismatch: isr has %s units but %s has %s units", 

2129 isrUnits, 

2130 calibName, 

2131 calibUnits, 

2132 ) 

2133 

2134 def convertIntToFloat(self, exposure): 

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

2136 

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

2138 immediately returned. For exposures that are converted to use 

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

2140 mask to zero. 

2141 

2142 Parameters 

2143 ---------- 

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

2145 The raw exposure to be converted. 

2146 

2147 Returns 

2148 ------- 

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

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

2151 

2152 Raises 

2153 ------ 

2154 RuntimeError 

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

2156 

2157 """ 

2158 if isinstance(exposure, afwImage.ExposureF): 2158 ↛ 2162line 2158 didn't jump to line 2162 because the condition on line 2158 was always true

2159 # Nothing to be done 

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

2161 return exposure 

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

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

2164 

2165 newexposure = exposure.convertF() 

2166 newexposure.variance[:] = 1 

2167 newexposure.mask[:] = 0x0 

2168 

2169 return newexposure 

2170 

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

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

2173 

2174 Parameters 

2175 ---------- 

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

2177 Input exposure to be masked. 

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

2179 Catalog of parameters defining the amplifier on this 

2180 exposure to mask. 

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

2182 List of defects. Used to determine if the entire 

2183 amplifier is bad. 

2184 

2185 Returns 

2186 ------- 

2187 badAmp : `Bool` 

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

2189 defects and unusable. 

2190 

2191 """ 

2192 maskedImage = ccdExposure.getMaskedImage() 

2193 

2194 badAmp = False 

2195 

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

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

2198 # defects definition. 

2199 if defects is not None: 

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

2201 

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

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

2204 # current ccdExposure). 

2205 if badAmp: 2205 ↛ 2206line 2205 didn't jump to line 2206 because the condition on line 2205 was never true

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

2207 afwImage.PARENT) 

2208 maskView = dataView.getMask() 

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

2210 del maskView 

2211 return badAmp 

2212 

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

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

2215 # masked now, though. 

2216 limits = dict() 

2217 if self.config.doSaturation and not badAmp: 

2218 # Set to the default from the camera model. 

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

2220 # And update if it is set in the config. 

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

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

2223 if self.config.doSuspect and not badAmp: 

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

2225 

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

2227 if not math.isnan(maskThreshold): 2227 ↛ 2226line 2227 didn't jump to line 2226 because the condition on line 2227 was always true

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

2229 isrFunctions.makeThresholdMask( 

2230 maskedImage=dataView, 

2231 threshold=maskThreshold, 

2232 growFootprints=0, 

2233 maskName=maskName 

2234 ) 

2235 

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

2237 # SAT pixels. 

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

2239 afwImage.PARENT) 

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

2241 self.config.suspectMaskName]) 

2242 if numpy.all(maskView.getArray() & maskVal > 0): 2242 ↛ 2243line 2242 didn't jump to line 2243 because the condition on line 2242 was never true

2243 badAmp = True 

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

2245 

2246 return badAmp 

2247 

2248 def overscanCorrection(self, ccdExposure, amp): 

2249 """Apply overscan correction in place. 

2250 

2251 This method does initial pixel rejection of the overscan 

2252 region. The overscan can also be optionally segmented to 

2253 allow for discontinuous overscan responses to be fit 

2254 separately. The actual overscan subtraction is performed by 

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

2256 after the amplifier is preprocessed. 

2257 

2258 Parameters 

2259 ---------- 

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

2261 Exposure to have overscan correction performed. 

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

2263 The amplifier to consider while correcting the overscan. 

2264 

2265 Returns 

2266 ------- 

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

2268 Result struct with components: 

2269 

2270 ``imageFit`` 

2271 Value or fit subtracted from the amplifier image data. 

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

2273 ``overscanFit`` 

2274 Value or fit subtracted from the overscan image data. 

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

2276 ``overscanImage`` 

2277 Image of the overscan region with the overscan 

2278 correction applied. This quantity is used to estimate 

2279 the amplifier read noise empirically. 

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

2281 ``edgeMask`` 

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

2283 ``overscanMean`` 

2284 Median overscan fit value. (`float`) 

2285 ``overscanSigma`` 

2286 Clipped standard deviation of the overscan after 

2287 correction. (`float`) 

2288 

2289 Raises 

2290 ------ 

2291 RuntimeError 

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

2293 

2294 See Also 

2295 -------- 

2296 lsst.ip.isr.overscan.OverscanTask 

2297 """ 

2298 if amp.getRawHorizontalOverscanBBox().isEmpty(): 2298 ↛ 2299line 2298 didn't jump to line 2299 because the condition on line 2298 was never true

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

2300 return None 

2301 

2302 # Perform overscan correction on subregions. 

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

2304 

2305 metadata = ccdExposure.getMetadata() 

2306 ampName = amp.getName() 

2307 

2308 keyBase = "LSST ISR OVERSCAN" 

2309 # The overscan statistics units will always match the units of 

2310 # the image at the point they are calculated. 

2311 metadata[f"{keyBase} SERIAL UNITS"] = metadata.get("LSST ISR UNITS") 

2312 

2313 # Updated quantities 

2314 if isinstance(overscanResults.overscanMean, float): 

2315 # Serial overscan correction only: 

2316 metadata[f"{keyBase} SERIAL MEAN {ampName}"] = overscanResults.overscanMean 

2317 metadata[f"{keyBase} SERIAL MEDIAN {ampName}"] = overscanResults.overscanMedian 

2318 metadata[f"{keyBase} SERIAL STDEV {ampName}"] = overscanResults.overscanSigma 

2319 

2320 metadata[f"{keyBase} RESIDUAL SERIAL MEAN {ampName}"] = overscanResults.residualMean 

2321 metadata[f"{keyBase} RESIDUAL SERIAL MEDIAN {ampName}"] = overscanResults.residualMedian 

2322 metadata[f"{keyBase} RESIDUAL SERIAL STDEV {ampName}"] = overscanResults.residualSigma 

2323 elif isinstance(overscanResults.overscanMean, tuple): 2323 ↛ 2342line 2323 didn't jump to line 2342 because the condition on line 2323 was always true

2324 # Both serial and parallel overscan have run: 

2325 metadata[f"{keyBase} PARALLEL UNITS"] = metadata.get("LSST ISR UNITS") 

2326 metadata[f"{keyBase} SERIAL MEAN {ampName}"] = overscanResults.overscanMean[0] 

2327 metadata[f"{keyBase} SERIAL MEDIAN {ampName}"] = overscanResults.overscanMedian[0] 

2328 metadata[f"{keyBase} SERIAL STDEV {ampName}"] = overscanResults.overscanSigma[0] 

2329 

2330 metadata[f"{keyBase} PARALLEL MEAN {ampName}"] = overscanResults.overscanMean[1] 

2331 metadata[f"{keyBase} PARALLEL MEDIAN {ampName}"] = overscanResults.overscanMedian[1] 

2332 metadata[f"{keyBase} PARALLEL STDEV {ampName}"] = overscanResults.overscanSigma[1] 

2333 

2334 metadata[f"{keyBase} RESIDUAL SERIAL MEAN {ampName}"] = overscanResults.residualMean[0] 

2335 metadata[f"{keyBase} RESIDUAL SERIAL MEDIAN {ampName}"] = overscanResults.residualMedian[0] 

2336 metadata[f"{keyBase} RESIDUAL SERIAL STDEV {ampName}"] = overscanResults.residualSigma[0] 

2337 

2338 metadata[f"{keyBase} RESIDUAL PARALLEL MEAN {ampName}"] = overscanResults.residualMean[1] 

2339 metadata[f"{keyBase} RESIDUAL PARALLEL MEDIAN {ampName}"] = overscanResults.residualMedian[1] 

2340 metadata[f"{keyBase} RESIDUAL PARALLEL STDEV {ampName}"] = overscanResults.residualSigma[1] 

2341 else: 

2342 self.log.warning("Unexpected type for overscan values; none added to header.") 

2343 

2344 return overscanResults 

2345 

2346 def updateVariance(self, ampExposure, amp, ptcDataset): 

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

2348 

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

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

2351 the value from the amplifier data is used. 

2352 

2353 Parameters 

2354 ---------- 

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

2356 Exposure to process. 

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

2358 Amplifier detector data. 

2359 ptcDataset : `lsst.ip.isr.PhotonTransferCurveDataset` 

2360 Effective PTC dataset containing the gains and read noise. 

2361 

2362 See also 

2363 -------- 

2364 lsst.ip.isr.isrFunctions.updateVariance 

2365 """ 

2366 ampName = amp.getName() 

2367 # At this point, the effective PTC should have 

2368 # gain and read noise values. 

2369 gain = ptcDataset.gain[ampName] 

2370 readNoise = ptcDataset.noise[ampName] 

2371 

2372 # The image will always be in adu and the noise 

2373 # will always be in electrons, and we will output 

2374 # the variance plane in the image units (adu^2). 

2375 isrFunctions.updateVariance( 

2376 maskedImage=ampExposure.getMaskedImage(), 

2377 gain=gain, 

2378 readNoise=readNoise, 

2379 ) 

2380 

2381 def maskNegativeVariance(self, exposure): 

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

2383 

2384 Parameters 

2385 ---------- 

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

2387 Exposure to process. 

2388 

2389 See Also 

2390 -------- 

2391 lsst.ip.isr.isrFunctions.updateVariance 

2392 """ 

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

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

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

2396 

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

2398 """Apply dark correction in place. 

2399 

2400 Parameters 

2401 ---------- 

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

2403 Exposure to process. 

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

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

2406 invert : `Bool`, optional 

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

2408 

2409 Raises 

2410 ------ 

2411 RuntimeError 

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

2413 have their dark time defined. 

2414 

2415 See Also 

2416 -------- 

2417 lsst.ip.isr.isrFunctions.darkCorrection 

2418 """ 

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

2420 if math.isnan(expScale): 

2421 raise RuntimeError("Exposure darktime is NaN.") 

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

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

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

2425 else: 

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

2427 # so getDarkTime() does not exist. 

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

2429 darkScale = 1.0 

2430 

2431 isrFunctions.darkCorrection( 

2432 maskedImage=exposure.getMaskedImage(), 

2433 darkMaskedImage=darkExposure.getMaskedImage(), 

2434 expScale=expScale, 

2435 darkScale=darkScale, 

2436 invert=invert, 

2437 trimToFit=self.config.doTrimToMatchCalib 

2438 ) 

2439 

2440 def doLinearize(self, detector): 

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

2442 

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

2444 amplifier. 

2445 

2446 Parameters 

2447 ---------- 

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

2449 Detector to get linearity type from. 

2450 

2451 Returns 

2452 ------- 

2453 doLinearize : `Bool` 

2454 If True, linearization should be performed. 

2455 """ 

2456 return self.config.doLinearize and \ 

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

2458 

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

2460 """Apply flat correction in place. 

2461 

2462 Parameters 

2463 ---------- 

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

2465 Exposure to process. 

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

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

2468 invert : `Bool`, optional 

2469 If True, unflatten an already flattened image. 

2470 

2471 See Also 

2472 -------- 

2473 lsst.ip.isr.isrFunctions.flatCorrection 

2474 """ 

2475 isrFunctions.flatCorrection( 

2476 maskedImage=exposure.getMaskedImage(), 

2477 flatMaskedImage=flatExposure.getMaskedImage(), 

2478 scalingType=self.config.flatScalingType, 

2479 userScale=self.config.flatUserScale, 

2480 invert=invert, 

2481 trimToFit=self.config.doTrimToMatchCalib 

2482 ) 

2483 

2484 def saturationDetection(self, exposure, amp): 

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

2486 

2487 Parameters 

2488 ---------- 

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

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

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

2492 Amplifier detector data. 

2493 

2494 See Also 

2495 -------- 

2496 lsst.ip.isr.isrFunctions.makeThresholdMask 

2497 """ 

2498 if not math.isnan(amp.getSaturation()): 2498 ↛ exitline 2498 didn't return from function 'saturationDetection' because the condition on line 2498 was always true

2499 maskedImage = exposure.getMaskedImage() 

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

2501 isrFunctions.makeThresholdMask( 

2502 maskedImage=dataView, 

2503 threshold=amp.getSaturation(), 

2504 growFootprints=0, 

2505 maskName=self.config.saturatedMaskName, 

2506 ) 

2507 

2508 def saturationInterpolation(self, exposure): 

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

2510 

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

2512 ensure that the saturated pixels have been identified in the 

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

2514 saturated regions may cross amplifier boundaries. 

2515 

2516 Parameters 

2517 ---------- 

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

2519 Exposure to process. 

2520 

2521 See Also 

2522 -------- 

2523 lsst.ip.isr.isrTask.saturationDetection 

2524 lsst.ip.isr.isrFunctions.interpolateFromMask 

2525 """ 

2526 isrFunctions.interpolateFromMask( 

2527 maskedImage=exposure.getMaskedImage(), 

2528 fwhm=self.config.fwhm, 

2529 growSaturatedFootprints=self.config.growSaturationFootprintSize, 

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

2531 useLegacyInterp=self.config.useLegacyInterp, 

2532 ) 

2533 

2534 def suspectDetection(self, exposure, amp): 

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

2536 

2537 Parameters 

2538 ---------- 

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

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

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

2542 Amplifier detector data. 

2543 

2544 See Also 

2545 -------- 

2546 lsst.ip.isr.isrFunctions.makeThresholdMask 

2547 

2548 Notes 

2549 ----- 

2550 Suspect pixels are pixels whose value is greater than 

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

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

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

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

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

2556 """ 

2557 suspectLevel = amp.getSuspectLevel() 

2558 if math.isnan(suspectLevel): 

2559 return 

2560 

2561 maskedImage = exposure.getMaskedImage() 

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

2563 isrFunctions.makeThresholdMask( 

2564 maskedImage=dataView, 

2565 threshold=suspectLevel, 

2566 growFootprints=0, 

2567 maskName=self.config.suspectMaskName, 

2568 ) 

2569 

2570 def maskDefect(self, exposure, defectBaseList): 

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

2572 

2573 Parameters 

2574 ---------- 

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

2576 Exposure to process. 

2577 defectBaseList : defect-type 

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

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

2580 

2581 Notes 

2582 ----- 

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

2584 boundaries. 

2585 """ 

2586 maskedImage = exposure.getMaskedImage() 

2587 if not isinstance(defectBaseList, Defects): 2587 ↛ 2589line 2587 didn't jump to line 2589 because the condition on line 2587 was never true

2588 # Promotes DefectBase to Defect 

2589 defectList = Defects(defectBaseList) 

2590 else: 

2591 defectList = defectBaseList 

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

2593 

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

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

2596 

2597 Parameters 

2598 ---------- 

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

2600 Exposure to process. 

2601 numEdgePixels : `int`, optional 

2602 Number of edge pixels to mask. 

2603 maskPlane : `str`, optional 

2604 Mask plane name to use. 

2605 level : `str`, optional 

2606 Level at which to mask edges. 

2607 """ 

2608 maskedImage = exposure.getMaskedImage() 

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

2610 

2611 if numEdgePixels > 0: 2611 ↛ exitline 2611 didn't return from function 'maskEdges' because the condition on line 2611 was always true

2612 if level == 'DETECTOR': 

2613 boxes = [maskedImage.getBBox()] 

2614 elif level == 'AMP': 2614 ↛ 2617line 2614 didn't jump to line 2617 because the condition on line 2614 was always true

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

2616 

2617 for box in boxes: 

2618 # This makes a bbox numEdgeSuspect pixels smaller than the 

2619 # image on each side 

2620 subImage = maskedImage[box] 

2621 box.grow(-numEdgePixels) 

2622 # Mask pixels outside box 

2623 SourceDetectionTask.setEdgeBits( 

2624 subImage, 

2625 box, 

2626 maskBitMask) 

2627 

2628 def maskAndInterpolateDefects(self, exposure, defectBaseList): 

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

2630 

2631 Parameters 

2632 ---------- 

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

2634 Exposure to process. 

2635 defectBaseList : defects-like 

2636 List of defects to mask and interpolate. Can be 

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

2638 

2639 See Also 

2640 -------- 

2641 lsst.ip.isr.isrTask.maskDefect 

2642 """ 

2643 self.maskDefect(exposure, defectBaseList) 

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

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

2646 isrFunctions.interpolateFromMask( 

2647 maskedImage=exposure.getMaskedImage(), 

2648 fwhm=self.config.fwhm, 

2649 growSaturatedFootprints=0, 

2650 maskNameList=["BAD"], 

2651 useLegacyInterp=self.config.useLegacyInterp, 

2652 ) 

2653 

2654 def maskNan(self, exposure): 

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

2656 

2657 Parameters 

2658 ---------- 

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

2660 Exposure to process. 

2661 

2662 Notes 

2663 ----- 

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

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

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

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

2668 preserve the historical name. 

2669 """ 

2670 maskedImage = exposure.getMaskedImage() 

2671 

2672 # Find and mask NaNs 

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

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

2675 numNans = maskNans(maskedImage, maskVal) 

2676 self.metadata["NUMNANS"] = numNans 

2677 if numNans > 0: 2677 ↛ 2678line 2677 didn't jump to line 2678 because the condition on line 2677 was never true

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

2679 

2680 def maskAndInterpolateNan(self, exposure): 

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

2682 in place. 

2683 

2684 Parameters 

2685 ---------- 

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

2687 Exposure to process. 

2688 

2689 See Also 

2690 -------- 

2691 lsst.ip.isr.isrTask.maskNan 

2692 """ 

2693 self.maskNan(exposure) 

2694 isrFunctions.interpolateFromMask( 

2695 maskedImage=exposure.getMaskedImage(), 

2696 fwhm=self.config.fwhm, 

2697 growSaturatedFootprints=0, 

2698 maskNameList=["UNMASKEDNAN"], 

2699 useLegacyInterp=self.config.useLegacyInterp, 

2700 ) 

2701 

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

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

2704 

2705 Parameters 

2706 ---------- 

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

2708 Exposure to process. 

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

2710 Configuration object containing parameters on which background 

2711 statistics and subgrids to use. 

2712 """ 

2713 if IsrQaConfig is not None: 2713 ↛ exitline 2713 didn't return from function 'measureBackground' because the condition on line 2713 was always true

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

2715 IsrQaConfig.flatness.nIter) 

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

2717 statsControl.setAndMask(maskVal) 

2718 maskedImage = exposure.getMaskedImage() 

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

2720 skyLevel = stats.getValue(afwMath.MEDIAN) 

2721 skySigma = stats.getValue(afwMath.STDEVCLIP) 

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

2723 metadata = exposure.getMetadata() 

2724 metadata["SKYLEVEL"] = skyLevel 

2725 metadata["SKYSIGMA"] = skySigma 

2726 

2727 # calcluating flatlevel over the subgrids 

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

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

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

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

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

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

2734 

2735 for j in range(nY): 

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

2737 for i in range(nX): 

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

2739 

2740 xLLC = xc - meshXHalf 

2741 yLLC = yc - meshYHalf 

2742 xURC = xc + meshXHalf - 1 

2743 yURC = yc + meshYHalf - 1 

2744 

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

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

2747 

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

2749 

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

2751 if len(good[0]) == 0: 

2752 # There are no good pixels. 

2753 self.log.warning("No good pixels to measure sky levels.") 

2754 skyMedian = numpy.nan 

2755 flatness = numpy.nan 

2756 flatness_rms = numpy.nan 

2757 flatness_pp = numpy.nan 

2758 else: 

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

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

2761 flatness_rms = numpy.std(flatness) 

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

2763 

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

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

2766 nX, nY, flatness_pp, flatness_rms) 

2767 

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

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

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

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

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

2773 

2774 def roughZeroPoint(self, exposure): 

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

2776 

2777 Parameters 

2778 ---------- 

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

2780 Exposure to process. 

2781 """ 

2782 filterLabel = exposure.getFilter() 

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

2784 

2785 if physicalFilter in self.config.fluxMag0T1: 2785 ↛ 2788line 2785 didn't jump to line 2788 because the condition on line 2785 was always true

2786 fluxMag0 = self.config.fluxMag0T1[physicalFilter] 

2787 else: 

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

2789 fluxMag0 = self.config.defaultFluxMag0T1 

2790 

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

2792 if expTime == 0.0: 

2793 self.log.debug("Received exposure with 0.0 expTime; skipping rough zero point.") 

2794 return 

2795 elif not expTime > 0: # handle NaN as well as <= 0 2795 ↛ 2796line 2795 didn't jump to line 2796 because the condition on line 2795 was never true

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

2797 return 

2798 

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

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

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

2802 

2803 @contextmanager 

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

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

2806 if the task is configured to apply them. 

2807 

2808 Parameters 

2809 ---------- 

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

2811 Exposure to process. 

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

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

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

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

2816 

2817 Yields 

2818 ------ 

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

2820 The flat and dark corrected exposure. 

2821 """ 

2822 if self.config.doDark and dark is not None: 2822 ↛ 2824line 2822 didn't jump to line 2824 because the condition on line 2822 was always true

2823 self.darkCorrection(exp, dark) 

2824 if self.config.doFlat: 2824 ↛ 2826line 2824 didn't jump to line 2826 because the condition on line 2824 was always true

2825 self.flatCorrection(exp, flat) 

2826 try: 

2827 yield exp 

2828 finally: 

2829 if self.config.doFlat: 2829 ↛ 2831line 2829 didn't jump to line 2831 because the condition on line 2829 was always true

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

2831 if self.config.doDark and dark is not None: 2831 ↛ exitline 2831 didn't return from function 'flatContext' because the condition on line 2831 was always true

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

2833 

2834 @deprecated( 

2835 reason=( 

2836 "makeBinnedImages is no longer used. " 

2837 "Please subtask lsst.ip.isr.BinImageDataTask instead." 

2838 ), 

2839 version="v28", category=FutureWarning 

2840 ) 

2841 def makeBinnedImages(self, exposure): 

2842 """Make visualizeVisit style binned exposures. 

2843 

2844 Parameters 

2845 ---------- 

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

2847 Exposure to bin. 

2848 

2849 Returns 

2850 ------- 

2851 bin1 : `lsst.afw.image.Exposure` 

2852 Binned exposure using binFactor1. 

2853 bin2 : `lsst.afw.image.Exposure` 

2854 Binned exposure using binFactor2. 

2855 """ 

2856 mi = exposure.getMaskedImage() 

2857 

2858 bin1 = afwMath.binImage(mi, self.config.binFactor1) 

2859 bin2 = afwMath.binImage(mi, self.config.binFactor2) 

2860 

2861 bin1 = afwImage.makeExposure(bin1) 

2862 bin2 = afwImage.makeExposure(bin2) 

2863 

2864 bin1.setInfo(exposure.getInfo()) 

2865 bin2.setInfo(exposure.getInfo()) 

2866 

2867 return bin1, bin2 

2868 

2869 def debugView(self, exposure, stepname): 

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

2871 

2872 Parameters 

2873 ---------- 

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

2875 Exposure to view. 

2876 stepname : `str` 

2877 State of processing to view. 

2878 """ 

2879 frame = getDebugFrame(self._display, stepname) 

2880 if frame: 2880 ↛ 2881line 2880 didn't jump to line 2881 because the condition on line 2880 was never true

2881 display = getDisplay(frame) 

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

2883 display.mtv(exposure) 

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

2885 while True: 

2886 ans = input(prompt).lower() 

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

2888 break 

2889 

2890 

2891class FakeAmp(object): 

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

2893 

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

2895 

2896 Parameters 

2897 ---------- 

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

2899 Exposure to generate a fake amplifier for. 

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

2901 Configuration to apply to the fake amplifier. 

2902 """ 

2903 

2904 def __init__(self, exposure, config): 

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

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

2907 self._gain = config.gain 

2908 self._readNoise = config.readNoise 

2909 self._saturation = config.saturation 

2910 

2911 def getBBox(self): 

2912 return self._bbox 

2913 

2914 def getRawBBox(self): 

2915 return self._bbox 

2916 

2917 def getRawHorizontalOverscanBBox(self): 

2918 return self._RawHorizontalOverscanBBox 

2919 

2920 def getGain(self): 

2921 return self._gain 

2922 

2923 def getReadNoise(self): 

2924 return self._readNoise 

2925 

2926 def getSaturation(self): 

2927 return self._saturation 

2928 

2929 def getSuspectLevel(self): 

2930 return float("NaN")