Coverage for tests/assembleCoaddTestUtils.py: 26%

228 statements  

« prev     ^ index     » next       coverage.py v6.4.1, created at 2022-06-29 03:24 -0700

1# This file is part of pipe_tasks. 

2# 

3# LSST Data Management System 

4# This product includes software developed by the 

5# LSST Project (http://www.lsst.org/). 

6# See COPYRIGHT file at the top of the source tree. 

7# 

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

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

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

11# (at your option) any later version. 

12# 

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

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

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

16# GNU General Public License for more details. 

17# 

18# You should have received a copy of the LSST License Statement and 

19# the GNU General Public License along with this program. If not, 

20# see <https://www.lsstcorp.org/LegalNotices/>. 

21# 

22"""Set up simulated test data and simplified APIs for AssembleCoaddTask 

23and its derived classes. 

24 

25This is not intended to test accessing data with the Butler and instead uses 

26mock Butler data references to pass in the simulated data. 

27""" 

28from astropy.time import Time 

29from astropy import units as u 

30from astropy.coordinates import SkyCoord, EarthLocation, Angle 

31import numpy as np 

32 

33from lsst.afw.cameraGeom.testUtils import DetectorWrapper 

34import lsst.afw.geom as afwGeom 

35import lsst.afw.image as afwImage 

36import lsst.daf.butler 

37import lsst.daf.persistence.dataId 

38import lsst.geom as geom 

39from lsst.geom import arcseconds, degrees 

40from lsst.meas.algorithms.testUtils import plantSources 

41from lsst.obs.base import MakeRawVisitInfoViaObsInfo 

42import lsst.pipe.base as pipeBase 

43from lsst.pipe.tasks.coaddInputRecorder import CoaddInputRecorderTask, CoaddInputRecorderConfig 

44 

45from astro_metadata_translator import makeObservationInfo 

46 

47__all__ = ["MockWarpReference", "makeMockSkyInfo", "MockCoaddTestData", 

48 "MockGen2WarpReference"] 

49 

50 

51class MockGen2WarpReference: 

52 """Very simple object that looks like a Gen2 data reference to a warped 

53 exposure. 

54 

55 Parameters 

56 ---------- 

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

58 The exposure to be retrieved by the data reference. 

59 exposurePsfMatched : `lsst.afw.image.Exposure`, optional 

60 The exposure to be retrieved by the data reference, with a degraded PSF. 

61 coaddName : `str` 

62 The type of coadd being produced. Typically 'deep'. 

63 patch : `str` 

64 Unique identifier for a subdivision of a tract. 

65 In Gen 2 the patch identifier consists 

66 of two integers separated by a comma. 

67 tract : `int` 

68 Unique identifier for a tract of a skyMap 

69 visit : `int` 

70 Unique identifier for an observation, 

71 potentially consisting of multiple ccds. 

72 """ 

73 datasetTypes = None 

74 "List of the names of exposures that can be retrieved." 

75 metadataTypes = None 

76 "List of the names of metadata objects that can be retrieved." 

77 dataLookup = None 

78 "Stores the data and metadata that can be retrieved." 

79 

80 def __init__(self, exposure, exposurePsfMatched=None, coaddName='deep', 

81 patch="2,3", tract=0, visit=100): 

82 self.coaddName = coaddName 

83 self.tract = tract 

84 self.patch = patch 

85 self.visit = visit 

86 visitInfo = exposure.getInfo().getVisitInfo() 

87 

88 self.datasetTypes = (f"{coaddName}Coadd_directWarp", f"{coaddName}Coadd_psfMatchedWarp", 

89 f"{coaddName}Coadd_directWarp_sub", f"{coaddName}Coadd_psfMatchedWarp_sub") 

90 

91 self.metadataTypes = (f"{coaddName}Coadd_directWarp_visitInfo", 

92 f"{coaddName}Coadd_psfMatchedWarp_visitInfo") 

93 

94 self.dataLookup = {f"{coaddName}Coadd_directWarp": exposure, 

95 f"{coaddName}Coadd_psfMatchedWarp": exposurePsfMatched, 

96 f"{coaddName}Coadd_directWarp_sub": exposure, 

97 f"{coaddName}Coadd_psfMatchedWarp_sub": exposurePsfMatched, 

98 f"{coaddName}Coadd_directWarp_visitInfo": visitInfo, 

99 f"{coaddName}Coadd_psfMatchedWarp_visitInfo": visitInfo} 

100 

101 def get(self, datasetType, bbox=None, **kwargs): 

102 """Retrieve the specified dataset using the API of the Gen 2 Butler. 

103 

104 Parameters 

105 ---------- 

106 datasetType : `str` 

107 Name of the type of exposure to retrieve. 

108 bbox : `lsst.geom.box.Box2I`, optional 

109 If supplied and the `datasetType ends in "_sub", 

110 then retrieve only a subregion of the exposure. 

111 **kwargs 

112 Additional keyword arguments such as `immediate=True` that would 

113 control internal butler behavior. 

114 

115 Returns 

116 ------- 

117 `lsst.afw.image.Exposure` or `lsst.afw.image.VisitInfo` 

118 Either the exposure or its metadata, depending on the datasetType. 

119 

120 Raises 

121 ------ 

122 KeyError 

123 If a bounding box is specified incorrectly for the datasetType. 

124 ValueError 

125 If an unknown datasetType is supplied. 

126 """ 

127 if "_sub" in datasetType: 

128 if bbox is None: 

129 raise KeyError(f"A bbox must be supplied for dataset {datasetType}") 

130 else: 

131 if bbox is not None: 

132 raise KeyError(f"A bbox cannot be supplied for dataset {datasetType}") 

133 if datasetType in self.datasetTypes: 

134 exp = self.dataLookup[datasetType].clone() 

135 if "_sub" in datasetType: 

136 return exp[bbox] 

137 else: 

138 return exp 

139 elif datasetType in self.metadataTypes: 

140 return self.dataLookup[datasetType] 

141 else: 

142 raise ValueError(f"Unknown datasetType {datasetType}. Must be one of {self.datasetTypes}.") 

143 

144 @property 

145 def dataId(self): 

146 """Generate a valid data identifier. 

147 

148 Returns 

149 ------- 

150 dataId : `lsst.daf.persistence.dataId` 

151 Data identifier dict for patch. 

152 """ 

153 return lsst.daf.persistence.dataId.DataId(tract=self.tract, patch=self.patch, visit=self.visit) 

154 

155 def datasetExists(self, tempExpName): 

156 """Mimic the Gen2 Butler API for determining whether a dataset exists. 

157 """ 

158 if tempExpName in self.datasetTypes: 

159 return True 

160 else: 

161 return False 

162 

163 

164class MockWarpReference(lsst.daf.butler.DeferredDatasetHandle): 

165 """Very simple object that looks like a Gen 3 data reference to a warped 

166 exposure. 

167 

168 Parameters 

169 ---------- 

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

171 The exposure to be retrieved by the data reference. 

172 coaddName : `str` 

173 The type of coadd being produced. Typically 'deep'. 

174 patch : `int` 

175 Unique identifier for a subdivision of a tract. 

176 tract : `int` 

177 Unique identifier for a tract of a skyMap 

178 visit : `int` 

179 Unique identifier for an observation, 

180 potentially consisting of multiple ccds. 

181 """ 

182 def __init__(self, exposure, coaddName='deep', patch=42, tract=0, visit=100): 

183 self.coaddName = coaddName 

184 self.exposure = exposure 

185 self.tract = tract 

186 self.patch = patch 

187 self.visit = visit 

188 

189 def get(self, bbox=None, component=None, parameters=None, **kwargs): 

190 """Retrieve the specified dataset using the API of the Gen 3 Butler. 

191 

192 Parameters 

193 ---------- 

194 bbox : `lsst.geom.box.Box2I`, optional 

195 If supplied, retrieve only a subregion of the exposure. 

196 component : `str`, optional 

197 If supplied, return the named metadata of the exposure. 

198 parameters : `dict`, optional 

199 If supplied, use the parameters to modify the exposure, 

200 typically by taking a subset. 

201 **kwargs 

202 Additional keyword arguments such as `immediate=True` that would 

203 control internal butler behavior. 

204 

205 Returns 

206 ------- 

207 `lsst.afw.image.Exposure` or `lsst.afw.image.VisitInfo` 

208 or `lsst.meas.algorithms.SingleGaussianPsf` 

209 Either the exposure or its metadata, depending on the datasetType. 

210 """ 

211 if component == 'psf': 

212 return self.exposure.getPsf() 

213 elif component == 'visitInfo': 

214 return self.exposure.getInfo().getVisitInfo() 

215 if parameters is not None: 

216 if "bbox" in parameters: 

217 bbox = parameters["bbox"] 

218 exp = self.exposure.clone() 

219 if bbox is not None: 

220 return exp[bbox] 

221 else: 

222 return exp 

223 

224 @property 

225 def dataId(self): 

226 """Generate a valid data identifier. 

227 

228 Returns 

229 ------- 

230 dataId : `lsst.daf.persistence.dataId` 

231 Data identifier dict for the patch. 

232 """ 

233 return lsst.daf.persistence.dataId.DataId(tract=self.tract, patch=self.patch, visit=self.visit) 

234 

235 def datasetExists(self, tempExpName): 

236 """Raise a more informative error if this Gen 2 method is called.""" 

237 raise NotImplementedError("Gen3 butler data references don't support `datasetExists`") 

238 

239 

240def makeMockSkyInfo(bbox, wcs, patch): 

241 """Construct a `Struct` containing the geometry of the patch to be coadded. 

242 

243 Parameters 

244 ---------- 

245 bbox : `lsst.geom.Box` 

246 Bounding box of the patch to be coadded. 

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

248 Coordinate system definition (wcs) for the exposure. 

249 

250 Returns 

251 ------- 

252 skyInfo : `lsst.pipe.base.Struct` 

253 Patch geometry information. 

254 """ 

255 def getIndex(): 

256 return patch 

257 patchInfo = pipeBase.Struct(getIndex=getIndex) 

258 skyInfo = pipeBase.Struct(bbox=bbox, wcs=wcs, patchInfo=patchInfo) 

259 return skyInfo 

260 

261 

262class MockCoaddTestData: 

263 """Generate repeatable simulated exposures with consistent metadata that 

264 are realistic enough to test the image coaddition algorithms. 

265 

266 Notes 

267 ----- 

268 The simple GaussianPsf used by lsst.meas.algorithms.testUtils.plantSources 

269 will always return an average position of (0, 0). 

270 The bounding box of the exposures MUST include (0, 0), or else the PSF will 

271 not be valid and `AssembleCoaddTask` will fail with the error 

272 'Could not find a valid average position for CoaddPsf'. 

273 

274 Parameters 

275 ---------- 

276 shape : `lsst.geom.Extent2I`, optional 

277 Size of the bounding box of the exposures to be simulated, in pixels. 

278 offset : `lsst.geom.Point2I`, optional 

279 Pixel coordinate of the lower left corner of the bounding box. 

280 backgroundLevel : `float`, optional 

281 Background value added to all pixels in the simulated images. 

282 seed : `int`, optional 

283 Seed value to initialize the random number generator. 

284 nSrc : `int`, optional 

285 Number of sources to simulate. 

286 fluxRange : `float`, optional 

287 Range in flux amplitude of the simulated sources. 

288 noiseLevel : `float`, optional 

289 Standard deviation of the noise to add to each pixel. 

290 sourceSigma : `float`, optional 

291 Average amplitude of the simulated sources, 

292 relative to ``noiseLevel`` 

293 minPsfSize : `float`, optional 

294 The smallest PSF width (sigma) to use, in pixels. 

295 maxPsfSize : `float`, optional 

296 The largest PSF width (sigma) to use, in pixels. 

297 pixelScale : `lsst.geom.Angle`, optional 

298 The plate scale of the simulated images. 

299 ra : `lsst.geom.Angle`, optional 

300 Right Ascension of the boresight of the camera for the observation. 

301 dec : `lsst.geom.Angle`, optional 

302 Declination of the boresight of the camera for the observation. 

303 ccd : `int`, optional 

304 CCD number to put in the metadata of the exposure. 

305 patch : `int`, optional 

306 Unique identifier for a subdivision of a tract. 

307 patchGen2 : `str`, optional 

308 Unique identifier for a subdivision of a tract. 

309 In Gen 2 the patch identifier consists 

310 of two integers separated by a comma. 

311 tract : `int`, optional 

312 Unique identifier for a tract of a skyMap. 

313 

314 Raises 

315 ------ 

316 ValueError 

317 If the bounding box does not contain the pixel coordinate (0, 0). 

318 This is due to `GaussianPsf` that is used by `lsst.meas.algorithms.testUtils.plantSources` 

319 lacking the option to specify the pixel origin. 

320 """ 

321 rotAngle = 0.*degrees 

322 "Rotation of the pixel grid on the sky, East from North (`lsst.geom.Angle`)." 

323 filterLabel = None 

324 """The filter definition, usually set in the current instruments' obs package. 

325 For these tests, a simple filter is defined without using an obs package (`lsst.afw.image.FilterLabel`). 

326 """ 

327 rngData = None 

328 """Pre-initialized random number generator for constructing the test images 

329 repeatably (`numpy.random.Generator`). 

330 """ 

331 rngMods = None 

332 """Pre-initialized random number generator for applying modifications to 

333 the test images for only some test cases (`numpy.random.Generator`). 

334 """ 

335 kernelSize = None 

336 "Width of the kernel used for simulating sources, in pixels." 

337 exposures = {} 

338 "The simulated test data, with variable PSF sizes (`dict` of `lsst.afw.image.Exposure`)" 

339 matchedExposures = {} 

340 """The simulated exposures, all with PSF width set to `maxPsfSize` 

341 (`dict` of `lsst.afw.image.Exposure`). 

342 """ 

343 photoCalib = afwImage.makePhotoCalibFromCalibZeroPoint(27, 10) 

344 "The photometric zero point to use for converting counts to flux units (`lsst.afw.image.PhotoCalib`)." 

345 badMaskPlanes = ["NO_DATA", "BAD"] 

346 "Mask planes that, if set, the associated pixel should not be included in the coaddTempExp." 

347 detector = None 

348 "Properties of the CCD for the exposure (`lsst.afw.cameraGeom.Detector`)." 

349 

350 def __init__(self, shape=geom.Extent2I(201, 301), offset=geom.Point2I(-123, -45), 

351 backgroundLevel=314.592, seed=42, nSrc=37, 

352 fluxRange=2., noiseLevel=5, sourceSigma=200., 

353 minPsfSize=1.5, maxPsfSize=3., 

354 pixelScale=0.2*arcseconds, ra=209.*degrees, dec=-20.25*degrees, 

355 ccd=37, patch=42, patchGen2="2,3", tract=0): 

356 self.ra = ra 

357 self.dec = dec 

358 self.pixelScale = pixelScale 

359 self.patch = patch 

360 self.patchGen2 = patchGen2 

361 self.tract = tract 

362 self.filterLabel = afwImage.FilterLabel(band="gTest", physical="gTest") 

363 self.rngData = np.random.default_rng(seed) 

364 self.rngMods = np.random.default_rng(seed + 1) 

365 self.bbox = geom.Box2I(offset, shape) 

366 if not self.bbox.contains(0, 0): 

367 raise ValueError(f"The bounding box must contain the coordinate (0, 0). {repr(self.bbox)}") 

368 self.wcs = self.makeDummyWcs() 

369 

370 # Set up properties of the simulations 

371 nSigmaForKernel = 5 

372 self.kernelSize = (int(maxPsfSize*nSigmaForKernel + 0.5)//2)*2 + 1 # make sure it is odd 

373 

374 bufferSize = self.kernelSize//2 

375 x0, y0 = self.bbox.getBegin() 

376 xSize, ySize = self.bbox.getDimensions() 

377 # Set the pixel coordinates and fluxes of the simulated sources. 

378 self.xLoc = self.rngData.random(nSrc)*(xSize - 2*bufferSize) + bufferSize + x0 

379 self.yLoc = self.rngData.random(nSrc)*(ySize - 2*bufferSize) + bufferSize + y0 

380 self.flux = (self.rngData.random(nSrc)*(fluxRange - 1.) + 1.)*sourceSigma*noiseLevel 

381 

382 self.backgroundLevel = backgroundLevel 

383 self.noiseLevel = noiseLevel 

384 self.minPsfSize = minPsfSize 

385 self.maxPsfSize = maxPsfSize 

386 self.detector = DetectorWrapper(name=f"detector {ccd}", id=ccd).detector 

387 

388 def setDummyCoaddInputs(self, exposure, expId): 

389 """Generate an `ExposureCatalog` as though the exposures had been 

390 processed using `warpAndPsfMatch`. 

391 

392 Parameters 

393 ---------- 

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

395 The exposure to construct a `CoaddInputs` `ExposureCatalog` for. 

396 expId : `int` 

397 A unique identifier for the visit. 

398 """ 

399 badPixelMask = afwImage.Mask.getPlaneBitMask(self.badMaskPlanes) 

400 nGoodPix = np.sum(exposure.getMask().getArray() & badPixelMask == 0) 

401 

402 config = CoaddInputRecorderConfig() 

403 inputRecorder = CoaddInputRecorderTask(config=config, name="inputRecorder") 

404 tempExpInputRecorder = inputRecorder.makeCoaddTempExpRecorder(expId, num=1) 

405 tempExpInputRecorder.addCalExp(exposure, expId, nGoodPix) 

406 tempExpInputRecorder.finish(exposure, nGoodPix=nGoodPix) 

407 

408 def makeCoaddTempExp(self, rawExposure, visitInfo, expId): 

409 """Add the metadata required by `AssembleCoaddTask` to an exposure. 

410 

411 Parameters 

412 ---------- 

413 rawExposure : `lsst.afw.image.Exposure` 

414 The simulated exposure. 

415 visitInfo : `lsst.afw.image.VisitInfo` 

416 VisitInfo containing metadata for the exposure. 

417 expId : `int` 

418 A unique identifier for the visit. 

419 

420 Returns 

421 ------- 

422 tempExp : `lsst.afw.image.Exposure` 

423 The exposure, with all of the metadata needed for coaddition. 

424 """ 

425 tempExp = rawExposure.clone() 

426 tempExp.setWcs(self.wcs) 

427 

428 tempExp.setFilter(self.filterLabel) 

429 tempExp.setPhotoCalib(self.photoCalib) 

430 tempExp.getInfo().setVisitInfo(visitInfo) 

431 tempExp.getInfo().setDetector(self.detector) 

432 self.setDummyCoaddInputs(tempExp, expId) 

433 return tempExp 

434 

435 def makeDummyWcs(self, rotAngle=None, pixelScale=None, crval=None, flipX=True): 

436 """Make a World Coordinate System object for testing. 

437 

438 Parameters 

439 ---------- 

440 rotAngle : `lsst.geom.Angle` 

441 Rotation of the CD matrix, East from North 

442 pixelScale : `lsst.geom.Angle` 

443 Pixel scale of the projection. 

444 crval : `lsst.afw.geom.SpherePoint` 

445 Coordinates of the reference pixel of the wcs. 

446 flipX : `bool`, optional 

447 Flip the direction of increasing Right Ascension. 

448 

449 Returns 

450 ------- 

451 wcs : `lsst.afw.geom.skyWcs.SkyWcs` 

452 A wcs that matches the inputs. 

453 """ 

454 if rotAngle is None: 

455 rotAngle = self.rotAngle 

456 if pixelScale is None: 

457 pixelScale = self.pixelScale 

458 if crval is None: 

459 crval = geom.SpherePoint(self.ra, self.dec) 

460 crpix = geom.Box2D(self.bbox).getCenter() 

461 cdMatrix = afwGeom.makeCdMatrix(scale=pixelScale, orientation=rotAngle, flipX=flipX) 

462 wcs = afwGeom.makeSkyWcs(crpix=crpix, crval=crval, cdMatrix=cdMatrix) 

463 return wcs 

464 

465 def makeDummyVisitInfo(self, exposureId, randomizeTime=False): 

466 """Make a self-consistent visitInfo object for testing. 

467 

468 Parameters 

469 ---------- 

470 exposureId : `int`, optional 

471 Unique integer identifier for this observation. 

472 randomizeTime : `bool`, optional 

473 Add a random offset within a 6 hour window to the observation time. 

474 

475 Returns 

476 ------- 

477 visitInfo : `lsst.afw.image.VisitInfo` 

478 VisitInfo for the exposure. 

479 """ 

480 lsstLat = -30.244639*u.degree 

481 lsstLon = -70.749417*u.degree 

482 lsstAlt = 2663.*u.m 

483 lsstTemperature = 20.*u.Celsius 

484 lsstHumidity = 40. # in percent 

485 lsstPressure = 73892.*u.pascal 

486 loc = EarthLocation(lat=lsstLat, 

487 lon=lsstLon, 

488 height=lsstAlt) 

489 

490 time = Time(2000.0, format="jyear", scale="tt") 

491 if randomizeTime: 

492 # Pick a random time within a 6 hour window 

493 time += 6*u.hour*(self.rngMods.random() - 0.5) 

494 radec = SkyCoord(dec=self.dec.asDegrees(), ra=self.ra.asDegrees(), 

495 unit='deg', obstime=time, frame='icrs', location=loc) 

496 airmass = float(1.0/np.sin(radec.altaz.alt)) 

497 obsInfo = makeObservationInfo(location=loc, 

498 detector_exposure_id=exposureId, 

499 datetime_begin=time, 

500 datetime_end=time, 

501 boresight_airmass=airmass, 

502 boresight_rotation_angle=Angle(0.*u.degree), 

503 boresight_rotation_coord='sky', 

504 temperature=lsstTemperature, 

505 pressure=lsstPressure, 

506 relative_humidity=lsstHumidity, 

507 tracking_radec=radec, 

508 altaz_begin=radec.altaz, 

509 observation_type='science', 

510 ) 

511 visitInfo = MakeRawVisitInfoViaObsInfo.observationInfo2visitInfo(obsInfo) 

512 return visitInfo 

513 

514 def makeTestImage(self, expId, noiseLevel=None, psfSize=None, backgroundLevel=None, 

515 detectionSigma=5., badRegionBox=None): 

516 """Make a reproduceable PSF-convolved masked image for testing. 

517 

518 Parameters 

519 ---------- 

520 expId : `int` 

521 A unique identifier to use to refer to the visit. 

522 noiseLevel : `float`, optional 

523 Standard deviation of the noise to add to each pixel. 

524 psfSize : `float`, optional 

525 Width of the PSF of the simulated sources, in pixels. 

526 backgroundLevel : `float`, optional 

527 Background value added to all pixels in the simulated images. 

528 detectionSigma : `float`, optional 

529 Threshold amplitude of the image to set the "DETECTED" mask. 

530 badRegionBox : `lsst.geom.Box2I`, optional 

531 Add a bad region bounding box (set to "BAD"). 

532 """ 

533 if backgroundLevel is None: 

534 backgroundLevel = self.backgroundLevel 

535 if noiseLevel is None: 

536 noiseLevel = 5. 

537 visitInfo = self.makeDummyVisitInfo(expId, randomizeTime=True) 

538 

539 if psfSize is None: 

540 psfSize = self.rngMods.random()*(self.maxPsfSize - self.minPsfSize) + self.minPsfSize 

541 nSrc = len(self.flux) 

542 sigmas = [psfSize for src in range(nSrc)] 

543 sigmasPsfMatched = [self.maxPsfSize for src in range(nSrc)] 

544 coordList = list(zip(self.xLoc, self.yLoc, self.flux, sigmas)) 

545 coordListPsfMatched = list(zip(self.xLoc, self.yLoc, self.flux, sigmasPsfMatched)) 

546 xSize, ySize = self.bbox.getDimensions() 

547 model = plantSources(self.bbox, self.kernelSize, self.backgroundLevel, 

548 coordList, addPoissonNoise=False) 

549 modelPsfMatched = plantSources(self.bbox, self.kernelSize, self.backgroundLevel, 

550 coordListPsfMatched, addPoissonNoise=False) 

551 model.variance.array = np.abs(model.image.array) + noiseLevel 

552 modelPsfMatched.variance.array = np.abs(modelPsfMatched.image.array) + noiseLevel 

553 noise = self.rngData.random((ySize, xSize))*noiseLevel 

554 noise -= np.median(noise) 

555 model.image.array += noise 

556 modelPsfMatched.image.array += noise 

557 detectedMask = afwImage.Mask.getPlaneBitMask("DETECTED") 

558 detectionThreshold = self.backgroundLevel + detectionSigma*noiseLevel 

559 model.mask.array[model.image.array > detectionThreshold] += detectedMask 

560 

561 if badRegionBox is not None: 

562 model.mask[badRegionBox] = afwImage.Mask.getPlaneBitMask("BAD") 

563 

564 exposure = self.makeCoaddTempExp(model, visitInfo, expId) 

565 matchedExposure = self.makeCoaddTempExp(modelPsfMatched, visitInfo, expId) 

566 return exposure, matchedExposure 

567 

568 @staticmethod 

569 def makeGen2DataRefList(exposures, matchedExposures, tract=0, patch="2,3", coaddName="deep"): 

570 """Make data references from the simulated exposures that can be 

571 retrieved using the Gen 2 Butler API. 

572 

573 Parameters 

574 ---------- 

575 tract : `int` 

576 Unique identifier for a tract of a skyMap. 

577 patch : `str` 

578 Unique identifier for a subdivision of a tract. 

579 coaddName : `str` 

580 The type of coadd being produced. Typically 'deep'. 

581 

582 Returns 

583 ------- 

584 dataRefList : `list` of `MockGen2WarpReference` 

585 The data references. 

586 

587 """ 

588 dataRefList = [] 

589 for expId in exposures: 

590 exposure = exposures[expId] 

591 exposurePsfMatched = matchedExposures[expId] 

592 dataRef = MockGen2WarpReference(exposure, exposurePsfMatched=exposurePsfMatched, 

593 coaddName=coaddName, tract=tract, patch=patch, visit=expId) 

594 dataRefList.append(dataRef) 

595 return dataRefList 

596 

597 @staticmethod 

598 def makeDataRefList(exposures, matchedExposures, warpType, tract=0, patch=42, coaddName="deep"): 

599 """Make data references from the simulated exposures that can be 

600 retrieved using the Gen 3 Butler API. 

601 

602 Parameters 

603 ---------- 

604 warpType : `str` 

605 Either 'direct' or 'psfMatched'. 

606 tract : `int`, optional 

607 Unique identifier for a tract of a skyMap. 

608 patch : `int`, optional 

609 Unique identifier for a subdivision of a tract. 

610 coaddName : `str`, optional 

611 The type of coadd being produced. Typically 'deep'. 

612 

613 Returns 

614 ------- 

615 dataRefList : `list` of `MockWarpReference` 

616 The data references. 

617 

618 Raises 

619 ------ 

620 ValueError 

621 If an unknown `warpType` is supplied. 

622 """ 

623 dataRefList = [] 

624 for expId in exposures: 

625 if warpType == 'direct': 

626 exposure = exposures[expId] 

627 elif warpType == 'psfMatched': 

628 exposure = matchedExposures[expId] 

629 else: 

630 raise ValueError("warpType must be one of 'direct' or 'psfMatched'") 

631 dataRef = MockWarpReference(exposure, coaddName=coaddName, 

632 tract=tract, patch=patch, visit=expId) 

633 dataRefList.append(dataRef) 

634 return dataRefList