Coverage for tests/assembleCoaddTestUtils.py: 26%
228 statements
« prev ^ index » next coverage.py v6.4, created at 2022-06-02 04:29 -0700
« prev ^ index » next coverage.py v6.4, created at 2022-06-02 04:29 -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.
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
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
45from astro_metadata_translator import makeObservationInfo
47__all__ = ["MockWarpReference", "makeMockSkyInfo", "MockCoaddTestData",
48 "MockGen2WarpReference"]
51class MockGen2WarpReference:
52 """Very simple object that looks like a Gen2 data reference to a warped
53 exposure.
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."
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()
88 self.datasetTypes = (f"{coaddName}Coadd_directWarp", f"{coaddName}Coadd_psfMatchedWarp",
89 f"{coaddName}Coadd_directWarp_sub", f"{coaddName}Coadd_psfMatchedWarp_sub")
91 self.metadataTypes = (f"{coaddName}Coadd_directWarp_visitInfo",
92 f"{coaddName}Coadd_psfMatchedWarp_visitInfo")
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}
101 def get(self, datasetType, bbox=None, **kwargs):
102 """Retrieve the specified dataset using the API of the Gen 2 Butler.
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.
115 Returns
116 -------
117 `lsst.afw.image.Exposure` or `lsst.afw.image.VisitInfo`
118 Either the exposure or its metadata, depending on the datasetType.
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}.")
144 @property
145 def dataId(self):
146 """Generate a valid data identifier.
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)
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
164class MockWarpReference(lsst.daf.butler.DeferredDatasetHandle):
165 """Very simple object that looks like a Gen 3 data reference to a warped
166 exposure.
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
189 def get(self, bbox=None, component=None, parameters=None, **kwargs):
190 """Retrieve the specified dataset using the API of the Gen 3 Butler.
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.
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
224 @property
225 def dataId(self):
226 """Generate a valid data identifier.
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)
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`")
240def makeMockSkyInfo(bbox, wcs, patch):
241 """Construct a `Struct` containing the geometry of the patch to be coadded.
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.
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
262class MockCoaddTestData:
263 """Generate repeatable simulated exposures with consistent metadata that
264 are realistic enough to test the image coaddition algorithms.
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'.
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.
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`)."
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()
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
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
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
388 def setDummyCoaddInputs(self, exposure, expId):
389 """Generate an `ExposureCatalog` as though the exposures had been
390 processed using `warpAndPsfMatch`.
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)
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)
408 def makeCoaddTempExp(self, rawExposure, visitInfo, expId):
409 """Add the metadata required by `AssembleCoaddTask` to an exposure.
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.
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)
428 tempExp.setFilterLabel(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
435 def makeDummyWcs(self, rotAngle=None, pixelScale=None, crval=None, flipX=True):
436 """Make a World Coordinate System object for testing.
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.
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
465 def makeDummyVisitInfo(self, exposureId, randomizeTime=False):
466 """Make a self-consistent visitInfo object for testing.
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.
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)
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
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.
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)
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
561 if badRegionBox is not None:
562 model.mask[badRegionBox] = afwImage.Mask.getPlaneBitMask("BAD")
564 exposure = self.makeCoaddTempExp(model, visitInfo, expId)
565 matchedExposure = self.makeCoaddTempExp(modelPsfMatched, visitInfo, expId)
566 return exposure, matchedExposure
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.
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'.
582 Returns
583 -------
584 dataRefList : `list` of `MockGen2WarpReference`
585 The data references.
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
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.
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'.
613 Returns
614 -------
615 dataRefList : `list` of `MockWarpReference`
616 The data references.
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