lsst.pipe.tasks g5d7e2d85e8+aa7cea86a2
mockObservation.py
Go to the documentation of this file.
2# LSST Data Management System
3# Copyright 2008, 2009, 2010, 2011, 2012 LSST Corporation.
4#
5# This product includes software developed by the
6# LSST Project (http://www.lsst.org/).
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 <http://www.lsstcorp.org/LegalNotices/>.
21#
22import numpy as np
23
24import lsst.pex.config
25import lsst.geom
26import lsst.afw.table
27import lsst.afw.geom
28from lsst.afw.cameraGeom import PIXELS, FOCAL_PLANE
29import lsst.afw.image
30import lsst.afw.math
32import lsst.pipe.base
33from lsst.meas.base.apCorrRegistry import getApCorrNameSet
34from lsst.meas.algorithms.testUtils import makeRandomTransmissionCurve
35
36
37class MockObservationConfig(lsst.pex.config.Config):
38 pixelScale = lsst.pex.config.Field(
39 dtype=float, default=0.2, optional=False,
40 doc="Pixel scale for mock WCSs in arcseconds/pixel"
41 )
42 doRotate = lsst.pex.config.Field(
43 dtype=bool, default=True, optional=False,
44 doc="Whether to randomly rotate observations relative to the tract Wcs"
45 )
46 fluxMag0 = lsst.pex.config.Field(
47 dtype=float, default=1E11, optional=False,
48 doc="Flux at zero magnitude used to define PhotoCalibs."
49 )
50 fluxMag0Err = lsst.pex.config.Field(
51 dtype=float, default=100.0, optional=False,
52 doc="Error on flux at zero magnitude used to define PhotoCalibs; used to add scatter as well."
53 )
54 expTime = lsst.pex.config.Field(
55 dtype=float, default=60.0, optional=False,
56 doc="Exposure time set in visitInfo (does not affect flux or noise level)"
57 )
58 psfImageSize = lsst.pex.config.Field(
59 dtype=int, default=21, optional=False,
60 doc="Image width and height of generated Psfs."
61 )
62 psfMinSigma = lsst.pex.config.Field(
63 dtype=float, default=1.5, optional=False,
64 doc="Minimum radius for generated Psfs."
65 )
66 psfMaxSigma = lsst.pex.config.Field(
67 dtype=float, default=3.0, optional=False,
68 doc="Maximum radius for generated Psfs."
69 )
70 apCorrOrder = lsst.pex.config.Field(
71 dtype=int, default=1, optional=False,
72 doc="Polynomial order for aperture correction fields"
73 )
74 seed = lsst.pex.config.Field(dtype=int, default=1, doc="Seed for numpy random number generator")
75
76
77class MockObservationTask(lsst.pipe.base.Task):
78 """Task to generate mock Exposure parameters (Wcs, Psf, PhotoCalib), intended for use as a subtask
79 of MockCoaddTask.
80
81 @todo:
82 - document "pa" in detail; angle of what to what?
83 - document the catalog parameter of the run method
84 """
85
86 ConfigClass = MockObservationConfig
87
88 def __init__(self, **kwds):
89 lsst.pipe.base.Task.__init__(self, **kwds)
91 self.ccdKeyccdKey = self.schemaschema.addField("ccd", type=np.int32, doc="CCD number")
92 self.visitKeyvisitKey = self.schemaschema.addField("visit", type=np.int32, doc="visit number")
93 self.pointingKeypointingKey = lsst.afw.table.CoordKey.addFields(self.schemaschema, "pointing", "center of visit")
94 self.filterKeyfilterKey = self.schemaschema.addField("filter", type=str, doc="Bandpass filter name", size=16)
95 self.rngrng = np.random.RandomState(self.config.seed)
96
97 def run(self, butler, n, tractInfo, camera, catalog=None):
98 """Driver that generates an ExposureCatalog of mock observations.
99
100 @param[in] butler: a data butler
101 @param[in] n: number of pointings
102 @param[in] camera: camera geometry (an lsst.afw.cameraGeom.Camera)
103 @param[in] catalog: catalog to which to add observations (an ExposureCatalog);
104 if None then a new catalog is created.
105
106 @todo figure out what `pa` is and use that knowledge to set `boresightRotAng` and `rotType`
107 """
108 if catalog is None:
109 catalog = lsst.afw.table.ExposureCatalog(self.schemaschema)
110 else:
111 if not catalog.getSchema().contains(self.schemaschema):
112 raise ValueError("Catalog schema does not match Task schema")
113 visit = 1
114
115 for position, pa in self.makePointingsmakePointings(n, tractInfo):
116 visitInfo = lsst.afw.image.VisitInfo(
117 exposureTime=self.config.expTime,
119 boresightRaDec=position,
120 )
121 for detector in camera:
122 photoCalib = self.buildPhotoCalibbuildPhotoCalib()
123 record = catalog.addNew()
124 record.setI(self.ccdKeyccdKey, detector.getId())
125 record.setI(self.visitKeyvisitKey, visit)
126 record.set(self.filterKeyfilterKey, 'r')
127 record.set(self.pointingKeypointingKey, position)
128 record.setWcs(self.buildWcsbuildWcs(position, pa, detector))
129 record.setPhotoCalib(photoCalib)
130 record.setVisitInfo(visitInfo)
131 record.setPsf(self.buildPsfbuildPsf(detector))
132 record.setApCorrMap(self.buildApCorrMapbuildApCorrMap(detector))
133 record.setTransmissionCurve(self.buildTransmissionCurvebuildTransmissionCurve(detector))
134 record.setBBox(detector.getBBox())
135 detectorId = detector.getId()
136 obj = butler.get("ccdExposureId", visit=visit, ccd=detectorId, immediate=True)
137 record.setId(obj)
138 visit += 1
139 return catalog
140
141 def makePointings(self, n, tractInfo):
142 """Generate (celestial) positions and rotation angles that define field locations.
143
144 Default implementation draws random pointings that are uniform in the tract's image
145 coordinate system.
146
147 @param[in] n: number of pointings
148 @param[in] tractInfo: skymap tract (a lsst.skymap.TractInfo)
149 @return a Python iterable over (coord, angle) pairs:
150 - coord is an ICRS object position (an lsst.geom.SpherePoint)
151 - angle is a position angle (???) (an lsst.geom.Angle)
152
153 The default implementation returns an iterator (i.e. the function is a "generator"),
154 but derived-class overrides may return any iterable.
155 """
156 wcs = tractInfo.getWcs()
157 bbox = lsst.geom.Box2D(tractInfo.getBBox())
158 bbox.grow(lsst.geom.Extent2D(-0.1 * bbox.getWidth(), -0.1 * bbox.getHeight()))
159 for i in range(n):
160 x = self.rngrng.rand() * bbox.getWidth() + bbox.getMinX()
161 y = self.rngrng.rand() * bbox.getHeight() + bbox.getMinY()
162 pa = 0.0 * lsst.geom.radians
163 if self.config.doRotate:
164 pa = self.rngrng.rand() * 2.0 * np.pi * lsst.geom.radians
165 yield wcs.pixelToSky(x, y), pa
166
167 def buildWcs(self, position, pa, detector):
168 """Build a simple TAN Wcs with no distortion and exactly-aligned CCDs.
169
170 @param[in] position: ICRS object position on sky (on lsst.geom.SpherePoint)
171 @param[in] pa: position angle (an lsst.geom.Angle)
172 @param[in] detector: detector information (an lsst.afw.cameraGeom.Detector)
173 """
174 crval = position
175 pixelScale = (self.config.pixelScale * lsst.geom.arcseconds).asDegrees()
178 crpix = detector.transform(lsst.geom.Point2D(0, 0), FOCAL_PLANE, PIXELS)
179 wcs = lsst.afw.geom.makeSkyWcs(crpix=crpix, crval=crval, cdMatrix=cd.getMatrix())
180 return wcs
181
183 """Build a simple PhotoCalib object with the calibration factor
184 drawn from a Gaussian defined by config.
185 """
187 self.rngrng.randn() * self.config.fluxMag0Err + self.config.fluxMag0,
188 self.config.fluxMag0Err
189 )
190 return photoCalib
191
192 def buildPsf(self, detector):
193 """Build a simple Gaussian Psf with linearly-varying ellipticity and size.
194
195 The Psf pattern increases sigma_x linearly along the x direction, and sigma_y
196 linearly along the y direction.
197
198 @param[in] detector: detector information (an lsst.afw.cameraGeom.Detector)
199 @return a psf (an instance of lsst.meas.algorithms.KernelPsf)
200 """
201 bbox = detector.getBBox()
202 dx = (self.config.psfMaxSigma - self.config.psfMinSigma) / bbox.getWidth()
203 dy = (self.config.psfMaxSigma - self.config.psfMinSigma) / bbox.getHeight()
204 sigmaXFunc = lsst.afw.math.PolynomialFunction2D(1)
205 sigmaXFunc.setParameter(0, self.config.psfMinSigma - dx * bbox.getMinX() - dy * bbox.getMinY())
206 sigmaXFunc.setParameter(1, dx)
207 sigmaXFunc.setParameter(2, 0.0)
208 sigmaYFunc = lsst.afw.math.PolynomialFunction2D(1)
209 sigmaYFunc.setParameter(0, self.config.psfMinSigma)
210 sigmaYFunc.setParameter(1, 0.0)
211 sigmaYFunc.setParameter(2, dy)
212 angleFunc = lsst.afw.math.PolynomialFunction2D(0)
213 spatialFuncList = []
214 spatialFuncList.append(sigmaXFunc)
215 spatialFuncList.append(sigmaYFunc)
216 spatialFuncList.append(angleFunc)
218 self.config.psfImageSize, self.config.psfImageSize,
219 lsst.afw.math.GaussianFunction2D(self.config.psfMinSigma, self.config.psfMinSigma),
220 spatialFuncList
221 )
222 return lsst.meas.algorithms.KernelPsf(kernel)
223
224 def buildApCorrMap(self, detector):
225 """Build an ApCorrMap with random linearly-varying fields for all
226 flux fields registered for aperture correction.
227
228 These flux field names are used only as strings; there is no
229 connection to any actual algorithms with those names or the PSF model.
230 """
231 order = self.config.apCorrOrder
232
233 def makeRandomBoundedField():
234 """Make an upper-left triangular coefficient array appropriate
235 for a 2-d polynomial."""
236 array = np.zeros((order + 1, order + 1), dtype=float)
237 for n in range(order + 1):
238 array[n, 0:order + 1 - n] = self.rngrng.randn(order + 1 - n)
239 return lsst.afw.math.ChebyshevBoundedField(bbox, array)
240
241 bbox = detector.getBBox()
242 apCorrMap = lsst.afw.image.ApCorrMap()
243 for name in getApCorrNameSet():
244 apCorrMap.set(name + "_instFlux", makeRandomBoundedField())
245 apCorrMap.set(name + "_instFluxErr", makeRandomBoundedField())
246 return apCorrMap
247
248 def buildTransmissionCurve(self, detector):
249 """Build a random spacially-varying TransmissionCurve."""
250 bbox = detector.getBBox()
251 return makeRandomTransmissionCurve(rng=self.rngrng, maxRadius=max(bbox.getWidth(), bbox.getHeight()))
static CoordKey addFields(afw::table::Schema &schema, std::string const &name, std::string const &doc)
static DateTime now(void)
static LinearTransform makeRotation(Angle t) noexcept
static LinearTransform makeScaling(double s) noexcept
def run(self, butler, n, tractInfo, camera, catalog=None)
std::shared_ptr< SkyWcs > makeSkyWcs(daf::base::PropertySet &metadata, bool strip=false)
std::shared_ptr< PhotoCalib > makePhotoCalibFromCalibZeroPoint(double instFluxMag0, double instFluxMag0Err)