LSSTApplications  8.0.0.0+107,8.0.0.1+13,9.1+18,9.2,master-g084aeec0a4,master-g0aced2eed8+6,master-g15627eb03c,master-g28afc54ef9,master-g3391ba5ea0,master-g3d0fb8ae5f,master-g4432ae2e89+36,master-g5c3c32f3ec+17,master-g60f1e072bb+1,master-g6a3ac32d1b,master-g76a88a4307+1,master-g7bce1f4e06+57,master-g8ff4092549+31,master-g98e65bf68e,master-ga6b77976b1+53,master-gae20e2b580+3,master-gb584cd3397+53,master-gc5448b162b+1,master-gc54cf9771d,master-gc69578ece6+1,master-gcbf758c456+22,master-gcec1da163f+63,master-gcf15f11bcc,master-gd167108223,master-gf44c96c709
LSSTDataManagementBasePackage
tests.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 #
3 # LSST Data Management System
4 # Copyright 2008, 2009, 2010, 2014 LSST Corporation.
5 #
6 # This product includes software developed by the
7 # LSST Project (http://www.lsst.org/).
8 #
9 # This program is free software: you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation, either version 3 of the License, or
12 # (at your option) any later version.
13 #
14 # This program is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
18 #
19 # You should have received a copy of the LSST License Statement and
20 # the GNU General Public License along with this program. If not,
21 # see <http://www.lsstcorp.org/LegalNotices/>.
22 #
23 
24 import numpy
25 import lsst.utils.tests
26 
27 import lsst.afw.table
28 import lsst.afw.image
29 import lsst.afw.detection
31 import lsst.afw.coord
32 
33 from .sfm import SingleFrameMeasurementTask
34 
35 class MakeTestData(object):
36  @staticmethod
37  def drawGaussian(image, flux, ellipse):
38  bbox = image.getBBox()
39  x, y = numpy.meshgrid(numpy.arange(bbox.getBeginX(), bbox.getEndX()),
40  numpy.arange(bbox.getBeginY(), bbox.getEndY()))
41  t = ellipse.getGridTransform()
42  xt = t[t.XX] * x + t[t.XY] * y + t[t.X]
43  yt = t[t.YX] * y + t[t.YY] * y + t[t.Y]
44  image.getArray()[:,:] = numpy.exp(-0.5*(xt**2 + yt**2))
45  image.getArray()[:,:] *= flux / image.getArray().sum()
46 
47  @staticmethod
48  def makeCatalog():
49  """Create and return the truth catalog and bounding box for the simulation.
50 
51  The simulated records will be in image coordinates, and no footprints will be attached.
52  """
54  nChildKey = schema.addField("deblend_nchild", type=int)
55  xKey = schema.addField("truth_x", type=float,
56  doc="true simulated centroid x", units="pixels")
57  yKey = schema.addField("truth_y", type=float,
58  doc="true simulated centroid y", units="pixels")
59  centroidKey = lsst.afw.table.Point2DKey(xKey, yKey)
60  fluxKey = schema.addField("truth_flux", type=float, doc="true flux", units="dn")
61  xxKey = schema.addField("truth_xx", type=float,
62  doc="true shape after PSF convolution", units="pixels^2")
63  yyKey = schema.addField("truth_yy", type=float,
64  doc="true shape after PSF convolution", units="pixels^2")
65  xyKey = schema.addField("truth_xy", type=float,
66  doc="true shape after PSF convolution", units="pixels^2")
67  shapeKey = lsst.afw.table.QuadrupoleKey(xxKey, yyKey, xyKey)
68  starFlagKey = schema.addField("truth_isStar", type="Flag", doc="set if the object is a star")
69  schema.setVersion(1)
70  catalog = lsst.afw.table.SourceCatalog(schema)
72  # a bright, isolated star near (50, 50)
73  record = catalog.addNew()
74  record.set(nChildKey, 0)
75  record.set(centroidKey, lsst.afw.geom.Point2D(50.1, 49.8))
76  record.set(fluxKey, 100000.0)
77  record.set(shapeKey, lsst.afw.geom.ellipses.Quadrupole(0.0, 0.0, 0.0))
78  record.set(starFlagKey, True)
79  # a moderately-resolved, isolated galaxy near (150, 50)
80  record = catalog.addNew()
81  record.set(nChildKey, 0)
82  record.set(centroidKey, lsst.afw.geom.Point2D(150.3, 50.2))
83  record.set(fluxKey, 75000.0)
84  record.set(shapeKey, lsst.afw.geom.ellipses.Quadrupole(8.0, 6.0, 0.5))
85  record.set(starFlagKey, False)
86  # a blend of a star and galaxy near (95, 150) and (105, 150)
87  parent = catalog.addNew()
88  parent.set(nChildKey, 2)
89  record1 = catalog.addNew()
90  record1.set(nChildKey, 0)
91  record1.setParent(parent.getId())
92  record1.set(centroidKey, lsst.afw.geom.Point2D(95.4, 149.9))
93  record1.set(fluxKey, 80000.0)
94  record1.set(shapeKey, lsst.afw.geom.ellipses.Quadrupole(0.0, 0.0, 0.0))
95  record1.set(starFlagKey, True)
96  record2 = catalog.addNew()
97  record2.set(nChildKey, 0)
98  record2.setParent(parent.getId())
99  record2.set(centroidKey, lsst.afw.geom.Point2D(104.8, 150.2))
100  record2.set(fluxKey, 120000.0)
101  record2.set(shapeKey, lsst.afw.geom.ellipses.Quadrupole(10.0, 7.0, -0.5))
102  record2.set(starFlagKey, False)
103  parent.set(fluxKey, record1.get(fluxKey) + record2.get(fluxKey))
104  parent.set(starFlagKey, False)
105  parent.set(
106  centroidKey,
108  (lsst.afw.geom.Extent2D(record1.get(centroidKey)) * record1.get(fluxKey)
109  + lsst.afw.geom.Extent2D(record2.get(centroidKey)) * record2.get(fluxKey))
110  / parent.get(fluxKey)
111  )
112  )
113  # we don't bother setting the truth values for parent's shape, since we don't need them
114  # for sims and don't expect to be able to measure them well anyway.
115  return catalog, bbox
116 
117  @staticmethod
118  def makeEmptyExposure(bbox, crval=None, psfSigma=2.0, psfDim=17, fluxMag0=1E12):
119  """Create an Exposure, with a Calib, Wcs, and Psf, but no pixel values set.
120 
121  @param[in] bbox Bounding box of the image (image coordinates) as returned by makeCatalog.
122  @param[in] crval afw.coord.Coord: center of the TAN WCS attached to the image.
123  @param[in] psfSigma Radius (sigma) of the Gaussian PSF attached to the image
124  @param[in] psfDim Width and height of the image's Gaussian PSF attached to the image
125  @param[in] fluxMag0 Flux at magnitude zero (in e-) used to set the Calib of the exposure.
126  """
127  if crval is None:
128  crval = lsst.afw.coord.IcrsCoord(45.0*lsst.afw.geom.degrees, 45.0*lsst.afw.geom.degrees)
129  exposure = lsst.afw.image.ExposureF(bbox)
130  crpix = lsst.afw.geom.Box2D(bbox).getCenter()
131  cdelt = (0.2 * lsst.afw.geom.arcseconds).asDegrees()
132  wcs = lsst.afw.image.makeWcs(crval, crpix, cdelt, 0.0, 0.0, cdelt)
133  psf = lsst.afw.detection.GaussianPsf(psfDim, psfDim, psfSigma)
134  calib = lsst.afw.image.Calib()
135  calib.setFluxMag0(fluxMag0)
136  exposure.setWcs(wcs)
137  exposure.setPsf(psf)
138  exposure.setCalib(calib)
139  return exposure
140 
141  @staticmethod
142  def fillImages(catalog, exposure, threshold=None, noise=100.0):
143  """Fill a simulated Exposure from makeEmptyExposure() with the objects defined by a truth catalog from
144  makeTruthCatalog(). Also attaches Footprints to the SourceRecords in the catalog, sets the
145  'coord' field, and fills in the detection mask planes. The Footprints will be regular Footprints
146  for non-blended objects, and HeavyFootprints for deblended objects, intended to mimic
147  better-than-realistic outputs from detection and deblending.
148 
149  @param[in,out] catalog SourceCatalog created by makeCatalog. HeavyFootprints and 'coord' will
150  be filled on return, and shapes will be convolved by the PSF shape.
151  @param[in,out] exposure ExposureF to fill, as created by makeEmptyExposure()
152  @param[in] threshold afw.detection.Threshold object used to determine the size of the
153  HeavyFootprints attached to the SourceCatalog (after thresholding, we'll
154  also grow the Footprints by the PSF sigma).
155  @param[in] noise Standard deviation of Gaussian noise added to image (constant across the
156  image; appropriate for sky-dominated limit).
157  """
158  if threshold is None:
159  threshold = lsst.afw.detection.Threshold(10.0, lsst.afw.detection.Threshold.VALUE)
160 
161  psf = lsst.afw.detection.GaussianPsf.cast(exposure.getPsf())
162  schema = catalog.schema
163  nChildKey = schema.find("deblend_nchild").key
164  centroidKey = lsst.afw.table.Point2DKey(schema.find("truth_x").key, schema.find("truth_y").key)
165  shapeKey = lsst.afw.table.QuadrupoleKey(schema.find("truth_xx").key, schema.find("truth_yy").key,
166  schema.find("truth_xy").key)
167  fluxKey = schema.find("truth_flux").key
168 
169  # First pass: generate full-size images, each containing a single object, and add them into
170  # the Exposure
171  images = {record.getId(): lsst.afw.image.ImageF(exposure.getBBox())
172  for record in catalog}
173  for record in catalog:
174  if record.get(nChildKey) != 0: continue
175  ellipse = lsst.afw.geom.ellipses.Ellipse(record.get(shapeKey).convolve(psf.computeShape()),
176  record.get(centroidKey))
177  record.set(shapeKey, ellipse.getCore())
178  MakeTestData.drawGaussian(images[record.getId()], record.get(fluxKey), ellipse)
179  exposure.getMaskedImage().getImage().getArray()[:,:] += images[record.getId()].getArray()[:,:]
180  if record.getParent() != 0: # parent images come from combining child images
181  images[record.getParent()] += images[record.getId()]
182 
183  exposure.getMaskedImage().getVariance().getArray()[:,:] = noise**2
184  exposure.getMaskedImage().getImage().getArray()[:,:] \
185  += numpy.random.randn(exposure.getHeight(), exposure.getWidth())*noise
186 
187  # Second pass: detect on single-object images to generate better-than-reality Footprints
188  for record in catalog:
189  # this detection doesn't really match what we'd do on real data (it can't, because we don't
190  # have noise yet), but it seems to yield reasonably-sized footprints.
191  fpSet = lsst.afw.detection.FootprintSet(images[record.getId()], threshold)
192  fpSet = lsst.afw.detection.FootprintSet(fpSet, int(psf.getSigma()+1.0), True)
193  if len(fpSet.getFootprints()) != 1:
194  raise ValueError("Threshold value results in multiple Footprints for a single object")
195  fpSet.setMask(exposure.getMaskedImage().getMask(), "DETECTED")
196  record.setFootprint(fpSet.getFootprints()[0])
197 
198  # Third pass: for all parent objects, make "perfectly deblended" child HeavyFootprints using the
199  # true, noise-free single-object images to divide the flux.
200  for parent in catalog.getChildren(0):
201  print "Processing parent object", parent.getId()
202  for child in catalog.getChildren(parent.getId()):
203  print "Processing child object", child.getId()
204  fraction = images[child.getId()].getArray() / images[parent.getId()].getArray()
205  deblend = lsst.afw.image.MaskedImageF(exposure.getMaskedImage(), True)
206  deblend.getImage().getArray()[:,:] *= fraction
207  child.setFootprint(lsst.afw.detection.HeavyFootprintF(child.getFootprint(), deblend))
208  return images
209 
211 
212  def setUp(self):
213  catalog, bbox = MakeTestData.makeCatalog()
214  exposure = MakeTestData.makeEmptyExposure(bbox)
215  MakeTestData.fillImages(catalog, exposure)
216  schema = catalog.schema
217  self.truth = catalog
218  self.calexp = exposure
219  self.centroidKey = lsst.afw.table.Point2DKey(schema.find("truth_x").key, schema.find("truth_y").key)
220  self.shapeKey = lsst.afw.table.QuadrupoleKey(schema.find("truth_xx").key, schema.find("truth_yy").key,
221  schema.find("truth_xy").key)
222  self.fluxKey = schema.find("truth_flux").key
223 
224  def tearDown(self):
225  del self.centroidKey
226  del self.shapeKey
227  del self.fluxKey
228  del self.truth
229  del self.calexp
230 
231  def runSingleFrameMeasurementTask(self, plugin, dependencies=(), config=None):
232  if config is None:
233  config = SingleFrameMeasurementTask.ConfigClass()
234  config.slots.centroid = None
235  config.slots.shape = None
236  config.slots.modelFlux = None
237  config.slots.apFlux = None
238  config.slots.psfFlux = None
239  config.slots.instFlux = None
240  config.plugins.names = (plugin,) + tuple(dependencies)
241  schemaMapper = lsst.afw.table.SchemaMapper(self.truth.schema)
242  schemaMapper.addMinimalSchema(self.truth.schema)
243  algMetadata = lsst.daf.base.PropertyList()
244  task = SingleFrameMeasurementTask(schema=schemaMapper.editOutputSchema(), algMetadata=algMetadata,
245  config=config)
246  measCat = lsst.afw.table.SourceCatalog(task.schema)
247  measCat.table.setMetadata(algMetadata)
248  measCat.extend(self.truth, schemaMapper)
249  measCat.getTable().defineModelFlux("truth")
250  measCat.getTable().defineCentroid("truth")
251  measCat.getTable().defineShape("truth")
252  task.run(measCat, self.calexp)
253  return measCat
An ellipse core with quadrupole moments as parameters.
Definition: Quadrupole.h:45
Class for storing ordered metadata with comments.
Definition: PropertyList.h:81
A mapping between the keys of two Schemas, used to copy data between them.
Definition: SchemaMapper.h:19
A Threshold is used to pass a threshold value to detection algorithms.
Definition: Threshold.h:44
An integer coordinate rectangle.
Definition: Box.h:53
Custom catalog class for record/table subclasses that are guaranteed to have an ID, and should generally be sorted by that ID.
Definition: fwd.h:55
An ellipse defined by an arbitrary BaseCore and a center point.
Definition: Ellipse.h:50
Wcs::Ptr makeWcs(coord::Coord const &crval, geom::Point2D const &crpix, double CD11, double CD12, double CD21, double CD22)
Create a Wcs object from crval, crpix, CD, using CD elements (useful from python) ...
Definition: makeWcs.cc:87
static Schema makeMinimalSchema()
Return a minimal schema for Source tables and records.
Definition: Source.h:233
double sum
Definition: NaiveFlux.cc:137
A set of Footprints, associated with a MaskedImage.
Definition: FootprintSet.h:53
void convolve(OutImageT &convolvedImage, InImageT const &inImage, KernelT const &kernel, ConvolutionControl const &convolutionControl=ConvolutionControl())
Convolve an Image or MaskedImage with a Kernel, setting pixels of an existing output image...
A floating-point coordinate rectangle geometry.
Definition: Box.h:271
A FunctorKey used to get or set a geom::ellipses::Quadrupole from an (xx,yy,xy) tuple of Keys...
Definition: aggregates.h:125
A class to handle Icrs coordinates (inherits from Coord)
Definition: Coord.h:156
A circularly symmetric Gaussian Psf class with no spatial variation, intended mostly for testing purp...
Definition: GaussianPsf.h:41