LSST Applications g1653933729+34a971ddd9,g1a997c3884+34a971ddd9,g2160c40384+da0d0eec6b,g28da252d5a+1236b942f7,g2bbee38e9b+e5a1bc5b38,g2bc492864f+e5a1bc5b38,g2ca4be77d2+192fe503f0,g2cdde0e794+704103fe75,g3156d2b45e+6e87dc994a,g347aa1857d+e5a1bc5b38,g35bb328faa+34a971ddd9,g3a166c0a6a+e5a1bc5b38,g3e281a1b8c+8ec26ec694,g4005a62e65+ba0306790b,g414038480c+9f5be647b3,g41af890bb2+c3a10c924f,g5065538af8+e7237db731,g5a0bb5165c+eae055db26,g717e5f8c0f+b65b5c3ae4,g80478fca09+4ce5a07937,g82479be7b0+08790af60f,g858d7b2824+b65b5c3ae4,g9125e01d80+34a971ddd9,ga5288a1d22+5df949a35e,gae0086650b+34a971ddd9,gb58c049af0+ace264a4f2,gbd397ab92a+2141afb137,gc28159a63d+e5a1bc5b38,gc805d3fbd4+b65b5c3ae4,gcf0d15dbbd+97632ccc20,gd6b7c0dfd1+de826e8718,gda6a2b7d83+97632ccc20,gdaeeff99f8+7774323b41,ge2409df99d+e6cadbf968,ge33fd446bb+b65b5c3ae4,ge79ae78c31+e5a1bc5b38,gf0baf85859+890af219f9,gf5289d68f6+a27069ed62,w.2024.37
LSST Data Management Base Package
Loading...
Searching...
No Matches
tests.py
Go to the documentation of this file.
1# This file is part of meas_base.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
21
22import warnings
23
24import numpy as np
25
26import lsst.geom
27import lsst.afw.table
28import lsst.afw.image
31import lsst.afw.geom
32import lsst.afw.coord
33import lsst.daf.base
35
36from .sfm import SingleFrameMeasurementTask
37from .forcedMeasurement import ForcedMeasurementTask
38from ._measBaseLib import CentroidResultKey
39
40__all__ = ("BlendContext", "TestDataset", "AlgorithmTestCase", "TransformTestCase",
41 "SingleFramePluginTransformSetupHelper", "ForcedPluginTransformSetupHelper",
42 "FluxTransformTestCase", "CentroidTransformTestCase")
43
44
46 """Context manager which adds multiple overlapping sources and a parent.
47
48 Notes
49 -----
50 This is used as the return value for `TestDataset.addBlend`, and this is
51 the only way it should be used.
52 """
53
54 def __init__(self, owner):
55 self.owner = owner
56 self.parentRecord = self.owner.catalog.addNew()
57 self.parentImage = lsst.afw.image.ImageF(self.owner.exposure.getBBox())
58 self.children = []
59
60 def __enter__(self):
61 # BlendContext is its own context manager, so we just return self.
62 return self
63
64 def addChild(self, instFlux, centroid, shape=None):
65 """Add a child to the blend; return corresponding truth catalog record.
66
67 instFlux : `float`
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)
74 """
75 record, image = self.owner.addSource(instFlux, centroid, shape)
76 record.set(self.owner.keys["parent"], self.parentRecord.getId())
77 self.parentImage += image
78 self.children.append((record, image))
79 return record
80
81 def __exit__(self, type_, value, tb):
82 # We're not using the context manager for any kind of exception safety
83 # or guarantees; we just want the nice "with" statement syntax.
84
85 if type_ is not None:
86 # exception was raised; just skip all this and let it propagate
87 return
88
89 # On exit, compute and set the truth values for the parent object.
90 self.parentRecord.set(self.owner.keys["nChild"], len(self.children))
91 # Compute instFlux from sum of component fluxes
92 instFlux = 0.0
93 for record, image in self.children:
94 instFlux += record.get(self.owner.keys["instFlux"])
95 self.parentRecord.set(self.owner.keys["instFlux"], instFlux)
96 # Compute centroid from instFlux-weighted mean of component centroids
97 x = 0.0
98 y = 0.0
99 for record, image in self.children:
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
103 self.parentRecord.set(self.owner.keys["centroid"], lsst.geom.Point2D(x, y))
104 # Compute shape from instFlux-weighted mean of offset component shapes
105 xx = 0.0
106 yy = 0.0
107 xy = 0.0
108 for record, image in self.children:
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))
116 # Run detection on the parent image to get the parent Footprint.
117 self.owner._installFootprint(self.parentRecord, self.parentImage)
118 # Create perfect HeavyFootprints for all children; these will need to
119 # be modified later to account for the noise we'll add to the image.
120 deblend = lsst.afw.image.MaskedImageF(self.owner.exposure.maskedImage, True)
121 for record, image in self.children:
122 deblend.image.array[:, :] = image.array
123 heavyFootprint = lsst.afw.detection.HeavyFootprintF(self.parentRecord.getFootprint(), deblend)
124 record.setFootprint(heavyFootprint)
125
126
128 """A simulated dataset consisuting of test image and truth catalog.
129
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.
135
136 Parameters
137 ----------
138 bbox : `lsst.geom.Box2I` or `lsst.geom.Box2D`
139 Bounding box of the test image.
140 threshold : `float`
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.
149 **kwds
150 Keyword arguments forwarded to makeEmptyExposure if exposure is `None`.
151
152 Notes
153 -----
154 Typical usage:
155
156 .. code-block: py
157
158 bbox = lsst.geom.Box2I(lsst.geom.Point2I(0,0), lsst.geom.Point2I(100,
159 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())
169 """
170
171 def __init__(self, bbox, threshold=10.0, exposure=None, **kwds):
172 if exposure is None:
173 exposure = self.makeEmptyExposure(bbox, **kwds)
174 self.threshold = lsst.afw.detection.Threshold(threshold, lsst.afw.detection.Threshold.VALUE)
175 self.exposure = exposure
176 self.psfShape = self.exposure.getPsf().computeShape(bbox.getCenter())
179
180 @classmethod
182 """Return the minimal schema needed to hold truth catalog fields.
183
184 Notes
185 -----
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
190 measurements.
191 """
192 if not hasattr(cls, "_schema"):
194 cls.keys = {}
195 cls.keys["coordErr"] = lsst.afw.table.CoordKey.addErrorFields(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"
204 )
205 cls.keys["centroid_sigma"] = lsst.afw.table.CovarianceMatrix2fKey.addFields(
206 schema, "truth", ['x', 'y'], "pixel"
207 )
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
212 )
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")
218 cls._schema = schema
219 schema = lsst.afw.table.Schema(cls._schema)
220 schema.disconnectAliases()
221 return schema
222
223 @staticmethod
224 def makePerturbedWcs(oldWcs, minScaleFactor=1.2, maxScaleFactor=1.5,
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.
229
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).
233
234 Parameters
235 ----------
236 oldWcs : `lsst.afw.geom.SkyWcs`
237 The input WCS.
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
244 30 degrees.
245 maxRotation : `lsst.geom.Angle` or `None`
246 Minimum rotation to apply to the input WCS. If `None`, defaults to
247 60 degrees.
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.
258 randomSeed : `int`
259 Random seed.
260
261 Returns
262 -------
263 newWcs : `lsst.afw.geom.SkyWcs`
264 A perturbed version of the input WCS.
265
266 Notes
267 -----
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``).
275
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
279 mutable).
280
281 The random number generator is primed with the seed given. If
282 `None`, a seed is automatically chosen.
283 """
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
293
294 def splitRandom(min1, max1, min2=None, max2=None):
295 if min2 is None:
296 min2 = -max1
297 if max2 is None:
298 max2 = -min1
299 if random_state.uniform() > 0.5:
300 return float(random_state.uniform(min1, max1))
301 else:
302 return float(random_state.uniform(min2, max2))
303 # Generate random perturbations
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)
310 # Compute new CD matrix
311 oldTransform = lsst.geom.LinearTransform(oldWcs.getCdMatrix())
312 rTransform = lsst.geom.LinearTransform.makeRotation(rotation)
313 sTransform = lsst.geom.LinearTransform.makeScaling(scaleFactor)
314 newTransform = oldTransform*rTransform*sTransform
315 matrix = newTransform.getMatrix()
316 # Compute new coordinate reference pixel (CRVAL)
317 oldSkyOrigin = oldWcs.getSkyOrigin()
318 newSkyOrigin = lsst.geom.SpherePoint(oldSkyOrigin.getRa() + refShiftRa,
319 oldSkyOrigin.getDec() + refShiftDec)
320 # Compute new pixel reference pixel (CRPIX)
321 oldPixOrigin = oldWcs.getPixelOrigin()
322 newPixOrigin = lsst.geom.Point2D(oldPixOrigin.getX() + pixShiftX,
323 oldPixOrigin.getY() + pixShiftY)
324 return lsst.afw.geom.makeSkyWcs(crpix=newPixOrigin, crval=newSkyOrigin, cdMatrix=matrix)
325
326 @staticmethod
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.
330
331 Parameters
332 ----------
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.
355
356 Returns
357 -------
358 exposure : `lsst.age.image.ExposureF`
359 An empty image.
360 """
361 if wcs is None:
362 if crval is None:
363 crval = lsst.geom.SpherePoint(45.0, 45.0, lsst.geom.degrees)
364 if cdelt is None:
365 cdelt = 0.2*lsst.geom.arcseconds
366 crpix = lsst.geom.Box2D(bbox).getCenter()
367 wcs = lsst.afw.geom.makeSkyWcs(crpix=crpix, crval=crval,
368 cdMatrix=lsst.afw.geom.makeCdMatrix(scale=cdelt))
369 exposure = lsst.afw.image.ExposureF(bbox)
370 psf = lsst.afw.detection.GaussianPsf(psfDim, psfDim, psfSigma)
371 photoCalib = lsst.afw.image.PhotoCalib(calibration)
372 visitInfo = lsst.afw.image.VisitInfo(id=visitId,
373 exposureTime=30.0,
374 date=lsst.daf.base.DateTime(mjd),
375 observatory=lsst.afw.coord.Observatory(11.1*lsst.geom.degrees,
376 22.2*lsst.geom.degrees,
377 0.333),
378 hasSimulatedContent=True)
379 exposure.setWcs(wcs)
380 exposure.setPsf(psf)
381 exposure.setPhotoCalib(photoCalib)
382 exposure.info.setVisitInfo(visitInfo)
383 exposure.info.setDetector(lsst.afw.cameraGeom.testUtils.DetectorWrapper(id=detector).detector)
384 return exposure
385
386 @staticmethod
387 def drawGaussian(bbox, instFlux, ellipse):
388 """Create an image of an elliptical Gaussian.
389
390 Parameters
391 ----------
392 bbox : `lsst.geom.Box2I` or `lsst.geom.Box2D`
393 Bounding box of image to create.
394 instFlux : `float`
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.
399
400 Returns
401 -------
402 image : `lsst.afw.image.ImageF`
403 An image of the Gaussian.
404 """
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())
412 return image
413
414 def _installFootprint(self, record, image, setPeakSignificance=True):
415 """Create simulated Footprint and add it to a truth catalog record.
416 """
418 if setPeakSignificance:
419 schema.addField("significance", type=float,
420 doc="Ratio of peak value to configured standard deviation.")
421 # Run detection on the single-source image
422 fpSet = lsst.afw.detection.FootprintSet(image, self.threshold, peakSchema=schema)
423 # the call below to the FootprintSet ctor is actually a grow operation
424 fpSet = lsst.afw.detection.FootprintSet(fpSet, int(self.psfShape.getDeterminantRadius() + 1.0), True)
425 if setPeakSignificance:
426 # This isn't a traditional significance, since we're using the VALUE
427 # threshold type, but it's the best we can do in that case.
428 for footprint in fpSet.getFootprints():
429 footprint.updatePeakSignificance(self.threshold.getValue())
430 # Update the full exposure's mask plane to indicate the detection
431 fpSet.setMask(self.exposure.mask, "DETECTED")
432 # Attach the new footprint to the exposure
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])
438
439 def addSource(self, instFlux, centroid, shape=None, setPeakSignificance=True):
440 """Add a source to the simulation.
441
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.
446
447 Parameters
448 ----------
449 instFlux : `float`
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.
461
462 Returns
463 -------
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.
468 """
469 # Create and set the truth catalog fields
470 record = self.catalog.addNew()
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] # CovarianceMatrixKey assumes symmetric x_y_Cov
476 record.set(self.keys["centroid_sigma"], covariance.astype(np.float32))
477 if shape is None:
478 record.set(self.keys["isStar"], True)
479 fullShape = self.psfShape
480 else:
481 record.set(self.keys["isStar"], False)
482 fullShape = shape.convolve(self.psfShape)
483 record.set(self.keys["shape"], fullShape)
484 # Create an image containing just this source
485 image = self.drawGaussian(self.exposure.getBBox(), instFlux,
486 lsst.afw.geom.Ellipse(fullShape, centroid))
487 # Generate a footprint for this source
488 self._installFootprint(record, image, setPeakSignificance)
489 # Actually add the source to the full exposure
490 self.exposure.image.array[:, :] += image.array
491 return record, image
492
493 def addBlend(self):
494 """Return a context manager which can add a blend of multiple sources.
495
496 Notes
497 -----
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.
501
502 Examples
503 --------
504 .. code-block: py
505 d = TestDataset(...)
506 with d.addBlend() as b:
507 b.addChild(flux1, centroid1)
508 b.addChild(flux2, centroid2, shape2)
509 """
510 return BlendContext(self)
511
512 def transform(self, wcs, **kwds):
513 """Copy this dataset transformed to a new WCS, with new Psf and PhotoCalib.
514
515 Parameters
516 ----------
517 wcs : `lsst.afw.geom.SkyWcs`
518 WCS for the new dataset.
519 **kwds
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.
524
525 Returns
526 -------
527 newDataset : `TestDataset`
528 Transformed copy of this dataset.
529 """
530 bboxD = lsst.geom.Box2D()
531 xyt = lsst.afw.geom.makeWcsPairTransform(self.exposure.getWcs(), wcs)
532 for corner in lsst.geom.Box2D(self.exposure.getBBox()).getCorners():
533 bboxD.include(xyt.applyForward(lsst.geom.Point2D(corner)))
534 bboxI = lsst.geom.Box2I(bboxD)
535 result = TestDataset(bbox=bboxI, wcs=wcs, **kwds)
536 oldPhotoCalib = self.exposure.getPhotoCalib()
537 newPhotoCalib = result.exposure.getPhotoCalib()
538 oldPsfShape = self.exposure.getPsf().computeShape(bboxD.getCenter())
539 for record in self.catalog:
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
548 else:
549 affine = lsst.afw.geom.linearizeTransform(xyt, oldCentroid)
550 oldFullShape = record.get(self.keys["shape"])
551 oldDeconvolvedShape = lsst.afw.geom.Quadrupole(
552 oldFullShape.getIxx() - oldPsfShape.getIxx(),
553 oldFullShape.getIyy() - oldPsfShape.getIyy(),
554 oldFullShape.getIxy() - oldPsfShape.getIxy(),
555 False
556 )
557 newDeconvolvedShape = oldDeconvolvedShape.transform(affine.getLinear())
558 result.addSource(newFlux, newCentroid, newDeconvolvedShape)
559 return result
560
561 def realize(self, noise, schema, randomSeed=1):
562 r"""Simulate an exposure and detection catalog for this dataset.
563
564 The simulation includes noise, and the detection catalog includes
565 `~lsst.afw.detection.heavyFootprint.HeavyFootprint`\ s.
566
567 Parameters
568 ----------
569 noise : `float`
570 Standard deviation of noise to be added to the exposure. The
571 noise will be Gaussian and constant, appropriate for the
572 sky-limited regime.
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.
581
582 Returns
583 -------
584 `exposure` : `lsst.afw.image.ExposureF`
585 Simulated image.
586 `catalog` : `lsst.afw.table.SourceCatalog`
587 Simulated detection catalog.
588 """
589 random_state = np.random.RandomState(randomSeed)
590 assert schema.contains(self.schema)
592 mapper.addMinimalSchema(self.schema, True)
593 exposure = self.exposure.clone()
594 exposure.variance.array[:, :] = noise**2
595 exposure.image.array[:, :] += random_state.randn(exposure.height, exposure.width)*noise
596 catalog = lsst.afw.table.SourceCatalog(schema)
597 catalog.extend(self.catalog, mapper=mapper)
598 # Loop over sources and generate new HeavyFootprints that divide up
599 # the noisy pixels, not the ideal no-noise pixels.
600 for record in catalog:
601 # parent objects have non-Heavy Footprints, which don't need to be
602 # updated after adding noise.
603 if record.getParent() == 0:
604 continue
605 # get flattened arrays that correspond to the no-noise and noisy
606 # parent images
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)
615 # N.B. this isn't a copy ctor - it's a copy from a vanilla
616 # Footprint, so it doesn't copy the arrays we don't want to
617 # change, and hence we have to do that ourselves below.
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)
623 lsst.afw.table.updateSourceCoords(exposure.wcs, catalog)
624 return exposure, catalog
625
626
628 """Base class for tests of measurement tasks.
629 """
630 def makeSingleFrameMeasurementConfig(self, plugin=None, dependencies=()):
631 """Create an instance of `SingleFrameMeasurementTask.ConfigClass`.
632
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.
636
637 Parameters
638 ----------
639 plugin : `str`
640 Name of measurement plugin to enable.
641 dependencies : iterable of `str`, optional
642 Names of dependencies of the measurement plugin.
643
644 Returns
645 -------
646 config : `SingleFrameMeasurementTask.ConfigClass`
647 The resulting task configuration.
648 """
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)
661 return config
662
663 def makeSingleFrameMeasurementTask(self, plugin=None, dependencies=(), config=None, schema=None,
664 algMetadata=None):
665 """Create a configured instance of `SingleFrameMeasurementTask`.
666
667 Parameters
668 ----------
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
681 generated.
682 algMetadata : `lsst.daf.base.PropertyList`, optional
683 Measurement algorithm metadata. If `None`, a default container
684 will be generated.
685
686 Returns
687 -------
688 task : `SingleFrameMeasurementTask`
689 A configured instance of the measurement task.
690 """
691 if config is None:
692 if plugin is None:
693 raise ValueError("Either plugin or config argument must not be None")
694 config = self.makeSingleFrameMeasurementConfig(plugin=plugin, dependencies=dependencies)
695 if schema is None:
696 schema = TestDataset.makeMinimalSchema()
697 # Clear all aliases so only those defined by config are set.
698 schema.setAliasMap(None)
699 if algMetadata is None:
700 algMetadata = lsst.daf.base.PropertyList()
701 return SingleFrameMeasurementTask(schema=schema, algMetadata=algMetadata, config=config)
702
703 def makeForcedMeasurementConfig(self, plugin=None, dependencies=()):
704 """Create an instance of `ForcedMeasurementTask.ConfigClass`.
705
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
710 coordinate system.
711
712 Parameters
713 ----------
714 plugin : `str`
715 Name of measurement plugin to enable.
716 dependencies : iterable of `str`, optional
717 Names of dependencies of the measurement plugin.
718
719 Returns
720 -------
721 config : `ForcedMeasurementTask.ConfigClass`
722 The resulting task configuration.
723 """
724
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")
734 return config
735
736 def makeForcedMeasurementTask(self, plugin=None, dependencies=(), config=None, refSchema=None,
737 algMetadata=None):
738 """Create a configured instance of `ForcedMeasurementTask`.
739
740 Parameters
741 ----------
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
754 generated.
755 algMetadata : `lsst.daf.base.PropertyList`, optional
756 Measurement algorithm metadata. If `None`, a default container
757 will be generated.
758
759 Returns
760 -------
761 task : `ForcedMeasurementTask`
762 A configured instance of the measurement task.
763 """
764 if config is None:
765 if plugin is None:
766 raise ValueError("Either plugin or config argument must not be None")
767 config = self.makeForcedMeasurementConfig(plugin=plugin, dependencies=dependencies)
768 if refSchema is None:
769 refSchema = TestDataset.makeMinimalSchema()
770 if algMetadata is None:
771 algMetadata = lsst.daf.base.PropertyList()
772 return ForcedMeasurementTask(refSchema=refSchema, algMetadata=algMetadata, config=config)
773
774
776 """Base class for testing measurement transformations.
777
778 Notes
779 -----
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
783 algorithms.
784
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.
789 """
790 name = "MeasurementTransformTest"
791 """The name used for the measurement algorithm (str).
792
793 Notes
794 -----
795 This determines the names of the fields in the resulting catalog. This
796 default should generally be fine, but subclasses can override if
797 required.
798 """
799
800 # These should be customized by subclassing.
801 controlClass = None
802 algorithmClass = None
803 transformClass = None
804
805 flagNames = ("flag",)
806 """Flags which may be set by the algorithm being tested (iterable of `str`).
807 """
808
809 # The plugin being tested should be registered under these names for
810 # single frame and forced measurement. Should be customized by
811 # subclassing.
812 singleFramePlugins = ()
813 forcedPlugins = ()
814
815 def setUp(self):
817 self.calexp = TestDataset.makeEmptyExposure(bbox)
818 self._setupTransform()
819
820 def tearDown(self):
821 del self.calexp
822 del self.inputCat
823 del self.mapper
824 del self.transform
825 del self.outputCat
826
827 def _populateCatalog(self, baseNames):
828 records = []
829 for flagValue in (True, False):
830 records.append(self.inputCat.addNew())
831 for baseName in baseNames:
832 for flagName in self.flagNames:
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)
836
837 def _checkOutput(self, baseNames):
838 for inSrc, outSrc in zip(self.inputCat, self.outputCat):
839 for baseName in baseNames:
840 self._compareFieldsInRecords(inSrc, outSrc, baseName)
841 for flagName in self.flagNames:
842 keyName = outSrc.schema.join(baseName, flagName)
843 if keyName in inSrc.schema:
844 self.assertEqual(outSrc.get(keyName), inSrc.get(keyName))
845 else:
846 self.assertFalse(keyName in outSrc.schema)
847
848 def _runTransform(self, doExtend=True):
849 if doExtend:
850 self.outputCat.extend(self.inputCat, mapper=self.mapper)
851 self.transform(self.inputCat, self.outputCat, self.calexp.getWcs(), self.calexp.getPhotoCalib())
852
853 def testTransform(self, baseNames=None):
854 """Test the transformation on a catalog containing random data.
855
856 Parameters
857 ----------
858 baseNames : iterable of `str`
859 Iterable of the initial parts of measurement field names.
860
861 Notes
862 -----
863 We check that:
864
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.
869
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 +
878 "_1", ...)``.
879 """
880 baseNames = baseNames or [self.name]
881 self._populateCatalog(baseNames)
884 self._checkOutput(baseNames)
885
886 def _checkRegisteredTransform(self, registry, name):
887 # If this is a Python-based transform, we can compare directly; if
888 # it's wrapped C++, we need to compare the wrapped class.
889 self.assertEqual(registry[name].PluginClass.getTransformClass(), self.transformClass)
890
892 """Test that the transformation is appropriately registered.
893 """
894 for pluginName in self.singleFramePlugins:
895 self._checkRegisteredTransform(lsst.meas.base.SingleFramePlugin.registry, pluginName)
896 for pluginName in self.forcedPlugins:
897 self._checkRegisteredTransform(lsst.meas.base.ForcedPlugin.registry, pluginName)
898
899
901
903 self.control = self.controlClass()
905 # Trick algorithms that depend on the slot centroid or alias into thinking they've been defined;
906 # it doesn't matter for this test since we won't actually use the plugins for anything besides
907 # defining the schema.
908 inputSchema.getAliasMap().set("slot_Centroid", "dummy")
909 inputSchema.getAliasMap().set("slot_Shape", "dummy")
910 self.algorithmClass(self.control, self.name, inputSchema)
911 inputSchema.getAliasMap().erase("slot_Centroid")
912 inputSchema.getAliasMap().erase("slot_Shape")
915 self.transform = self.transformClass(self.control, self.name, self.mapper)
916 self.outputCat = lsst.afw.table.BaseCatalog(self.mapper.getOutputSchema())
917
918
920
922 self.control = self.controlClass()
925 # Trick algorithms that depend on the slot centroid or alias into thinking they've been defined;
926 # it doesn't matter for this test since we won't actually use the plugins for anything besides
927 # defining the schema.
928 inputMapper.editOutputSchema().getAliasMap().set("slot_Centroid", "dummy")
929 inputMapper.editOutputSchema().getAliasMap().set("slot_Shape", "dummy")
930 self.algorithmClass(self.control, self.name, inputMapper, lsst.daf.base.PropertyList())
931 inputMapper.editOutputSchema().getAliasMap().erase("slot_Centroid")
932 inputMapper.editOutputSchema().getAliasMap().erase("slot_Shape")
933 self.inputCat = lsst.afw.table.SourceCatalog(inputMapper.getOutputSchema())
934 self.mapper = lsst.afw.table.SchemaMapper(inputMapper.getOutputSchema())
935 self.transform = self.transformClass(self.control, self.name, self.mapper)
936 self.outputCat = lsst.afw.table.BaseCatalog(self.mapper.getOutputSchema())
937
938
940
941 def _setFieldsInRecords(self, records, name):
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()
945
946 # Negative instFluxes should be converted to NaNs.
947 assert len(records) > 1
948 records[0][record.schema.join(name, 'instFlux')] = -1
949
950 def _compareFieldsInRecords(self, inSrc, outSrc, name):
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)
958 else:
959 # negative instFlux results in NaN magnitude, but can still have finite 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')]))
963 else:
964 mag = self.calexp.getPhotoCalib().instFluxToMagnitude(inSrc[instFluxName],
965 inSrc[instFluxErrName])
966 self.assertEqual(outSrc[outSrc.schema.join(name, 'magErr')], mag.error)
967
968
970
971 def _setFieldsInRecords(self, records, name):
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()
975 # Some algorithms set no errors; some set only sigma on x & y; some provide
976 # a full covariance matrix. Set only those which exist in the schema.
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()
981
982 def _compareFieldsInRecords(self, inSrc, outSrc, name):
983 centroidResultKey = CentroidResultKey(inSrc.schema[self.name])
984 centroidResult = centroidResultKey.get(inSrc)
985
986 coord = lsst.afw.table.CoordKey(outSrc.schema[self.name]).get(outSrc)
987 coordTruth = self.calexp.getWcs().pixelToSky(centroidResult.getCentroid())
988 self.assertEqual(coordTruth, coord)
989
990 # If the centroid has an associated uncertainty matrix, the coordinate
991 # must have one too, and vice versa.
992 try:
993 coordErr = lsst.afw.table.CovarianceMatrix2fKey(outSrc.schema[self.name],
994 ["ra", "dec"]).get(outSrc)
996 self.assertFalse(centroidResultKey.getCentroidErr().isValid())
997 else:
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)
table::Key< int > transform
Hold the location of an observatory.
Definition Observatory.h:43
A set of Footprints, associated with a MaskedImage.
A circularly symmetric Gaussian Psf class with no spatial variation, intended mostly for testing purp...
Definition GaussianPsf.h:42
static afw::table::Schema makeMinimalSchema()
Return a minimal schema for Peak tables and records.
Definition Peak.h:137
A Threshold is used to pass a threshold value to detection algorithms.
Definition Threshold.h:43
An ellipse defined by an arbitrary BaseCore and a center point.
Definition Ellipse.h:51
An ellipse core with quadrupole moments as parameters.
Definition Quadrupole.h:47
The photometric calibration of an exposure.
Definition PhotoCalib.h:114
Information about a single exposure of an imaging camera.
Definition VisitInfo.h:68
A FunctorKey used to get or set celestial coordinates from a pair of lsst::geom::Angle keys.
Definition aggregates.h:292
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.
Definition Schema.h:51
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.
Definition Source.h:258
Class for handling dates/times, including MJD, UTC, and TAI.
Definition DateTime.h:64
Class for storing ordered metadata with comments.
A floating-point coordinate rectangle geometry.
Definition Box.h:413
An integer coordinate rectangle.
Definition Box.h:55
A 2D linear coordinate transformation.
static LinearTransform makeRotation(Angle t) noexcept
static LinearTransform makeScaling(double s) noexcept
Point in an unspecified spherical coordinate system.
Definition SpherePoint.h:57
makeSingleFrameMeasurementConfig(self, plugin=None, dependencies=())
Definition tests.py:630
makeForcedMeasurementConfig(self, plugin=None, dependencies=())
Definition tests.py:703
makeForcedMeasurementTask(self, plugin=None, dependencies=(), config=None, refSchema=None, algMetadata=None)
Definition tests.py:737
makeSingleFrameMeasurementTask(self, plugin=None, dependencies=(), config=None, schema=None, algMetadata=None)
Definition tests.py:664
addChild(self, instFlux, centroid, shape=None)
Definition tests.py:64
__exit__(self, type_, value, tb)
Definition tests.py:81
_compareFieldsInRecords(self, inSrc, outSrc, name)
Definition tests.py:982
_compareFieldsInRecords(self, inSrc, outSrc, name)
Definition tests.py:950
_setFieldsInRecords(self, records, name)
Definition tests.py:941
makePerturbedWcs(oldWcs, minScaleFactor=1.2, maxScaleFactor=1.5, minRotation=None, maxRotation=None, minRefShift=None, maxRefShift=None, minPixShift=2.0, maxPixShift=4.0, randomSeed=1)
Definition tests.py:227
realize(self, noise, schema, randomSeed=1)
Definition tests.py:561
addSource(self, instFlux, centroid, shape=None, setPeakSignificance=True)
Definition tests.py:439
_installFootprint(self, record, image, setPeakSignificance=True)
Definition tests.py:414
drawGaussian(bbox, instFlux, ellipse)
Definition tests.py:387
makeEmptyExposure(bbox, wcs=None, crval=None, cdelt=None, psfSigma=2.0, psfDim=17, calibration=4, visitId=1234, mjd=60000.0, detector=1)
Definition tests.py:328
__init__(self, bbox, threshold=10.0, exposure=None, **kwds)
Definition tests.py:171
testTransform(self, baseNames=None)
Definition tests.py:853
_runTransform(self, doExtend=True)
Definition tests.py:848
_checkRegisteredTransform(self, registry, name)
Definition tests.py:886
Reports attempts to exceed implementation-defined length limits for some classes.
Definition Runtime.h:76
Reports attempts to access elements using an invalid key.
Definition Runtime.h:151
bool isValid
Definition fits.cc:404
std::shared_ptr< SkyWcs > makeSkyWcs(daf::base::PropertySet &metadata, bool strip=false)
Construct a SkyWcs from FITS keywords.
Definition SkyWcs.cc:521
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.
Definition SkyWcs.cc:133
std::shared_ptr< TransformPoint2ToPoint2 > makeWcsPairTransform(SkyWcs const &src, SkyWcs const &dst)
A Transform obtained by putting two SkyWcs objects "back to back".
Definition SkyWcs.cc:146
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.
Definition wcsUtils.cc:125