36from .sfm
import SingleFrameMeasurementTask
37from .forcedMeasurement
import ForcedMeasurementTask
38from ._measBaseLib
import CentroidResultKey
40__all__ = (
"BlendContext",
"TestDataset",
"AlgorithmTestCase",
"TransformTestCase",
41 "SingleFramePluginTransformSetupHelper",
"ForcedPluginTransformSetupHelper",
42 "FluxTransformTestCase",
"CentroidTransformTestCase")
46 """Context manager which adds multiple overlapping sources and a parent.
50 This is used as the return value for `TestDataset.addBlend`, and this is
51 the only way it should be used.
64 def addChild(self, instFlux, centroid, shape=None):
65 """Add a child to the blend; return corresponding truth catalog record.
68 Total instFlux of the source to be added.
69 centroid : `lsst.geom.Point2D`
70 Position of the source to be added.
71 shape : `lsst.afw.geom.Quadrupole`
72 Second moments of the source before PSF convolution. Note that
73 the truth catalog records post-convolution moments)
75 record, image = self.
owner.addSource(instFlux, centroid, shape)
78 self.
children.append((record, image))
94 instFlux += record.get(self.
owner.keys[
"instFlux"])
100 w = record.get(self.
owner.keys[
"instFlux"])/instFlux
101 x += record.get(self.
owner.keys[
"centroid"].getX())*w
102 y += record.get(self.
owner.keys[
"centroid"].getY())*w
109 w = record.get(self.
owner.keys[
"instFlux"])/instFlux
110 dx = record.get(self.
owner.keys[
"centroid"].getX()) - x
111 dy = record.get(self.
owner.keys[
"centroid"].getY()) - y
112 xx += (record.get(self.
owner.keys[
"shape"].getIxx()) + dx**2)*w
113 yy += (record.get(self.
owner.keys[
"shape"].getIyy()) + dy**2)*w
114 xy += (record.get(self.
owner.keys[
"shape"].getIxy()) + dx*dy)*w
115 self.
parentRecord.set(self.
owner.keys[
"shape"], lsst.afw.geom.Quadrupole(xx, yy, xy))
120 deblend = lsst.afw.image.MaskedImageF(self.
owner.exposure.maskedImage,
True)
122 deblend.image.array[:, :] = image.array
123 heavyFootprint = lsst.afw.detection.HeavyFootprintF(self.
parentRecord.getFootprint(), deblend)
124 record.setFootprint(heavyFootprint)
128 """A simulated dataset consisuting of test image and truth catalog.
130 TestDataset creates an idealized image made of pure Gaussians (including a
131 Gaussian PSF), with simple noise and idealized Footprints/HeavyFootprints
132 that simulated the outputs of detection and deblending. Multiple noise
133 realizations can be created from the same underlying sources, allowing
134 uncertainty estimates to be verified via Monte Carlo.
138 bbox : `lsst.geom.Box2I` or `lsst.geom.Box2D`
139 Bounding box of the test image.
141 Threshold absolute value used to determine footprints for
142 simulated sources. This thresholding will be applied before noise is
143 actually added to images (or before the noise level is even known), so
144 this will necessarily produce somewhat artificial footprints.
145 exposure : `lsst.afw.image.ExposureF`
146 The image to which test sources should be added. Ownership should
147 be considered transferred from the caller to the TestDataset.
148 Must have a Gaussian PSF for truth catalog shapes to be exact.
150 Keyword arguments forwarded to makeEmptyExposure if exposure is `None`.
158 bbox = lsst.geom.Box2I(lsst.geom.Point2I(0,0), lsst.geom.Point2I(100,
160 dataset = TestDataset(bbox)
161 dataset.addSource(instFlux=1E5, centroid=lsst.geom.Point2D(25, 26))
162 dataset.addSource(instFlux=2E5, centroid=lsst.geom.Point2D(75, 24),
163 shape=lsst.afw.geom.Quadrupole(8, 7, 2))
164 with dataset.addBlend() as family:
165 family.addChild(instFlux=2E5, centroid=lsst.geom.Point2D(50, 72))
166 family.addChild(instFlux=1.5E5, centroid=lsst.geom.Point2D(51, 74))
167 exposure, catalog = dataset.realize(noise=100.0,
168 schema=TestDataset.makeMinimalSchema())
171 def __init__(self, bbox, threshold=10.0, exposure=None, **kwds):
182 """Return the minimal schema needed to hold truth catalog fields.
186 When `TestDataset.realize` is called, the schema must include at least
187 these fields. Usually it will include additional fields for
188 measurement algorithm outputs, allowing the same catalog to be used
189 for both truth values (the fields from the minimal schema) and the
192 if not hasattr(cls,
"_schema"):
196 cls.
keys[
"parent"] = schema.find(
"parent").key
197 cls.
keys[
"nChild"] = schema.addField(
"deblend_nChild", type=np.int32)
198 cls.
keys[
"instFlux"] = schema.addField(
"truth_instFlux", type=np.float64,
199 doc=
"true instFlux", units=
"count")
200 cls.
keys[
"instFluxErr"] = schema.addField(
"truth_instFluxErr", type=np.float64,
201 doc=
"true instFluxErr", units=
"count")
202 cls.
keys[
"centroid"] = lsst.afw.table.Point2DKey.addFields(
203 schema,
"truth",
"true simulated centroid",
"pixel"
205 cls.
keys[
"centroid_sigma"] = lsst.afw.table.CovarianceMatrix2fKey.addFields(
206 schema,
"truth", [
'x',
'y'],
"pixel"
208 cls.
keys[
"centroid_flag"] = schema.addField(
"truth_flag", type=
"Flag",
209 doc=
"set if the object is a star")
211 schema,
"truth",
"true shape after PSF convolution", lsst.afw.table.CoordinateType.PIXEL
213 cls.
keys[
"isStar"] = schema.addField(
"truth_isStar", type=
"Flag",
214 doc=
"set if the object is a star")
215 cls.
keys[
"negative"] = schema.addField(
"is_negative", type=
"Flag",
216 doc=
"set if source was detected as significantly negative"
218 schema.getAliasMap().set(
"slot_Shape",
"truth")
219 schema.getAliasMap().set(
"slot_Centroid",
"truth")
220 schema.getAliasMap().set(
"slot_ModelFlux",
"truth")
223 schema.disconnectAliases()
228 minRotation=None, maxRotation=None,
229 minRefShift=None, maxRefShift=None,
230 minPixShift=2.0, maxPixShift=4.0, randomSeed=1):
231 """Return a perturbed version of the input WCS.
233 Create a new undistorted TAN WCS that is similar but not identical to
234 another, with random scaling, rotation, and offset (in both pixel
235 position and reference position).
239 oldWcs : `lsst.afw.geom.SkyWcs`
241 minScaleFactor : `float`
242 Minimum scale factor to apply to the input WCS.
243 maxScaleFactor : `float`
244 Maximum scale factor to apply to the input WCS.
245 minRotation : `lsst.geom.Angle` or `None`
246 Minimum rotation to apply to the input WCS. If `None`, defaults to
248 maxRotation : `lsst.geom.Angle` or `None`
249 Minimum rotation to apply to the input WCS. If `None`, defaults to
251 minRefShift : `lsst.geom.Angle` or `None`
252 Miniumum shift to apply to the input WCS reference value. If
253 `None`, defaults to 0.5 arcsec.
254 maxRefShift : `lsst.geom.Angle` or `None`
255 Miniumum shift to apply to the input WCS reference value. If
256 `None`, defaults to 1.0 arcsec.
257 minPixShift : `float`
258 Minimum shift to apply to the input WCS reference pixel.
259 maxPixShift : `float`
260 Maximum shift to apply to the input WCS reference pixel.
266 newWcs : `lsst.afw.geom.SkyWcs`
267 A perturbed version of the input WCS.
271 The maximum and minimum arguments are interpreted as absolute values
272 for a split range that covers both positive and negative values (as
273 this method is used in testing, it is typically most important to
274 avoid perturbations near zero). Scale factors are treated somewhat
275 differently: the actual scale factor is chosen between
276 ``minScaleFactor`` and ``maxScaleFactor`` OR (``1/maxScaleFactor``)
277 and (``1/minScaleFactor``).
279 The default range for rotation is 30-60 degrees, and the default range
280 for reference shift is 0.5-1.0 arcseconds (these cannot be safely
281 included directly as default values because Angle objects are
284 The random number generator is primed with the seed given. If
285 `None`, a seed is automatically chosen.
287 random_state = np.random.RandomState(randomSeed)
288 if minRotation
is None:
289 minRotation = 30.0*lsst.geom.degrees
290 if maxRotation
is None:
291 maxRotation = 60.0*lsst.geom.degrees
292 if minRefShift
is None:
293 minRefShift = 0.5*lsst.geom.arcseconds
294 if maxRefShift
is None:
295 maxRefShift = 1.0*lsst.geom.arcseconds
297 def splitRandom(min1, max1, min2=None, max2=None):
302 if random_state.uniform() > 0.5:
303 return float(random_state.uniform(min1, max1))
305 return float(random_state.uniform(min2, max2))
307 scaleFactor = splitRandom(minScaleFactor, maxScaleFactor, 1.0/maxScaleFactor, 1.0/minScaleFactor)
308 rotation = splitRandom(minRotation.asRadians(), maxRotation.asRadians())*lsst.geom.radians
309 refShiftRa = splitRandom(minRefShift.asRadians(), maxRefShift.asRadians())*lsst.geom.radians
310 refShiftDec = splitRandom(minRefShift.asRadians(), maxRefShift.asRadians())*lsst.geom.radians
311 pixShiftX = splitRandom(minPixShift, maxPixShift)
312 pixShiftY = splitRandom(minPixShift, maxPixShift)
317 newTransform = oldTransform*rTransform*sTransform
318 matrix = newTransform.getMatrix()
320 oldSkyOrigin = oldWcs.getSkyOrigin()
322 oldSkyOrigin.getDec() + refShiftDec)
324 oldPixOrigin = oldWcs.getPixelOrigin()
326 oldPixOrigin.getY() + pixShiftY)
330 def makeEmptyExposure(bbox, wcs=None, crval=None, cdelt=None, psfSigma=2.0, psfDim=17, calibration=4,
331 visitId=1234, mjd=60000.0, detector=1):
332 """Create an Exposure, with a PhotoCalib, Wcs, and Psf, but no pixel values.
336 bbox : `lsst.geom.Box2I` or `lsst.geom.Box2D`
337 Bounding box of the image in image coordinates.
338 wcs : `lsst.afw.geom.SkyWcs`, optional
339 New WCS for the exposure (created from CRVAL and CDELT if `None`).
340 crval : `lsst.afw.geom.SpherePoint`, optional
341 ICRS center of the TAN WCS attached to the image. If `None`, (45
342 degrees, 45 degrees) is assumed.
343 cdelt : `lsst.geom.Angle`, optional
344 Pixel scale of the image. If `None`, 0.2 arcsec is assumed.
345 psfSigma : `float`, optional
346 Radius (sigma) of the Gaussian PSF attached to the image
347 psfDim : `int`, optional
348 Width and height of the image's Gaussian PSF attached to the image
349 calibration : `float`, optional
350 The spatially-constant calibration (in nJy/count) to set the
351 PhotoCalib of the exposure.
352 visitId : `int`, optional
353 Visit id to store in VisitInfo.
354 mjd : `float`, optional
355 Modified Julian Date of this exposure to store in VisitInfo.
356 detector: `int`, optional
357 Detector id to assign to the attached Detector object.
361 exposure : `lsst.age.image.ExposureF`
368 cdelt = 0.2*lsst.geom.arcseconds
372 exposure = lsst.afw.image.ExposureF(bbox)
379 22.2*lsst.geom.degrees,
381 hasSimulatedContent=
True)
384 exposure.setPhotoCalib(photoCalib)
385 exposure.info.setVisitInfo(visitInfo)
391 """Create an image of an elliptical Gaussian.
395 bbox : `lsst.geom.Box2I` or `lsst.geom.Box2D`
396 Bounding box of image to create.
398 Total instrumental flux of the Gaussian (normalized analytically,
399 not using pixel values).
400 ellipse : `lsst.afw.geom.Ellipse`
401 Defines the centroid and shape.
405 image : `lsst.afw.image.ImageF`
406 An image of the Gaussian.
408 x, y = np.meshgrid(np.arange(bbox.getBeginX(), bbox.getEndX()),
409 np.arange(bbox.getBeginY(), bbox.getEndY()))
410 t = ellipse.getGridTransform()
411 xt = t[t.XX] * x + t[t.XY] * y + t[t.X]
412 yt = t[t.YX] * x + t[t.YY] * y + t[t.Y]
413 image = lsst.afw.image.ImageF(bbox)
414 image.array[:, :] = np.exp(-0.5*(xt**2 + yt**2))*instFlux/(2.0*ellipse.getCore().getArea())
418 """Create simulated Footprint and add it to a truth catalog record.
421 if setPeakSignificance:
422 schema.addField(
"significance", type=float,
423 doc=
"Ratio of peak value to configured standard deviation.")
430 if setPeakSignificance:
433 for footprint
in fpSet.getFootprints():
434 footprint.updatePeakSignificance(self.
threshold.getValue())
438 fpSet.setMask(self.
exposure.mask,
"DETECTED")
440 if len(fpSet.getFootprints()) > 1:
441 raise RuntimeError(
"Threshold value results in multiple Footprints for a single object")
442 if len(fpSet.getFootprints()) == 0:
443 raise RuntimeError(
"Threshold value results in zero Footprints for object; "
444 "did you forget to set negative=True for a negative source?")
445 record.setFootprint(fpSet.getFootprints()[0])
447 def addSource(self, instFlux, centroid, shape=None, setPeakSignificance=True,
449 """Add a source to the simulation.
451 To insert a point source with a given signal-to-noise (sn), the total
452 ``instFlux`` should be: ``sn*noise*psf_scale``, where ``noise`` is the
453 noise you will pass to ``realize()``, and
454 ``psf_scale=sqrt(4*pi*r^2)``, where ``r`` is the width of the PSF.
459 Total instFlux of the source to be added.
460 centroid : `lsst.geom.Point2D`
461 Position of the source to be added.
462 shape : `lsst.afw.geom.Quadrupole`
463 Second moments of the source before PSF convolution. Note that the
464 truth catalog records post-convolution moments. If `None`, a point
465 source will be added.
466 setPeakSignificance : `bool`
467 Set the ``significance`` field for peaks in the footprints?
468 See ``lsst.meas.algorithms.SourceDetectionTask.setPeakSignificance``
469 for how this field is computed for real datasets.
471 Treat this as a negative source, using a negative polarity
472 Threshold to create the footprint. `instFlux` should be negative
477 record : `lsst.afw.table.SourceRecord`
478 A truth catalog record.
479 image : `lsst.afw.image.ImageF`
480 Single-source image corresponding to the new source.
484 record.set(self.
keys[
"instFlux"], instFlux)
485 record.set(self.
keys[
"instFluxErr"], 0)
486 record.set(self.
keys[
"centroid"], centroid)
487 record.set(self.
keys[
"negative"], negative)
488 covariance = np.random.normal(0, 0.1, 4).reshape(2, 2)
489 covariance[0, 1] = covariance[1, 0]
490 record.set(self.
keys[
"centroid_sigma"], covariance.astype(np.float32))
492 record.set(self.
keys[
"isStar"],
True)
495 record.set(self.
keys[
"isStar"],
False)
496 fullShape = shape.convolve(self.
psfShape)
497 record.set(self.
keys[
"shape"], fullShape)
500 lsst.afw.geom.Ellipse(fullShape, centroid))
504 self.
exposure.image.array[:, :] += image.array
508 """Return a context manager which can add a blend of multiple sources.
512 Note that nothing stops you from creating overlapping sources just using the addSource() method,
513 but addBlend() is necesssary to create a parent object and deblended HeavyFootprints of the type
514 produced by the detection and deblending pipelines.
520 with d.addBlend() as b:
521 b.addChild(flux1, centroid1)
522 b.addChild(flux2, centroid2, shape2)
527 """Copy this dataset transformed to a new WCS, with new Psf and PhotoCalib.
531 wcs : `lsst.afw.geom.SkyWcs`
532 WCS for the new dataset.
534 Additional keyword arguments passed on to
535 `TestDataset.makeEmptyExposure`. If not specified, these revert
536 to the defaults for `~TestDataset.makeEmptyExposure`, not the
537 values in the current dataset.
541 newDataset : `TestDataset`
542 Transformed copy of this dataset.
550 oldPhotoCalib = self.
exposure.getPhotoCalib()
551 newPhotoCalib = result.exposure.getPhotoCalib()
552 oldPsfShape = self.
exposure.getPsf().computeShape(bboxD.getCenter())
554 if record.get(self.
keys[
"nChild"]):
555 raise NotImplementedError(
"Transforming blended sources in TestDatasets is not supported")
556 magnitude = oldPhotoCalib.instFluxToMagnitude(record.get(self.
keys[
"instFlux"]))
557 newFlux = newPhotoCalib.magnitudeToInstFlux(magnitude)
558 oldCentroid = record.get(self.
keys[
"centroid"])
559 newCentroid = xyt.applyForward(oldCentroid)
560 if record.get(self.
keys[
"isStar"]):
561 newDeconvolvedShape =
None
564 oldFullShape = record.get(self.
keys[
"shape"])
565 oldDeconvolvedShape = lsst.afw.geom.Quadrupole(
566 oldFullShape.getIxx() - oldPsfShape.getIxx(),
567 oldFullShape.getIyy() - oldPsfShape.getIyy(),
568 oldFullShape.getIxy() - oldPsfShape.getIxy(),
571 newDeconvolvedShape = oldDeconvolvedShape.transform(affine.getLinear())
572 result.addSource(newFlux, newCentroid, newDeconvolvedShape)
575 def realize(self, noise, schema, randomSeed=1):
576 r"""Simulate an exposure and detection catalog for this dataset.
578 The simulation includes noise, and the detection catalog includes
579 `~lsst.afw.detection.heavyFootprint.HeavyFootprint`\ s.
584 Standard deviation of noise to be added to the exposure. The
585 noise will be Gaussian and constant, appropriate for the
587 schema : `lsst.afw.table.Schema`
588 Schema of the new catalog to be created. Must start with
589 ``self.schema`` (i.e. ``schema.contains(self.schema)`` must be
590 `True`), but typically contains fields for already-configured
591 measurement algorithms as well.
592 randomSeed : `int`, optional
593 Seed for the random number generator.
594 If `None`, a seed is chosen automatically.
598 `exposure` : `lsst.afw.image.ExposureF`
600 `catalog` : `lsst.afw.table.SourceCatalog`
601 Simulated detection catalog.
603 random_state = np.random.RandomState(randomSeed)
604 assert schema.contains(self.
schema)
606 mapper.addMinimalSchema(self.
schema,
True)
608 exposure.variance.array[:, :] = noise**2
609 exposure.image.array[:, :] += random_state.randn(exposure.height, exposure.width)*noise
611 catalog.extend(self.
catalog, mapper=mapper)
614 for record
in catalog:
617 if record.getParent() == 0:
621 parent = catalog.find(record.getParent())
622 footprint = parent.getFootprint()
623 parentFluxArrayNoNoise = np.zeros(footprint.getArea(), dtype=np.float32)
624 footprint.spans.flatten(parentFluxArrayNoNoise, self.
exposure.image.array, self.
exposure.getXY0())
625 parentFluxArrayNoisy = np.zeros(footprint.getArea(), dtype=np.float32)
626 footprint.spans.flatten(parentFluxArrayNoisy, exposure.image.array, exposure.getXY0())
627 oldHeavy = record.getFootprint()
628 fraction = (oldHeavy.getImageArray() / parentFluxArrayNoNoise)
632 newHeavy = lsst.afw.detection.HeavyFootprintF(oldHeavy)
633 newHeavy.getImageArray()[:] = parentFluxArrayNoisy*fraction
634 newHeavy.getMaskArray()[:] = oldHeavy.getMaskArray()
635 newHeavy.getVarianceArray()[:] = oldHeavy.getVarianceArray()
636 record.setFootprint(newHeavy)
638 return exposure, catalog
642 """Base class for tests of measurement tasks.
645 """Create an instance of `SingleFrameMeasurementTask.ConfigClass`.
647 Only the specified plugin and its dependencies will be run; the
648 Centroid, Shape, and ModelFlux slots will be set to the truth fields
649 generated by the `TestDataset` class.
654 Name of measurement plugin to enable.
655 dependencies : iterable of `str`, optional
656 Names of dependencies of the measurement plugin.
660 config : `SingleFrameMeasurementTask.ConfigClass`
661 The resulting task configuration.
663 config = SingleFrameMeasurementTask.ConfigClass()
664 with warnings.catch_warnings():
665 warnings.filterwarnings(
"ignore", message=
"ignoreSlotPluginChecks", category=FutureWarning)
666 config = SingleFrameMeasurementTask.ConfigClass(ignoreSlotPluginChecks=
True)
667 config.slots.centroid =
"truth"
668 config.slots.shape =
"truth"
669 config.slots.modelFlux =
None
670 config.slots.apFlux =
None
671 config.slots.psfFlux =
None
672 config.slots.gaussianFlux =
None
673 config.slots.calibFlux =
None
674 config.plugins.names = (plugin,) + tuple(dependencies)
679 """Create a configured instance of `SingleFrameMeasurementTask`.
683 plugin : `str`, optional
684 Name of measurement plugin to enable. If `None`, a configuration
685 must be supplied as the ``config`` parameter. If both are
686 specified, ``config`` takes precedence.
687 dependencies : iterable of `str`, optional
688 Names of dependencies of the specified measurement plugin.
689 config : `SingleFrameMeasurementTask.ConfigClass`, optional
690 Configuration for the task. If `None`, a measurement plugin must
691 be supplied as the ``plugin`` paramter. If both are specified,
692 ``config`` takes precedence.
693 schema : `lsst.afw.table.Schema`, optional
694 Measurement table schema. If `None`, a default schema is
696 algMetadata : `lsst.daf.base.PropertyList`, optional
697 Measurement algorithm metadata. If `None`, a default container
702 task : `SingleFrameMeasurementTask`
703 A configured instance of the measurement task.
707 raise ValueError(
"Either plugin or config argument must not be None")
710 schema = TestDataset.makeMinimalSchema()
712 schema.setAliasMap(
None)
713 if algMetadata
is None:
718 """Create an instance of `ForcedMeasurementTask.ConfigClass`.
720 In addition to the plugins specified in the plugin and dependencies
721 arguments, the `TransformedCentroid` and `TransformedShape` plugins
722 will be run and used as the centroid and shape slots; these simply
723 transform the reference catalog centroid and shape to the measurement
729 Name of measurement plugin to enable.
730 dependencies : iterable of `str`, optional
731 Names of dependencies of the measurement plugin.
735 config : `ForcedMeasurementTask.ConfigClass`
736 The resulting task configuration.
739 config = ForcedMeasurementTask.ConfigClass()
740 config.slots.centroid =
"base_TransformedCentroid"
741 config.slots.shape =
"base_TransformedShape"
742 config.slots.modelFlux =
None
743 config.slots.apFlux =
None
744 config.slots.psfFlux =
None
745 config.slots.gaussianFlux =
None
746 config.plugins.names = (plugin,) + tuple(dependencies) + (
"base_TransformedCentroid",
747 "base_TransformedShape")
752 """Create a configured instance of `ForcedMeasurementTask`.
756 plugin : `str`, optional
757 Name of measurement plugin to enable. If `None`, a configuration
758 must be supplied as the ``config`` parameter. If both are
759 specified, ``config`` takes precedence.
760 dependencies : iterable of `str`, optional
761 Names of dependencies of the specified measurement plugin.
762 config : `SingleFrameMeasurementTask.ConfigClass`, optional
763 Configuration for the task. If `None`, a measurement plugin must
764 be supplied as the ``plugin`` paramter. If both are specified,
765 ``config`` takes precedence.
766 refSchema : `lsst.afw.table.Schema`, optional
767 Reference table schema. If `None`, a default schema is
769 algMetadata : `lsst.daf.base.PropertyList`, optional
770 Measurement algorithm metadata. If `None`, a default container
775 task : `ForcedMeasurementTask`
776 A configured instance of the measurement task.
780 raise ValueError(
"Either plugin or config argument must not be None")
782 if refSchema
is None:
783 refSchema = TestDataset.makeMinimalSchema()
784 if algMetadata
is None:
790 """Base class for testing measurement transformations.
794 We test both that the transform itself operates successfully (fluxes are
795 converted to magnitudes, flags are propagated properly) and that the
796 transform is registered as the default for the appropriate measurement
799 In the simple case of one-measurement-per-transformation, the developer
800 need not directly write any tests themselves: simply customizing the class
801 variables is all that is required. More complex measurements (e.g.
802 multiple aperture fluxes) require extra effort.
804 name =
"MeasurementTransformTest"
805 """The name used for the measurement algorithm (str).
809 This determines the names of the fields in the resulting catalog. This
810 default should generally be fine, but subclasses can override if
816 algorithmClass =
None
817 transformClass =
None
819 flagNames = (
"flag",)
820 """Flags which may be set by the algorithm being tested (iterable of `str`).
826 singleFramePlugins = ()
831 self.
calexp = TestDataset.makeEmptyExposure(bbox)
832 self._setupTransform()
843 for flagValue
in (
True,
False):
844 records.append(self.
inputCat.addNew())
845 for baseName
in baseNames:
847 if records[-1].schema.join(baseName, flagName)
in records[-1].schema:
848 records[-1].set(records[-1].schema.join(baseName, flagName), flagValue)
849 self._setFieldsInRecords(records, baseName)
853 for baseName
in baseNames:
854 self._compareFieldsInRecords(inSrc, outSrc, baseName)
856 keyName = outSrc.schema.join(baseName, flagName)
857 if keyName
in inSrc.schema:
858 self.assertEqual(outSrc.get(keyName), inSrc.get(keyName))
860 self.assertFalse(keyName
in outSrc.schema)
868 """Test the transformation on a catalog containing random data.
872 baseNames : iterable of `str`
873 Iterable of the initial parts of measurement field names.
879 - An appropriate exception is raised on an attempt to transform
880 between catalogs with different numbers of rows;
881 - Otherwise, all appropriate conversions are properly appled and that
882 flags have been propagated.
884 The ``baseNames`` argument requires some explanation. This should be
885 an iterable of the leading parts of the field names for each
886 measurement; that is, everything that appears before ``_instFlux``,
887 ``_flag``, etc. In the simple case of a single measurement per plugin,
888 this is simply equal to ``self.name`` (thus measurements are stored as
889 ``self.name + "_instFlux"``, etc). More generally, the developer may
890 specify whatever iterable they require. For example, to handle
891 multiple apertures, we could have ``(self.name + "_0", self.name +
894 baseNames = baseNames
or [self.
name]
903 self.assertEqual(registry[name].PluginClass.getTransformClass(), self.
transformClass)
906 """Test that the transformation is appropriately registered.
922 inputSchema.getAliasMap().set(
"slot_Centroid",
"dummy")
923 inputSchema.getAliasMap().set(
"slot_Shape",
"dummy")
925 inputSchema.getAliasMap().erase(
"slot_Centroid")
926 inputSchema.getAliasMap().erase(
"slot_Shape")
942 inputMapper.editOutputSchema().getAliasMap().set(
"slot_Centroid",
"dummy")
943 inputMapper.editOutputSchema().getAliasMap().set(
"slot_Shape",
"dummy")
945 inputMapper.editOutputSchema().getAliasMap().erase(
"slot_Centroid")
946 inputMapper.editOutputSchema().getAliasMap().erase(
"slot_Shape")
956 for record
in records:
957 record[record.schema.join(name,
'instFlux')] = np.random.random()
958 record[record.schema.join(name,
'instFluxErr')] = np.random.random()
961 assert len(records) > 1
962 records[0][record.schema.join(name,
'instFlux')] = -1
965 instFluxName = inSrc.schema.join(name,
'instFlux')
966 instFluxErrName = inSrc.schema.join(name,
'instFluxErr')
967 if inSrc[instFluxName] > 0:
968 mag = self.
calexp.getPhotoCalib().instFluxToMagnitude(inSrc[instFluxName],
969 inSrc[instFluxErrName])
970 self.assertEqual(outSrc[outSrc.schema.join(name,
'mag')], mag.value)
971 self.assertEqual(outSrc[outSrc.schema.join(name,
'magErr')], mag.error)
974 self.assertTrue(np.isnan(outSrc[outSrc.schema.join(name,
'mag')]))
975 if np.isnan(inSrc[instFluxErrName]):
976 self.assertTrue(np.isnan(outSrc[outSrc.schema.join(name,
'magErr')]))
978 mag = self.
calexp.getPhotoCalib().instFluxToMagnitude(inSrc[instFluxName],
979 inSrc[instFluxErrName])
980 self.assertEqual(outSrc[outSrc.schema.join(name,
'magErr')], mag.error)
986 for record
in records:
987 record[record.schema.join(name,
'x')] = np.random.random()
988 record[record.schema.join(name,
'y')] = np.random.random()
991 for fieldSuffix
in (
'xErr',
'yErr',
'x_y_Cov'):
992 fieldName = record.schema.join(name, fieldSuffix)
993 if fieldName
in record.schema:
994 record[fieldName] = np.random.random()
998 centroidResult = centroidResultKey.get(inSrc)
1001 coordTruth = self.
calexp.getWcs().pixelToSky(centroidResult.getCentroid())
1002 self.assertEqual(coordTruth, coord)
1007 coordErr = lsst.afw.table.CovarianceMatrix2fKey(outSrc.schema[self.
name],
1008 [
"ra",
"dec"]).get(outSrc)
1010 self.assertFalse(centroidResultKey.getCentroidErr().isValid())
1012 transform = self.
calexp.getWcs().linearizePixelToSky(coordTruth, lsst.geom.radians)
1013 coordErrTruth = np.dot(np.dot(transform.getLinear().getMatrix(),
1014 centroidResult.getCentroidErr()),
1015 transform.getLinear().getMatrix().transpose())
1016 np.testing.assert_array_almost_equal(np.array(coordErrTruth), coordErr)
static afw::table::Schema makeMinimalSchema()
static ErrorKey addErrorFields(Schema &schema)
static QuadrupoleKey addFields(Schema &schema, std::string const &name, std::string const &doc, CoordinateType coordType=CoordinateType::PIXEL)
static Schema makeMinimalSchema()
A FunctorKey for CentroidResult.
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)
drawGaussian(bbox, instFlux, ellipse)
makeEmptyExposure(bbox, wcs=None, crval=None, cdelt=None, psfSigma=2.0, psfDim=17, calibration=4, visitId=1234, mjd=60000.0, detector=1)
_installFootprint(self, record, image, setPeakSignificance=True, negative=False)
addSource(self, instFlux, centroid, shape=None, setPeakSignificance=True, negative=False)
transform(self, wcs, **kwds)
__init__(self, bbox, threshold=10.0, exposure=None, **kwds)
std::shared_ptr< SkyWcs > makeSkyWcs(daf::base::PropertySet &metadata, bool strip=false)
Eigen::Matrix2d makeCdMatrix(lsst::geom::Angle const &scale, lsst::geom::Angle const &orientation=0 *lsst::geom::degrees, bool flipX=false)
std::shared_ptr< TransformPoint2ToPoint2 > makeWcsPairTransform(SkyWcs const &src, SkyWcs const &dst)
lsst::geom::AffineTransform linearizeTransform(TransformPoint2ToPoint2 const &original, lsst::geom::Point2D const &inPoint)
void updateSourceCoords(geom::SkyWcs const &wcs, SourceCollection &sourceList, bool include_covariance=true)