Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

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 

29import lsst.afw.math as afwMath 

30import lsst.afw.cameraGeom.utils as afwUtils 

31import lsst.afw.cameraGeom.testUtils as afwTestUtils 

32from lsst.meas.algorithms import Defects 

33import lsst.pex.config as pexConfig 

34import lsst.pipe.base as pipeBase 

35from .crosstalk import X_FLIP, Y_FLIP 

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 

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

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

460 ampDataS = exposure.image[ampS.getBBox() if self.config.isTrimmed 

461 else ampS.getRawDataBBox()] 

462 ampDataT = exposure.image[ampT.getBBox() if self.config.isTrimmed 

463 else ampT.getRawDataBBox()] 

464 ampDataS = afwMath.flipImage(ampDataS, 

465 (X_FLIP[ampS.getReadoutCorner()] ^ 

466 X_FLIP[ampT.getReadoutCorner()]), 

467 (Y_FLIP[ampS.getReadoutCorner()] ^ 

468 Y_FLIP[ampT.getReadoutCorner()])) 

469 self.amplifierAddCT(ampDataS, ampDataT, self.crosstalkCoeffs[idxT][idxS]) 

470 

471 for amp in exposure.getDetector(): 

472 bbox = None 

473 if self.config.isTrimmed is True: 

474 bbox = amp.getBBox() 

475 else: 

476 bbox = amp.getRawDataBBox() 

477 

478 ampData = exposure.image[bbox] 

479 

480 if self.config.doAddBias is True: 

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

482 self.config.readNoise / self.config.gain) 

483 

484 if self.config.doAddOverscan is True: 

485 oscanBBox = amp.getRawHorizontalOverscanBBox() 

486 oscanData = exposure.image[oscanBBox] 

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

488 self.config.readNoise / self.config.gain) 

489 

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

491 1.0 * self.config.overscanScale) 

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

493 1.0 * self.config.overscanScale) 

494 

495 if self.config.doGenerateAmpDict is True: 

496 expDict = dict() 

497 for amp in exposure.getDetector(): 

498 expDict[amp.getName()] = exposure 

499 return expDict 

500 else: 

501 return exposure 

502 

503 # afw primatives to construct the image structure 

504 def getCamera(self): 

505 """Construct a test camera object. 

506 

507 Returns 

508 ------- 

509 camera : `lsst.afw.cameraGeom.camera` 

510 Test camera. 

511 """ 

512 cameraWrapper = afwTestUtils.CameraWrapper( 

513 plateScale=self.config.plateScale, 

514 radialDistortion=self.config.radialDistortion, 

515 isLsstLike=self.config.isLsstLike, 

516 ) 

517 camera = cameraWrapper.camera 

518 return camera 

519 

520 def getExposure(self): 

521 """Construct a test exposure. 

522 

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

524 unlikely header keywords that can be removed during ISR 

525 processing to exercise that code. 

526 

527 Returns 

528 ------- 

529 exposure : `lsst.afw.exposure.Exposure` 

530 Construct exposure containing masked image of the 

531 appropriate size. 

532 """ 

533 camera = self.getCamera() 

534 detector = camera[self.config.detectorIndex] 

535 image = afwUtils.makeImageFromCcd(detector, 

536 isTrimmed=self.config.isTrimmed, 

537 showAmpGain=False, 

538 rcMarkSize=0, 

539 binSize=1, 

540 imageFactory=afwImage.ImageF) 

541 

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

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

544 image.assign(0.0) 

545 

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

547 exposure = afwImage.makeExposure(maskedImage) 

548 exposure.setDetector(detector) 

549 exposure.setWcs(self.getWcs()) 

550 

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

552 exposure.getInfo().setVisitInfo(visitInfo) 

553 

554 metadata = exposure.getMetadata() 

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

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

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

558 

559 ccd = exposure.getDetector() 

560 newCcd = ccd.rebuild() 

561 newCcd.clear() 

562 for amp in ccd: 

563 newAmp = amp.rebuild() 

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

565 newAmp.setLinearityType("Polynomial") 

566 newAmp.setGain(self.config.gain) 

567 newAmp.setSuspectLevel(25000.0) 

568 newAmp.setSaturation(32000.0) 

569 newCcd.append(newAmp) 

570 exposure.setDetector(newCcd.finish()) 

571 

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

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

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

575 

576 return exposure 

577 

578 def getWcs(self): 

579 """Construct a dummy WCS object. 

580 

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

582 

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

584 listed in the afwTestUtils camera definition. 

585 

586 Returns 

587 ------- 

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

589 Test WCS transform. 

590 """ 

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

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

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

594 

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

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

597 exposure coordinate. 

598 

599 Parameters 

600 ---------- 

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

602 Amplifier image to use for conversions. 

603 x : `int` 

604 X-coordinate of the point to transform. 

605 y : `int` 

606 Y-coordinate of the point to transform. 

607 

608 Returns 

609 ------- 

610 u : `int` 

611 Transformed x-coordinate. 

612 v : `int` 

613 Transformed y-coordinate. 

614 

615 Notes 

616 ----- 

617 The output is transposed intentionally here, to match the 

618 internal transpose between numpy and afw.image coordinates. 

619 """ 

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

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

622 

623 return (v, u) 

624 

625 # Simple data values. 

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

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

628 

629 This method operates in the amplifier coordinate frame. 

630 

631 Parameters 

632 ---------- 

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

634 Amplifier image to operate on. 

635 mean : `float` 

636 Mean value of the Gaussian noise. 

637 sigma : `float` 

638 Sigma of the Gaussian noise. 

639 """ 

640 ampArr = ampData.array 

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

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

643 

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

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

646 

647 This method operates in the amplifier coordinate frame. 

648 

649 Parameters 

650 ---------- 

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

652 Amplifier image to operate on. 

653 start : `float` 

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

655 end : `float` 

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

657 """ 

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

659 ampArr = ampData.array 

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

661 np.zeros(ampData.getDimensions()).transpose()) 

662 

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

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

665 

666 This method operates in the amplifier coordinate frame. 

667 

668 Parameters 

669 ---------- 

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

671 Amplifier image to operate on. 

672 scale : `float` 

673 Peak flux of the source to add. 

674 x0 : `float` 

675 X-coordinate of the source peak. 

676 y0 : `float` 

677 Y-coordinate of the source peak. 

678 """ 

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

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

681 ampData.array[y][x] = (ampData.array[y][x] + 

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

683 

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

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

686 

687 This method operates in the amplifier coordinate frame. 

688 

689 Parameters 

690 ---------- 

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

692 Amplifier image to add scaled copy from. 

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

694 Amplifier image to add scaled copy to. 

695 scale : `float` 

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

697 

698 Notes 

699 ----- 

700 This simulates simple crosstalk between amplifiers. 

701 """ 

702 ampDataTarget.array[:] = (ampDataTarget.array[:] + 

703 scale * ampDataSource.array[:]) 

704 

705 # Functional form data values. 

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

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

708 

709 Parameters 

710 ---------- 

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

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

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

714 Amplifier image to operate on. 

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

716 Peak intensity scaling for the ripple. 

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

718 Fringe center 

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

720 Fringe center 

721 

722 Notes 

723 ----- 

724 This uses an offset sinc function to generate a ripple 

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

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

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

728 the frame of the full trimmed image. 

729 """ 

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

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

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

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

734 scale * 

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

736 ((v - y0) / 50)**2))) 

737 

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

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

740 

741 Parameters 

742 ---------- 

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

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

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

746 Amplifier image to operate on. 

747 fracDrop : `float` 

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

749 u0 : `float` 

750 Peak location in detector coordinates. 

751 v0 : `float` 

752 Peak location in detector coordinates. 

753 

754 Notes 

755 ----- 

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

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

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

759 the frame of the full trimmed image. 

760 """ 

761 if fracDrop >= 1.0: 

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

763 

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

765 

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

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

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

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

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

771 

772 

773class RawMock(IsrMock): 

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

775 """ 

776 def __init__(self, **kwargs): 

777 super().__init__(**kwargs) 

778 self.config.isTrimmed = False 

779 self.config.doGenerateImage = True 

780 self.config.doGenerateAmpDict = False 

781 self.config.doAddOverscan = True 

782 self.config.doAddSky = True 

783 self.config.doAddSource = True 

784 self.config.doAddCrosstalk = False 

785 self.config.doAddBias = True 

786 self.config.doAddDark = True 

787 

788 

789class TrimmedRawMock(RawMock): 

790 """Generate a trimmed raw exposure. 

791 """ 

792 def __init__(self, **kwargs): 

793 super().__init__(**kwargs) 

794 self.config.isTrimmed = True 

795 self.config.doAddOverscan = False 

796 

797 

798class CalibratedRawMock(RawMock): 

799 """Generate a trimmed raw exposure. 

800 """ 

801 def __init__(self, **kwargs): 

802 super().__init__(**kwargs) 

803 self.config.isTrimmed = True 

804 self.config.doGenerateImage = True 

805 self.config.doAddOverscan = False 

806 self.config.doAddSky = True 

807 self.config.doAddSource = True 

808 self.config.doAddCrosstalk = False 

809 

810 self.config.doAddBias = False 

811 self.config.doAddDark = False 

812 self.config.doAddFlat = False 

813 self.config.doAddFringe = True 

814 

815 self.config.biasLevel = 0.0 

816 self.config.readNoise = 10.0 

817 

818 

819class RawDictMock(RawMock): 

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

821 """ 

822 def __init__(self, **kwargs): 

823 super().__init__(**kwargs) 

824 self.config.doGenerateAmpDict = True 

825 

826 

827class MasterMock(IsrMock): 

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

829 """ 

830 def __init__(self, **kwargs): 

831 super().__init__(**kwargs) 

832 self.config.isTrimmed = True 

833 self.config.doGenerateImage = True 

834 self.config.doAddOverscan = False 

835 self.config.doAddSky = False 

836 self.config.doAddSource = False 

837 self.config.doAddCrosstalk = False 

838 

839 self.config.doAddBias = False 

840 self.config.doAddDark = False 

841 self.config.doAddFlat = False 

842 self.config.doAddFringe = False 

843 

844 

845class BiasMock(MasterMock): 

846 """Simulated master bias calibration. 

847 """ 

848 def __init__(self, **kwargs): 

849 super().__init__(**kwargs) 

850 self.config.doAddBias = True 

851 self.config.readNoise = 10.0 

852 

853 

854class DarkMock(MasterMock): 

855 """Simulated master dark calibration. 

856 """ 

857 def __init__(self, **kwargs): 

858 super().__init__(**kwargs) 

859 self.config.doAddDark = True 

860 self.config.darkTime = 1.0 

861 

862 

863class FlatMock(MasterMock): 

864 """Simulated master flat calibration. 

865 """ 

866 def __init__(self, **kwargs): 

867 super().__init__(**kwargs) 

868 self.config.doAddFlat = True 

869 

870 

871class FringeMock(MasterMock): 

872 """Simulated master fringe calibration. 

873 """ 

874 def __init__(self, **kwargs): 

875 super().__init__(**kwargs) 

876 self.config.doAddFringe = True 

877 

878 

879class UntrimmedFringeMock(FringeMock): 

880 """Simulated untrimmed master fringe calibration. 

881 """ 

882 def __init__(self, **kwargs): 

883 super().__init__(**kwargs) 

884 self.config.isTrimmed = False 

885 

886 

887class BfKernelMock(IsrMock): 

888 """Simulated brighter-fatter kernel. 

889 """ 

890 def __init__(self, **kwargs): 

891 super().__init__(**kwargs) 

892 self.config.doGenerateImage = False 

893 self.config.doGenerateData = True 

894 self.config.doBrighterFatter = True 

895 self.config.doDefects = False 

896 self.config.doCrosstalkCoeffs = False 

897 self.config.doTransmissionCurve = False 

898 

899 

900class DefectMock(IsrMock): 

901 """Simulated defect list. 

902 """ 

903 def __init__(self, **kwargs): 

904 super().__init__(**kwargs) 

905 self.config.doGenerateImage = False 

906 self.config.doGenerateData = True 

907 self.config.doBrighterFatter = False 

908 self.config.doDefects = True 

909 self.config.doCrosstalkCoeffs = False 

910 self.config.doTransmissionCurve = False 

911 

912 

913class CrosstalkCoeffMock(IsrMock): 

914 """Simulated crosstalk coefficient matrix. 

915 """ 

916 def __init__(self, **kwargs): 

917 super().__init__(**kwargs) 

918 self.config.doGenerateImage = False 

919 self.config.doGenerateData = True 

920 self.config.doBrighterFatter = False 

921 self.config.doDefects = False 

922 self.config.doCrosstalkCoeffs = True 

923 self.config.doTransmissionCurve = False 

924 

925 

926class TransmissionMock(IsrMock): 

927 """Simulated transmission curve. 

928 """ 

929 def __init__(self, **kwargs): 

930 super().__init__(**kwargs) 

931 self.config.doGenerateImage = False 

932 self.config.doGenerateData = True 

933 self.config.doBrighterFatter = False 

934 self.config.doDefects = False 

935 self.config.doCrosstalkCoeffs = False 

936 self.config.doTransmissionCurve = True 

937 

938 

939class DataRefMock(object): 

940 """Simulated gen2 butler data ref. 

941 

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

943 likely to be called for data in ISR processing. 

944 

945 """ 

946 dataId = "isrMock Fake Data" 

947 darkval = 2. # e-/sec 

948 oscan = 250. # DN 

949 gradient = .10 

950 exptime = 15.0 # seconds 

951 darkexptime = 15.0 # seconds 

952 

953 def __init__(self, **kwargs): 

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

955 self.config = kwargs['config'] 

956 else: 

957 self.config = None 

958 

959 def expectImage(self): 

960 if self.config is None: 

961 self.config = IsrMockConfig() 

962 self.config.doGenerateImage = True 

963 self.config.doGenerateData = False 

964 

965 def expectData(self): 

966 if self.config is None: 

967 self.config = IsrMockConfig() 

968 self.config.doGenerateImage = False 

969 self.config.doGenerateData = True 

970 

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

972 """Return an appropriate data product. 

973 

974 Parameters 

975 ---------- 

976 dataType : `str` 

977 Type of data product to return. 

978 

979 Returns 

980 ------- 

981 mock : IsrMock.run() result 

982 The output product. 

983 """ 

984 if "_filename" in dataType: 

985 self.expectData() 

986 return tempfile.mktemp(), "mock" 

987 elif 'transmission_' in dataType: 

988 self.expectData() 

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

990 elif dataType == 'ccdExposureId': 

991 self.expectData() 

992 return 20090913 

993 elif dataType == 'camera': 

994 self.expectData() 

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

996 elif dataType == 'raw': 

997 self.expectImage() 

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

999 elif dataType == 'bias': 

1000 self.expectImage() 

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

1002 elif dataType == 'dark': 

1003 self.expectImage() 

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

1005 elif dataType == 'flat': 

1006 self.expectImage() 

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

1008 elif dataType == 'fringe': 

1009 self.expectImage() 

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

1011 elif dataType == 'defects': 

1012 self.expectData() 

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

1014 elif dataType == 'bfKernel': 

1015 self.expectData() 

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

1017 elif dataType == 'linearizer': 

1018 return None 

1019 elif dataType == 'crosstalkSources': 

1020 return None 

1021 else: 

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

1023 

1024 def put(self, exposure, filename): 

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

1026 

1027 Parameters 

1028 ---------- 

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

1030 Image data to write out. 

1031 filename : `str` 

1032 Base name of the output file. 

1033 """ 

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

1035 

1036 

1037class FringeDataRefMock(object): 

1038 """Simulated gen2 butler data ref. 

1039 

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

1041 likely to be called for data in ISR processing. 

1042 

1043 """ 

1044 dataId = "isrMock Fake Data" 

1045 darkval = 2. # e-/sec 

1046 oscan = 250. # DN 

1047 gradient = .10 

1048 exptime = 15 # seconds 

1049 darkexptime = 40. # seconds 

1050 

1051 def __init__(self, **kwargs): 

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

1053 self.config = kwargs['config'] 

1054 else: 

1055 self.config = IsrMockConfig() 

1056 self.config.isTrimmed = True 

1057 self.config.doAddFringe = True 

1058 self.config.readNoise = 10.0 

1059 

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

1061 """Return an appropriate data product. 

1062 

1063 Parameters 

1064 ---------- 

1065 dataType : `str` 

1066 Type of data product to return. 

1067 

1068 Returns 

1069 ------- 

1070 mock : IsrMock.run() result 

1071 The output product. 

1072 """ 

1073 if "_filename" in dataType: 

1074 return tempfile.mktemp(), "mock" 

1075 elif 'transmission_' in dataType: 

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

1077 elif dataType == 'ccdExposureId': 

1078 return 20090913 

1079 elif dataType == 'camera': 

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

1081 elif dataType == 'raw': 

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

1083 elif dataType == 'bias': 

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

1085 elif dataType == 'dark': 

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

1087 elif dataType == 'flat': 

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

1089 elif dataType == 'fringe': 

1090 fringes = [] 

1091 configCopy = copy.deepcopy(self.config) 

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

1093 configCopy.fringeScale = [1.0] 

1094 configCopy.fringeX0 = [x] 

1095 configCopy.fringeY0 = [y] 

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

1097 return fringes 

1098 elif dataType == 'defects': 

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

1100 elif dataType == 'bfKernel': 

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

1102 elif dataType == 'linearizer': 

1103 return None 

1104 elif dataType == 'crosstalkSources': 

1105 return None 

1106 else: 

1107 return None 

1108 

1109 def put(self, exposure, filename): 

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

1111 

1112 Parameters 

1113 ---------- 

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

1115 Image data to write out. 

1116 filename : `str` 

1117 Base name of the output file. 

1118 """ 

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