Coverage for python/lsst/ip/isr/isrMock.py: 27%

429 statements  

« prev     ^ index     » next       coverage.py v6.4.2, created at 2022-07-14 16:32 -0700

1# This file is part of ip_isr. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

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

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

7# for details of code ownership. 

8# 

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

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

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

12# (at your option) any later version. 

13# 

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

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

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

17# GNU General Public License for more details. 

18# 

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

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

21 

22import copy 

23import numpy as np 

24import tempfile 

25 

26import lsst.geom 

27import lsst.afw.geom as afwGeom 

28import lsst.afw.image as afwImage 

29 

30import lsst.afw.cameraGeom.utils as afwUtils 

31import lsst.afw.cameraGeom.testUtils as afwTestUtils 

32import lsst.pex.config as pexConfig 

33import lsst.pipe.base as pipeBase 

34from .crosstalk import CrosstalkCalib 

35from .defects import Defects 

36 

37__all__ = ["IsrMockConfig", "IsrMock", "RawMock", "TrimmedRawMock", "RawDictMock", 

38 "CalibratedRawMock", "MasterMock", 

39 "BiasMock", "DarkMock", "FlatMock", "FringeMock", "UntrimmedFringeMock", 

40 "BfKernelMock", "DefectMock", "CrosstalkCoeffMock", "TransmissionMock", 

41 "DataRefMock"] 

42 

43 

44class IsrMockConfig(pexConfig.Config): 

45 """Configuration parameters for isrMock. 

46 

47 These parameters produce generic fixed position signals from 

48 various sources, and combine them in a way that matches how those 

49 signals are combined to create real data. The camera used is the 

50 test camera defined by the afwUtils code. 

51 """ 

52 # Detector parameters. "Exposure" parameters. 

53 isLsstLike = pexConfig.Field( 

54 dtype=bool, 

55 default=False, 

56 doc="If True, products have one raw image per amplifier, otherwise, one raw image per detector.", 

57 ) 

58 plateScale = pexConfig.Field( 

59 dtype=float, 

60 default=20.0, 

61 doc="Plate scale used in constructing mock camera.", 

62 ) 

63 radialDistortion = pexConfig.Field( 

64 dtype=float, 

65 default=0.925, 

66 doc="Radial distortion term used in constructing mock camera.", 

67 ) 

68 isTrimmed = pexConfig.Field( 

69 dtype=bool, 

70 default=True, 

71 doc="If True, amplifiers have been trimmed and mosaicked to remove regions outside the data BBox.", 

72 ) 

73 detectorIndex = pexConfig.Field( 

74 dtype=int, 

75 default=20, 

76 doc="Index for the detector to use. The default value uses a standard 2x4 array of amps.", 

77 ) 

78 rngSeed = pexConfig.Field( 

79 dtype=int, 

80 default=20000913, 

81 doc="Seed for random number generator used to add noise.", 

82 ) 

83 # TODO: DM-18345 Check that mocks scale correctly when gain != 1.0 

84 gain = pexConfig.Field( 

85 dtype=float, 

86 default=1.0, 

87 doc="Gain for simulated data in e^-/DN.", 

88 ) 

89 readNoise = pexConfig.Field( 

90 dtype=float, 

91 default=5.0, 

92 doc="Read noise of the detector in e-.", 

93 ) 

94 expTime = pexConfig.Field( 

95 dtype=float, 

96 default=5.0, 

97 doc="Exposure time for simulated data.", 

98 ) 

99 

100 # Signal parameters 

101 skyLevel = pexConfig.Field( 

102 dtype=float, 

103 default=1000.0, 

104 doc="Background contribution to be generated from 'the sky' in DN.", 

105 ) 

106 sourceFlux = pexConfig.ListField( 

107 dtype=float, 

108 default=[45000.0], 

109 doc="Peak flux level (in DN) of simulated 'astronomical sources'.", 

110 ) 

111 sourceAmp = pexConfig.ListField( 

112 dtype=int, 

113 default=[0], 

114 doc="Amplifier to place simulated 'astronomical sources'.", 

115 ) 

116 sourceX = pexConfig.ListField( 

117 dtype=float, 

118 default=[50.0], 

119 doc="Peak position (in amplifier coordinates) of simulated 'astronomical sources'.", 

120 ) 

121 sourceY = pexConfig.ListField( 

122 dtype=float, 

123 default=[25.0], 

124 doc="Peak position (in amplifier coordinates) of simulated 'astronomical sources'.", 

125 ) 

126 overscanScale = pexConfig.Field( 

127 dtype=float, 

128 default=100.0, 

129 doc="Amplitude (in DN) of the ramp function to add to overscan data.", 

130 ) 

131 biasLevel = pexConfig.Field( 

132 dtype=float, 

133 default=8000.0, 

134 doc="Background contribution to be generated from the bias offset in DN.", 

135 ) 

136 darkRate = pexConfig.Field( 

137 dtype=float, 

138 default=5.0, 

139 doc="Background level contribution (in e-/s) to be generated from dark current.", 

140 ) 

141 darkTime = pexConfig.Field( 

142 dtype=float, 

143 default=5.0, 

144 doc="Exposure time for the dark current contribution.", 

145 ) 

146 flatDrop = pexConfig.Field( 

147 dtype=float, 

148 default=0.1, 

149 doc="Fractional flux drop due to flat from center to edge of detector along x-axis.", 

150 ) 

151 fringeScale = pexConfig.ListField( 

152 dtype=float, 

153 default=[200.0], 

154 doc="Peak fluxes for the components of the fringe ripple in DN.", 

155 ) 

156 fringeX0 = pexConfig.ListField( 

157 dtype=float, 

158 default=[-100], 

159 doc="Center position for the fringe ripples.", 

160 ) 

161 fringeY0 = pexConfig.ListField( 

162 dtype=float, 

163 default=[-0], 

164 doc="Center position for the fringe ripples.", 

165 ) 

166 

167 # Inclusion parameters 

168 doAddSky = pexConfig.Field( 

169 dtype=bool, 

170 default=True, 

171 doc="Apply 'sky' signal to output image.", 

172 ) 

173 doAddSource = pexConfig.Field( 

174 dtype=bool, 

175 default=True, 

176 doc="Add simulated source to output image.", 

177 ) 

178 doAddCrosstalk = pexConfig.Field( 

179 dtype=bool, 

180 default=False, 

181 doc="Apply simulated crosstalk to output image. This cannot be corrected by ISR, " 

182 "as detector.hasCrosstalk()==False.", 

183 ) 

184 doAddOverscan = pexConfig.Field( 

185 dtype=bool, 

186 default=True, 

187 doc="If untrimmed, add overscan ramp to overscan and data regions.", 

188 ) 

189 doAddBias = pexConfig.Field( 

190 dtype=bool, 

191 default=True, 

192 doc="Add bias signal to data.", 

193 ) 

194 doAddDark = pexConfig.Field( 

195 dtype=bool, 

196 default=True, 

197 doc="Add dark signal to data.", 

198 ) 

199 doAddFlat = pexConfig.Field( 

200 dtype=bool, 

201 default=True, 

202 doc="Add flat signal to data.", 

203 ) 

204 doAddFringe = pexConfig.Field( 

205 dtype=bool, 

206 default=True, 

207 doc="Add fringe signal to data.", 

208 ) 

209 

210 # Datasets to create and return instead of generating an image. 

211 doTransmissionCurve = pexConfig.Field( 

212 dtype=bool, 

213 default=False, 

214 doc="Return a simulated transmission curve.", 

215 ) 

216 doDefects = pexConfig.Field( 

217 dtype=bool, 

218 default=False, 

219 doc="Return a simulated defect list.", 

220 ) 

221 doBrighterFatter = pexConfig.Field( 

222 dtype=bool, 

223 default=False, 

224 doc="Return a simulated brighter-fatter kernel.", 

225 ) 

226 doCrosstalkCoeffs = pexConfig.Field( 

227 dtype=bool, 

228 default=False, 

229 doc="Return the matrix of crosstalk coefficients.", 

230 ) 

231 doDataRef = pexConfig.Field( 

232 dtype=bool, 

233 default=False, 

234 doc="Return a simulated gen2 butler dataRef.", 

235 ) 

236 doGenerateImage = pexConfig.Field( 

237 dtype=bool, 

238 default=False, 

239 doc="Return the generated output image if True.", 

240 ) 

241 doGenerateData = pexConfig.Field( 

242 dtype=bool, 

243 default=False, 

244 doc="Return a non-image data structure if True.", 

245 ) 

246 doGenerateAmpDict = pexConfig.Field( 

247 dtype=bool, 

248 default=False, 

249 doc="Return a dict of exposure amplifiers instead of an afwImage.Exposure.", 

250 ) 

251 

252 

253class IsrMock(pipeBase.Task): 

254 """Class to generate consistent mock images for ISR testing. 

255 

256 ISR testing currently relies on one-off fake images that do not 

257 accurately mimic the full set of detector effects. This class 

258 uses the test camera/detector/amplifier structure defined in 

259 `lsst.afw.cameraGeom.testUtils` to avoid making the test data 

260 dependent on any of the actual obs package formats. 

261 """ 

262 ConfigClass = IsrMockConfig 

263 _DefaultName = "isrMock" 

264 

265 def __init__(self, **kwargs): 

266 super().__init__(**kwargs) 

267 self.rng = np.random.RandomState(self.config.rngSeed) 

268 self.crosstalkCoeffs = np.array([[0.0, 0.0, 0.0, 0.0, 0.0, -1e-3, 0.0, 0.0], 

269 [1e-2, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], 

270 [1e-2, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], 

271 [1e-2, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], 

272 [1e-2, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], 

273 [1e-2, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], 

274 [1e-2, 0.0, 0.0, 2.2e-2, 0.0, 0.0, 0.0, 0.0], 

275 [1e-2, 5e-3, 5e-4, 3e-3, 4e-2, 5e-3, 5e-3, 0.0]]) 

276 

277 self.bfKernel = np.array([[1., 4., 7., 4., 1.], 

278 [4., 16., 26., 16., 4.], 

279 [7., 26., 41., 26., 7.], 

280 [4., 16., 26., 16., 4.], 

281 [1., 4., 7., 4., 1.]]) / 273.0 

282 

283 def run(self): 

284 """Generate a mock ISR product, and return it. 

285 

286 Returns 

287 ------- 

288 image : `lsst.afw.image.Exposure` 

289 Simulated ISR image with signals added. 

290 dataProduct : 

291 Simulated ISR data products. 

292 None : 

293 Returned if no valid configuration was found. 

294 

295 Raises 

296 ------ 

297 RuntimeError 

298 Raised if both doGenerateImage and doGenerateData are specified. 

299 """ 

300 if self.config.doGenerateImage and self.config.doGenerateData: 

301 raise RuntimeError("Only one of doGenerateImage and doGenerateData may be specified.") 

302 elif self.config.doGenerateImage: 

303 return self.makeImage() 

304 elif self.config.doGenerateData: 

305 return self.makeData() 

306 else: 

307 return None 

308 

309 def makeData(self): 

310 """Generate simulated ISR data. 

311 

312 Currently, only the class defined crosstalk coefficient 

313 matrix, brighter-fatter kernel, a constant unity transmission 

314 curve, or a simple single-entry defect list can be generated. 

315 

316 Returns 

317 ------- 

318 dataProduct : 

319 Simulated ISR data product. 

320 """ 

321 if sum(map(bool, [self.config.doBrighterFatter, 

322 self.config.doDefects, 

323 self.config.doTransmissionCurve, 

324 self.config.doCrosstalkCoeffs])) != 1: 

325 raise RuntimeError("Only one data product can be generated at a time.") 

326 elif self.config.doBrighterFatter is True: 

327 return self.makeBfKernel() 

328 elif self.config.doDefects is True: 

329 return self.makeDefectList() 

330 elif self.config.doTransmissionCurve is True: 

331 return self.makeTransmissionCurve() 

332 elif self.config.doCrosstalkCoeffs is True: 

333 return self.crosstalkCoeffs 

334 else: 

335 return None 

336 

337 def makeBfKernel(self): 

338 """Generate a simple Gaussian brighter-fatter kernel. 

339 

340 Returns 

341 ------- 

342 kernel : `numpy.ndarray` 

343 Simulated brighter-fatter kernel. 

344 """ 

345 return self.bfKernel 

346 

347 def makeDefectList(self): 

348 """Generate a simple single-entry defect list. 

349 

350 Returns 

351 ------- 

352 defectList : `lsst.meas.algorithms.Defects` 

353 Simulated defect list 

354 """ 

355 return Defects([lsst.geom.Box2I(lsst.geom.Point2I(0, 0), 

356 lsst.geom.Extent2I(40, 50))]) 

357 

358 def makeCrosstalkCoeff(self): 

359 """Generate the simulated crosstalk coefficients. 

360 

361 Returns 

362 ------- 

363 coeffs : `numpy.ndarray` 

364 Simulated crosstalk coefficients. 

365 """ 

366 

367 return self.crosstalkCoeffs 

368 

369 def makeTransmissionCurve(self): 

370 """Generate a simulated flat transmission curve. 

371 

372 Returns 

373 ------- 

374 transmission : `lsst.afw.image.TransmissionCurve` 

375 Simulated transmission curve. 

376 """ 

377 

378 return afwImage.TransmissionCurve.makeIdentity() 

379 

380 def makeImage(self): 

381 """Generate a simulated ISR image. 

382 

383 Returns 

384 ------- 

385 exposure : `lsst.afw.image.Exposure` or `dict` 

386 Simulated ISR image data. 

387 

388 Notes 

389 ----- 

390 This method currently constructs a "raw" data image by: 

391 * Generating a simulated sky with noise 

392 * Adding a single Gaussian "star" 

393 * Adding the fringe signal 

394 * Multiplying the frame by the simulated flat 

395 * Adding dark current (and noise) 

396 * Adding a bias offset (and noise) 

397 * Adding an overscan gradient parallel to the pixel y-axis 

398 * Simulating crosstalk by adding a scaled version of each 

399 amplifier to each other amplifier. 

400 

401 The exposure with image data constructed this way is in one of 

402 three formats. 

403 * A single image, with overscan and prescan regions retained 

404 * A single image, with overscan and prescan regions trimmed 

405 * A `dict`, containing the amplifer data indexed by the 

406 amplifier name. 

407 

408 The nonlinearity, CTE, and brighter fatter are currently not 

409 implemented. 

410 

411 Note that this method generates an image in the reverse 

412 direction as the ISR processing, as the output image here has 

413 had a series of instrument effects added to an idealized 

414 exposure. 

415 """ 

416 exposure = self.getExposure() 

417 

418 for idx, amp in enumerate(exposure.getDetector()): 

419 bbox = None 

420 if self.config.isTrimmed is True: 

421 bbox = amp.getBBox() 

422 else: 

423 bbox = amp.getRawDataBBox() 

424 

425 ampData = exposure.image[bbox] 

426 

427 if self.config.doAddSky is True: 

428 self.amplifierAddNoise(ampData, self.config.skyLevel, np.sqrt(self.config.skyLevel)) 

429 

430 if self.config.doAddSource is True: 

431 for sourceAmp, sourceFlux, sourceX, sourceY in zip(self.config.sourceAmp, 

432 self.config.sourceFlux, 

433 self.config.sourceX, 

434 self.config.sourceY): 

435 if idx == sourceAmp: 

436 self.amplifierAddSource(ampData, sourceFlux, sourceX, sourceY) 

437 

438 if self.config.doAddFringe is True: 

439 self.amplifierAddFringe(amp, ampData, np.array(self.config.fringeScale), 

440 x0=np.array(self.config.fringeX0), 

441 y0=np.array(self.config.fringeY0)) 

442 

443 if self.config.doAddFlat is True: 

444 if ampData.getArray().sum() == 0.0: 

445 self.amplifierAddNoise(ampData, 1.0, 0.0) 

446 u0 = exposure.getDimensions().getX() 

447 v0 = exposure.getDimensions().getY() 

448 self.amplifierMultiplyFlat(amp, ampData, self.config.flatDrop, u0=u0, v0=v0) 

449 

450 if self.config.doAddDark is True: 

451 self.amplifierAddNoise(ampData, 

452 self.config.darkRate * self.config.darkTime / self.config.gain, 

453 np.sqrt(self.config.darkRate 

454 * self.config.darkTime / self.config.gain)) 

455 

456 if self.config.doAddCrosstalk is True: 

457 ctCalib = CrosstalkCalib() 

458 for idxS, ampS in enumerate(exposure.getDetector()): 

459 for idxT, ampT in enumerate(exposure.getDetector()): 

460 ampDataT = exposure.image[ampT.getBBox() 

461 if self.config.isTrimmed else ampT.getRawDataBBox()] 

462 outAmp = ctCalib.extractAmp(exposure.getImage(), ampS, ampT, 

463 isTrimmed=self.config.isTrimmed) 

464 self.amplifierAddCT(outAmp, ampDataT, self.crosstalkCoeffs[idxT][idxS]) 

465 

466 for amp in exposure.getDetector(): 

467 bbox = None 

468 if self.config.isTrimmed is True: 

469 bbox = amp.getBBox() 

470 else: 

471 bbox = amp.getRawDataBBox() 

472 

473 ampData = exposure.image[bbox] 

474 

475 if self.config.doAddBias is True: 

476 self.amplifierAddNoise(ampData, self.config.biasLevel, 

477 self.config.readNoise / self.config.gain) 

478 

479 if self.config.doAddOverscan is True: 

480 oscanBBox = amp.getRawHorizontalOverscanBBox() 

481 oscanData = exposure.image[oscanBBox] 

482 self.amplifierAddNoise(oscanData, self.config.biasLevel, 

483 self.config.readNoise / self.config.gain) 

484 

485 self.amplifierAddYGradient(ampData, -1.0 * self.config.overscanScale, 

486 1.0 * self.config.overscanScale) 

487 self.amplifierAddYGradient(oscanData, -1.0 * self.config.overscanScale, 

488 1.0 * self.config.overscanScale) 

489 

490 if self.config.doGenerateAmpDict is True: 

491 expDict = dict() 

492 for amp in exposure.getDetector(): 

493 expDict[amp.getName()] = exposure 

494 return expDict 

495 else: 

496 return exposure 

497 

498 # afw primatives to construct the image structure 

499 def getCamera(self): 

500 """Construct a test camera object. 

501 

502 Returns 

503 ------- 

504 camera : `lsst.afw.cameraGeom.camera` 

505 Test camera. 

506 """ 

507 cameraWrapper = afwTestUtils.CameraWrapper( 

508 plateScale=self.config.plateScale, 

509 radialDistortion=self.config.radialDistortion, 

510 isLsstLike=self.config.isLsstLike, 

511 ) 

512 camera = cameraWrapper.camera 

513 return camera 

514 

515 def getExposure(self): 

516 """Construct a test exposure. 

517 

518 The test exposure has a simple WCS set, as well as a list of 

519 unlikely header keywords that can be removed during ISR 

520 processing to exercise that code. 

521 

522 Returns 

523 ------- 

524 exposure : `lsst.afw.exposure.Exposure` 

525 Construct exposure containing masked image of the 

526 appropriate size. 

527 """ 

528 camera = self.getCamera() 

529 detector = camera[self.config.detectorIndex] 

530 image = afwUtils.makeImageFromCcd(detector, 

531 isTrimmed=self.config.isTrimmed, 

532 showAmpGain=False, 

533 rcMarkSize=0, 

534 binSize=1, 

535 imageFactory=afwImage.ImageF) 

536 

537 var = afwImage.ImageF(image.getDimensions()) 

538 mask = afwImage.Mask(image.getDimensions()) 

539 image.assign(0.0) 

540 

541 maskedImage = afwImage.makeMaskedImage(image, mask, var) 

542 exposure = afwImage.makeExposure(maskedImage) 

543 exposure.setDetector(detector) 

544 exposure.setWcs(self.getWcs()) 

545 

546 visitInfo = afwImage.VisitInfo(exposureTime=self.config.expTime, darkTime=self.config.darkTime) 

547 exposure.getInfo().setVisitInfo(visitInfo) 

548 

549 metadata = exposure.getMetadata() 

550 metadata.add("SHEEP", 7.3, "number of sheep on farm") 

551 metadata.add("MONKEYS", 155, "monkeys per tree") 

552 metadata.add("VAMPIRES", 4, "How scary are vampires.") 

553 

554 ccd = exposure.getDetector() 

555 newCcd = ccd.rebuild() 

556 newCcd.clear() 

557 for amp in ccd: 

558 newAmp = amp.rebuild() 

559 newAmp.setLinearityCoeffs((0., 1., 0., 0.)) 

560 newAmp.setLinearityType("Polynomial") 

561 newAmp.setGain(self.config.gain) 

562 newAmp.setSuspectLevel(25000.0) 

563 newAmp.setSaturation(32000.0) 

564 newCcd.append(newAmp) 

565 exposure.setDetector(newCcd.finish()) 

566 

567 exposure.image.array[:] = np.zeros(exposure.getImage().getDimensions()).transpose() 

568 exposure.mask.array[:] = np.zeros(exposure.getMask().getDimensions()).transpose() 

569 exposure.variance.array[:] = np.zeros(exposure.getVariance().getDimensions()).transpose() 

570 

571 return exposure 

572 

573 def getWcs(self): 

574 """Construct a dummy WCS object. 

575 

576 Taken from the deprecated ip_isr/examples/exampleUtils.py. 

577 

578 This is not guaranteed, given the distortion and pixel scale 

579 listed in the afwTestUtils camera definition. 

580 

581 Returns 

582 ------- 

583 wcs : `lsst.afw.geom.SkyWcs` 

584 Test WCS transform. 

585 """ 

586 return afwGeom.makeSkyWcs(crpix=lsst.geom.Point2D(0.0, 100.0), 

587 crval=lsst.geom.SpherePoint(45.0, 25.0, lsst.geom.degrees), 

588 cdMatrix=afwGeom.makeCdMatrix(scale=1.0*lsst.geom.degrees)) 

589 

590 def localCoordToExpCoord(self, ampData, x, y): 

591 """Convert between a local amplifier coordinate and the full 

592 exposure coordinate. 

593 

594 Parameters 

595 ---------- 

596 ampData : `lsst.afw.image.ImageF` 

597 Amplifier image to use for conversions. 

598 x : `int` 

599 X-coordinate of the point to transform. 

600 y : `int` 

601 Y-coordinate of the point to transform. 

602 

603 Returns 

604 ------- 

605 u : `int` 

606 Transformed x-coordinate. 

607 v : `int` 

608 Transformed y-coordinate. 

609 

610 Notes 

611 ----- 

612 The output is transposed intentionally here, to match the 

613 internal transpose between numpy and afw.image coordinates. 

614 """ 

615 u = x + ampData.getBBox().getBeginX() 

616 v = y + ampData.getBBox().getBeginY() 

617 

618 return (v, u) 

619 

620 # Simple data values. 

621 def amplifierAddNoise(self, ampData, mean, sigma): 

622 """Add Gaussian noise to an amplifier's image data. 

623 

624 This method operates in the amplifier coordinate frame. 

625 

626 Parameters 

627 ---------- 

628 ampData : `lsst.afw.image.ImageF` 

629 Amplifier image to operate on. 

630 mean : `float` 

631 Mean value of the Gaussian noise. 

632 sigma : `float` 

633 Sigma of the Gaussian noise. 

634 """ 

635 ampArr = ampData.array 

636 ampArr[:] = ampArr[:] + self.rng.normal(mean, sigma, 

637 size=ampData.getDimensions()).transpose() 

638 

639 def amplifierAddYGradient(self, ampData, start, end): 

640 """Add a y-axis linear gradient to an amplifier's image data. 

641 

642 This method operates in the amplifier coordinate frame. 

643 

644 Parameters 

645 ---------- 

646 ampData : `lsst.afw.image.ImageF` 

647 Amplifier image to operate on. 

648 start : `float` 

649 Start value of the gradient (at y=0). 

650 end : `float` 

651 End value of the gradient (at y=ymax). 

652 """ 

653 nPixY = ampData.getDimensions().getY() 

654 ampArr = ampData.array 

655 ampArr[:] = ampArr[:] + (np.interp(range(nPixY), (0, nPixY - 1), (start, end)).reshape(nPixY, 1) 

656 + np.zeros(ampData.getDimensions()).transpose()) 

657 

658 def amplifierAddSource(self, ampData, scale, x0, y0): 

659 """Add a single Gaussian source to an amplifier. 

660 

661 This method operates in the amplifier coordinate frame. 

662 

663 Parameters 

664 ---------- 

665 ampData : `lsst.afw.image.ImageF` 

666 Amplifier image to operate on. 

667 scale : `float` 

668 Peak flux of the source to add. 

669 x0 : `float` 

670 X-coordinate of the source peak. 

671 y0 : `float` 

672 Y-coordinate of the source peak. 

673 """ 

674 for x in range(0, ampData.getDimensions().getX()): 

675 for y in range(0, ampData.getDimensions().getY()): 

676 ampData.array[y][x] = (ampData.array[y][x] 

677 + scale * np.exp(-0.5 * ((x - x0)**2 + (y - y0)**2) / 3.0**2)) 

678 

679 def amplifierAddCT(self, ampDataSource, ampDataTarget, scale): 

680 """Add a scaled copy of an amplifier to another, simulating crosstalk. 

681 

682 This method operates in the amplifier coordinate frame. 

683 

684 Parameters 

685 ---------- 

686 ampDataSource : `lsst.afw.image.ImageF` 

687 Amplifier image to add scaled copy from. 

688 ampDataTarget : `lsst.afw.image.ImageF` 

689 Amplifier image to add scaled copy to. 

690 scale : `float` 

691 Flux scale of the copy to add to the target. 

692 

693 Notes 

694 ----- 

695 This simulates simple crosstalk between amplifiers. 

696 """ 

697 ampDataTarget.array[:] = (ampDataTarget.array[:] 

698 + scale * ampDataSource.array[:]) 

699 

700 # Functional form data values. 

701 def amplifierAddFringe(self, amp, ampData, scale, x0=100, y0=0): 

702 """Add a fringe-like ripple pattern to an amplifier's image data. 

703 

704 Parameters 

705 ---------- 

706 amp : `~lsst.afw.ampInfo.AmpInfoRecord` 

707 Amplifier to operate on. Needed for amp<->exp coordinate 

708 transforms. 

709 ampData : `lsst.afw.image.ImageF` 

710 Amplifier image to operate on. 

711 scale : `numpy.array` or `float` 

712 Peak intensity scaling for the ripple. 

713 x0 : `numpy.array` or `float`, optional 

714 Fringe center 

715 y0 : `numpy.array` or `float`, optional 

716 Fringe center 

717 

718 Notes 

719 ----- 

720 This uses an offset sinc function to generate a ripple 

721 pattern. True fringes have much finer structure, but this 

722 pattern should be visually identifiable. The (x, y) 

723 coordinates are in the frame of the amplifier, and (u, v) in 

724 the frame of the full trimmed image. 

725 """ 

726 for x in range(0, ampData.getDimensions().getX()): 

727 for y in range(0, ampData.getDimensions().getY()): 

728 (u, v) = self.localCoordToExpCoord(amp, x, y) 

729 ampData.getArray()[y][x] = np.sum((ampData.getArray()[y][x] 

730 + scale * np.sinc(((u - x0) / 50)**2 

731 + ((v - y0) / 50)**2))) 

732 

733 def amplifierMultiplyFlat(self, amp, ampData, fracDrop, u0=100.0, v0=100.0): 

734 """Multiply an amplifier's image data by a flat-like pattern. 

735 

736 Parameters 

737 ---------- 

738 amp : `lsst.afw.ampInfo.AmpInfoRecord` 

739 Amplifier to operate on. Needed for amp<->exp coordinate 

740 transforms. 

741 ampData : `lsst.afw.image.ImageF` 

742 Amplifier image to operate on. 

743 fracDrop : `float` 

744 Fractional drop from center to edge of detector along x-axis. 

745 u0 : `float` 

746 Peak location in detector coordinates. 

747 v0 : `float` 

748 Peak location in detector coordinates. 

749 

750 Notes 

751 ----- 

752 This uses a 2-d Gaussian to simulate an illumination pattern 

753 that falls off towards the edge of the detector. The (x, y) 

754 coordinates are in the frame of the amplifier, and (u, v) in 

755 the frame of the full trimmed image. 

756 """ 

757 if fracDrop >= 1.0: 

758 raise RuntimeError("Flat fractional drop cannot be greater than 1.0") 

759 

760 sigma = u0 / np.sqrt(-2.0 * np.log(fracDrop)) 

761 

762 for x in range(0, ampData.getDimensions().getX()): 

763 for y in range(0, ampData.getDimensions().getY()): 

764 (u, v) = self.localCoordToExpCoord(amp, x, y) 

765 f = np.exp(-0.5 * ((u - u0)**2 + (v - v0)**2) / sigma**2) 

766 ampData.array[y][x] = (ampData.array[y][x] * f) 

767 

768 

769class RawMock(IsrMock): 

770 """Generate a raw exposure suitable for ISR. 

771 """ 

772 def __init__(self, **kwargs): 

773 super().__init__(**kwargs) 

774 self.config.isTrimmed = False 

775 self.config.doGenerateImage = True 

776 self.config.doGenerateAmpDict = False 

777 self.config.doAddOverscan = True 

778 self.config.doAddSky = True 

779 self.config.doAddSource = True 

780 self.config.doAddCrosstalk = False 

781 self.config.doAddBias = True 

782 self.config.doAddDark = True 

783 

784 

785class TrimmedRawMock(RawMock): 

786 """Generate a trimmed raw exposure. 

787 """ 

788 def __init__(self, **kwargs): 

789 super().__init__(**kwargs) 

790 self.config.isTrimmed = True 

791 self.config.doAddOverscan = False 

792 

793 

794class CalibratedRawMock(RawMock): 

795 """Generate a trimmed raw exposure. 

796 """ 

797 def __init__(self, **kwargs): 

798 super().__init__(**kwargs) 

799 self.config.isTrimmed = True 

800 self.config.doGenerateImage = True 

801 self.config.doAddOverscan = False 

802 self.config.doAddSky = True 

803 self.config.doAddSource = True 

804 self.config.doAddCrosstalk = False 

805 

806 self.config.doAddBias = False 

807 self.config.doAddDark = False 

808 self.config.doAddFlat = False 

809 self.config.doAddFringe = True 

810 

811 self.config.biasLevel = 0.0 

812 self.config.readNoise = 10.0 

813 

814 

815class RawDictMock(RawMock): 

816 """Generate a raw exposure dict suitable for ISR. 

817 """ 

818 def __init__(self, **kwargs): 

819 super().__init__(**kwargs) 

820 self.config.doGenerateAmpDict = True 

821 

822 

823class MasterMock(IsrMock): 

824 """Parent class for those that make master calibrations. 

825 """ 

826 def __init__(self, **kwargs): 

827 super().__init__(**kwargs) 

828 self.config.isTrimmed = True 

829 self.config.doGenerateImage = True 

830 self.config.doAddOverscan = False 

831 self.config.doAddSky = False 

832 self.config.doAddSource = False 

833 self.config.doAddCrosstalk = False 

834 

835 self.config.doAddBias = False 

836 self.config.doAddDark = False 

837 self.config.doAddFlat = False 

838 self.config.doAddFringe = False 

839 

840 

841class BiasMock(MasterMock): 

842 """Simulated master bias calibration. 

843 """ 

844 def __init__(self, **kwargs): 

845 super().__init__(**kwargs) 

846 self.config.doAddBias = True 

847 self.config.readNoise = 10.0 

848 

849 

850class DarkMock(MasterMock): 

851 """Simulated master dark calibration. 

852 """ 

853 def __init__(self, **kwargs): 

854 super().__init__(**kwargs) 

855 self.config.doAddDark = True 

856 self.config.darkTime = 1.0 

857 

858 

859class FlatMock(MasterMock): 

860 """Simulated master flat calibration. 

861 """ 

862 def __init__(self, **kwargs): 

863 super().__init__(**kwargs) 

864 self.config.doAddFlat = True 

865 

866 

867class FringeMock(MasterMock): 

868 """Simulated master fringe calibration. 

869 """ 

870 def __init__(self, **kwargs): 

871 super().__init__(**kwargs) 

872 self.config.doAddFringe = True 

873 

874 

875class UntrimmedFringeMock(FringeMock): 

876 """Simulated untrimmed master fringe calibration. 

877 """ 

878 def __init__(self, **kwargs): 

879 super().__init__(**kwargs) 

880 self.config.isTrimmed = False 

881 

882 

883class BfKernelMock(IsrMock): 

884 """Simulated brighter-fatter kernel. 

885 """ 

886 def __init__(self, **kwargs): 

887 super().__init__(**kwargs) 

888 self.config.doGenerateImage = False 

889 self.config.doGenerateData = True 

890 self.config.doBrighterFatter = True 

891 self.config.doDefects = False 

892 self.config.doCrosstalkCoeffs = False 

893 self.config.doTransmissionCurve = False 

894 

895 

896class DefectMock(IsrMock): 

897 """Simulated defect list. 

898 """ 

899 def __init__(self, **kwargs): 

900 super().__init__(**kwargs) 

901 self.config.doGenerateImage = False 

902 self.config.doGenerateData = True 

903 self.config.doBrighterFatter = False 

904 self.config.doDefects = True 

905 self.config.doCrosstalkCoeffs = False 

906 self.config.doTransmissionCurve = False 

907 

908 

909class CrosstalkCoeffMock(IsrMock): 

910 """Simulated crosstalk coefficient matrix. 

911 """ 

912 def __init__(self, **kwargs): 

913 super().__init__(**kwargs) 

914 self.config.doGenerateImage = False 

915 self.config.doGenerateData = True 

916 self.config.doBrighterFatter = False 

917 self.config.doDefects = False 

918 self.config.doCrosstalkCoeffs = True 

919 self.config.doTransmissionCurve = False 

920 

921 

922class TransmissionMock(IsrMock): 

923 """Simulated transmission curve. 

924 """ 

925 def __init__(self, **kwargs): 

926 super().__init__(**kwargs) 

927 self.config.doGenerateImage = False 

928 self.config.doGenerateData = True 

929 self.config.doBrighterFatter = False 

930 self.config.doDefects = False 

931 self.config.doCrosstalkCoeffs = False 

932 self.config.doTransmissionCurve = True 

933 

934 

935class DataRefMock(object): 

936 """Simulated gen2 butler data ref. 

937 

938 Currently only supports get and put operations, which are most 

939 likely to be called for data in ISR processing. 

940 

941 """ 

942 dataId = "isrMock Fake Data" 

943 darkval = 2. # e-/sec 

944 oscan = 250. # DN 

945 gradient = .10 

946 exptime = 15.0 # seconds 

947 darkexptime = 15.0 # seconds 

948 

949 def __init__(self, **kwargs): 

950 if 'config' in kwargs.keys(): 

951 self.config = kwargs['config'] 

952 else: 

953 self.config = None 

954 

955 def expectImage(self): 

956 if self.config is None: 

957 self.config = IsrMockConfig() 

958 self.config.doGenerateImage = True 

959 self.config.doGenerateData = False 

960 

961 def expectData(self): 

962 if self.config is None: 

963 self.config = IsrMockConfig() 

964 self.config.doGenerateImage = False 

965 self.config.doGenerateData = True 

966 

967 def get(self, dataType, **kwargs): 

968 """Return an appropriate data product. 

969 

970 Parameters 

971 ---------- 

972 dataType : `str` 

973 Type of data product to return. 

974 

975 Returns 

976 ------- 

977 mock : IsrMock.run() result 

978 The output product. 

979 """ 

980 if "_filename" in dataType: 

981 self.expectData() 

982 return tempfile.mktemp(), "mock" 

983 elif 'transmission_' in dataType: 

984 self.expectData() 

985 return TransmissionMock(config=self.config).run() 

986 elif dataType == 'ccdExposureId': 

987 self.expectData() 

988 return 20090913 

989 elif dataType == 'camera': 

990 self.expectData() 

991 return IsrMock(config=self.config).getCamera() 

992 elif dataType == 'raw': 

993 self.expectImage() 

994 return RawMock(config=self.config).run() 

995 elif dataType == 'bias': 

996 self.expectImage() 

997 return BiasMock(config=self.config).run() 

998 elif dataType == 'dark': 

999 self.expectImage() 

1000 return DarkMock(config=self.config).run() 

1001 elif dataType == 'flat': 

1002 self.expectImage() 

1003 return FlatMock(config=self.config).run() 

1004 elif dataType == 'fringe': 

1005 self.expectImage() 

1006 return FringeMock(config=self.config).run() 

1007 elif dataType == 'defects': 

1008 self.expectData() 

1009 return DefectMock(config=self.config).run() 

1010 elif dataType == 'bfKernel': 

1011 self.expectData() 

1012 return BfKernelMock(config=self.config).run() 

1013 elif dataType == 'linearizer': 

1014 return None 

1015 elif dataType == 'crosstalkSources': 

1016 return None 

1017 else: 

1018 raise RuntimeError("ISR DataRefMock cannot return %s.", dataType) 

1019 

1020 def put(self, exposure, filename): 

1021 """Write an exposure to a FITS file. 

1022 

1023 Parameters 

1024 ---------- 

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

1026 Image data to write out. 

1027 filename : `str` 

1028 Base name of the output file. 

1029 """ 

1030 exposure.writeFits(filename+".fits") 

1031 

1032 

1033class FringeDataRefMock(object): 

1034 """Simulated gen2 butler data ref. 

1035 

1036 Currently only supports get and put operations, which are most 

1037 likely to be called for data in ISR processing. 

1038 

1039 """ 

1040 dataId = "isrMock Fake Data" 

1041 darkval = 2. # e-/sec 

1042 oscan = 250. # DN 

1043 gradient = .10 

1044 exptime = 15 # seconds 

1045 darkexptime = 40. # seconds 

1046 

1047 def __init__(self, **kwargs): 

1048 if 'config' in kwargs.keys(): 

1049 self.config = kwargs['config'] 

1050 else: 

1051 self.config = IsrMockConfig() 

1052 self.config.isTrimmed = True 

1053 self.config.doAddFringe = True 

1054 self.config.readNoise = 10.0 

1055 

1056 def get(self, dataType, **kwargs): 

1057 """Return an appropriate data product. 

1058 

1059 Parameters 

1060 ---------- 

1061 dataType : `str` 

1062 Type of data product to return. 

1063 

1064 Returns 

1065 ------- 

1066 mock : IsrMock.run() result 

1067 The output product. 

1068 """ 

1069 if "_filename" in dataType: 

1070 return tempfile.mktemp(), "mock" 

1071 elif 'transmission_' in dataType: 

1072 return TransmissionMock(config=self.config).run() 

1073 elif dataType == 'ccdExposureId': 

1074 return 20090913 

1075 elif dataType == 'camera': 

1076 return IsrMock(config=self.config).getCamera() 

1077 elif dataType == 'raw': 

1078 return CalibratedRawMock(config=self.config).run() 

1079 elif dataType == 'bias': 

1080 return BiasMock(config=self.config).run() 

1081 elif dataType == 'dark': 

1082 return DarkMock(config=self.config).run() 

1083 elif dataType == 'flat': 

1084 return FlatMock(config=self.config).run() 

1085 elif dataType == 'fringe': 

1086 fringes = [] 

1087 configCopy = copy.deepcopy(self.config) 

1088 for scale, x, y in zip(self.config.fringeScale, self.config.fringeX0, self.config.fringeY0): 

1089 configCopy.fringeScale = [1.0] 

1090 configCopy.fringeX0 = [x] 

1091 configCopy.fringeY0 = [y] 

1092 fringes.append(FringeMock(config=configCopy).run()) 

1093 return fringes 

1094 elif dataType == 'defects': 

1095 return DefectMock(config=self.config).run() 

1096 elif dataType == 'bfKernel': 

1097 return BfKernelMock(config=self.config).run() 

1098 elif dataType == 'linearizer': 

1099 return None 

1100 elif dataType == 'crosstalkSources': 

1101 return None 

1102 else: 

1103 return None 

1104 

1105 def put(self, exposure, filename): 

1106 """Write an exposure to a FITS file. 

1107 

1108 Parameters 

1109 ---------- 

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

1111 Image data to write out. 

1112 filename : `str` 

1113 Base name of the output file. 

1114 """ 

1115 exposure.writeFits(filename+".fits")