24 from builtins
import zip
25 from builtins
import object
30 import lsst.afw.detection
32 import lsst.afw.geom.ellipses
34 import lsst.pex.exceptions
36 from .sfm
import SingleFrameMeasurementTask
37 from .forcedMeasurement
import ForcedMeasurementTask
38 from .
import CentroidResultKey
40 __all__ = (
"BlendContext",
"TestDataset",
"AlgorithmTestCase",
"TransformTestCase",
41 "SingleFramePluginTransformSetupHelper",
"ForcedPluginTransformSetupHelper",
42 "FluxTransformTestCase",
"CentroidTransformTestCase")
47 A Python context manager used to add multiple overlapping sources along with a parent source
48 that represents all of them together.
50 This is used as the return value for TestDataset.addBlend(), and this is the only way it should
51 be used. The only public method is addChild().
57 self.
parentImage = lsst.afw.image.ImageF(self.owner.exposure.getBBox())
64 def addChild(self, flux, centroid, shape=None):
66 Add a child source to the blend, and return the truth catalog record that corresponds to it.
68 @param[in] flux Total flux of the source to be added.
69 @param[in] centroid Position of the source to be added (lsst.afw.geom.Point2D).
70 @param[in] shape 2nd moments of the source before PSF convolution
71 (lsst.afw.geom.ellipses.Quadrupole). Note that the truth catalog
72 records post-convolution moments)
74 record, image = self.owner.addSource(flux, centroid, shape)
75 record.set(self.owner.keys[
"parent"], self.parentRecord.getId())
77 self.children.append((record, image))
86 self.parentRecord.set(self.owner.keys[
"nChild"], len(self.
children))
90 flux += record.get(self.owner.keys[
"flux"])
91 self.parentRecord.set(self.owner.keys[
"flux"], flux)
96 w = record.get(self.owner.keys[
"flux"])/flux
97 x += record.get(self.owner.keys[
"centroid"].getX())*w
98 y += record.get(self.owner.keys[
"centroid"].getY())*w
99 self.parentRecord.set(self.owner.keys[
"centroid"], lsst.afw.geom.Point2D(x, y))
105 w = record.get(self.owner.keys[
"flux"])/flux
106 dx = record.get(self.owner.keys[
"centroid"].getX()) - x
107 dy = record.get(self.owner.keys[
"centroid"].getY()) - y
108 xx += (record.get(self.owner.keys[
"shape"].getIxx()) + dx**2)*w
109 yy += (record.get(self.owner.keys[
"shape"].getIyy()) + dy**2)*w
110 xy += (record.get(self.owner.keys[
"shape"].getIxy()) + dx*dy)*w
111 self.parentRecord.set(self.owner.keys[
"shape"], lsst.afw.geom.ellipses.Quadrupole(xx, yy, xy))
116 deblend = lsst.afw.image.MaskedImageF(self.owner.exposure.getMaskedImage(),
True)
118 deblend.getImage().getArray()[:, :] = image.getArray()
119 heavyFootprint = lsst.afw.detection.HeavyFootprintF(self.parentRecord.getFootprint(), deblend)
120 record.setFootprint(heavyFootprint)
125 A simulated dataset consisting of a test image and an associated truth catalog.
127 TestDataset creates an idealized image made of pure Gaussians (including a Gaussian PSF),
128 with simple noise and idealized Footprints/HeavyFootprints that simulated the outputs
129 of detection and deblending. Multiple noise realizations can be created from the same
130 underlying sources, allowing uncertainty estimates to be verified via Monte Carlo.
134 bbox = lsst.afw.geom.Box2I(lsst.afw.geom.Point2I(0,0), lsst.afw.geom.Point2I(100, 100))
135 dataset = TestDataset(bbox)
136 dataset.addSource(flux=1E5, centroid=lsst.afw.geom.Point2D(25, 26))
137 dataset.addSource(flux=2E5, centroid=lsst.afw.geom.Point2D(75, 24),
138 shape=lsst.afw.geom.ellipses.Quadrupole(8, 7, 2))
139 with dataset.addBlend() as family:
140 family.addChild(flux=2E5, centroid=lsst.afw.geom.Point2D(50, 72))
141 family.addChild(flux=1.5E5, centroid=lsst.afw.geom.Point2D(51, 74))
142 exposure, catalog = dataset.realize(noise=100.0, schema=TestDataset.makeMinimalSchema())
148 """Return the minimal schema needed to hold truth catalog fields.
150 When TestDataset.realize() is called, the schema must include at least these fields.
151 Usually it will include additional fields for measurement algorithm outputs, allowing
152 the same catalog to be used for both truth values (the fields from the minimal schema)
153 and the measurements.
155 if not hasattr(cls,
"_schema"):
156 schema = lsst.afw.table.SourceTable.makeMinimalSchema()
158 cls.keys[
"parent"] = schema.find(
"parent").key
159 cls.keys[
"nChild"] = schema.addField(
"deblend_nChild", type=np.int32)
160 cls.keys[
"flux"] = schema.addField(
"truth_flux", type=np.float64, doc=
"true flux", units=
"count")
161 cls.keys[
"centroid"] = lsst.afw.table.Point2DKey.addFields(
162 schema,
"truth",
"true simulated centroid",
"pixel"
164 cls.keys[
"centroid_flag"] = schema.addField(
"truth_flag", type=
"Flag",
165 doc=
"set if the object is a star")
166 cls.keys[
"shape"] = lsst.afw.table.QuadrupoleKey.addFields(
167 schema,
"truth",
"true shape after PSF convolution", lsst.afw.table.CoordinateType.PIXEL
169 cls.keys[
"isStar"] = schema.addField(
"truth_isStar", type=
"Flag",
170 doc=
"set if the object is a star")
171 schema.getAliasMap().set(
"slot_Shape",
"truth")
172 schema.getAliasMap().set(
"slot_Centroid",
"truth")
173 schema.getAliasMap().set(
"slot_ModelFlux",
"truth")
174 schema.getCitizen().markPersistent()
176 schema = lsst.afw.table.Schema(cls._schema)
177 schema.disconnectAliases()
182 minRotation=
None, maxRotation=
None,
183 minRefShift=
None, maxRefShift=
None,
184 minPixShift=2.0, maxPixShift=4.0, randomSeed=
None):
186 Create a new undistorted TanWcs that is similar but not identical to another, with random
187 scaling, rotation, and offset (in both pixel position and reference position).
189 The maximum and minimum arguments are interpreted as absolute values for a split
190 range that covers both positive and negative values (as this method is used
191 in testing, it is typically most important to avoid perturbations near zero).
192 Scale factors are treated somewhat differently: the actual scale factor is chosen between
193 minScaleFactor and maxScaleFactor OR (1/maxScaleFactor) and (1/minScaleFactor).
195 The default range for rotation is 30-60 degrees, and the default range for reference shift
196 is 0.5-1.0 arcseconds (these cannot be safely included directly as default values because Angle
197 objects are mutable).
199 The random number generator is primed with the seed given. If ``None``, a seed is
200 automatically chosen.
202 random_state = np.random.RandomState(randomSeed)
203 if minRotation
is None:
204 minRotation = 30.0*lsst.afw.geom.degrees
205 if maxRotation
is None:
206 maxRotation = 60.0*lsst.afw.geom.degrees
207 if minRefShift
is None:
208 minRefShift = 0.5*lsst.afw.geom.arcseconds
209 if maxRefShift
is None:
210 maxRefShift = 1.0*lsst.afw.geom.arcseconds
212 def splitRandom(min1, max1, min2=None, max2=None):
217 if random_state.uniform() > 0.5:
218 return float(random_state.uniform(min1, max1))
220 return float(random_state.uniform(min2, max2))
222 scaleFactor = splitRandom(minScaleFactor, maxScaleFactor, 1.0/maxScaleFactor, 1.0/minScaleFactor)
223 rotation = splitRandom(minRotation.asRadians(), maxRotation.asRadians())*lsst.afw.geom.radians
224 refShiftRa = splitRandom(minRefShift.asRadians(), maxRefShift.asRadians())*lsst.afw.geom.radians
225 refShiftDec = splitRandom(minRefShift.asRadians(), maxRefShift.asRadians())*lsst.afw.geom.radians
226 pixShiftX = splitRandom(minPixShift, maxPixShift)
227 pixShiftY = splitRandom(minPixShift, maxPixShift)
229 oldTransform = lsst.afw.geom.LinearTransform(oldWcs.getCDMatrix())
230 rTransform = lsst.afw.geom.LinearTransform.makeRotation(rotation)
231 sTransform = lsst.afw.geom.LinearTransform.makeScaling(scaleFactor)
232 newTransform = oldTransform*rTransform*sTransform
233 matrix = newTransform.getMatrix()
235 oldSkyOrigin = oldWcs.getSkyOrigin().toIcrs()
236 newSkyOrigin = lsst.afw.coord.IcrsCoord(oldSkyOrigin.getRa() + refShiftRa,
237 oldSkyOrigin.getDec() + refShiftDec)
239 oldPixOrigin = oldWcs.getPixelOrigin()
240 newPixOrigin = lsst.afw.geom.Point2D(oldPixOrigin.getX() + pixShiftX,
241 oldPixOrigin.getY() + pixShiftY)
242 return lsst.afw.image.makeWcs(newSkyOrigin, newPixOrigin,
243 matrix[0, 0], matrix[0, 1], matrix[1, 0], matrix[1, 1])
246 def makeEmptyExposure(bbox, wcs=None, crval=None, cdelt=None, psfSigma=2.0, psfDim=17, fluxMag0=1E12):
248 Create an Exposure, with a Calib, Wcs, and Psf, but no pixel values set.
250 @param[in] bbox Bounding box of the image (image coordinates) as returned by makeCatalog.
251 @param[in] wcs New Wcs for the exposure (created from crval and cdelt if None).
252 @param[in] crval afw.coord.Coord: center of the TAN WCS attached to the image.
253 @param[in] cdelt afw.geom.Angle: pixel scale of the image
254 @param[in] psfSigma Radius (sigma) of the Gaussian PSF attached to the image
255 @param[in] psfDim Width and height of the image's Gaussian PSF attached to the image
256 @param[in] fluxMag0 Flux at magnitude zero (in e-) used to set the Calib of the exposure.
260 crval = lsst.afw.coord.IcrsCoord(45.0*lsst.afw.geom.degrees, 45.0*lsst.afw.geom.degrees)
262 cdelt = 0.2*lsst.afw.geom.arcseconds
263 crpix = lsst.afw.geom.Box2D(bbox).getCenter()
264 wcs = lsst.afw.image.makeWcs(crval, crpix, cdelt.asDegrees(), 0.0, 0.0, cdelt.asDegrees())
265 exposure = lsst.afw.image.ExposureF(bbox)
266 psf = lsst.afw.detection.GaussianPsf(psfDim, psfDim, psfSigma)
267 calib = lsst.afw.image.Calib()
268 calib.setFluxMag0(fluxMag0)
271 exposure.setCalib(calib)
277 Create an image of an elliptical Gaussian.
279 @param[in,out] bbox Bounding box of image to create.
280 @param[in] flux Total flux of the Gaussian (normalized analytically, not using pixel
282 @param[in] ellipse lsst.afw.geom.ellipses.Ellipse holding the centroid and shape.
284 x, y = np.meshgrid(np.arange(bbox.getBeginX(), bbox.getEndX()),
285 np.arange(bbox.getBeginY(), bbox.getEndY()))
286 t = ellipse.getGridTransform()
287 xt = t[t.XX] * x + t[t.XY] * y + t[t.X]
288 yt = t[t.YX] * x + t[t.YY] * y + t[t.Y]
289 image = lsst.afw.image.ImageF(bbox)
290 image.getArray()[:, :] = np.exp(-0.5*(xt**2 + yt**2))*flux/(2.0*ellipse.getCore().getArea())
293 def __init__(self, bbox, threshold=10.0, exposure=None, **kwds):
295 Initialize the dataset.
297 @param[in] bbox Bounding box of the test image.
298 @param[in] threshold Threshold absolute value used to determine footprints for
299 simulated sources. This thresholding will be applied before noise is
300 actually added to images (or before the noise level is even known), so
301 this will necessarily produce somewhat artificial footprints.
302 @param[in] exposure lsst.afw.image.ExposureF test sources should be added to. Ownership should
303 be considered transferred from the caller to the TestDataset.
304 Must have a Gaussian Psf for truth catalog shapes to be exact.
305 @param[in] **kwds Keyword arguments forwarded to makeEmptyExposure if exposure is None.
309 self.
threshold = lsst.afw.detection.Threshold(threshold, lsst.afw.detection.Threshold.VALUE)
311 self.
psfShape = self.exposure.getPsf().computeShape()
315 def _installFootprint(self, record, image):
316 """Create a Footprint for a simulated source and add it to its truth catalog record.
319 fpSet = lsst.afw.detection.FootprintSet(image, self.
threshold)
321 fpSet = lsst.afw.detection.FootprintSet(fpSet, int(self.psfShape.getDeterminantRadius() + 1.0),
True)
323 fpSet.setMask(self.exposure.getMaskedImage().getMask(),
"DETECTED")
325 if len(fpSet.getFootprints()) > 1:
326 raise RuntimeError(
"Threshold value results in multiple Footprints for a single object")
327 if len(fpSet.getFootprints()) == 0:
328 raise RuntimeError(
"Threshold value results in zero Footprints for object")
329 record.setFootprint(fpSet.getFootprints()[0])
333 Add a source to the simulation
335 @param[in] flux Total flux of the source to be added.
336 @param[in] centroid Position of the source to be added (lsst.afw.geom.Point2D).
337 @param[in] shape 2nd moments of the source before PSF convolution
338 (lsst.afw.geom.ellipses.Quadrupole). Note that the truth catalog
339 records post-convolution moments). If None, a point source
342 @return a truth catalog record and single-source image corresponding to the new source.
345 record = self.catalog.addNew()
346 record.set(self.keys[
"flux"], flux)
347 record.set(self.keys[
"centroid"], centroid)
349 record.set(self.keys[
"isStar"],
True)
352 record.set(self.keys[
"isStar"],
False)
353 fullShape = shape.convolve(self.
psfShape)
354 record.set(self.keys[
"shape"], fullShape)
356 image = self.
drawGaussian(self.exposure.getBBox(), flux,
357 lsst.afw.geom.ellipses.Ellipse(fullShape, centroid))
361 self.exposure.getMaskedImage().getImage().getArray()[:, :] += image.getArray()
366 Return a context manager that allows a blend of multiple sources to be added.
371 with d.addBlend() as b:
372 b.addChild(flux1, centroid1)
373 b.addChild(flux2, centroid2, shape2)
376 Note that nothing stops you from creating overlapping sources just using the addSource() method,
377 but addBlend() is necesssary to create a parent object and deblended HeavyFootprints of the type
378 produced by the detection and deblending pipelines.
384 Create a copy of the dataset transformed to a new WCS, with new Psf and Calib.
386 @param[in] wcs Wcs for the new dataset.
387 @param[in] **kwds Additional keyword arguments passed on to makeEmptyExposure. If not
388 specified, these revert to the defaults for makeEmptyExposure, not the
389 values in the current dataset.
391 bboxD = lsst.afw.geom.Box2D()
392 xyt = lsst.afw.image.XYTransformFromWcsPair(wcs, self.exposure.getWcs())
393 for corner
in lsst.afw.geom.Box2D(self.exposure.getBBox()).getCorners():
394 bboxD.include(xyt.forwardTransform(lsst.afw.geom.Point2D(corner)))
395 bboxI = lsst.afw.geom.Box2I(bboxD)
397 oldCalib = self.exposure.getCalib()
398 newCalib = result.exposure.getCalib()
399 oldPsfShape = self.exposure.getPsf().computeShape()
401 if record.get(self.keys[
"nChild"]):
402 raise NotImplementedError(
"Transforming blended sources in TestDatasets is not supported")
403 magnitude = oldCalib.getMagnitude(record.get(self.keys[
"flux"]))
404 newFlux = newCalib.getFlux(magnitude)
405 oldCentroid = record.get(self.keys[
"centroid"])
406 newCentroid = xyt.forwardTransform(oldCentroid)
407 if record.get(self.keys[
"isStar"]):
408 newDeconvolvedShape =
None
410 affine = xyt.linearizeForwardTransform(oldCentroid)
411 oldFullShape = record.get(self.keys[
"shape"])
412 oldDeconvolvedShape = lsst.afw.geom.ellipses.Quadrupole(
413 oldFullShape.getIxx() - oldPsfShape.getIxx(),
414 oldFullShape.getIyy() - oldPsfShape.getIyy(),
415 oldFullShape.getIxy() - oldPsfShape.getIxy(),
418 newDeconvolvedShape = oldDeconvolvedShape.transform(affine.getLinear())
419 result.addSource(newFlux, newCentroid, newDeconvolvedShape)
422 def realize(self, noise, schema, randomSeed=None):
424 Create a simulated with noise and a simulated post-detection catalog with (Heavy)Footprints.
426 @param[in] noise Standard deviation of noise to be added to the exposure. The noise will be
427 Gaussian and constant, appropriate for the sky-limited regime.
428 @param[in] schema Schema of the new catalog to be created. Must start with self.schema (i.e.
429 schema.contains(self.schema) must be True), but typically contains fields for
430 already-configured measurement algorithms as well.
431 @param[in] randomSeed Seed for the random number generator. If None, a seed is chosen automatically.
433 @return a tuple of (exposure, catalog)
435 random_state = np.random.RandomState(randomSeed)
436 assert schema.contains(self.
schema)
437 mapper = lsst.afw.table.SchemaMapper(self.
schema)
438 mapper.addMinimalSchema(self.
schema,
True)
439 exposure = self.exposure.clone()
440 exposure.getMaskedImage().getVariance().getArray()[:, :] = noise**2
441 exposure.getMaskedImage().getImage().getArray()[:, :] \
442 += random_state.randn(exposure.getHeight(), exposure.getWidth())*noise
443 catalog = lsst.afw.table.SourceCatalog(schema)
444 catalog.extend(self.
catalog, mapper=mapper)
447 for record
in catalog:
449 if record.getParent() == 0:
452 parent = catalog.find(record.getParent())
453 footprint = parent.getFootprint()
454 parentFluxArrayNoNoise = np.zeros(footprint.getArea(), dtype=np.float32)
455 footprint.spans.flatten(parentFluxArrayNoNoise,
456 self.exposure.getMaskedImage().getImage().getArray(),
457 self.exposure.getXY0())
458 parentFluxArrayNoisy = np.zeros(footprint.getArea(), dtype=np.float32)
459 footprint.spans.flatten(parentFluxArrayNoisy,
460 exposure.getMaskedImage().getImage().getArray(),
462 oldHeavy = record.getFootprint()
463 fraction = (oldHeavy.getImageArray() / parentFluxArrayNoNoise)
466 newHeavy = lsst.afw.detection.HeavyFootprintF(oldHeavy)
467 newHeavy.getImageArray()[:] = parentFluxArrayNoisy*fraction
468 newHeavy.getMaskArray()[:] = oldHeavy.getMaskArray()
469 newHeavy.getVarianceArray()[:] = oldHeavy.getVarianceArray()
470 record.setFootprint(newHeavy)
471 return exposure, catalog
477 """Convenience function to create a Config instance for SingleFrameMeasurementTask
479 The plugin and its dependencies will be the only plugins run, while the Centroid, Shape,
480 and ModelFlux slots will be set to the truth fields generated by the TestDataset class.
482 config = SingleFrameMeasurementTask.ConfigClass()
483 config.slots.centroid =
"truth"
484 config.slots.shape =
"truth"
485 config.slots.modelFlux =
None
486 config.slots.apFlux =
None
487 config.slots.psfFlux =
None
488 config.slots.instFlux =
None
489 config.slots.calibFlux =
None
490 config.plugins.names = (plugin,) + tuple(dependencies)
495 """Convenience function to create a SingleFrameMeasurementTask with a simple configuration.
499 raise ValueError(
"Either plugin or config argument must not be None")
502 schema = TestDataset.makeMinimalSchema()
504 schema.setAliasMap(
None)
505 if algMetadata
is None:
506 algMetadata = lsst.daf.base.PropertyList()
507 return SingleFrameMeasurementTask(schema=schema, algMetadata=algMetadata, config=config)
510 """Convenience function to create a Config instance for ForcedMeasurementTask
512 In addition to the plugins specified in the plugin and dependencies arguments,
513 the TransformedCentroid and TransformedShape plugins will be run and used as the
514 Centroid and Shape slots; these simply transform the reference catalog centroid
515 and shape to the measurement coordinate system.
517 config = ForcedMeasurementTask.ConfigClass()
518 config.slots.centroid =
"base_TransformedCentroid"
519 config.slots.shape =
"base_TransformedShape"
520 config.slots.modelFlux =
None
521 config.slots.apFlux =
None
522 config.slots.psfFlux =
None
523 config.slots.instFlux =
None
524 config.plugins.names = (plugin,) + tuple(dependencies) + (
"base_TransformedCentroid",
525 "base_TransformedShape")
530 """Convenience function to create a ForcedMeasurementTask with a simple configuration.
534 raise ValueError(
"Either plugin or config argument must not be None")
536 if refSchema
is None:
537 refSchema = TestDataset.makeMinimalSchema()
538 if algMetadata
is None:
539 algMetadata = lsst.daf.base.PropertyList()
540 return ForcedMeasurementTask(refSchema=refSchema, algMetadata=algMetadata, config=config)
545 Base class for testing measurement transformations.
547 We test both that the transform itself operates successfully (fluxes are
548 converted to magnitudes, flags are propagated properly) and that the
549 transform is registered as the default for the appropriate measurement
552 In the simple case of one-measurement-per-transformation, the developer
553 need not directly write any tests themselves: simply customizing the class
554 variables is all that is required. More complex measurements (e.g.
555 multiple aperture fluxes) require extra effort.
560 name =
"MeasurementTransformTest"
564 algorithmClass =
None
565 transformClass =
None
569 flagNames = (
"flag",)
574 singleFramePlugins = ()
578 bbox = lsst.afw.geom.Box2I(lsst.afw.geom.Point2I(0, 0), lsst.afw.geom.Point2I(200, 200))
579 self.
calexp = TestDataset.makeEmptyExposure(bbox)
580 self._setupTransform()
589 def _populateCatalog(self, baseNames):
591 for flagValue
in (
True,
False):
592 records.append(self.inputCat.addNew())
593 for baseName
in baseNames:
595 if records[-1].schema.join(baseName, flagName)
in records[-1].schema:
596 records[-1].set(records[-1].schema.join(baseName, flagName), flagValue)
597 self._setFieldsInRecords(records, baseName)
599 def _checkOutput(self, baseNames):
600 for inSrc, outSrc
in zip(self.inputCat, self.outputCat):
601 for baseName
in baseNames:
602 self._compareFieldsInRecords(inSrc, outSrc, baseName)
604 keyName = outSrc.schema.join(baseName, flagName)
605 if keyName
in inSrc.schema:
606 self.assertEqual(outSrc.get(keyName), inSrc.get(keyName))
608 self.assertFalse(keyName
in outSrc.schema)
610 def _runTransform(self, doExtend=True):
612 self.outputCat.extend(self.inputCat, mapper=self.mapper)
613 self.transform(self.inputCat, self.outputCat, self.calexp.getWcs(), self.calexp.getCalib())
617 Test the operation of the transformation on a catalog containing random data.
621 * An appropriate exception is raised on an attempt to transform between catalogs with different
623 * Otherwise, all appropriate conversions are properly appled and that flags have been propagated.
625 The `baseNames` argument requires some explanation. This should be an iterable of the leading parts of
626 the field names for each measurement; that is, everything that appears before `_flux`, `_flag`, etc.
627 In the simple case of a single measurement per plugin, this is simply equal to `self.name` (thus
628 measurements are stored as `self.name + "_flux"`, etc). More generally, the developer may specify
629 whatever iterable they require. For example, to handle multiple apertures, we could have
630 `(self.name + "_0", self.name + "_1", ...)`.
632 @param[in] baseNames Iterable of the initial parts of measurement field names.
634 baseNames = baseNames
or [self.
name]
636 self.assertRaises(lsst.pex.exceptions.LengthError, self.
_runTransform,
False)
640 def _checkRegisteredTransform(self, registry, name):
643 self.assertEqual(registry[name].PluginClass.getTransformClass(), self.
transformClass)
647 Test that the transformation is appropriately registered with the relevant measurement algorithms.
657 def _setupTransform(self):
659 inputSchema = lsst.afw.table.SourceTable.makeMinimalSchema()
663 inputSchema.getAliasMap().set(
"slot_Centroid",
"dummy")
664 inputSchema.getAliasMap().set(
"slot_Shape",
"dummy")
665 self.algorithmClass(self.
control, self.name, inputSchema)
666 inputSchema.getAliasMap().erase(
"slot_Centroid")
667 inputSchema.getAliasMap().erase(
"slot_Shape")
668 self.
inputCat = lsst.afw.table.SourceCatalog(inputSchema)
669 self.
mapper = lsst.afw.table.SchemaMapper(inputSchema)
671 self.
outputCat = lsst.afw.table.BaseCatalog(self.mapper.getOutputSchema())
676 def _setupTransform(self):
678 inputMapper = lsst.afw.table.SchemaMapper(lsst.afw.table.SourceTable.makeMinimalSchema(),
679 lsst.afw.table.SourceTable.makeMinimalSchema())
683 inputMapper.editOutputSchema().getAliasMap().set(
"slot_Centroid",
"dummy")
684 inputMapper.editOutputSchema().getAliasMap().set(
"slot_Shape",
"dummy")
685 self.algorithmClass(self.
control, self.name, inputMapper, lsst.daf.base.PropertyList())
686 inputMapper.editOutputSchema().getAliasMap().erase(
"slot_Centroid")
687 inputMapper.editOutputSchema().getAliasMap().erase(
"slot_Shape")
688 self.
inputCat = lsst.afw.table.SourceCatalog(inputMapper.getOutputSchema())
689 self.
mapper = lsst.afw.table.SchemaMapper(inputMapper.getOutputSchema())
691 self.
outputCat = lsst.afw.table.BaseCatalog(self.mapper.getOutputSchema())
696 def _setFieldsInRecords(self, records, name):
697 for record
in records:
698 record[record.schema.join(name,
'flux')] = np.random.random()
699 record[record.schema.join(name,
'fluxSigma')] = np.random.random()
702 assert len(records) > 1
703 records[0][record.schema.join(name,
'flux')] = -1
705 def _compareFieldsInRecords(self, inSrc, outSrc, name):
706 fluxName, fluxSigmaName = inSrc.schema.join(name,
'flux'), inSrc.schema.join(name,
'fluxSigma')
707 if inSrc[fluxName] > 0:
708 mag, magErr = self.calexp.getCalib().getMagnitude(inSrc[fluxName], inSrc[fluxSigmaName])
709 self.assertEqual(outSrc[outSrc.schema.join(name,
'mag')], mag)
710 self.assertEqual(outSrc[outSrc.schema.join(name,
'magErr')], magErr)
712 self.assertTrue(np.isnan(outSrc[outSrc.schema.join(name,
'mag')]))
713 self.assertTrue(np.isnan(outSrc[outSrc.schema.join(name,
'magErr')]))
718 def _setFieldsInRecords(self, records, name):
719 for record
in records:
720 record[record.schema.join(name,
'x')] = np.random.random()
721 record[record.schema.join(name,
'y')] = np.random.random()
724 for fieldSuffix
in (
'xSigma',
'ySigma',
'x_y_Cov'):
725 fieldName = record.schema.join(name, fieldSuffix)
726 if fieldName
in record.schema:
727 record[fieldName] = np.random.random()
729 def _compareFieldsInRecords(self, inSrc, outSrc, name):
730 centroidResultKey = CentroidResultKey(inSrc.schema[self.
name])
731 centroidResult = centroidResultKey.get(inSrc)
733 coord = lsst.afw.table.CoordKey(outSrc.schema[self.
name]).get(outSrc)
734 coordTruth = self.calexp.getWcs().pixelToSky(centroidResult.getCentroid())
735 self.assertEqual(coordTruth, coord)
740 coordErr = lsst.afw.table.CovarianceMatrix2fKey(outSrc.schema[self.
name],
741 [
"ra",
"dec"]).get(outSrc)
742 except lsst.pex.exceptions.NotFoundError:
743 self.assertFalse(centroidResultKey.getCentroidErr().isValid())
745 transform = self.calexp.getWcs().linearizePixelToSky(coordTruth, lsst.afw.geom.radians)
746 coordErrTruth = np.dot(np.dot(transform.getLinear().getMatrix(),
747 centroidResult.getCentroidErr()),
748 transform.getLinear().getMatrix().transpose())
749 np.testing.assert_array_almost_equal(np.array(coordErrTruth), coordErr)
def addBlend
Return a context manager that allows a blend of multiple sources to be added.
def addSource
Add a source to the simulation.
def addChild
Add a child source to the blend, and return the truth catalog record that corresponds to it...
def drawGaussian
Create an image of an elliptical Gaussian.
def makeEmptyExposure
Create an Exposure, with a Calib, Wcs, and Psf, but no pixel values set.
def transform
Create a copy of the dataset transformed to a new WCS, with new Psf and Calib.
def makeSingleFrameMeasurementTask
A simulated dataset consisting of a test image and an associated truth catalog.
def makeForcedMeasurementTask
def realize
Create a simulated with noise and a simulated post-detection catalog with (Heavy)Footprints.
def __init__
Initialize the dataset.
def makePerturbedWcs
Create a new undistorted TanWcs that is similar but not identical to another, with random scaling...
def makeForcedMeasurementConfig
def makeSingleFrameMeasurementConfig
A Python context manager used to add multiple overlapping sources along with a parent source that rep...