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