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
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 schema.getAliasMap().set(
"slot_Shape",
"truth")
216 schema.getAliasMap().set(
"slot_Centroid",
"truth")
217 schema.getAliasMap().set(
"slot_ModelFlux",
"truth")
220 schema.disconnectAliases()
225 minRotation=None, maxRotation=None,
226 minRefShift=None, maxRefShift=None,
227 minPixShift=2.0, maxPixShift=4.0, randomSeed=1):
228 """Return a perturbed version of the input WCS.
230 Create a new undistorted TAN WCS that is similar but not identical to
231 another, with random scaling, rotation, and offset (in both pixel
232 position and reference position).
236 oldWcs : `lsst.afw.geom.SkyWcs`
238 minScaleFactor : `float`
239 Minimum scale factor to apply to the input WCS.
240 maxScaleFactor : `float`
241 Maximum scale factor to apply to the input WCS.
242 minRotation : `lsst.geom.Angle` or `None`
243 Minimum rotation to apply to the input WCS. If `None`, defaults to
245 maxRotation : `lsst.geom.Angle` or `None`
246 Minimum rotation to apply to the input WCS. If `None`, defaults to
248 minRefShift : `lsst.geom.Angle` or `None`
249 Miniumum shift to apply to the input WCS reference value. If
250 `None`, defaults to 0.5 arcsec.
251 maxRefShift : `lsst.geom.Angle` or `None`
252 Miniumum shift to apply to the input WCS reference value. If
253 `None`, defaults to 1.0 arcsec.
254 minPixShift : `float`
255 Minimum shift to apply to the input WCS reference pixel.
256 maxPixShift : `float`
257 Maximum shift to apply to the input WCS reference pixel.
263 newWcs : `lsst.afw.geom.SkyWcs`
264 A perturbed version of the input WCS.
268 The maximum and minimum arguments are interpreted as absolute values
269 for a split range that covers both positive and negative values (as
270 this method is used in testing, it is typically most important to
271 avoid perturbations near zero). Scale factors are treated somewhat
272 differently: the actual scale factor is chosen between
273 ``minScaleFactor`` and ``maxScaleFactor`` OR (``1/maxScaleFactor``)
274 and (``1/minScaleFactor``).
276 The default range for rotation is 30-60 degrees, and the default range
277 for reference shift is 0.5-1.0 arcseconds (these cannot be safely
278 included directly as default values because Angle objects are
281 The random number generator is primed with the seed given. If
282 `None`, a seed is automatically chosen.
284 random_state = np.random.RandomState(randomSeed)
285 if minRotation
is None:
286 minRotation = 30.0*lsst.geom.degrees
287 if maxRotation
is None:
288 maxRotation = 60.0*lsst.geom.degrees
289 if minRefShift
is None:
290 minRefShift = 0.5*lsst.geom.arcseconds
291 if maxRefShift
is None:
292 maxRefShift = 1.0*lsst.geom.arcseconds
294 def splitRandom(min1, max1, min2=None, max2=None):
299 if random_state.uniform() > 0.5:
300 return float(random_state.uniform(min1, max1))
302 return float(random_state.uniform(min2, max2))
304 scaleFactor = splitRandom(minScaleFactor, maxScaleFactor, 1.0/maxScaleFactor, 1.0/minScaleFactor)
305 rotation = splitRandom(minRotation.asRadians(), maxRotation.asRadians())*lsst.geom.radians
306 refShiftRa = splitRandom(minRefShift.asRadians(), maxRefShift.asRadians())*lsst.geom.radians
307 refShiftDec = splitRandom(minRefShift.asRadians(), maxRefShift.asRadians())*lsst.geom.radians
308 pixShiftX = splitRandom(minPixShift, maxPixShift)
309 pixShiftY = splitRandom(minPixShift, maxPixShift)
314 newTransform = oldTransform*rTransform*sTransform
315 matrix = newTransform.getMatrix()
317 oldSkyOrigin = oldWcs.getSkyOrigin()
319 oldSkyOrigin.getDec() + refShiftDec)
321 oldPixOrigin = oldWcs.getPixelOrigin()
323 oldPixOrigin.getY() + pixShiftY)
327 def makeEmptyExposure(bbox, wcs=None, crval=None, cdelt=None, psfSigma=2.0, psfDim=17, calibration=4,
328 visitId=1234, mjd=60000.0, detector=1):
329 """Create an Exposure, with a PhotoCalib, Wcs, and Psf, but no pixel values.
333 bbox : `lsst.geom.Box2I` or `lsst.geom.Box2D`
334 Bounding box of the image in image coordinates.
335 wcs : `lsst.afw.geom.SkyWcs`, optional
336 New WCS for the exposure (created from CRVAL and CDELT if `None`).
337 crval : `lsst.afw.geom.SpherePoint`, optional
338 ICRS center of the TAN WCS attached to the image. If `None`, (45
339 degrees, 45 degrees) is assumed.
340 cdelt : `lsst.geom.Angle`, optional
341 Pixel scale of the image. If `None`, 0.2 arcsec is assumed.
342 psfSigma : `float`, optional
343 Radius (sigma) of the Gaussian PSF attached to the image
344 psfDim : `int`, optional
345 Width and height of the image's Gaussian PSF attached to the image
346 calibration : `float`, optional
347 The spatially-constant calibration (in nJy/count) to set the
348 PhotoCalib of the exposure.
349 visitId : `int`, optional
350 Visit id to store in VisitInfo.
351 mjd : `float`, optional
352 Modified Julian Date of this exposure to store in VisitInfo.
353 detector: `int`, optional
354 Detector id to assign to the attached Detector object.
358 exposure : `lsst.age.image.ExposureF`
365 cdelt = 0.2*lsst.geom.arcseconds
369 exposure = lsst.afw.image.ExposureF(bbox)
376 22.2*lsst.geom.degrees,
378 hasSimulatedContent=
True)
381 exposure.setPhotoCalib(photoCalib)
382 exposure.info.setVisitInfo(visitInfo)
388 """Create an image of an elliptical Gaussian.
392 bbox : `lsst.geom.Box2I` or `lsst.geom.Box2D`
393 Bounding box of image to create.
395 Total instrumental flux of the Gaussian (normalized analytically,
396 not using pixel values).
397 ellipse : `lsst.afw.geom.Ellipse`
398 Defines the centroid and shape.
402 image : `lsst.afw.image.ImageF`
403 An image of the Gaussian.
405 x, y = np.meshgrid(np.arange(bbox.getBeginX(), bbox.getEndX()),
406 np.arange(bbox.getBeginY(), bbox.getEndY()))
407 t = ellipse.getGridTransform()
408 xt = t[t.XX] * x + t[t.XY] * y + t[t.X]
409 yt = t[t.YX] * x + t[t.YY] * y + t[t.Y]
410 image = lsst.afw.image.ImageF(bbox)
411 image.array[:, :] = np.exp(-0.5*(xt**2 + yt**2))*instFlux/(2.0*ellipse.getCore().getArea())
415 """Create simulated Footprint and add it to a truth catalog record.
418 if setPeakSignificance:
419 schema.addField(
"significance", type=float,
420 doc=
"Ratio of peak value to configured standard deviation.")
425 if setPeakSignificance:
428 for footprint
in fpSet.getFootprints():
429 footprint.updatePeakSignificance(self.
threshold.getValue())
431 fpSet.setMask(self.
exposure.mask,
"DETECTED")
433 if len(fpSet.getFootprints()) > 1:
434 raise RuntimeError(
"Threshold value results in multiple Footprints for a single object")
435 if len(fpSet.getFootprints()) == 0:
436 raise RuntimeError(
"Threshold value results in zero Footprints for object")
437 record.setFootprint(fpSet.getFootprints()[0])
439 def addSource(self, instFlux, centroid, shape=None, setPeakSignificance=True):
440 """Add a source to the simulation.
442 To insert a point source with a given signal-to-noise (sn), the total
443 ``instFlux`` should be: ``sn*noise*psf_scale``, where ``noise`` is the
444 noise you will pass to ``realize()``, and
445 ``psf_scale=sqrt(4*pi*r^2)``, where ``r`` is the width of the PSF.
450 Total instFlux of the source to be added.
451 centroid : `lsst.geom.Point2D`
452 Position of the source to be added.
453 shape : `lsst.afw.geom.Quadrupole`
454 Second moments of the source before PSF convolution. Note that the
455 truth catalog records post-convolution moments. If `None`, a point
456 source will be added.
457 setPeakSignificance : `bool`
458 Set the ``significance`` field for peaks in the footprints?
459 See ``lsst.meas.algorithms.SourceDetectionTask.setPeakSignificance``
460 for how this field is computed for real datasets.
464 record : `lsst.afw.table.SourceRecord`
465 A truth catalog record.
466 image : `lsst.afw.image.ImageF`
467 Single-source image corresponding to the new source.
471 record.set(self.
keys[
"instFlux"], instFlux)
472 record.set(self.
keys[
"instFluxErr"], 0)
473 record.set(self.
keys[
"centroid"], centroid)
474 covariance = np.random.normal(0, 0.1, 4).reshape(2, 2)
475 covariance[0, 1] = covariance[1, 0]
476 record.set(self.
keys[
"centroid_sigma"], covariance.astype(np.float32))
478 record.set(self.
keys[
"isStar"],
True)
481 record.set(self.
keys[
"isStar"],
False)
482 fullShape = shape.convolve(self.
psfShape)
483 record.set(self.
keys[
"shape"], fullShape)
490 self.
exposure.image.array[:, :] += image.array
494 """Return a context manager which can add a blend of multiple sources.
498 Note that nothing stops you from creating overlapping sources just using the addSource() method,
499 but addBlend() is necesssary to create a parent object and deblended HeavyFootprints of the type
500 produced by the detection and deblending pipelines.
506 with d.addBlend() as b:
507 b.addChild(flux1, centroid1)
508 b.addChild(flux2, centroid2, shape2)
513 """Copy this dataset transformed to a new WCS, with new Psf and PhotoCalib.
517 wcs : `lsst.afw.geom.SkyWcs`
518 WCS for the new dataset.
520 Additional keyword arguments passed on to
521 `TestDataset.makeEmptyExposure`. If not specified, these revert
522 to the defaults for `~TestDataset.makeEmptyExposure`, not the
523 values in the current dataset.
527 newDataset : `TestDataset`
528 Transformed copy of this dataset.
536 oldPhotoCalib = self.
exposure.getPhotoCalib()
537 newPhotoCalib = result.exposure.getPhotoCalib()
538 oldPsfShape = self.
exposure.getPsf().computeShape(bboxD.getCenter())
540 if record.get(self.
keys[
"nChild"]):
541 raise NotImplementedError(
"Transforming blended sources in TestDatasets is not supported")
542 magnitude = oldPhotoCalib.instFluxToMagnitude(record.get(self.
keys[
"instFlux"]))
543 newFlux = newPhotoCalib.magnitudeToInstFlux(magnitude)
544 oldCentroid = record.get(self.
keys[
"centroid"])
545 newCentroid = xyt.applyForward(oldCentroid)
546 if record.get(self.
keys[
"isStar"]):
547 newDeconvolvedShape =
None
550 oldFullShape = record.get(self.
keys[
"shape"])
552 oldFullShape.getIxx() - oldPsfShape.getIxx(),
553 oldFullShape.getIyy() - oldPsfShape.getIyy(),
554 oldFullShape.getIxy() - oldPsfShape.getIxy(),
557 newDeconvolvedShape = oldDeconvolvedShape.transform(affine.getLinear())
558 result.addSource(newFlux, newCentroid, newDeconvolvedShape)
561 def realize(self, noise, schema, randomSeed=1):
562 r"""Simulate an exposure and detection catalog for this dataset.
564 The simulation includes noise, and the detection catalog includes
565 `~lsst.afw.detection.heavyFootprint.HeavyFootprint`\ s.
570 Standard deviation of noise to be added to the exposure. The
571 noise will be Gaussian and constant, appropriate for the
573 schema : `lsst.afw.table.Schema`
574 Schema of the new catalog to be created. Must start with
575 ``self.schema`` (i.e. ``schema.contains(self.schema)`` must be
576 `True`), but typically contains fields for already-configured
577 measurement algorithms as well.
578 randomSeed : `int`, optional
579 Seed for the random number generator.
580 If `None`, a seed is chosen automatically.
584 `exposure` : `lsst.afw.image.ExposureF`
586 `catalog` : `lsst.afw.table.SourceCatalog`
587 Simulated detection catalog.
589 random_state = np.random.RandomState(randomSeed)
590 assert schema.contains(self.
schema)
592 mapper.addMinimalSchema(self.
schema,
True)
594 exposure.variance.array[:, :] = noise**2
595 exposure.image.array[:, :] += random_state.randn(exposure.height, exposure.width)*noise
597 catalog.extend(self.
catalog, mapper=mapper)
600 for record
in catalog:
603 if record.getParent() == 0:
607 parent = catalog.find(record.getParent())
608 footprint = parent.getFootprint()
609 parentFluxArrayNoNoise = np.zeros(footprint.getArea(), dtype=np.float32)
610 footprint.spans.flatten(parentFluxArrayNoNoise, self.
exposure.image.array, self.
exposure.getXY0())
611 parentFluxArrayNoisy = np.zeros(footprint.getArea(), dtype=np.float32)
612 footprint.spans.flatten(parentFluxArrayNoisy, exposure.image.array, exposure.getXY0())
613 oldHeavy = record.getFootprint()
614 fraction = (oldHeavy.getImageArray() / parentFluxArrayNoNoise)
618 newHeavy = lsst.afw.detection.HeavyFootprintF(oldHeavy)
619 newHeavy.getImageArray()[:] = parentFluxArrayNoisy*fraction
620 newHeavy.getMaskArray()[:] = oldHeavy.getMaskArray()
621 newHeavy.getVarianceArray()[:] = oldHeavy.getVarianceArray()
622 record.setFootprint(newHeavy)
624 return exposure, catalog
628 """Base class for tests of measurement tasks.
631 """Create an instance of `SingleFrameMeasurementTask.ConfigClass`.
633 Only the specified plugin and its dependencies will be run; the
634 Centroid, Shape, and ModelFlux slots will be set to the truth fields
635 generated by the `TestDataset` class.
640 Name of measurement plugin to enable.
641 dependencies : iterable of `str`, optional
642 Names of dependencies of the measurement plugin.
646 config : `SingleFrameMeasurementTask.ConfigClass`
647 The resulting task configuration.
649 config = SingleFrameMeasurementTask.ConfigClass()
650 with warnings.catch_warnings():
651 warnings.filterwarnings(
"ignore", message=
"ignoreSlotPluginChecks", category=FutureWarning)
652 config = SingleFrameMeasurementTask.ConfigClass(ignoreSlotPluginChecks=
True)
653 config.slots.centroid =
"truth"
654 config.slots.shape =
"truth"
655 config.slots.modelFlux =
None
656 config.slots.apFlux =
None
657 config.slots.psfFlux =
None
658 config.slots.gaussianFlux =
None
659 config.slots.calibFlux =
None
660 config.plugins.names = (plugin,) + tuple(dependencies)
665 """Create a configured instance of `SingleFrameMeasurementTask`.
669 plugin : `str`, optional
670 Name of measurement plugin to enable. If `None`, a configuration
671 must be supplied as the ``config`` parameter. If both are
672 specified, ``config`` takes precedence.
673 dependencies : iterable of `str`, optional
674 Names of dependencies of the specified measurement plugin.
675 config : `SingleFrameMeasurementTask.ConfigClass`, optional
676 Configuration for the task. If `None`, a measurement plugin must
677 be supplied as the ``plugin`` paramter. If both are specified,
678 ``config`` takes precedence.
679 schema : `lsst.afw.table.Schema`, optional
680 Measurement table schema. If `None`, a default schema is
682 algMetadata : `lsst.daf.base.PropertyList`, optional
683 Measurement algorithm metadata. If `None`, a default container
688 task : `SingleFrameMeasurementTask`
689 A configured instance of the measurement task.
693 raise ValueError(
"Either plugin or config argument must not be None")
696 schema = TestDataset.makeMinimalSchema()
698 schema.setAliasMap(
None)
699 if algMetadata
is None:
704 """Create an instance of `ForcedMeasurementTask.ConfigClass`.
706 In addition to the plugins specified in the plugin and dependencies
707 arguments, the `TransformedCentroid` and `TransformedShape` plugins
708 will be run and used as the centroid and shape slots; these simply
709 transform the reference catalog centroid and shape to the measurement
715 Name of measurement plugin to enable.
716 dependencies : iterable of `str`, optional
717 Names of dependencies of the measurement plugin.
721 config : `ForcedMeasurementTask.ConfigClass`
722 The resulting task configuration.
725 config = ForcedMeasurementTask.ConfigClass()
726 config.slots.centroid =
"base_TransformedCentroid"
727 config.slots.shape =
"base_TransformedShape"
728 config.slots.modelFlux =
None
729 config.slots.apFlux =
None
730 config.slots.psfFlux =
None
731 config.slots.gaussianFlux =
None
732 config.plugins.names = (plugin,) + tuple(dependencies) + (
"base_TransformedCentroid",
733 "base_TransformedShape")
738 """Create a configured instance of `ForcedMeasurementTask`.
742 plugin : `str`, optional
743 Name of measurement plugin to enable. If `None`, a configuration
744 must be supplied as the ``config`` parameter. If both are
745 specified, ``config`` takes precedence.
746 dependencies : iterable of `str`, optional
747 Names of dependencies of the specified measurement plugin.
748 config : `SingleFrameMeasurementTask.ConfigClass`, optional
749 Configuration for the task. If `None`, a measurement plugin must
750 be supplied as the ``plugin`` paramter. If both are specified,
751 ``config`` takes precedence.
752 refSchema : `lsst.afw.table.Schema`, optional
753 Reference table schema. If `None`, a default schema is
755 algMetadata : `lsst.daf.base.PropertyList`, optional
756 Measurement algorithm metadata. If `None`, a default container
761 task : `ForcedMeasurementTask`
762 A configured instance of the measurement task.
766 raise ValueError(
"Either plugin or config argument must not be None")
768 if refSchema
is None:
769 refSchema = TestDataset.makeMinimalSchema()
770 if algMetadata
is None:
776 """Base class for testing measurement transformations.
780 We test both that the transform itself operates successfully (fluxes are
781 converted to magnitudes, flags are propagated properly) and that the
782 transform is registered as the default for the appropriate measurement
785 In the simple case of one-measurement-per-transformation, the developer
786 need not directly write any tests themselves: simply customizing the class
787 variables is all that is required. More complex measurements (e.g.
788 multiple aperture fluxes) require extra effort.
790 name =
"MeasurementTransformTest"
791 """The name used for the measurement algorithm (str).
795 This determines the names of the fields in the resulting catalog. This
796 default should generally be fine, but subclasses can override if
802 algorithmClass =
None
803 transformClass =
None
805 flagNames = (
"flag",)
806 """Flags which may be set by the algorithm being tested (iterable of `str`).
812 singleFramePlugins = ()
817 self.
calexp = TestDataset.makeEmptyExposure(bbox)
818 self._setupTransform()
829 for flagValue
in (
True,
False):
830 records.append(self.
inputCat.addNew())
831 for baseName
in baseNames:
833 if records[-1].schema.join(baseName, flagName)
in records[-1].schema:
834 records[-1].set(records[-1].schema.join(baseName, flagName), flagValue)
835 self._setFieldsInRecords(records, baseName)
839 for baseName
in baseNames:
840 self._compareFieldsInRecords(inSrc, outSrc, baseName)
842 keyName = outSrc.schema.join(baseName, flagName)
843 if keyName
in inSrc.schema:
844 self.assertEqual(outSrc.get(keyName), inSrc.get(keyName))
846 self.assertFalse(keyName
in outSrc.schema)
854 """Test the transformation on a catalog containing random data.
858 baseNames : iterable of `str`
859 Iterable of the initial parts of measurement field names.
865 - An appropriate exception is raised on an attempt to transform
866 between catalogs with different numbers of rows;
867 - Otherwise, all appropriate conversions are properly appled and that
868 flags have been propagated.
870 The ``baseNames`` argument requires some explanation. This should be
871 an iterable of the leading parts of the field names for each
872 measurement; that is, everything that appears before ``_instFlux``,
873 ``_flag``, etc. In the simple case of a single measurement per plugin,
874 this is simply equal to ``self.name`` (thus measurements are stored as
875 ``self.name + "_instFlux"``, etc). More generally, the developer may
876 specify whatever iterable they require. For example, to handle
877 multiple apertures, we could have ``(self.name + "_0", self.name +
880 baseNames = baseNames
or [self.
name]
889 self.assertEqual(registry[name].PluginClass.getTransformClass(), self.
transformClass)
892 """Test that the transformation is appropriately registered.
908 inputSchema.getAliasMap().set(
"slot_Centroid",
"dummy")
909 inputSchema.getAliasMap().set(
"slot_Shape",
"dummy")
911 inputSchema.getAliasMap().erase(
"slot_Centroid")
912 inputSchema.getAliasMap().erase(
"slot_Shape")
928 inputMapper.editOutputSchema().getAliasMap().set(
"slot_Centroid",
"dummy")
929 inputMapper.editOutputSchema().getAliasMap().set(
"slot_Shape",
"dummy")
931 inputMapper.editOutputSchema().getAliasMap().erase(
"slot_Centroid")
932 inputMapper.editOutputSchema().getAliasMap().erase(
"slot_Shape")
942 for record
in records:
943 record[record.schema.join(name,
'instFlux')] = np.random.random()
944 record[record.schema.join(name,
'instFluxErr')] = np.random.random()
947 assert len(records) > 1
948 records[0][record.schema.join(name,
'instFlux')] = -1
951 instFluxName = inSrc.schema.join(name,
'instFlux')
952 instFluxErrName = inSrc.schema.join(name,
'instFluxErr')
953 if inSrc[instFluxName] > 0:
954 mag = self.
calexp.getPhotoCalib().instFluxToMagnitude(inSrc[instFluxName],
955 inSrc[instFluxErrName])
956 self.assertEqual(outSrc[outSrc.schema.join(name,
'mag')], mag.value)
957 self.assertEqual(outSrc[outSrc.schema.join(name,
'magErr')], mag.error)
960 self.assertTrue(np.isnan(outSrc[outSrc.schema.join(name,
'mag')]))
961 if np.isnan(inSrc[instFluxErrName]):
962 self.assertTrue(np.isnan(outSrc[outSrc.schema.join(name,
'magErr')]))
964 mag = self.
calexp.getPhotoCalib().instFluxToMagnitude(inSrc[instFluxName],
965 inSrc[instFluxErrName])
966 self.assertEqual(outSrc[outSrc.schema.join(name,
'magErr')], mag.error)
972 for record
in records:
973 record[record.schema.join(name,
'x')] = np.random.random()
974 record[record.schema.join(name,
'y')] = np.random.random()
977 for fieldSuffix
in (
'xErr',
'yErr',
'x_y_Cov'):
978 fieldName = record.schema.join(name, fieldSuffix)
979 if fieldName
in record.schema:
980 record[fieldName] = np.random.random()
983 centroidResultKey = CentroidResultKey(inSrc.schema[self.
name])
984 centroidResult = centroidResultKey.get(inSrc)
987 coordTruth = self.
calexp.getWcs().pixelToSky(centroidResult.getCentroid())
988 self.assertEqual(coordTruth, coord)
993 coordErr = lsst.afw.table.CovarianceMatrix2fKey(outSrc.schema[self.
name],
994 [
"ra",
"dec"]).get(outSrc)
996 self.assertFalse(centroidResultKey.getCentroidErr().
isValid())
998 transform = self.
calexp.getWcs().linearizePixelToSky(coordTruth, lsst.geom.radians)
999 coordErrTruth = np.dot(np.dot(transform.getLinear().getMatrix(),
1000 centroidResult.getCentroidErr()),
1001 transform.getLinear().getMatrix().transpose())
1002 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, detector=1)
__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.