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
114 self.
parentRecord.set(self.
owner.keys[
"shape"], lsst.afw.geom.Quadrupole(xx, yy, xy))
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
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
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 """Create an Exposure, with a PhotoCalib, Wcs, and Psf, but no pixel values.
331 bbox : `lsst.geom.Box2I` or `lsst.geom.Box2D`
332 Bounding box of the image in image coordinates.
333 wcs : `lsst.afw.geom.SkyWcs`, optional
334 New WCS for the exposure (created from CRVAL and CDELT if `None`).
335 crval : `lsst.afw.geom.SpherePoint`, optional
336 ICRS center of the TAN WCS attached to the image. If `None`, (45
337 degrees, 45 degrees) is assumed.
338 cdelt : `lsst.geom.Angle`, optional
339 Pixel scale of the image. If `None`, 0.2 arcsec is assumed.
340 psfSigma : `float`, optional
341 Radius (sigma) of the Gaussian PSF attached to the image
342 psfDim : `int`, optional
343 Width and height of the image's Gaussian PSF attached to the image
344 calibration : `float`, optional
345 The spatially-constant calibration (in nJy/count) to set the
346 PhotoCalib of the exposure.
350 exposure : `lsst.age.image.ExposureF`
357 cdelt = 0.2*lsst.geom.arcseconds
361 exposure = lsst.afw.image.ExposureF(bbox)
368 22.2*lsst.geom.degrees,
372 exposure.setPhotoCalib(photoCalib)
373 exposure.info.setVisitInfo(visitInfo)
378 """Create an image of an elliptical Gaussian.
382 bbox : `lsst.geom.Box2I` or `lsst.geom.Box2D`
383 Bounding box of image to create.
385 Total instrumental flux of the Gaussian (normalized analytically,
386 not using pixel values).
387 ellipse : `lsst.afw.geom.Ellipse`
388 Defines the centroid and shape.
392 image : `lsst.afw.image.ImageF`
393 An image of the Gaussian.
395 x, y = np.meshgrid(np.arange(bbox.getBeginX(), bbox.getEndX()),
396 np.arange(bbox.getBeginY(), bbox.getEndY()))
397 t = ellipse.getGridTransform()
398 xt = t[t.XX] * x + t[t.XY] * y + t[t.X]
399 yt = t[t.YX] * x + t[t.YY] * y + t[t.Y]
400 image = lsst.afw.image.ImageF(bbox)
401 image.array[:, :] = np.exp(-0.5*(xt**2 + yt**2))*instFlux/(2.0*ellipse.getCore().getArea())
405 """Create simulated Footprint and add it to a truth catalog record.
408 if setPeakSignificance:
409 schema.addField(
"significance", type=float,
410 doc=
"Ratio of peak value to configured standard deviation.")
415 if setPeakSignificance:
418 for footprint
in fpSet.getFootprints():
419 footprint.updatePeakSignificance(self.
threshold.getValue())
421 fpSet.setMask(self.
exposure.mask,
"DETECTED")
423 if len(fpSet.getFootprints()) > 1:
424 raise RuntimeError(
"Threshold value results in multiple Footprints for a single object")
425 if len(fpSet.getFootprints()) == 0:
426 raise RuntimeError(
"Threshold value results in zero Footprints for object")
427 record.setFootprint(fpSet.getFootprints()[0])
429 def addSource(self, instFlux, centroid, shape=None, setPeakSignificance=True):
430 """Add a source to the simulation.
432 To insert a point source with a given signal-to-noise (sn), the total
433 ``instFlux`` should be: ``sn*noise*psf_scale``, where ``noise`` is the
434 noise you will pass to ``realize()``, and
435 ``psf_scale=sqrt(4*pi*r^2)``, where ``r`` is the width of the PSF.
440 Total instFlux of the source to be added.
441 centroid : `lsst.geom.Point2D`
442 Position of the source to be added.
443 shape : `lsst.afw.geom.Quadrupole`
444 Second moments of the source before PSF convolution. Note that the
445 truth catalog records post-convolution moments. If `None`, a point
446 source will be added.
447 setPeakSignificance : `bool`
448 Set the ``significance`` field for peaks in the footprints?
449 See ``lsst.meas.algorithms.SourceDetectionTask.setPeakSignificance``
450 for how this field is computed for real datasets.
454 record : `lsst.afw.table.SourceRecord`
455 A truth catalog record.
456 image : `lsst.afw.image.ImageF`
457 Single-source image corresponding to the new source.
461 record.set(self.
keys[
"instFlux"], instFlux)
462 record.set(self.
keys[
"instFluxErr"], 0)
463 record.set(self.
keys[
"centroid"], centroid)
464 covariance = np.random.normal(0, 0.1, 4).
reshape(2, 2)
465 covariance[0, 1] = covariance[1, 0]
466 record.set(self.
keys[
"centroid_sigma"], covariance.astype(np.float32))
468 record.set(self.
keys[
"isStar"],
True)
471 record.set(self.
keys[
"isStar"],
False)
472 fullShape = shape.convolve(self.
psfShape)
473 record.set(self.
keys[
"shape"], fullShape)
476 lsst.afw.geom.Ellipse(fullShape, centroid))
480 self.
exposure.image.array[:, :] += image.array
484 """Return a context manager which can add a blend of multiple sources.
488 Note that nothing stops you from creating overlapping sources just using the addSource() method,
489 but addBlend() is necesssary to create a parent object and deblended HeavyFootprints of the type
490 produced by the detection and deblending pipelines.
496 with d.addBlend() as b:
497 b.addChild(flux1, centroid1)
498 b.addChild(flux2, centroid2, shape2)
502 def transform(self, wcs, **kwds):
503 """Copy this dataset transformed to a new WCS, with new Psf and PhotoCalib.
507 wcs : `lsst.afw.geom.SkyWcs`
508 WCS for the new dataset.
510 Additional keyword arguments passed on to
511 `TestDataset.makeEmptyExposure`. If not specified, these revert
512 to the defaults for `~TestDataset.makeEmptyExposure`, not the
513 values in the current dataset.
517 newDataset : `TestDataset`
518 Transformed copy of this dataset.
526 oldPhotoCalib = self.
exposure.getPhotoCalib()
527 newPhotoCalib = result.exposure.getPhotoCalib()
528 oldPsfShape = self.
exposure.getPsf().computeShape(bboxD.getCenter())
530 if record.get(self.
keys[
"nChild"]):
532 magnitude = oldPhotoCalib.instFluxToMagnitude(record.get(self.
keys[
"instFlux"]))
533 newFlux = newPhotoCalib.magnitudeToInstFlux(magnitude)
534 oldCentroid = record.get(self.
keys[
"centroid"])
535 newCentroid = xyt.applyForward(oldCentroid)
536 if record.get(self.
keys[
"isStar"]):
537 newDeconvolvedShape =
None
540 oldFullShape = record.get(self.
keys[
"shape"])
541 oldDeconvolvedShape = lsst.afw.geom.Quadrupole(
542 oldFullShape.getIxx() - oldPsfShape.getIxx(),
543 oldFullShape.getIyy() - oldPsfShape.getIyy(),
544 oldFullShape.getIxy() - oldPsfShape.getIxy(),
547 newDeconvolvedShape = oldDeconvolvedShape.transform(affine.getLinear())
548 result.addSource(newFlux, newCentroid, newDeconvolvedShape)
551 def realize(self, noise, schema, randomSeed=1):
552 r"""Simulate an exposure and detection catalog for this dataset.
554 The simulation includes noise, and the detection catalog includes
555 `~lsst.afw.detection.heavyFootprint.HeavyFootprint`\ s.
560 Standard deviation of noise to be added to the exposure. The
561 noise will be Gaussian and constant, appropriate for the
563 schema : `lsst.afw.table.Schema`
564 Schema of the new catalog to be created. Must start with
565 ``self.schema`` (i.e. ``schema.contains(self.schema)`` must be
566 `True`), but typically contains fields for already-configured
567 measurement algorithms as well.
568 randomSeed : `int`, optional
569 Seed for the random number generator.
570 If `None`, a seed is chosen automatically.
574 `exposure` : `lsst.afw.image.ExposureF`
576 `catalog` : `lsst.afw.table.SourceCatalog`
577 Simulated detection catalog.
579 random_state = np.random.RandomState(randomSeed)
580 assert schema.contains(self.
schema)
582 mapper.addMinimalSchema(self.
schema,
True)
584 exposure.variance.array[:, :] = noise**2
585 exposure.image.array[:, :] += random_state.randn(exposure.height, exposure.width)*noise
587 catalog.extend(self.
catalog, mapper=mapper)
590 for record
in catalog:
593 if record.getParent() == 0:
597 parent = catalog.find(record.getParent())
598 footprint = parent.getFootprint()
599 parentFluxArrayNoNoise = np.zeros(footprint.getArea(), dtype=np.float32)
600 footprint.spans.flatten(parentFluxArrayNoNoise, self.
exposure.image.array, self.
exposure.getXY0())
601 parentFluxArrayNoisy = np.zeros(footprint.getArea(), dtype=np.float32)
602 footprint.spans.flatten(parentFluxArrayNoisy, exposure.image.array, exposure.getXY0())
603 oldHeavy = record.getFootprint()
604 fraction = (oldHeavy.getImageArray() / parentFluxArrayNoNoise)
608 newHeavy = lsst.afw.detection.HeavyFootprintF(oldHeavy)
609 newHeavy.getImageArray()[:] = parentFluxArrayNoisy*fraction
610 newHeavy.getMaskArray()[:] = oldHeavy.getMaskArray()
611 newHeavy.getVarianceArray()[:] = oldHeavy.getVarianceArray()
612 record.setFootprint(newHeavy)
614 return exposure, catalog
618 """Base class for tests of measurement tasks.
621 """Create an instance of `SingleFrameMeasurementTask.ConfigClass`.
623 Only the specified plugin and its dependencies will be run; the
624 Centroid, Shape, and ModelFlux slots will be set to the truth fields
625 generated by the `TestDataset` class.
630 Name of measurement plugin to enable.
631 dependencies : iterable of `str`, optional
632 Names of dependencies of the measurement plugin.
636 config : `SingleFrameMeasurementTask.ConfigClass`
637 The resulting task configuration.
639 config = SingleFrameMeasurementTask.ConfigClass()
640 with warnings.catch_warnings():
641 warnings.filterwarnings(
"ignore", message=
"ignoreSlotPluginChecks", category=FutureWarning)
642 config = SingleFrameMeasurementTask.ConfigClass(ignoreSlotPluginChecks=
True)
643 config.slots.centroid =
"truth"
644 config.slots.shape =
"truth"
645 config.slots.modelFlux =
None
646 config.slots.apFlux =
None
647 config.slots.psfFlux =
None
648 config.slots.gaussianFlux =
None
649 config.slots.calibFlux =
None
650 config.plugins.names = (plugin,) + tuple(dependencies)
655 """Create a configured instance of `SingleFrameMeasurementTask`.
659 plugin : `str`, optional
660 Name of measurement plugin to enable. If `None`, a configuration
661 must be supplied as the ``config`` parameter. If both are
662 specified, ``config`` takes precedence.
663 dependencies : iterable of `str`, optional
664 Names of dependencies of the specified measurement plugin.
665 config : `SingleFrameMeasurementTask.ConfigClass`, optional
666 Configuration for the task. If `None`, a measurement plugin must
667 be supplied as the ``plugin`` paramter. If both are specified,
668 ``config`` takes precedence.
669 schema : `lsst.afw.table.Schema`, optional
670 Measurement table schema. If `None`, a default schema is
672 algMetadata : `lsst.daf.base.PropertyList`, optional
673 Measurement algorithm metadata. If `None`, a default container
678 task : `SingleFrameMeasurementTask`
679 A configured instance of the measurement task.
683 raise ValueError(
"Either plugin or config argument must not be None")
686 schema = TestDataset.makeMinimalSchema()
688 schema.setAliasMap(
None)
689 if algMetadata
is None:
694 """Create an instance of `ForcedMeasurementTask.ConfigClass`.
696 In addition to the plugins specified in the plugin and dependencies
697 arguments, the `TransformedCentroid` and `TransformedShape` plugins
698 will be run and used as the centroid and shape slots; these simply
699 transform the reference catalog centroid and shape to the measurement
705 Name of measurement plugin to enable.
706 dependencies : iterable of `str`, optional
707 Names of dependencies of the measurement plugin.
711 config : `ForcedMeasurementTask.ConfigClass`
712 The resulting task configuration.
715 config = ForcedMeasurementTask.ConfigClass()
716 config.slots.centroid =
"base_TransformedCentroid"
717 config.slots.shape =
"base_TransformedShape"
718 config.slots.modelFlux =
None
719 config.slots.apFlux =
None
720 config.slots.psfFlux =
None
721 config.slots.gaussianFlux =
None
722 config.plugins.names = (plugin,) + tuple(dependencies) + (
"base_TransformedCentroid",
723 "base_TransformedShape")
728 """Create a configured instance of `ForcedMeasurementTask`.
732 plugin : `str`, optional
733 Name of measurement plugin to enable. If `None`, a configuration
734 must be supplied as the ``config`` parameter. If both are
735 specified, ``config`` takes precedence.
736 dependencies : iterable of `str`, optional
737 Names of dependencies of the specified measurement plugin.
738 config : `SingleFrameMeasurementTask.ConfigClass`, optional
739 Configuration for the task. If `None`, a measurement plugin must
740 be supplied as the ``plugin`` paramter. If both are specified,
741 ``config`` takes precedence.
742 refSchema : `lsst.afw.table.Schema`, optional
743 Reference table schema. If `None`, a default schema is
745 algMetadata : `lsst.daf.base.PropertyList`, optional
746 Measurement algorithm metadata. If `None`, a default container
751 task : `ForcedMeasurementTask`
752 A configured instance of the measurement task.
756 raise ValueError(
"Either plugin or config argument must not be None")
758 if refSchema
is None:
759 refSchema = TestDataset.makeMinimalSchema()
760 if algMetadata
is None:
766 """Base class for testing measurement transformations.
770 We test both that the transform itself operates successfully (fluxes are
771 converted to magnitudes, flags are propagated properly) and that the
772 transform is registered as the default for the appropriate measurement
775 In the simple case of one-measurement-per-transformation, the developer
776 need not directly write any tests themselves: simply customizing the class
777 variables is all that is required. More complex measurements (e.g.
778 multiple aperture fluxes) require extra effort.
780 name =
"MeasurementTransformTest"
781 """The name used for the measurement algorithm (str).
785 This determines the names of the fields in the resulting catalog. This
786 default should generally be fine, but subclasses can override if
792 algorithmClass =
None
793 transformClass =
None
795 flagNames = (
"flag",)
796 """Flags which may be set by the algorithm being tested (iterable of `str`).
802 singleFramePlugins = ()
807 self.
calexp = TestDataset.makeEmptyExposure(bbox)
808 self._setupTransform()
819 for flagValue
in (
True,
False):
820 records.append(self.
inputCat.addNew())
821 for baseName
in baseNames:
823 if records[-1].schema.join(baseName, flagName)
in records[-1].schema:
824 records[-1].set(records[-1].schema.join(baseName, flagName), flagValue)
825 self._setFieldsInRecords(records, baseName)
829 for baseName
in baseNames:
830 self._compareFieldsInRecords(inSrc, outSrc, baseName)
832 keyName = outSrc.schema.join(baseName, flagName)
833 if keyName
in inSrc.schema:
834 self.assertEqual(outSrc.get(keyName), inSrc.get(keyName))
836 self.assertFalse(keyName
in outSrc.schema)
844 """Test the transformation on a catalog containing random data.
848 baseNames : iterable of `str`
849 Iterable of the initial parts of measurement field names.
855 - An appropriate exception is raised on an attempt to transform
856 between catalogs with different numbers of rows;
857 - Otherwise, all appropriate conversions are properly appled and that
858 flags have been propagated.
860 The ``baseNames`` argument requires some explanation. This should be
861 an iterable of the leading parts of the field names for each
862 measurement; that is, everything that appears before ``_instFlux``,
863 ``_flag``, etc. In the simple case of a single measurement per plugin,
864 this is simply equal to ``self.name`` (thus measurements are stored as
865 ``self.name + "_instFlux"``, etc). More generally, the developer may
866 specify whatever iterable they require. For example, to handle
867 multiple apertures, we could have ``(self.name + "_0", self.name +
870 baseNames = baseNames
or [self.
name]
879 self.assertEqual(registry[name].PluginClass.getTransformClass(), self.
transformClass)
882 """Test that the transformation is appropriately registered.
898 inputSchema.getAliasMap().set(
"slot_Centroid",
"dummy")
899 inputSchema.getAliasMap().set(
"slot_Shape",
"dummy")
901 inputSchema.getAliasMap().erase(
"slot_Centroid")
902 inputSchema.getAliasMap().erase(
"slot_Shape")
918 inputMapper.editOutputSchema().getAliasMap().set(
"slot_Centroid",
"dummy")
919 inputMapper.editOutputSchema().getAliasMap().set(
"slot_Shape",
"dummy")
921 inputMapper.editOutputSchema().getAliasMap().erase(
"slot_Centroid")
922 inputMapper.editOutputSchema().getAliasMap().erase(
"slot_Shape")
932 for record
in records:
933 record[record.schema.join(name,
'instFlux')] = np.random.random()
934 record[record.schema.join(name,
'instFluxErr')] = np.random.random()
937 assert len(records) > 1
938 records[0][record.schema.join(name,
'instFlux')] = -1
941 instFluxName = inSrc.schema.join(name,
'instFlux')
942 instFluxErrName = inSrc.schema.join(name,
'instFluxErr')
943 if inSrc[instFluxName] > 0:
944 mag = self.
calexp.getPhotoCalib().instFluxToMagnitude(inSrc[instFluxName],
945 inSrc[instFluxErrName])
946 self.assertEqual(outSrc[outSrc.schema.join(name,
'mag')], mag.value)
947 self.assertEqual(outSrc[outSrc.schema.join(name,
'magErr')], mag.error)
950 self.assertTrue(np.isnan(outSrc[outSrc.schema.join(name,
'mag')]))
951 if np.isnan(inSrc[instFluxErrName]):
952 self.assertTrue(np.isnan(outSrc[outSrc.schema.join(name,
'magErr')]))
954 mag = self.
calexp.getPhotoCalib().instFluxToMagnitude(inSrc[instFluxName],
955 inSrc[instFluxErrName])
956 self.assertEqual(outSrc[outSrc.schema.join(name,
'magErr')], mag.error)
962 for record
in records:
963 record[record.schema.join(name,
'x')] = np.random.random()
964 record[record.schema.join(name,
'y')] = np.random.random()
967 for fieldSuffix
in (
'xErr',
'yErr',
'x_y_Cov'):
968 fieldName = record.schema.join(name, fieldSuffix)
969 if fieldName
in record.schema:
970 record[fieldName] = np.random.random()
973 centroidResultKey = CentroidResultKey(inSrc.schema[self.
name])
974 centroidResult = centroidResultKey.get(inSrc)
977 coordTruth = self.
calexp.getWcs().pixelToSky(centroidResult.getCentroid())
978 self.assertEqual(coordTruth, coord)
983 coordErr = lsst.afw.table.CovarianceMatrix2fKey(outSrc.schema[self.
name],
984 [
"ra",
"dec"]).get(outSrc)
986 self.assertFalse(centroidResultKey.getCentroidErr().isValid())
988 transform = self.
calexp.getWcs().linearizePixelToSky(coordTruth, lsst.geom.radians)
989 coordErrTruth = np.dot(np.dot(transform.getLinear().getMatrix(),
990 centroidResult.getCentroidErr()),
991 transform.getLinear().getMatrix().transpose())
992 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()
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)
__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)