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