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

425 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-01-13 03:20 -0800

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 

35 

36import lsst.afw.cameraGeom.utils as afwUtils 

37import lsst.afw.cameraGeom.testUtils as afwTestUtils 

38import lsst.pex.config as pexConfig 

39import lsst.pipe.base as pipeBase 

40from .crosstalk import CrosstalkCalib 

41from .defects import Defects 

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 

392 * Generating a simulated sky with noise 

393 * Adding a single Gaussian "star" 

394 * Adding the fringe signal 

395 * Multiplying the frame by the simulated flat 

396 * Adding dark current (and noise) 

397 * Adding a bias offset (and noise) 

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

399 * Simulating crosstalk by adding a scaled version of each 

400 amplifier to each other amplifier. 

401 

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

403 three formats. 

404 

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

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

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

408 amplifier name. 

409 

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

411 implemented. 

412 

413 Note that this method generates an image in the reverse 

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

415 had a series of instrument effects added to an idealized 

416 exposure. 

417 """ 

418 exposure = self.getExposure() 

419 

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

421 bbox = None 

422 if self.config.isTrimmed is True: 

423 bbox = amp.getBBox() 

424 else: 

425 bbox = amp.getRawDataBBox() 

426 

427 ampData = exposure.image[bbox] 

428 

429 if self.config.doAddSky is True: 

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

431 

432 if self.config.doAddSource is True: 

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

434 self.config.sourceFlux, 

435 self.config.sourceX, 

436 self.config.sourceY): 

437 if idx == sourceAmp: 

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

439 

440 if self.config.doAddFringe is True: 

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

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

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

444 

445 if self.config.doAddFlat is True: 

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

447 self.amplifierAddNoise(ampData, 1.0, 0.0) 

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

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

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

451 

452 if self.config.doAddDark is True: 

453 self.amplifierAddNoise(ampData, 

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

455 np.sqrt(self.config.darkRate 

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

457 

458 if self.config.doAddCrosstalk is True: 

459 ctCalib = CrosstalkCalib() 

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

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

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

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

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

465 isTrimmed=self.config.isTrimmed) 

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

467 

468 for amp in exposure.getDetector(): 

469 bbox = None 

470 if self.config.isTrimmed is True: 

471 bbox = amp.getBBox() 

472 else: 

473 bbox = amp.getRawDataBBox() 

474 

475 ampData = exposure.image[bbox] 

476 

477 if self.config.doAddBias is True: 

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

479 self.config.readNoise / self.config.gain) 

480 

481 if self.config.doAddOverscan is True: 

482 oscanBBox = amp.getRawHorizontalOverscanBBox() 

483 oscanData = exposure.image[oscanBBox] 

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

485 self.config.readNoise / self.config.gain) 

486 

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

488 1.0 * self.config.overscanScale) 

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

490 1.0 * self.config.overscanScale) 

491 

492 if self.config.doGenerateAmpDict is True: 

493 expDict = dict() 

494 for amp in exposure.getDetector(): 

495 expDict[amp.getName()] = exposure 

496 return expDict 

497 else: 

498 return exposure 

499 

500 # afw primatives to construct the image structure 

501 def getCamera(self): 

502 """Construct a test camera object. 

503 

504 Returns 

505 ------- 

506 camera : `lsst.afw.cameraGeom.camera` 

507 Test camera. 

508 """ 

509 cameraWrapper = afwTestUtils.CameraWrapper( 

510 plateScale=self.config.plateScale, 

511 radialDistortion=self.config.radialDistortion, 

512 isLsstLike=self.config.isLsstLike, 

513 ) 

514 camera = cameraWrapper.camera 

515 return camera 

516 

517 def getExposure(self): 

518 """Construct a test exposure. 

519 

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

521 unlikely header keywords that can be removed during ISR 

522 processing to exercise that code. 

523 

524 Returns 

525 ------- 

526 exposure : `lsst.afw.exposure.Exposure` 

527 Construct exposure containing masked image of the 

528 appropriate size. 

529 """ 

530 camera = self.getCamera() 

531 detector = camera[self.config.detectorIndex] 

532 image = afwUtils.makeImageFromCcd(detector, 

533 isTrimmed=self.config.isTrimmed, 

534 showAmpGain=False, 

535 rcMarkSize=0, 

536 binSize=1, 

537 imageFactory=afwImage.ImageF) 

538 

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

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

541 image.assign(0.0) 

542 

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

544 exposure = afwImage.makeExposure(maskedImage) 

545 exposure.setDetector(detector) 

546 exposure.setWcs(self.getWcs()) 

547 

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

549 exposure.getInfo().setVisitInfo(visitInfo) 

550 

551 metadata = exposure.getMetadata() 

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

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

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

555 

556 ccd = exposure.getDetector() 

557 newCcd = ccd.rebuild() 

558 newCcd.clear() 

559 for amp in ccd: 

560 newAmp = amp.rebuild() 

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

562 newAmp.setLinearityType("Polynomial") 

563 newAmp.setGain(self.config.gain) 

564 newAmp.setSuspectLevel(25000.0) 

565 newAmp.setSaturation(32000.0) 

566 newCcd.append(newAmp) 

567 exposure.setDetector(newCcd.finish()) 

568 

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

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

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

572 

573 return exposure 

574 

575 def getWcs(self): 

576 """Construct a dummy WCS object. 

577 

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

579 

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

581 listed in the afwTestUtils camera definition. 

582 

583 Returns 

584 ------- 

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

586 Test WCS transform. 

587 """ 

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

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

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

591 

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

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

594 exposure coordinate. 

595 

596 Parameters 

597 ---------- 

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

599 Amplifier image to use for conversions. 

600 x : `int` 

601 X-coordinate of the point to transform. 

602 y : `int` 

603 Y-coordinate of the point to transform. 

604 

605 Returns 

606 ------- 

607 u : `int` 

608 Transformed x-coordinate. 

609 v : `int` 

610 Transformed y-coordinate. 

611 

612 Notes 

613 ----- 

614 The output is transposed intentionally here, to match the 

615 internal transpose between numpy and afw.image coordinates. 

616 """ 

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

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

619 

620 return (v, u) 

621 

622 # Simple data values. 

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

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

625 

626 This method operates in the amplifier coordinate frame. 

627 

628 Parameters 

629 ---------- 

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

631 Amplifier image to operate on. 

632 mean : `float` 

633 Mean value of the Gaussian noise. 

634 sigma : `float` 

635 Sigma of the Gaussian noise. 

636 """ 

637 ampArr = ampData.array 

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

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

640 

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

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

643 

644 This method operates in the amplifier coordinate frame. 

645 

646 Parameters 

647 ---------- 

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

649 Amplifier image to operate on. 

650 start : `float` 

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

652 end : `float` 

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

654 """ 

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

656 ampArr = ampData.array 

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

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

659 

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

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

662 

663 This method operates in the amplifier coordinate frame. 

664 

665 Parameters 

666 ---------- 

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

668 Amplifier image to operate on. 

669 scale : `float` 

670 Peak flux of the source to add. 

671 x0 : `float` 

672 X-coordinate of the source peak. 

673 y0 : `float` 

674 Y-coordinate of the source peak. 

675 """ 

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

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

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

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

680 

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

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

683 

684 This method operates in the amplifier coordinate frame. 

685 

686 Parameters 

687 ---------- 

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

689 Amplifier image to add scaled copy from. 

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

691 Amplifier image to add scaled copy to. 

692 scale : `float` 

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

694 

695 Notes 

696 ----- 

697 This simulates simple crosstalk between amplifiers. 

698 """ 

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

700 + scale * ampDataSource.array[:]) 

701 

702 # Functional form data values. 

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

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

705 

706 Parameters 

707 ---------- 

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

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

710 transforms. 

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

712 Amplifier image to operate on. 

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

714 Peak intensity scaling for the ripple. 

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

716 Fringe center 

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

718 Fringe center 

719 

720 Notes 

721 ----- 

722 This uses an offset sinc function to generate a ripple 

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

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

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

726 the frame of the full trimmed image. 

727 """ 

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

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

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

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

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

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

734 

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

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

737 

738 Parameters 

739 ---------- 

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

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

742 transforms. 

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

744 Amplifier image to operate on. 

745 fracDrop : `float` 

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

747 u0 : `float` 

748 Peak location in detector coordinates. 

749 v0 : `float` 

750 Peak location in detector coordinates. 

751 

752 Notes 

753 ----- 

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

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

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

757 the frame of the full trimmed image. 

758 """ 

759 if fracDrop >= 1.0: 

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

761 

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

763 

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

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

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

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

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

769 

770 

771class RawMock(IsrMock): 

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

773 """ 

774 def __init__(self, **kwargs): 

775 super().__init__(**kwargs) 

776 self.config.isTrimmed = False 

777 self.config.doGenerateImage = True 

778 self.config.doGenerateAmpDict = False 

779 self.config.doAddOverscan = True 

780 self.config.doAddSky = True 

781 self.config.doAddSource = True 

782 self.config.doAddCrosstalk = False 

783 self.config.doAddBias = True 

784 self.config.doAddDark = True 

785 

786 

787class TrimmedRawMock(RawMock): 

788 """Generate a trimmed raw exposure. 

789 """ 

790 def __init__(self, **kwargs): 

791 super().__init__(**kwargs) 

792 self.config.isTrimmed = True 

793 self.config.doAddOverscan = False 

794 

795 

796class CalibratedRawMock(RawMock): 

797 """Generate a trimmed raw exposure. 

798 """ 

799 def __init__(self, **kwargs): 

800 super().__init__(**kwargs) 

801 self.config.isTrimmed = True 

802 self.config.doGenerateImage = True 

803 self.config.doAddOverscan = False 

804 self.config.doAddSky = True 

805 self.config.doAddSource = True 

806 self.config.doAddCrosstalk = False 

807 

808 self.config.doAddBias = False 

809 self.config.doAddDark = False 

810 self.config.doAddFlat = False 

811 self.config.doAddFringe = True 

812 

813 self.config.biasLevel = 0.0 

814 self.config.readNoise = 10.0 

815 

816 

817class RawDictMock(RawMock): 

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

819 """ 

820 def __init__(self, **kwargs): 

821 super().__init__(**kwargs) 

822 self.config.doGenerateAmpDict = True 

823 

824 

825class MasterMock(IsrMock): 

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

827 """ 

828 def __init__(self, **kwargs): 

829 super().__init__(**kwargs) 

830 self.config.isTrimmed = True 

831 self.config.doGenerateImage = True 

832 self.config.doAddOverscan = False 

833 self.config.doAddSky = False 

834 self.config.doAddSource = False 

835 self.config.doAddCrosstalk = False 

836 

837 self.config.doAddBias = False 

838 self.config.doAddDark = False 

839 self.config.doAddFlat = False 

840 self.config.doAddFringe = False 

841 

842 

843class BiasMock(MasterMock): 

844 """Simulated master bias calibration. 

845 """ 

846 def __init__(self, **kwargs): 

847 super().__init__(**kwargs) 

848 self.config.doAddBias = True 

849 self.config.readNoise = 10.0 

850 

851 

852class DarkMock(MasterMock): 

853 """Simulated master dark calibration. 

854 """ 

855 def __init__(self, **kwargs): 

856 super().__init__(**kwargs) 

857 self.config.doAddDark = True 

858 self.config.darkTime = 1.0 

859 

860 

861class FlatMock(MasterMock): 

862 """Simulated master flat calibration. 

863 """ 

864 def __init__(self, **kwargs): 

865 super().__init__(**kwargs) 

866 self.config.doAddFlat = True 

867 

868 

869class FringeMock(MasterMock): 

870 """Simulated master fringe calibration. 

871 """ 

872 def __init__(self, **kwargs): 

873 super().__init__(**kwargs) 

874 self.config.doAddFringe = True 

875 

876 

877class UntrimmedFringeMock(FringeMock): 

878 """Simulated untrimmed master fringe calibration. 

879 """ 

880 def __init__(self, **kwargs): 

881 super().__init__(**kwargs) 

882 self.config.isTrimmed = False 

883 

884 

885class BfKernelMock(IsrMock): 

886 """Simulated brighter-fatter kernel. 

887 """ 

888 def __init__(self, **kwargs): 

889 super().__init__(**kwargs) 

890 self.config.doGenerateImage = False 

891 self.config.doGenerateData = True 

892 self.config.doBrighterFatter = True 

893 self.config.doDefects = False 

894 self.config.doCrosstalkCoeffs = False 

895 self.config.doTransmissionCurve = False 

896 

897 

898class DefectMock(IsrMock): 

899 """Simulated defect list. 

900 """ 

901 def __init__(self, **kwargs): 

902 super().__init__(**kwargs) 

903 self.config.doGenerateImage = False 

904 self.config.doGenerateData = True 

905 self.config.doBrighterFatter = False 

906 self.config.doDefects = True 

907 self.config.doCrosstalkCoeffs = False 

908 self.config.doTransmissionCurve = False 

909 

910 

911class CrosstalkCoeffMock(IsrMock): 

912 """Simulated crosstalk coefficient matrix. 

913 """ 

914 def __init__(self, **kwargs): 

915 super().__init__(**kwargs) 

916 self.config.doGenerateImage = False 

917 self.config.doGenerateData = True 

918 self.config.doBrighterFatter = False 

919 self.config.doDefects = False 

920 self.config.doCrosstalkCoeffs = True 

921 self.config.doTransmissionCurve = False 

922 

923 

924class TransmissionMock(IsrMock): 

925 """Simulated transmission curve. 

926 """ 

927 def __init__(self, **kwargs): 

928 super().__init__(**kwargs) 

929 self.config.doGenerateImage = False 

930 self.config.doGenerateData = True 

931 self.config.doBrighterFatter = False 

932 self.config.doDefects = False 

933 self.config.doCrosstalkCoeffs = False 

934 self.config.doTransmissionCurve = True 

935 

936 

937class MockDataContainer(object): 

938 """Container for holding ISR mock objects. 

939 """ 

940 dataId = "isrMock Fake Data" 

941 darkval = 2. # e-/sec 

942 oscan = 250. # DN 

943 gradient = .10 

944 exptime = 15.0 # seconds 

945 darkexptime = 15.0 # seconds 

946 

947 def __init__(self, **kwargs): 

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

949 self.config = kwargs['config'] 

950 else: 

951 self.config = None 

952 

953 def expectImage(self): 

954 if self.config is None: 

955 self.config = IsrMockConfig() 

956 self.config.doGenerateImage = True 

957 self.config.doGenerateData = False 

958 

959 def expectData(self): 

960 if self.config is None: 

961 self.config = IsrMockConfig() 

962 self.config.doGenerateImage = False 

963 self.config.doGenerateData = True 

964 

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

966 """Return an appropriate data product. 

967 

968 Parameters 

969 ---------- 

970 dataType : `str` 

971 Type of data product to return. 

972 

973 Returns 

974 ------- 

975 mock : IsrMock.run() result 

976 The output product. 

977 """ 

978 if "_filename" in dataType: 

979 self.expectData() 

980 return tempfile.mktemp(), "mock" 

981 elif 'transmission_' in dataType: 

982 self.expectData() 

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

984 elif dataType == 'ccdExposureId': 

985 self.expectData() 

986 return 20090913 

987 elif dataType == 'camera': 

988 self.expectData() 

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

990 elif dataType == 'raw': 

991 self.expectImage() 

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

993 elif dataType == 'bias': 

994 self.expectImage() 

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

996 elif dataType == 'dark': 

997 self.expectImage() 

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

999 elif dataType == 'flat': 

1000 self.expectImage() 

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

1002 elif dataType == 'fringe': 

1003 self.expectImage() 

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

1005 elif dataType == 'defects': 

1006 self.expectData() 

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

1008 elif dataType == 'bfKernel': 

1009 self.expectData() 

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

1011 elif dataType == 'linearizer': 

1012 return None 

1013 elif dataType == 'crosstalkSources': 

1014 return None 

1015 else: 

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

1017 

1018 

1019class MockFringeContainer(object): 

1020 """Container for mock fringe data. 

1021 """ 

1022 dataId = "isrMock Fake Data" 

1023 darkval = 2. # e-/sec 

1024 oscan = 250. # DN 

1025 gradient = .10 

1026 exptime = 15 # seconds 

1027 darkexptime = 40. # seconds 

1028 

1029 def __init__(self, **kwargs): 

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

1031 self.config = kwargs['config'] 

1032 else: 

1033 self.config = IsrMockConfig() 

1034 self.config.isTrimmed = True 

1035 self.config.doAddFringe = True 

1036 self.config.readNoise = 10.0 

1037 

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

1039 """Return an appropriate data product. 

1040 

1041 Parameters 

1042 ---------- 

1043 dataType : `str` 

1044 Type of data product to return. 

1045 

1046 Returns 

1047 ------- 

1048 mock : IsrMock.run() result 

1049 The output product. 

1050 """ 

1051 if "_filename" in dataType: 

1052 return tempfile.mktemp(), "mock" 

1053 elif 'transmission_' in dataType: 

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

1055 elif dataType == 'ccdExposureId': 

1056 return 20090913 

1057 elif dataType == 'camera': 

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

1059 elif dataType == 'raw': 

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

1061 elif dataType == 'bias': 

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

1063 elif dataType == 'dark': 

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

1065 elif dataType == 'flat': 

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

1067 elif dataType == 'fringe': 

1068 fringes = [] 

1069 configCopy = copy.deepcopy(self.config) 

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

1071 configCopy.fringeScale = [1.0] 

1072 configCopy.fringeX0 = [x] 

1073 configCopy.fringeY0 = [y] 

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

1075 return fringes 

1076 elif dataType == 'defects': 

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

1078 elif dataType == 'bfKernel': 

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

1080 elif dataType == 'linearizer': 

1081 return None 

1082 elif dataType == 'crosstalkSources': 

1083 return None 

1084 else: 

1085 return None