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

478 statements  

« prev     ^ index     » next       coverage.py v7.5.1, created at 2024-05-15 02:10 -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 

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

23 "CalibratedRawMock", "MasterMock", 

24 "BiasMock", "DarkMock", "FlatMock", "FringeMock", "UntrimmedFringeMock", 

25 "BfKernelMock", "DefectMock", "CrosstalkCoeffMock", "TransmissionMock", 

26 "MockDataContainer", "MockFringeContainer"] 

27 

28import copy 

29import numpy as np 

30import tempfile 

31 

32import lsst.geom 

33import lsst.afw.geom as afwGeom 

34import lsst.afw.image as afwImage 

35from lsstDebug import getDebugFrame 

36 

37import lsst.afw.cameraGeom.utils as afwUtils 

38import lsst.afw.cameraGeom.testUtils as afwTestUtils 

39from lsst.afw.cameraGeom import ReadoutCorner 

40import lsst.pex.config as pexConfig 

41import lsst.pipe.base as pipeBase 

42from .crosstalk import CrosstalkCalib 

43from .defects import Defects 

44 

45 

46class IsrMockConfig(pexConfig.Config): 

47 """Configuration parameters for isrMock. 

48 

49 These parameters produce generic fixed position signals from 

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

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

52 test camera defined by the afwUtils code. 

53 """ 

54 # Detector parameters. "Exposure" parameters. 

55 isLsstLike = pexConfig.Field( 

56 dtype=bool, 

57 default=False, 

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

59 ) 

60 plateScale = pexConfig.Field( 

61 dtype=float, 

62 default=20.0, 

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

64 ) 

65 radialDistortion = pexConfig.Field( 

66 dtype=float, 

67 default=0.925, 

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

69 ) 

70 isTrimmed = pexConfig.Field( 

71 dtype=bool, 

72 default=True, 

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

74 ) 

75 detectorIndex = pexConfig.Field( 

76 dtype=int, 

77 default=20, 

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

79 ) 

80 rngSeed = pexConfig.Field( 

81 dtype=int, 

82 default=20000913, 

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

84 ) 

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

86 gain = pexConfig.Field( 

87 dtype=float, 

88 default=1.0, 

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

90 ) 

91 readNoise = pexConfig.Field( 

92 dtype=float, 

93 default=5.0, 

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

95 ) 

96 expTime = pexConfig.Field( 

97 dtype=float, 

98 default=5.0, 

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

100 ) 

101 

102 # Signal parameters 

103 skyLevel = pexConfig.Field( 

104 dtype=float, 

105 default=1000.0, 

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

107 ) 

108 sourceFlux = pexConfig.ListField( 

109 dtype=float, 

110 default=[45000.0], 

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

112 ) 

113 sourceAmp = pexConfig.ListField( 

114 dtype=int, 

115 default=[0], 

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

117 ) 

118 sourceX = pexConfig.ListField( 

119 dtype=float, 

120 default=[50.0], 

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

122 ) 

123 sourceY = pexConfig.ListField( 

124 dtype=float, 

125 default=[25.0], 

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

127 ) 

128 overscanScale = pexConfig.Field( 

129 dtype=float, 

130 default=100.0, 

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

132 ) 

133 biasLevel = pexConfig.Field( 

134 dtype=float, 

135 default=8000.0, 

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

137 ) 

138 darkRate = pexConfig.Field( 

139 dtype=float, 

140 default=5.0, 

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

142 ) 

143 darkTime = pexConfig.Field( 

144 dtype=float, 

145 default=5.0, 

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

147 ) 

148 flatDrop = pexConfig.Field( 

149 dtype=float, 

150 default=0.1, 

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

152 ) 

153 fringeScale = pexConfig.ListField( 

154 dtype=float, 

155 default=[200.0], 

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

157 ) 

158 fringeX0 = pexConfig.ListField( 

159 dtype=float, 

160 default=[-100], 

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

162 ) 

163 fringeY0 = pexConfig.ListField( 

164 dtype=float, 

165 default=[-0], 

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

167 ) 

168 

169 # Inclusion parameters 

170 doAddSky = pexConfig.Field( 

171 dtype=bool, 

172 default=True, 

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

174 ) 

175 doAddSource = pexConfig.Field( 

176 dtype=bool, 

177 default=True, 

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

179 ) 

180 doAddCrosstalk = pexConfig.Field( 

181 dtype=bool, 

182 default=False, 

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

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

185 ) 

186 doAddOverscan = pexConfig.Field( 

187 dtype=bool, 

188 default=True, 

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

190 ) 

191 doAddBias = pexConfig.Field( 

192 dtype=bool, 

193 default=True, 

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

195 ) 

196 doAddDark = pexConfig.Field( 

197 dtype=bool, 

198 default=True, 

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

200 ) 

201 doAddFlat = pexConfig.Field( 

202 dtype=bool, 

203 default=True, 

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

205 ) 

206 doAddFringe = pexConfig.Field( 

207 dtype=bool, 

208 default=True, 

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

210 ) 

211 

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

213 doTransmissionCurve = pexConfig.Field( 

214 dtype=bool, 

215 default=False, 

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

217 ) 

218 doDefects = pexConfig.Field( 

219 dtype=bool, 

220 default=False, 

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

222 ) 

223 doBrighterFatter = pexConfig.Field( 

224 dtype=bool, 

225 default=False, 

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

227 ) 

228 doCrosstalkCoeffs = pexConfig.Field( 

229 dtype=bool, 

230 default=False, 

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

232 ) 

233 doDataRef = pexConfig.Field( 

234 dtype=bool, 

235 default=False, 

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

237 ) 

238 doGenerateImage = pexConfig.Field( 

239 dtype=bool, 

240 default=False, 

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

242 ) 

243 doGenerateData = pexConfig.Field( 

244 dtype=bool, 

245 default=False, 

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

247 ) 

248 doGenerateAmpDict = pexConfig.Field( 

249 dtype=bool, 

250 default=False, 

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

252 ) 

253 

254 

255class IsrMock(pipeBase.Task): 

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

257 

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

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

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

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

262 dependent on any of the actual obs package formats. 

263 """ 

264 ConfigClass = IsrMockConfig 

265 _DefaultName = "isrMock" 

266 

267 def __init__(self, **kwargs): 

268 super().__init__(**kwargs) 

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

270 self.crosstalkCoeffs = np.array([[0.0, 0.0, 0.0, 0.0, 0.0, -1e-3, 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, 0.0, 0.0, 0.0, 0.0, 0.0], 

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

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

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

278 if getDebugFrame(self._display, "mockCrosstalkCoeffs"): 

279 self.crosstalkCoeffs = np.array([[0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], 

280 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], 

281 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], 

282 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], 

283 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], 

284 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], 

285 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], 

286 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]]) 

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

288 [4., 16., 26., 16., 4.], 

289 [7., 26., 41., 26., 7.], 

290 [4., 16., 26., 16., 4.], 

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

292 

293 def run(self): 

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

295 

296 Returns 

297 ------- 

298 image : `lsst.afw.image.Exposure` 

299 Simulated ISR image with signals added. 

300 dataProduct : 

301 Simulated ISR data products. 

302 None : 

303 Returned if no valid configuration was found. 

304 

305 Raises 

306 ------ 

307 RuntimeError 

308 Raised if both doGenerateImage and doGenerateData are specified. 

309 """ 

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

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

312 elif self.config.doGenerateImage: 

313 return self.makeImage() 

314 elif self.config.doGenerateData: 

315 return self.makeData() 

316 else: 

317 return None 

318 

319 def makeData(self): 

320 """Generate simulated ISR data. 

321 

322 Currently, only the class defined crosstalk coefficient 

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

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

325 

326 Returns 

327 ------- 

328 dataProduct : 

329 Simulated ISR data product. 

330 """ 

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

332 self.config.doDefects, 

333 self.config.doTransmissionCurve, 

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

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

336 elif self.config.doBrighterFatter is True: 

337 return self.makeBfKernel() 

338 elif self.config.doDefects is True: 

339 return self.makeDefectList() 

340 elif self.config.doTransmissionCurve is True: 

341 return self.makeTransmissionCurve() 

342 elif self.config.doCrosstalkCoeffs is True: 

343 return self.crosstalkCoeffs 

344 else: 

345 return None 

346 

347 def makeBfKernel(self): 

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

349 

350 Returns 

351 ------- 

352 kernel : `numpy.ndarray` 

353 Simulated brighter-fatter kernel. 

354 """ 

355 return self.bfKernel 

356 

357 def makeDefectList(self): 

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

359 

360 Returns 

361 ------- 

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

363 Simulated defect list 

364 """ 

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

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

367 

368 def makeCrosstalkCoeff(self): 

369 """Generate the simulated crosstalk coefficients. 

370 

371 Returns 

372 ------- 

373 coeffs : `numpy.ndarray` 

374 Simulated crosstalk coefficients. 

375 """ 

376 

377 return self.crosstalkCoeffs 

378 

379 def makeTransmissionCurve(self): 

380 """Generate a simulated flat transmission curve. 

381 

382 Returns 

383 ------- 

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

385 Simulated transmission curve. 

386 """ 

387 

388 return afwImage.TransmissionCurve.makeIdentity() 

389 

390 def makeImage(self): 

391 """Generate a simulated ISR image. 

392 

393 Returns 

394 ------- 

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

396 Simulated ISR image data. 

397 

398 Notes 

399 ----- 

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

401 

402 * Generating a simulated sky with noise 

403 * Adding a single Gaussian "star" 

404 * Adding the fringe signal 

405 * Multiplying the frame by the simulated flat 

406 * Adding dark current (and noise) 

407 * Adding a bias offset (and noise) 

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

409 * Simulating crosstalk by adding a scaled version of each 

410 amplifier to each other amplifier. 

411 

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

413 three formats. 

414 

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

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

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

418 amplifier name. 

419 

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

421 implemented. 

422 

423 Note that this method generates an image in the reverse 

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

425 had a series of instrument effects added to an idealized 

426 exposure. 

427 """ 

428 exposure = self.getExposure() 

429 

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

431 bbox = None 

432 if self.config.isTrimmed is True: 

433 bbox = amp.getBBox() 

434 else: 

435 bbox = amp.getRawDataBBox() 

436 

437 ampData = exposure.image[bbox] 

438 

439 if self.config.doAddSky is True: 

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

441 

442 if self.config.doAddSource is True: 

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

444 self.config.sourceFlux, 

445 self.config.sourceX, 

446 self.config.sourceY): 

447 if idx == sourceAmp: 

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

449 

450 if self.config.doAddFringe is True: 

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

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

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

454 

455 if self.config.doAddFlat is True: 

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

457 self.amplifierAddNoise(ampData, 1.0, 0.0) 

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

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

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

461 

462 if self.config.doAddDark is True: 

463 self.amplifierAddNoise(ampData, 

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

465 np.sqrt(self.config.darkRate 

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

467 

468 if self.config.doAddCrosstalk is True: 

469 ctCalib = CrosstalkCalib() 

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

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

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

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

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

475 isTrimmed=self.config.isTrimmed) 

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

477 

478 for amp in exposure.getDetector(): 

479 bbox = None 

480 if self.config.isTrimmed is True: 

481 bbox = amp.getBBox() 

482 else: 

483 bbox = amp.getRawDataBBox() 

484 

485 ampData = exposure.image[bbox] 

486 

487 if self.config.doAddBias is True: 

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

489 self.config.readNoise / self.config.gain) 

490 

491 if self.config.doAddOverscan is True: 

492 oscanBBox = amp.getRawHorizontalOverscanBBox() 

493 oscanData = exposure.image[oscanBBox] 

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

495 self.config.readNoise / self.config.gain) 

496 

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

498 1.0 * self.config.overscanScale) 

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

500 1.0 * self.config.overscanScale) 

501 

502 if self.config.doGenerateAmpDict is True: 

503 expDict = dict() 

504 for amp in exposure.getDetector(): 

505 expDict[amp.getName()] = exposure 

506 return expDict 

507 else: 

508 return exposure 

509 

510 # afw primatives to construct the image structure 

511 def getCamera(self): 

512 """Construct a test camera object. 

513 

514 Returns 

515 ------- 

516 camera : `lsst.afw.cameraGeom.camera` 

517 Test camera. 

518 """ 

519 cameraWrapper = afwTestUtils.CameraWrapper( 

520 plateScale=self.config.plateScale, 

521 radialDistortion=self.config.radialDistortion, 

522 isLsstLike=self.config.isLsstLike, 

523 ) 

524 camera = cameraWrapper.camera 

525 return camera 

526 

527 def getExposure(self): 

528 """Construct a test exposure. 

529 

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

531 unlikely header keywords that can be removed during ISR 

532 processing to exercise that code. 

533 

534 Returns 

535 ------- 

536 exposure : `lsst.afw.exposure.Exposure` 

537 Construct exposure containing masked image of the 

538 appropriate size. 

539 """ 

540 camera = self.getCamera() 

541 detector = camera[self.config.detectorIndex] 

542 image = afwUtils.makeImageFromCcd(detector, 

543 isTrimmed=self.config.isTrimmed, 

544 showAmpGain=False, 

545 rcMarkSize=0, 

546 binSize=1, 

547 imageFactory=afwImage.ImageF) 

548 

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

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

551 image.assign(0.0) 

552 

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

554 exposure = afwImage.makeExposure(maskedImage) 

555 exposure.setDetector(detector) 

556 exposure.setWcs(self.getWcs()) 

557 

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

559 exposure.getInfo().setVisitInfo(visitInfo) 

560 

561 metadata = exposure.getMetadata() 

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

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

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

565 

566 ccd = exposure.getDetector() 

567 newCcd = ccd.rebuild() 

568 newCcd.clear() 

569 readoutMap = { 

570 'LL': ReadoutCorner.LL, 

571 'LR': ReadoutCorner.LR, 

572 'UR': ReadoutCorner.UR, 

573 'UL': ReadoutCorner.UL, 

574 } 

575 for amp in ccd: 

576 newAmp = amp.rebuild() 

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

578 newAmp.setLinearityType("Polynomial") 

579 newAmp.setGain(self.config.gain) 

580 newAmp.setSuspectLevel(25000.0) 

581 newAmp.setSaturation(32000.0) 

582 readoutCorner = 'LL' 

583 

584 # Apply flips to bbox where needed 

585 imageBBox = amp.getRawDataBBox() 

586 rawBbox = amp.getRawBBox() 

587 parallelOscanBBox = amp.getRawParallelOverscanBBox() 

588 serialOscanBBox = amp.getRawSerialOverscanBBox() 

589 prescanBBox = amp.getRawPrescanBBox() 

590 

591 if self.config.isLsstLike: 

592 # This follows cameraGeom.testUtils 

593 xoffset, yoffset = amp.getRawXYOffset() 

594 offext = lsst.geom.Extent2I(xoffset, yoffset) 

595 flipx = bool(amp.getRawFlipX()) 

596 flipy = bool(amp.getRawFlipY()) 

597 if flipx: 

598 xExt = rawBbox.getDimensions().getX() 

599 rawBbox.flipLR(xExt) 

600 imageBBox.flipLR(xExt) 

601 parallelOscanBBox.flipLR(xExt) 

602 serialOscanBBox.flipLR(xExt) 

603 prescanBBox.flipLR(xExt) 

604 if flipy: 

605 yExt = rawBbox.getDimensions().getY() 

606 rawBbox.flipTB(yExt) 

607 imageBBox.flipTB(yExt) 

608 parallelOscanBBox.flipTB(yExt) 

609 serialOscanBBox.flipTB(yExt) 

610 prescanBBox.flipTB(yExt) 

611 if not flipx and not flipy: 

612 readoutCorner = 'LL' 

613 elif flipx and not flipy: 

614 readoutCorner = 'LR' 

615 elif flipx and flipy: 

616 readoutCorner = 'UR' 

617 elif not flipx and flipy: 

618 readoutCorner = 'UL' 

619 rawBbox.shift(offext) 

620 imageBBox.shift(offext) 

621 parallelOscanBBox.shift(offext) 

622 serialOscanBBox.shift(offext) 

623 prescanBBox.shift(offext) 

624 newAmp.setReadoutCorner(readoutMap[readoutCorner]) 

625 newAmp.setRawBBox(rawBbox) 

626 newAmp.setRawDataBBox(imageBBox) 

627 newAmp.setRawParallelOverscanBBox(parallelOscanBBox) 

628 newAmp.setRawSerialOverscanBBox(serialOscanBBox) 

629 newAmp.setRawPrescanBBox(prescanBBox) 

630 newAmp.setRawFlipX(False) 

631 newAmp.setRawFlipY(False) 

632 no_offset = lsst.geom.Extent2I(0, 0) 

633 newAmp.setRawXYOffset(no_offset) 

634 

635 newCcd.append(newAmp) 

636 

637 exposure.setDetector(newCcd.finish()) 

638 

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

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

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

642 

643 return exposure 

644 

645 def getWcs(self): 

646 """Construct a dummy WCS object. 

647 

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

649 

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

651 listed in the afwTestUtils camera definition. 

652 

653 Returns 

654 ------- 

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

656 Test WCS transform. 

657 """ 

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

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

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

661 

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

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

664 exposure coordinate. 

665 

666 Parameters 

667 ---------- 

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

669 Amplifier image to use for conversions. 

670 x : `int` 

671 X-coordinate of the point to transform. 

672 y : `int` 

673 Y-coordinate of the point to transform. 

674 

675 Returns 

676 ------- 

677 u : `int` 

678 Transformed x-coordinate. 

679 v : `int` 

680 Transformed y-coordinate. 

681 

682 Notes 

683 ----- 

684 The output is transposed intentionally here, to match the 

685 internal transpose between numpy and afw.image coordinates. 

686 """ 

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

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

689 

690 return (v, u) 

691 

692 # Simple data values. 

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

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

695 

696 This method operates in the amplifier coordinate frame. 

697 

698 Parameters 

699 ---------- 

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

701 Amplifier image to operate on. 

702 mean : `float` 

703 Mean value of the Gaussian noise. 

704 sigma : `float` 

705 Sigma of the Gaussian noise. 

706 """ 

707 ampArr = ampData.array 

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

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

710 

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

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

713 

714 This method operates in the amplifier coordinate frame. 

715 

716 Parameters 

717 ---------- 

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

719 Amplifier image to operate on. 

720 start : `float` 

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

722 end : `float` 

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

724 """ 

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

726 ampArr = ampData.array 

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

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

729 

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

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

732 

733 This method operates in the amplifier coordinate frame. 

734 

735 Parameters 

736 ---------- 

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

738 Amplifier image to operate on. 

739 scale : `float` 

740 Peak flux of the source to add. 

741 x0 : `float` 

742 X-coordinate of the source peak. 

743 y0 : `float` 

744 Y-coordinate of the source peak. 

745 """ 

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

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

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

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

750 

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

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

753 

754 This method operates in the amplifier coordinate frame. 

755 

756 Parameters 

757 ---------- 

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

759 Amplifier image to add scaled copy from. 

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

761 Amplifier image to add scaled copy to. 

762 scale : `float` 

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

764 

765 Notes 

766 ----- 

767 This simulates simple crosstalk between amplifiers. 

768 """ 

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

770 + scale * ampDataSource.array[:]) 

771 

772 # Functional form data values. 

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

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

775 

776 Parameters 

777 ---------- 

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

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

780 transforms. 

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

782 Amplifier image to operate on. 

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

784 Peak intensity scaling for the ripple. 

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

786 Fringe center 

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

788 Fringe center 

789 

790 Notes 

791 ----- 

792 This uses an offset sinc function to generate a ripple 

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

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

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

796 the frame of the full trimmed image. 

797 """ 

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

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

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

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

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

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

804 

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

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

807 

808 Parameters 

809 ---------- 

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

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

812 transforms. 

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

814 Amplifier image to operate on. 

815 fracDrop : `float` 

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

817 u0 : `float` 

818 Peak location in detector coordinates. 

819 v0 : `float` 

820 Peak location in detector coordinates. 

821 

822 Notes 

823 ----- 

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

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

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

827 the frame of the full trimmed image. 

828 """ 

829 if fracDrop >= 1.0: 

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

831 

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

833 

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

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

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

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

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

839 

840 

841class RawMock(IsrMock): 

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

843 """ 

844 def __init__(self, **kwargs): 

845 super().__init__(**kwargs) 

846 self.config.isTrimmed = False 

847 self.config.doGenerateImage = True 

848 self.config.doGenerateAmpDict = False 

849 self.config.doAddOverscan = True 

850 self.config.doAddSky = True 

851 self.config.doAddSource = True 

852 self.config.doAddCrosstalk = False 

853 self.config.doAddBias = True 

854 self.config.doAddDark = True 

855 

856 

857class TrimmedRawMock(RawMock): 

858 """Generate a trimmed raw exposure. 

859 """ 

860 def __init__(self, **kwargs): 

861 super().__init__(**kwargs) 

862 self.config.isTrimmed = True 

863 self.config.doAddOverscan = False 

864 

865 

866class CalibratedRawMock(RawMock): 

867 """Generate a trimmed raw exposure. 

868 """ 

869 def __init__(self, **kwargs): 

870 super().__init__(**kwargs) 

871 self.config.isTrimmed = True 

872 self.config.doGenerateImage = True 

873 self.config.doAddOverscan = False 

874 self.config.doAddSky = True 

875 self.config.doAddSource = True 

876 self.config.doAddCrosstalk = False 

877 

878 self.config.doAddBias = False 

879 self.config.doAddDark = False 

880 self.config.doAddFlat = False 

881 self.config.doAddFringe = True 

882 

883 self.config.biasLevel = 0.0 

884 self.config.readNoise = 10.0 

885 

886 

887class RawDictMock(RawMock): 

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

889 """ 

890 def __init__(self, **kwargs): 

891 super().__init__(**kwargs) 

892 self.config.doGenerateAmpDict = True 

893 

894 

895class MasterMock(IsrMock): 

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

897 """ 

898 def __init__(self, **kwargs): 

899 super().__init__(**kwargs) 

900 self.config.isTrimmed = True 

901 self.config.doGenerateImage = True 

902 self.config.doAddOverscan = False 

903 self.config.doAddSky = False 

904 self.config.doAddSource = False 

905 self.config.doAddCrosstalk = False 

906 

907 self.config.doAddBias = False 

908 self.config.doAddDark = False 

909 self.config.doAddFlat = False 

910 self.config.doAddFringe = False 

911 

912 

913class BiasMock(MasterMock): 

914 """Simulated master bias calibration. 

915 """ 

916 def __init__(self, **kwargs): 

917 super().__init__(**kwargs) 

918 self.config.doAddBias = True 

919 self.config.readNoise = 10.0 

920 

921 

922class DarkMock(MasterMock): 

923 """Simulated master dark calibration. 

924 """ 

925 def __init__(self, **kwargs): 

926 super().__init__(**kwargs) 

927 self.config.doAddDark = True 

928 self.config.darkTime = 1.0 

929 

930 

931class FlatMock(MasterMock): 

932 """Simulated master flat calibration. 

933 """ 

934 def __init__(self, **kwargs): 

935 super().__init__(**kwargs) 

936 self.config.doAddFlat = True 

937 

938 

939class FringeMock(MasterMock): 

940 """Simulated master fringe calibration. 

941 """ 

942 def __init__(self, **kwargs): 

943 super().__init__(**kwargs) 

944 self.config.doAddFringe = True 

945 

946 

947class UntrimmedFringeMock(FringeMock): 

948 """Simulated untrimmed master fringe calibration. 

949 """ 

950 def __init__(self, **kwargs): 

951 super().__init__(**kwargs) 

952 self.config.isTrimmed = False 

953 

954 

955class BfKernelMock(IsrMock): 

956 """Simulated brighter-fatter kernel. 

957 """ 

958 def __init__(self, **kwargs): 

959 super().__init__(**kwargs) 

960 self.config.doGenerateImage = False 

961 self.config.doGenerateData = True 

962 self.config.doBrighterFatter = True 

963 self.config.doDefects = False 

964 self.config.doCrosstalkCoeffs = False 

965 self.config.doTransmissionCurve = False 

966 

967 

968class DefectMock(IsrMock): 

969 """Simulated defect list. 

970 """ 

971 def __init__(self, **kwargs): 

972 super().__init__(**kwargs) 

973 self.config.doGenerateImage = False 

974 self.config.doGenerateData = True 

975 self.config.doBrighterFatter = False 

976 self.config.doDefects = True 

977 self.config.doCrosstalkCoeffs = False 

978 self.config.doTransmissionCurve = False 

979 

980 

981class CrosstalkCoeffMock(IsrMock): 

982 """Simulated crosstalk coefficient matrix. 

983 """ 

984 def __init__(self, **kwargs): 

985 super().__init__(**kwargs) 

986 self.config.doGenerateImage = False 

987 self.config.doGenerateData = True 

988 self.config.doBrighterFatter = False 

989 self.config.doDefects = False 

990 self.config.doCrosstalkCoeffs = True 

991 self.config.doTransmissionCurve = False 

992 

993 

994class TransmissionMock(IsrMock): 

995 """Simulated transmission curve. 

996 """ 

997 def __init__(self, **kwargs): 

998 super().__init__(**kwargs) 

999 self.config.doGenerateImage = False 

1000 self.config.doGenerateData = True 

1001 self.config.doBrighterFatter = False 

1002 self.config.doDefects = False 

1003 self.config.doCrosstalkCoeffs = False 

1004 self.config.doTransmissionCurve = True 

1005 

1006 

1007class MockDataContainer(object): 

1008 """Container for holding ISR mock objects. 

1009 """ 

1010 dataId = "isrMock Fake Data" 

1011 darkval = 2. # e-/sec 

1012 oscan = 250. # DN 

1013 gradient = .10 

1014 exptime = 15.0 # seconds 

1015 darkexptime = 15.0 # seconds 

1016 

1017 def __init__(self, **kwargs): 

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

1019 self.config = kwargs['config'] 

1020 else: 

1021 self.config = None 

1022 

1023 def expectImage(self): 

1024 if self.config is None: 

1025 self.config = IsrMockConfig() 

1026 self.config.doGenerateImage = True 

1027 self.config.doGenerateData = False 

1028 

1029 def expectData(self): 

1030 if self.config is None: 

1031 self.config = IsrMockConfig() 

1032 self.config.doGenerateImage = False 

1033 self.config.doGenerateData = True 

1034 

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

1036 """Return an appropriate data product. 

1037 

1038 Parameters 

1039 ---------- 

1040 dataType : `str` 

1041 Type of data product to return. 

1042 

1043 Returns 

1044 ------- 

1045 mock : IsrMock.run() result 

1046 The output product. 

1047 """ 

1048 if "_filename" in dataType: 

1049 self.expectData() 

1050 return tempfile.mktemp(), "mock" 

1051 elif 'transmission_' in dataType: 

1052 self.expectData() 

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

1054 elif dataType == 'ccdExposureId': 

1055 self.expectData() 

1056 return 20090913 

1057 elif dataType == 'camera': 

1058 self.expectData() 

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

1060 elif dataType == 'raw': 

1061 self.expectImage() 

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

1063 elif dataType == 'bias': 

1064 self.expectImage() 

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

1066 elif dataType == 'dark': 

1067 self.expectImage() 

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

1069 elif dataType == 'flat': 

1070 self.expectImage() 

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

1072 elif dataType == 'fringe': 

1073 self.expectImage() 

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

1075 elif dataType == 'defects': 

1076 self.expectData() 

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

1078 elif dataType == 'bfKernel': 

1079 self.expectData() 

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

1081 elif dataType == 'linearizer': 

1082 return None 

1083 elif dataType == 'crosstalkSources': 

1084 return None 

1085 else: 

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

1087 

1088 

1089class MockFringeContainer(object): 

1090 """Container for mock fringe data. 

1091 """ 

1092 dataId = "isrMock Fake Data" 

1093 darkval = 2. # e-/sec 

1094 oscan = 250. # DN 

1095 gradient = .10 

1096 exptime = 15 # seconds 

1097 darkexptime = 40. # seconds 

1098 

1099 def __init__(self, **kwargs): 

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

1101 self.config = kwargs['config'] 

1102 else: 

1103 self.config = IsrMockConfig() 

1104 self.config.isTrimmed = True 

1105 self.config.doAddFringe = True 

1106 self.config.readNoise = 10.0 

1107 

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

1109 """Return an appropriate data product. 

1110 

1111 Parameters 

1112 ---------- 

1113 dataType : `str` 

1114 Type of data product to return. 

1115 

1116 Returns 

1117 ------- 

1118 mock : IsrMock.run() result 

1119 The output product. 

1120 """ 

1121 if "_filename" in dataType: 

1122 return tempfile.mktemp(), "mock" 

1123 elif 'transmission_' in dataType: 

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

1125 elif dataType == 'ccdExposureId': 

1126 return 20090913 

1127 elif dataType == 'camera': 

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

1129 elif dataType == 'raw': 

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

1131 elif dataType == 'bias': 

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

1133 elif dataType == 'dark': 

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

1135 elif dataType == 'flat': 

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

1137 elif dataType == 'fringe': 

1138 fringes = [] 

1139 configCopy = copy.deepcopy(self.config) 

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

1141 configCopy.fringeScale = [1.0] 

1142 configCopy.fringeX0 = [x] 

1143 configCopy.fringeY0 = [y] 

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

1145 return fringes 

1146 elif dataType == 'defects': 

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

1148 elif dataType == 'bfKernel': 

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

1150 elif dataType == 'linearizer': 

1151 return None 

1152 elif dataType == 'crosstalkSources': 

1153 return None 

1154 else: 

1155 return None