35from .sfm
import SingleFrameMeasurementTask
36from .forcedMeasurement
import ForcedMeasurementTask
37from ._measBaseLib
import CentroidResultKey
39__all__ = (
"BlendContext",
"TestDataset",
"AlgorithmTestCase",
"TransformTestCase",
40 "SingleFramePluginTransformSetupHelper",
"ForcedPluginTransformSetupHelper",
41 "FluxTransformTestCase",
"CentroidTransformTestCase")
45 """Context manager which adds multiple overlapping sources and a parent.
49 This is used as the return value for `TestDataset.addBlend`, and this is
50 the only way it should be used.
63 def addChild(self, instFlux, centroid, shape=None):
64 """Add a child to the blend; return corresponding truth catalog record.
67 Total instFlux of the source to be added.
68 centroid : `lsst.geom.Point2D`
69 Position of the source to be added.
70 shape : `lsst.afw.geom.Quadrupole`
71 Second moments of the source before PSF convolution. Note that
72 the truth catalog records post-convolution moments)
74 record, image = self.
owner.addSource(instFlux, centroid, shape)
77 self.
children.append((record, image))
93 instFlux += record.get(self.
owner.keys[
"instFlux"])
99 w = record.get(self.
owner.keys[
"instFlux"])/instFlux
100 x += record.get(self.
owner.keys[
"centroid"].getX())*w
101 y += record.get(self.
owner.keys[
"centroid"].getY())*w
108 w = record.get(self.
owner.keys[
"instFlux"])/instFlux
109 dx = record.get(self.
owner.keys[
"centroid"].getX()) - x
110 dy = record.get(self.
owner.keys[
"centroid"].getY()) - y
111 xx += (record.get(self.
owner.keys[
"shape"].getIxx()) + dx**2)*w
112 yy += (record.get(self.
owner.keys[
"shape"].getIyy()) + dy**2)*w
113 xy += (record.get(self.
owner.keys[
"shape"].getIxy()) + dx*dy)*w
119 deblend = lsst.afw.image.MaskedImageF(self.
owner.exposure.maskedImage,
True)
121 deblend.image.array[:, :] = image.array
122 heavyFootprint = lsst.afw.detection.HeavyFootprintF(self.
parentRecord.getFootprint(), deblend)
123 record.setFootprint(heavyFootprint)
127 """A simulated dataset consisuting of test image and truth catalog.
129 TestDataset creates an idealized image made of pure Gaussians (including a
130 Gaussian PSF), with simple noise and idealized Footprints/HeavyFootprints
131 that simulated the outputs of detection and deblending. Multiple noise
132 realizations can be created from the same underlying sources, allowing
133 uncertainty estimates to be verified via Monte Carlo.
137 bbox : `lsst.geom.Box2I` or `lsst.geom.Box2D`
138 Bounding box of the test image.
140 Threshold absolute value used to determine footprints for
141 simulated sources. This thresholding will be applied before noise is
142 actually added to images (or before the noise level is even known), so
143 this will necessarily produce somewhat artificial footprints.
144 exposure : `lsst.afw.image.ExposureF`
145 The image to which test sources should be added. Ownership should
146 be considered transferred from the caller to the TestDataset.
147 Must have a Gaussian PSF for truth catalog shapes to be exact.
149 Keyword arguments forwarded to makeEmptyExposure if exposure is `None`.
157 bbox = lsst.geom.Box2I(lsst.geom.Point2I(0,0), lsst.geom.Point2I(100,
159 dataset = TestDataset(bbox)
160 dataset.addSource(instFlux=1E5, centroid=lsst.geom.Point2D(25, 26))
161 dataset.addSource(instFlux=2E5, centroid=lsst.geom.Point2D(75, 24),
162 shape=lsst.afw.geom.Quadrupole(8, 7, 2))
163 with dataset.addBlend() as family:
164 family.addChild(instFlux=2E5, centroid=lsst.geom.Point2D(50, 72))
165 family.addChild(instFlux=1.5E5, centroid=lsst.geom.Point2D(51, 74))
166 exposure, catalog = dataset.realize(noise=100.0,
167 schema=TestDataset.makeMinimalSchema())
170 def __init__(self, bbox, threshold=10.0, exposure=None, **kwds):
181 """Return the minimal schema needed to hold truth catalog fields.
185 When `TestDataset.realize` is called, the schema must include at least
186 these fields. Usually it will include additional fields for
187 measurement algorithm outputs, allowing the same catalog to be used
188 for both truth values (the fields from the minimal schema) and the
191 if not hasattr(cls,
"_schema"):
195 cls.
keys[
"parent"] = schema.find(
"parent").key
196 cls.
keys[
"nChild"] = schema.addField(
"deblend_nChild", type=np.int32)
197 cls.
keys[
"instFlux"] = schema.addField(
"truth_instFlux", type=np.float64,
198 doc=
"true instFlux", units=
"count")
199 cls.
keys[
"instFluxErr"] = schema.addField(
"truth_instFluxErr", type=np.float64,
200 doc=
"true instFluxErr", units=
"count")
201 cls.
keys[
"centroid"] = lsst.afw.table.Point2DKey.addFields(
202 schema,
"truth",
"true simulated centroid",
"pixel"
204 cls.
keys[
"centroid_sigma"] = lsst.afw.table.CovarianceMatrix2fKey.addFields(
205 schema,
"truth", [
'x',
'y'],
"pixel"
207 cls.
keys[
"centroid_flag"] = schema.addField(
"truth_flag", type=
"Flag",
208 doc=
"set if the object is a star")
210 schema,
"truth",
"true shape after PSF convolution", lsst.afw.table.CoordinateType.PIXEL
212 cls.
keys[
"isStar"] = schema.addField(
"truth_isStar", type=
"Flag",
213 doc=
"set if the object is a star")
214 schema.getAliasMap().set(
"slot_Shape",
"truth")
215 schema.getAliasMap().set(
"slot_Centroid",
"truth")
216 schema.getAliasMap().set(
"slot_ModelFlux",
"truth")
219 schema.disconnectAliases()
224 minRotation=None, maxRotation=None,
225 minRefShift=None, maxRefShift=None,
226 minPixShift=2.0, maxPixShift=4.0, randomSeed=1):
227 """Return a perturbed version of the input WCS.
229 Create a new undistorted TAN WCS that is similar but not identical to
230 another, with random scaling, rotation, and offset (in both pixel
231 position and reference position).
235 oldWcs : `lsst.afw.geom.SkyWcs`
237 minScaleFactor : `float`
238 Minimum scale factor to apply to the input WCS.
239 maxScaleFactor : `float`
240 Maximum scale factor to apply to the input WCS.
241 minRotation : `lsst.geom.Angle` or `None`
242 Minimum rotation to apply to the input WCS. If `None`, defaults to
244 maxRotation : `lsst.geom.Angle` or `None`
245 Minimum rotation to apply to the input WCS. If `None`, defaults to
247 minRefShift : `lsst.geom.Angle` or `None`
248 Miniumum shift to apply to the input WCS reference value. If
249 `None`, defaults to 0.5 arcsec.
250 maxRefShift : `lsst.geom.Angle` or `None`
251 Miniumum shift to apply to the input WCS reference value. If
252 `None`, defaults to 1.0 arcsec.
253 minPixShift : `float`
254 Minimum shift to apply to the input WCS reference pixel.
255 maxPixShift : `float`
256 Maximum shift to apply to the input WCS reference pixel.
262 newWcs : `lsst.afw.geom.SkyWcs`
263 A perturbed version of the input WCS.
267 The maximum and minimum arguments are interpreted as absolute values
268 for a split range that covers both positive and negative values (as
269 this method is used in testing, it is typically most important to
270 avoid perturbations near zero). Scale factors are treated somewhat
271 differently: the actual scale factor is chosen between
272 ``minScaleFactor`` and ``maxScaleFactor`` OR (``1/maxScaleFactor``)
273 and (``1/minScaleFactor``).
275 The default range for rotation is 30-60 degrees, and the default range
276 for reference shift is 0.5-1.0 arcseconds (these cannot be safely
277 included directly as default values because Angle objects are
280 The random number generator is primed with the seed given. If
281 `None`, a seed is automatically chosen.
283 random_state = np.random.RandomState(randomSeed)
284 if minRotation
is None:
285 minRotation = 30.0*lsst.geom.degrees
286 if maxRotation
is None:
287 maxRotation = 60.0*lsst.geom.degrees
288 if minRefShift
is None:
289 minRefShift = 0.5*lsst.geom.arcseconds
290 if maxRefShift
is None:
291 maxRefShift = 1.0*lsst.geom.arcseconds
293 def splitRandom(min1, max1, min2=None, max2=None):
298 if random_state.uniform() > 0.5:
299 return float(random_state.uniform(min1, max1))
301 return float(random_state.uniform(min2, max2))
303 scaleFactor = splitRandom(minScaleFactor, maxScaleFactor, 1.0/maxScaleFactor, 1.0/minScaleFactor)
304 rotation = splitRandom(minRotation.asRadians(), maxRotation.asRadians())*lsst.geom.radians
305 refShiftRa = splitRandom(minRefShift.asRadians(), maxRefShift.asRadians())*lsst.geom.radians
306 refShiftDec = splitRandom(minRefShift.asRadians(), maxRefShift.asRadians())*lsst.geom.radians
307 pixShiftX = splitRandom(minPixShift, maxPixShift)
308 pixShiftY = splitRandom(minPixShift, maxPixShift)
313 newTransform = oldTransform*rTransform*sTransform
314 matrix = newTransform.getMatrix()
316 oldSkyOrigin = oldWcs.getSkyOrigin()
318 oldSkyOrigin.getDec() + refShiftDec)
320 oldPixOrigin = oldWcs.getPixelOrigin()
322 oldPixOrigin.getY() + pixShiftY)
326 def makeEmptyExposure(bbox, wcs=None, crval=None, cdelt=None, psfSigma=2.0, psfDim=17, calibration=4,
327 visitId=1234, mjd=60000.0):
328 """Create an Exposure, with a PhotoCalib, Wcs, and Psf, but no pixel values.
332 bbox : `lsst.geom.Box2I` or `lsst.geom.Box2D`
333 Bounding box of the image in image coordinates.
334 wcs : `lsst.afw.geom.SkyWcs`, optional
335 New WCS for the exposure (created from CRVAL and CDELT if `None`).
336 crval : `lsst.afw.geom.SpherePoint`, optional
337 ICRS center of the TAN WCS attached to the image. If `None`, (45
338 degrees, 45 degrees) is assumed.
339 cdelt : `lsst.geom.Angle`, optional
340 Pixel scale of the image. If `None`, 0.2 arcsec is assumed.
341 psfSigma : `float`, optional
342 Radius (sigma) of the Gaussian PSF attached to the image
343 psfDim : `int`, optional
344 Width and height of the image's Gaussian PSF attached to the image
345 calibration : `float`, optional
346 The spatially-constant calibration (in nJy/count) to set the
347 PhotoCalib of the exposure.
348 visitId : `int`, optional
349 Visit id to store in VisitInfo.
350 mjd : `float`, optional
351 Modified Julian Date of this exposure to store in VisitInfo.
355 exposure : `lsst.age.image.ExposureF`
362 cdelt = 0.2*lsst.geom.arcseconds
366 exposure = lsst.afw.image.ExposureF(bbox)
373 22.2*lsst.geom.degrees,
375 hasSimulatedContent=
True)
378 exposure.setPhotoCalib(photoCalib)
379 exposure.info.setVisitInfo(visitInfo)
384 """Create an image of an elliptical Gaussian.
388 bbox : `lsst.geom.Box2I` or `lsst.geom.Box2D`
389 Bounding box of image to create.
391 Total instrumental flux of the Gaussian (normalized analytically,
392 not using pixel values).
393 ellipse : `lsst.afw.geom.Ellipse`
394 Defines the centroid and shape.
398 image : `lsst.afw.image.ImageF`
399 An image of the Gaussian.
401 x, y = np.meshgrid(np.arange(bbox.getBeginX(), bbox.getEndX()),
402 np.arange(bbox.getBeginY(), bbox.getEndY()))
403 t = ellipse.getGridTransform()
404 xt = t[t.XX] * x + t[t.XY] * y + t[t.X]
405 yt = t[t.YX] * x + t[t.YY] * y + t[t.Y]
406 image = lsst.afw.image.ImageF(bbox)
407 image.array[:, :] = np.exp(-0.5*(xt**2 + yt**2))*instFlux/(2.0*ellipse.getCore().getArea())
411 """Create simulated Footprint and add it to a truth catalog record.
414 if setPeakSignificance:
415 schema.addField(
"significance", type=float,
416 doc=
"Ratio of peak value to configured standard deviation.")
421 if setPeakSignificance:
424 for footprint
in fpSet.getFootprints():
425 footprint.updatePeakSignificance(self.
threshold.getValue())
427 fpSet.setMask(self.
exposure.mask,
"DETECTED")
429 if len(fpSet.getFootprints()) > 1:
430 raise RuntimeError(
"Threshold value results in multiple Footprints for a single object")
431 if len(fpSet.getFootprints()) == 0:
432 raise RuntimeError(
"Threshold value results in zero Footprints for object")
433 record.setFootprint(fpSet.getFootprints()[0])
435 def addSource(self, instFlux, centroid, shape=None, setPeakSignificance=True):
436 """Add a source to the simulation.
438 To insert a point source with a given signal-to-noise (sn), the total
439 ``instFlux`` should be: ``sn*noise*psf_scale``, where ``noise`` is the
440 noise you will pass to ``realize()``, and
441 ``psf_scale=sqrt(4*pi*r^2)``, where ``r`` is the width of the PSF.
446 Total instFlux of the source to be added.
447 centroid : `lsst.geom.Point2D`
448 Position of the source to be added.
449 shape : `lsst.afw.geom.Quadrupole`
450 Second moments of the source before PSF convolution. Note that the
451 truth catalog records post-convolution moments. If `None`, a point
452 source will be added.
453 setPeakSignificance : `bool`
454 Set the ``significance`` field for peaks in the footprints?
455 See ``lsst.meas.algorithms.SourceDetectionTask.setPeakSignificance``
456 for how this field is computed for real datasets.
460 record : `lsst.afw.table.SourceRecord`
461 A truth catalog record.
462 image : `lsst.afw.image.ImageF`
463 Single-source image corresponding to the new source.
467 record.set(self.
keys[
"instFlux"], instFlux)
468 record.set(self.
keys[
"instFluxErr"], 0)
469 record.set(self.
keys[
"centroid"], centroid)
470 covariance = np.random.normal(0, 0.1, 4).reshape(2, 2)
471 covariance[0, 1] = covariance[1, 0]
472 record.set(self.
keys[
"centroid_sigma"], covariance.astype(np.float32))
474 record.set(self.
keys[
"isStar"],
True)
477 record.set(self.
keys[
"isStar"],
False)
478 fullShape = shape.convolve(self.
psfShape)
479 record.set(self.
keys[
"shape"], fullShape)
486 self.
exposure.image.array[:, :] += image.array
490 """Return a context manager which can add a blend of multiple sources.
494 Note that nothing stops you from creating overlapping sources just using the addSource() method,
495 but addBlend() is necesssary to create a parent object and deblended HeavyFootprints of the type
496 produced by the detection and deblending pipelines.
502 with d.addBlend() as b:
503 b.addChild(flux1, centroid1)
504 b.addChild(flux2, centroid2, shape2)
509 """Copy this dataset transformed to a new WCS, with new Psf and PhotoCalib.
513 wcs : `lsst.afw.geom.SkyWcs`
514 WCS for the new dataset.
516 Additional keyword arguments passed on to
517 `TestDataset.makeEmptyExposure`. If not specified, these revert
518 to the defaults for `~TestDataset.makeEmptyExposure`, not the
519 values in the current dataset.
523 newDataset : `TestDataset`
524 Transformed copy of this dataset.
532 oldPhotoCalib = self.
exposure.getPhotoCalib()
533 newPhotoCalib = result.exposure.getPhotoCalib()
534 oldPsfShape = self.
exposure.getPsf().computeShape(bboxD.getCenter())
536 if record.get(self.
keys[
"nChild"]):
537 raise NotImplementedError(
"Transforming blended sources in TestDatasets is not supported")
538 magnitude = oldPhotoCalib.instFluxToMagnitude(record.get(self.
keys[
"instFlux"]))
539 newFlux = newPhotoCalib.magnitudeToInstFlux(magnitude)
540 oldCentroid = record.get(self.
keys[
"centroid"])
541 newCentroid = xyt.applyForward(oldCentroid)
542 if record.get(self.
keys[
"isStar"]):
543 newDeconvolvedShape =
None
546 oldFullShape = record.get(self.
keys[
"shape"])
548 oldFullShape.getIxx() - oldPsfShape.getIxx(),
549 oldFullShape.getIyy() - oldPsfShape.getIyy(),
550 oldFullShape.getIxy() - oldPsfShape.getIxy(),
553 newDeconvolvedShape = oldDeconvolvedShape.transform(affine.getLinear())
554 result.addSource(newFlux, newCentroid, newDeconvolvedShape)
557 def realize(self, noise, schema, randomSeed=1):
558 r"""Simulate an exposure and detection catalog for this dataset.
560 The simulation includes noise, and the detection catalog includes
561 `~lsst.afw.detection.heavyFootprint.HeavyFootprint`\ s.
566 Standard deviation of noise to be added to the exposure. The
567 noise will be Gaussian and constant, appropriate for the
569 schema : `lsst.afw.table.Schema`
570 Schema of the new catalog to be created. Must start with
571 ``self.schema`` (i.e. ``schema.contains(self.schema)`` must be
572 `True`), but typically contains fields for already-configured
573 measurement algorithms as well.
574 randomSeed : `int`, optional
575 Seed for the random number generator.
576 If `None`, a seed is chosen automatically.
580 `exposure` : `lsst.afw.image.ExposureF`
582 `catalog` : `lsst.afw.table.SourceCatalog`
583 Simulated detection catalog.
585 random_state = np.random.RandomState(randomSeed)
586 assert schema.contains(self.
schema)
588 mapper.addMinimalSchema(self.
schema,
True)
590 exposure.variance.array[:, :] = noise**2
591 exposure.image.array[:, :] += random_state.randn(exposure.height, exposure.width)*noise
593 catalog.extend(self.
catalog, mapper=mapper)
596 for record
in catalog:
599 if record.getParent() == 0:
603 parent = catalog.find(record.getParent())
604 footprint = parent.getFootprint()
605 parentFluxArrayNoNoise = np.zeros(footprint.getArea(), dtype=np.float32)
606 footprint.spans.flatten(parentFluxArrayNoNoise, self.
exposure.image.array, self.
exposure.getXY0())
607 parentFluxArrayNoisy = np.zeros(footprint.getArea(), dtype=np.float32)
608 footprint.spans.flatten(parentFluxArrayNoisy, exposure.image.array, exposure.getXY0())
609 oldHeavy = record.getFootprint()
610 fraction = (oldHeavy.getImageArray() / parentFluxArrayNoNoise)
614 newHeavy = lsst.afw.detection.HeavyFootprintF(oldHeavy)
615 newHeavy.getImageArray()[:] = parentFluxArrayNoisy*fraction
616 newHeavy.getMaskArray()[:] = oldHeavy.getMaskArray()
617 newHeavy.getVarianceArray()[:] = oldHeavy.getVarianceArray()
618 record.setFootprint(newHeavy)
620 return exposure, catalog
624 """Base class for tests of measurement tasks.
627 """Create an instance of `SingleFrameMeasurementTask.ConfigClass`.
629 Only the specified plugin and its dependencies will be run; the
630 Centroid, Shape, and ModelFlux slots will be set to the truth fields
631 generated by the `TestDataset` class.
636 Name of measurement plugin to enable.
637 dependencies : iterable of `str`, optional
638 Names of dependencies of the measurement plugin.
642 config : `SingleFrameMeasurementTask.ConfigClass`
643 The resulting task configuration.
645 config = SingleFrameMeasurementTask.ConfigClass()
646 with warnings.catch_warnings():
647 warnings.filterwarnings(
"ignore", message=
"ignoreSlotPluginChecks", category=FutureWarning)
648 config = SingleFrameMeasurementTask.ConfigClass(ignoreSlotPluginChecks=
True)
649 config.slots.centroid =
"truth"
650 config.slots.shape =
"truth"
651 config.slots.modelFlux =
None
652 config.slots.apFlux =
None
653 config.slots.psfFlux =
None
654 config.slots.gaussianFlux =
None
655 config.slots.calibFlux =
None
656 config.plugins.names = (plugin,) + tuple(dependencies)
661 """Create a configured instance of `SingleFrameMeasurementTask`.
665 plugin : `str`, optional
666 Name of measurement plugin to enable. If `None`, a configuration
667 must be supplied as the ``config`` parameter. If both are
668 specified, ``config`` takes precedence.
669 dependencies : iterable of `str`, optional
670 Names of dependencies of the specified measurement plugin.
671 config : `SingleFrameMeasurementTask.ConfigClass`, optional
672 Configuration for the task. If `None`, a measurement plugin must
673 be supplied as the ``plugin`` paramter. If both are specified,
674 ``config`` takes precedence.
675 schema : `lsst.afw.table.Schema`, optional
676 Measurement table schema. If `None`, a default schema is
678 algMetadata : `lsst.daf.base.PropertyList`, optional
679 Measurement algorithm metadata. If `None`, a default container
684 task : `SingleFrameMeasurementTask`
685 A configured instance of the measurement task.
689 raise ValueError(
"Either plugin or config argument must not be None")
692 schema = TestDataset.makeMinimalSchema()
694 schema.setAliasMap(
None)
695 if algMetadata
is None:
700 """Create an instance of `ForcedMeasurementTask.ConfigClass`.
702 In addition to the plugins specified in the plugin and dependencies
703 arguments, the `TransformedCentroid` and `TransformedShape` plugins
704 will be run and used as the centroid and shape slots; these simply
705 transform the reference catalog centroid and shape to the measurement
711 Name of measurement plugin to enable.
712 dependencies : iterable of `str`, optional
713 Names of dependencies of the measurement plugin.
717 config : `ForcedMeasurementTask.ConfigClass`
718 The resulting task configuration.
721 config = ForcedMeasurementTask.ConfigClass()
722 config.slots.centroid =
"base_TransformedCentroid"
723 config.slots.shape =
"base_TransformedShape"
724 config.slots.modelFlux =
None
725 config.slots.apFlux =
None
726 config.slots.psfFlux =
None
727 config.slots.gaussianFlux =
None
728 config.plugins.names = (plugin,) + tuple(dependencies) + (
"base_TransformedCentroid",
729 "base_TransformedShape")
734 """Create a configured instance of `ForcedMeasurementTask`.
738 plugin : `str`, optional
739 Name of measurement plugin to enable. If `None`, a configuration
740 must be supplied as the ``config`` parameter. If both are
741 specified, ``config`` takes precedence.
742 dependencies : iterable of `str`, optional
743 Names of dependencies of the specified measurement plugin.
744 config : `SingleFrameMeasurementTask.ConfigClass`, optional
745 Configuration for the task. If `None`, a measurement plugin must
746 be supplied as the ``plugin`` paramter. If both are specified,
747 ``config`` takes precedence.
748 refSchema : `lsst.afw.table.Schema`, optional
749 Reference table schema. If `None`, a default schema is
751 algMetadata : `lsst.daf.base.PropertyList`, optional
752 Measurement algorithm metadata. If `None`, a default container
757 task : `ForcedMeasurementTask`
758 A configured instance of the measurement task.
762 raise ValueError(
"Either plugin or config argument must not be None")
764 if refSchema
is None:
765 refSchema = TestDataset.makeMinimalSchema()
766 if algMetadata
is None:
772 """Base class for testing measurement transformations.
776 We test both that the transform itself operates successfully (fluxes are
777 converted to magnitudes, flags are propagated properly) and that the
778 transform is registered as the default for the appropriate measurement
781 In the simple case of one-measurement-per-transformation, the developer
782 need not directly write any tests themselves: simply customizing the class
783 variables is all that is required. More complex measurements (e.g.
784 multiple aperture fluxes) require extra effort.
786 name =
"MeasurementTransformTest"
787 """The name used for the measurement algorithm (str).
791 This determines the names of the fields in the resulting catalog. This
792 default should generally be fine, but subclasses can override if
798 algorithmClass =
None
799 transformClass =
None
801 flagNames = (
"flag",)
802 """Flags which may be set by the algorithm being tested (iterable of `str`).
808 singleFramePlugins = ()
813 self.
calexp = TestDataset.makeEmptyExposure(bbox)
814 self._setupTransform()
825 for flagValue
in (
True,
False):
826 records.append(self.
inputCat.addNew())
827 for baseName
in baseNames:
829 if records[-1].schema.join(baseName, flagName)
in records[-1].schema:
830 records[-1].set(records[-1].schema.join(baseName, flagName), flagValue)
831 self._setFieldsInRecords(records, baseName)
835 for baseName
in baseNames:
836 self._compareFieldsInRecords(inSrc, outSrc, baseName)
838 keyName = outSrc.schema.join(baseName, flagName)
839 if keyName
in inSrc.schema:
840 self.assertEqual(outSrc.get(keyName), inSrc.get(keyName))
842 self.assertFalse(keyName
in outSrc.schema)
850 """Test the transformation on a catalog containing random data.
854 baseNames : iterable of `str`
855 Iterable of the initial parts of measurement field names.
861 - An appropriate exception is raised on an attempt to transform
862 between catalogs with different numbers of rows;
863 - Otherwise, all appropriate conversions are properly appled and that
864 flags have been propagated.
866 The ``baseNames`` argument requires some explanation. This should be
867 an iterable of the leading parts of the field names for each
868 measurement; that is, everything that appears before ``_instFlux``,
869 ``_flag``, etc. In the simple case of a single measurement per plugin,
870 this is simply equal to ``self.name`` (thus measurements are stored as
871 ``self.name + "_instFlux"``, etc). More generally, the developer may
872 specify whatever iterable they require. For example, to handle
873 multiple apertures, we could have ``(self.name + "_0", self.name +
876 baseNames = baseNames
or [self.
name]
885 self.assertEqual(registry[name].PluginClass.getTransformClass(), self.
transformClass)
888 """Test that the transformation is appropriately registered.
904 inputSchema.getAliasMap().set(
"slot_Centroid",
"dummy")
905 inputSchema.getAliasMap().set(
"slot_Shape",
"dummy")
907 inputSchema.getAliasMap().erase(
"slot_Centroid")
908 inputSchema.getAliasMap().erase(
"slot_Shape")
924 inputMapper.editOutputSchema().getAliasMap().set(
"slot_Centroid",
"dummy")
925 inputMapper.editOutputSchema().getAliasMap().set(
"slot_Shape",
"dummy")
927 inputMapper.editOutputSchema().getAliasMap().erase(
"slot_Centroid")
928 inputMapper.editOutputSchema().getAliasMap().erase(
"slot_Shape")
938 for record
in records:
939 record[record.schema.join(name,
'instFlux')] = np.random.random()
940 record[record.schema.join(name,
'instFluxErr')] = np.random.random()
943 assert len(records) > 1
944 records[0][record.schema.join(name,
'instFlux')] = -1
947 instFluxName = inSrc.schema.join(name,
'instFlux')
948 instFluxErrName = inSrc.schema.join(name,
'instFluxErr')
949 if inSrc[instFluxName] > 0:
950 mag = self.
calexp.getPhotoCalib().instFluxToMagnitude(inSrc[instFluxName],
951 inSrc[instFluxErrName])
952 self.assertEqual(outSrc[outSrc.schema.join(name,
'mag')], mag.value)
953 self.assertEqual(outSrc[outSrc.schema.join(name,
'magErr')], mag.error)
956 self.assertTrue(np.isnan(outSrc[outSrc.schema.join(name,
'mag')]))
957 if np.isnan(inSrc[instFluxErrName]):
958 self.assertTrue(np.isnan(outSrc[outSrc.schema.join(name,
'magErr')]))
960 mag = self.
calexp.getPhotoCalib().instFluxToMagnitude(inSrc[instFluxName],
961 inSrc[instFluxErrName])
962 self.assertEqual(outSrc[outSrc.schema.join(name,
'magErr')], mag.error)
968 for record
in records:
969 record[record.schema.join(name,
'x')] = np.random.random()
970 record[record.schema.join(name,
'y')] = np.random.random()
973 for fieldSuffix
in (
'xErr',
'yErr',
'x_y_Cov'):
974 fieldName = record.schema.join(name, fieldSuffix)
975 if fieldName
in record.schema:
976 record[fieldName] = np.random.random()
979 centroidResultKey = CentroidResultKey(inSrc.schema[self.
name])
980 centroidResult = centroidResultKey.get(inSrc)
983 coordTruth = self.
calexp.getWcs().pixelToSky(centroidResult.getCentroid())
984 self.assertEqual(coordTruth, coord)
989 coordErr = lsst.afw.table.CovarianceMatrix2fKey(outSrc.schema[self.
name],
990 [
"ra",
"dec"]).get(outSrc)
992 self.assertFalse(centroidResultKey.getCentroidErr().
isValid())
994 transform = self.
calexp.getWcs().linearizePixelToSky(coordTruth, lsst.geom.radians)
995 coordErrTruth = np.dot(np.dot(transform.getLinear().getMatrix(),
996 centroidResult.getCentroidErr()),
997 transform.getLinear().getMatrix().transpose())
998 np.testing.assert_array_almost_equal(np.array(coordErrTruth), coordErr)
Hold the location of an observatory.
A circularly symmetric Gaussian Psf class with no spatial variation, intended mostly for testing purp...
static afw::table::Schema makeMinimalSchema()
Return a minimal schema for Peak tables and records.
A Threshold is used to pass a threshold value to detection algorithms.
An ellipse defined by an arbitrary BaseCore and a center point.
An ellipse core with quadrupole moments as parameters.
The photometric calibration of an exposure.
Information about a single exposure of an imaging camera.
A FunctorKey used to get or set celestial coordinates from a pair of lsst::geom::Angle keys.
static ErrorKey addErrorFields(Schema &schema)
static QuadrupoleKey addFields(Schema &schema, std::string const &name, std::string const &doc, CoordinateType coordType=CoordinateType::PIXEL)
Add a set of quadrupole subfields to a schema and return a QuadrupoleKey that points to them.
Defines the fields and offsets for a table.
A mapping between the keys of two Schemas, used to copy data between them.
static Schema makeMinimalSchema()
Return a minimal schema for Source tables and records.
Class for handling dates/times, including MJD, UTC, and TAI.
Class for storing ordered metadata with comments.
A floating-point coordinate rectangle geometry.
An integer coordinate rectangle.
Point in an unspecified spherical coordinate system.
makeSingleFrameMeasurementConfig(self, plugin=None, dependencies=())
makeForcedMeasurementConfig(self, plugin=None, dependencies=())
makeForcedMeasurementTask(self, plugin=None, dependencies=(), config=None, refSchema=None, algMetadata=None)
makeSingleFrameMeasurementTask(self, plugin=None, dependencies=(), config=None, schema=None, algMetadata=None)
addChild(self, instFlux, centroid, shape=None)
__exit__(self, type_, value, tb)
makePerturbedWcs(oldWcs, minScaleFactor=1.2, maxScaleFactor=1.5, minRotation=None, maxRotation=None, minRefShift=None, maxRefShift=None, minPixShift=2.0, maxPixShift=4.0, randomSeed=1)
realize(self, noise, schema, randomSeed=1)
addSource(self, instFlux, centroid, shape=None, setPeakSignificance=True)
_installFootprint(self, record, image, setPeakSignificance=True)
drawGaussian(bbox, instFlux, ellipse)
makeEmptyExposure(bbox, wcs=None, crval=None, cdelt=None, psfSigma=2.0, psfDim=17, calibration=4, visitId=1234, mjd=60000.0)
__init__(self, bbox, threshold=10.0, exposure=None, **kwds)
Reports attempts to exceed implementation-defined length limits for some classes.
Reports attempts to access elements using an invalid key.
std::shared_ptr< SkyWcs > makeSkyWcs(daf::base::PropertySet &metadata, bool strip=false)
Construct a SkyWcs from FITS keywords.
Eigen::Matrix2d makeCdMatrix(lsst::geom::Angle const &scale, lsst::geom::Angle const &orientation=0 *lsst::geom::degrees, bool flipX=false)
Make a WCS CD matrix.
std::shared_ptr< TransformPoint2ToPoint2 > makeWcsPairTransform(SkyWcs const &src, SkyWcs const &dst)
A Transform obtained by putting two SkyWcs objects "back to back".
lsst::geom::AffineTransform linearizeTransform(TransformPoint2ToPoint2 const &original, lsst::geom::Point2D const &inPoint)
Approximate a Transform by its local linearization.
void updateSourceCoords(geom::SkyWcs const &wcs, SourceCollection &sourceList, bool include_covariance=true)
Update sky coordinates in a collection of source objects.