LSST Applications g0f08755f38+c89d42e150,g1635faa6d4+b6cf076a36,g1653933729+a8ce1bb630,g1a0ca8cf93+4c08b13bf7,g28da252d5a+f33f8200ef,g29321ee8c0+0187be18b1,g2bbee38e9b+9634bc57db,g2bc492864f+9634bc57db,g2cdde0e794+c2c89b37c4,g3156d2b45e+41e33cbcdc,g347aa1857d+9634bc57db,g35bb328faa+a8ce1bb630,g3a166c0a6a+9634bc57db,g3e281a1b8c+9f2c4e2fc3,g414038480c+077ccc18e7,g41af890bb2+e740673f1a,g5fbc88fb19+17cd334064,g7642f7d749+c89d42e150,g781aacb6e4+a8ce1bb630,g80478fca09+f8b2ab54e1,g82479be7b0+e2bd23ab8b,g858d7b2824+c89d42e150,g9125e01d80+a8ce1bb630,g9726552aa6+10f999ec6a,ga5288a1d22+065360aec4,gacf8899fa4+9553554aa7,gae0086650b+a8ce1bb630,gb58c049af0+d64f4d3760,gbd46683f8f+ac57cbb13d,gc28159a63d+9634bc57db,gcf0d15dbbd+e37acf7834,gda3e153d99+c89d42e150,gda6a2b7d83+e37acf7834,gdaeeff99f8+1711a396fd,ge2409df99d+cb1e6652d6,ge79ae78c31+9634bc57db,gf0baf85859+147a0692ba,gf3967379c6+02b11634a5,w.2024.45
LSST Data Management Base Package
Loading...
Searching...
No Matches
peekExposure.py
Go to the documentation of this file.
1# This file is part of pipe_tasks.
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/>.
21from __future__ import annotations
22
23__all__ = [
24 "PeekExposureTaskConfig",
25 "PeekExposureTask",
26]
27
28from typing import Any
29
30import astropy
31import numpy as np
32import numpy.typing as npt
33
34import lsst.afw.display as afwDisplay
35import lsst.afw.geom as afwGeom
36import lsst.afw.image as afwImage
37import lsst.afw.math as afwMath
38import lsst.afw.table as afwTable
39import lsst.daf.base as dafBase
40import lsst.geom as geom
41import lsst.pex.config as pexConfig
42import lsst.pipe.base as pipeBase
43from lsst.afw.detection import Psf
44from lsst.afw.geom.ellipses import Quadrupole
45from lsst.afw.image import ImageD
46from lsst.afw.table import SourceTable
47from lsst.geom import Box2I, Extent2I, LinearTransform, Point2D, Point2I, SpherePoint, arcseconds, degrees
48from lsst.meas.algorithms import SourceDetectionTask, SubtractBackgroundTask
49from lsst.meas.algorithms.installGaussianPsf import InstallGaussianPsfTask
50from lsst.meas.base import IdGenerator, SingleFrameMeasurementTask
51from lsst.meas.extensions import shapeHSM # noqa: F401
52
53IDX_SENTINEL = -99999
54
55# Adding obs_lsst to the table file isn't possible, so hard code this because
56# importing it from obs_lsst isn't possible without a deferred import, which
57# isn't possible because of the tests and the build order.
58FILTER_DELIMITER = "~"
59
60
62 """Check if an exposure is dispersed.
63
64 Note this is copied from `atmospec.utils.isDispersedExp` to avoid a
65 circular import.
66
67 Parameters
68 ----------
69 exp : `lsst.afw.image.Exposure`
70 The exposure.
71
72 Returns
73 -------
74 isDispersed : `bool`
75 Whether it is a dispersed image or not.
76 """
77 filterFullName = exp.filter.physicalLabel
78 if FILTER_DELIMITER not in filterFullName:
79 raise RuntimeError(f"Error parsing filter name {filterFullName}")
80 filt, grating = filterFullName.split(FILTER_DELIMITER)
81 if grating.upper().startswith('EMPTY'):
82 return False
83 return True
84
85
86def _estimateMode(data: npt.NDArray[np.float64], frac: float = 0.5) -> float:
87 """Estimate the mode of a 1d distribution.
88
89 Finds the smallest interval containing the fraction ``frac`` of the data,
90 then takes the median of the values in that interval.
91
92 Parameters
93 ----------
94 data : array-like
95 1d array of data values
96 frac : float, optional
97 Fraction of data to include in the mode interval. Default is 0.5.
98
99 Returns
100 -------
101 mode : float
102 Estimated mode of the data.
103 """
104
105 data = data[np.isfinite(data)]
106 if len(data) == 0:
107 return np.nan
108 elif len(data) == 1:
109 return data[0]
110
111 data = np.sort(data)
112 interval = int(np.ceil(frac * len(data)))
113 spans = data[interval:] - data[:-interval]
114 start = np.argmin(spans)
115 return np.median(data[start: start + interval])
116
117
119 wcs: afwGeom.SkyWcs,
120 bearing: geom.Angle,
121 imagePoint: geom.Point2D,
122 skyPoint: geom.SpherePoint | None = None,
123) -> geom.Extent2D:
124 """Compute unit vector along given bearing at given point in the sky.
125
126 Parameters
127 ----------
128 wcs : `lsst.afw.geom.SkyWcs`
129 World Coordinate System of image.
130 bearing : `lsst.geom.Angle`
131 Bearing (angle North of East) at which to compute unit vector.
132 imagePoint : `lsst.geom.Point2D`
133 Point in the image.
134 skyPoint : `lsst.geom.SpherePoint`, optional
135 Point in the sky.
136
137 Returns
138 -------
139 unitVector : `lsst.geom.Extent2D`
140 Unit vector in the direction of bearing.
141 """
142 if skyPoint is None:
143 skyPoint = wcs.pixelToSky(imagePoint)
144 dpt = wcs.skyToPixel(skyPoint.offset(bearing, 1e-4 * degrees)) - imagePoint
145 return dpt / dpt.computeNorm()
146
147
148def roseVectors(wcs: afwGeom.SkyWcs, imagePoint: geom.Point2D, parAng: geom.Angle | None = None) -> dict:
149 """Compute unit vectors in the N/W and optionally alt/az directions.
150
151 Parameters
152 ----------
153 wcs : `lsst.afw.geom.SkyWcs`
154 World Coordinate System of image.
155 imagePoint : `lsst.geom.Point2D`
156 Point in the image
157 parAng : `lsst.geom.Angle`, optional
158 Parallactic angle (position angle of zenith measured East from North)
159 (default: None)
160
161 Returns
162 -------
163 unitVectors : `dict` of `lsst.geom.Extent2D`
164 Unit vectors in the N, W, alt, and az directions.
165 """
166 ncp = SpherePoint(0 * degrees, 90 * degrees) # North Celestial Pole
167 skyPoint = wcs.pixelToSky(imagePoint)
168 bearing = skyPoint.bearingTo(ncp)
169
170 out = dict()
171 out["N"] = _bearingToUnitVector(wcs, bearing, imagePoint, skyPoint=skyPoint)
172 out["W"] = _bearingToUnitVector(wcs, bearing + 90 * degrees, imagePoint, skyPoint=skyPoint)
173
174 if parAng is not None:
175 out["alt"] = _bearingToUnitVector(wcs, bearing - parAng, imagePoint, skyPoint=skyPoint)
176 out["az"] = _bearingToUnitVector(wcs, bearing - parAng + 90 * degrees, imagePoint, skyPoint=skyPoint)
177
178 return out
179
180
182 display: afwDisplay.Display,
183 wcs: afwGeom.SkyWcs,
184 imagePoint: geom.Point2D,
185 parAng: geom.Angle | None = None,
186 len: float = 50,
187) -> None:
188 """Display unit vectors along N/W and optionally alt/az directions.
189
190 Parameters
191 ----------
192 display : `lsst.afw.display.Display`
193 Display on which to render rose.
194 wcs : `lsst.afw.geom.SkyWcs`
195 World Coordinate System of image.
196 imagePoint : `lsst.geom.Point2D`
197 Point in the image at which to render rose.
198 parAng : `lsst.geom.Angle`, optional
199 Parallactic angle (position angle of zenith measured East from North)
200 (default: None)
201 len : `float`, optional
202 Length of the rose vectors (default: 50)
203 """
204 unitVectors = roseVectors(wcs, imagePoint, parAng=parAng)
205 colors = dict(N="r", W="r", alt="g", az="g")
206 for name, unitVector in unitVectors.items():
207 display.line([imagePoint, imagePoint + len * unitVector], ctype=colors[name])
208 display.dot(name, *(imagePoint + 1.6 * len * unitVector), ctype=colors[name])
209
210
212 def __init__(self, size: float, outerRad: float, innerRad: float):
213 Psf.__init__(self, isFixed=True)
214 self.size = size
215 self.outerRad = outerRad
216 self.innerRad = innerRad
217 self.dimensions = Extent2I(size, size)
218
219 def __deepcopy__(self, memo: Any | None = None) -> DonutPsf:
220 return DonutPsf(self.size, self.outerRad, self.innerRad)
221
222 def resized(self, width: float, height: float) -> DonutPsf:
223 assert width == height
224 return DonutPsf(width, self.outerRad, self.innerRad)
225
227 self, position: Point2D | None = None, color: afwImage.Color | None = None
228 ) -> ImageD:
229 bbox = self.computeBBox(self.getAveragePosition())
230 img = ImageD(bbox, 0.0)
231 x, y = np.ogrid[bbox.minY: bbox.maxY + 1, bbox.minX: bbox.maxX + 1]
232 rsqr = x**2 + y**2
233 w = (rsqr < self.outerRad**2) & (rsqr > self.innerRad**2)
234 img.array[w] = 1.0
235 img.array /= np.sum(img.array)
236 return img
237
238 def _doComputeBBox(self, position: Point2D | None = None, color: afwImage.Color | None = None) -> Box2I:
239 return Box2I(Point2I(-self.dimensions / 2), self.dimensions)
240
242 self, position: Point2D | None = None, color: afwImage.Color | None = None
243 ) -> Quadrupole:
244 Ixx = self.outerRad**4 - self.innerRad**4
245 Ixx /= self.outerRad**2 - self.innerRad**2
246 return Quadrupole(Ixx, Ixx, 0.0)
247
249 self, radius: float, position: Point2D | None = None, color: afwImage.Color | None = None
250 ) -> float:
251 return 1 - np.exp(-0.5 * (radius / self.sigma) ** 2)
252
253 def __eq__(self, rhs: object) -> bool:
254 if isinstance(rhs, DonutPsf):
255 return self.size == rhs.size and self.outerRad == rhs.outerRad and self.innerRad == rhs.innerRad
256 return False
257
258
259class PeekTaskConfig(pexConfig.Config):
260 """Config class for the PeekTask."""
261
262 installPsf = pexConfig.ConfigurableField(
263 target=InstallGaussianPsfTask,
264 doc="Install a PSF model",
265 ) # type: ignore
266 doInstallPsf: pexConfig.Field[bool] = pexConfig.Field(
267 dtype=bool,
268 default=True,
269 doc="Install a PSF model?",
270 )
271 background = pexConfig.ConfigurableField(
272 target=SubtractBackgroundTask,
273 doc="Estimate background",
274 ) # type: ignore
275 detection = pexConfig.ConfigurableField(target=SourceDetectionTask, doc="Detect sources") # type: ignore
276 measurement = pexConfig.ConfigurableField(
277 target=SingleFrameMeasurementTask, doc="Measure sources"
278 ) # type: ignore
279 defaultBinSize: pexConfig.Field[int] = pexConfig.Field(
280 dtype=int,
281 default=1,
282 doc="Default binning factor for exposure (often overridden).",
283 )
284
285 def setDefaults(self) -> None:
286 super().setDefaults()
287 # Configure to be aggressively fast.
288 self.detection.thresholdValue = 5.0
289 self.detection.includeThresholdMultiplier = 10.0
290 self.detection.reEstimateBackground = False
291 self.detection.doTempLocalBackground = False
292 self.measurement.doReplaceWithNoise = False
293 self.detection.minPixels = 40
294 self.installPsf.fwhm = 5.0
295 self.installPsf.width = 21
296 # minimal set of measurements
297 self.measurement.plugins.names = [
298 "base_PixelFlags",
299 "base_SdssCentroid",
300 "ext_shapeHSM_HsmSourceMoments",
301 "base_GaussianFlux",
302 "base_PsfFlux",
303 "base_CircularApertureFlux",
304 ]
305 self.measurement.slots.shape = "ext_shapeHSM_HsmSourceMoments"
306
307
308class PeekTask(pipeBase.Task):
309 """Peek at exposure to quickly detect and measure both the brightest source
310 in the image, and a set of sources representative of the exposure's overall
311 image quality.
312
313 Optionally bins image and then:
314 - installs a simple PSF model
315 - measures and subtracts the background
316 - detects sources
317 - measures sources
318
319 Designed to be quick at the expense of primarily completeness, but also to
320 a lesser extent accuracy.
321 """
322
323 ConfigClass = PeekTaskConfig
324 config: PeekTaskConfig
325 installPsf: InstallGaussianPsfTask
326 background: SubtractBackgroundTask
327 detection: SourceDetectionTask
328 measurement: SingleFrameMeasurementTask
329 _DefaultName = "peek"
330
331 def __init__(self, schema: Any | None = None, **kwargs: Any):
332 super().__init__(**kwargs)
333
334 if schema is None:
335 schema = SourceTable.makeMinimalSchema()
336 self.schema = schema
337
338 self.makeSubtask("installPsf")
339 self.makeSubtask("background")
340 self.makeSubtask("detection", schema=self.schema)
342 self.makeSubtask("measurement", schema=self.schema, algMetadata=self.algMetadata)
343
344 def run(self, exposure: afwImage.Exposure, binSize: int | None = None) -> pipeBase.Struct:
345 """Peek at exposure.
346
347 Parameters
348 ----------
349 exposure : `lsst.afw.image.Exposure`
350 Exposure at which to peek.
351 binSize : `int`, optional
352 Binning factor for exposure. Default is None, which will use the
353 default binning factor from the config.
354
355 Returns
356 -------
357 result : `pipeBase.Struct`
358 Result of peeking.
359 Struct containing:
360 - sourceCat : `lsst.afw.table.SourceCatalog`
361 Source catalog from the binned exposure.
362 """
363 if binSize is None:
364 binSize = self.config.defaultBinSize
365
366 if binSize != 1:
367 mi = exposure.getMaskedImage()
368 binned = afwMath.binImage(mi, binSize)
369 exposure.setMaskedImage(binned)
370
371 if self.config.doInstallPsf:
372 self.installPsf.run(exposure=exposure)
373
374 self.background.run(exposure)
375
376 idGenerator = IdGenerator()
377 sourceIdFactory = idGenerator.make_table_id_factory()
378 table = SourceTable.make(self.schema, sourceIdFactory)
379 table.setMetadata(self.algMetadata)
380 sourceCat = self.detection.run(table=table, exposure=exposure, doSmooth=True).sources
381
382 self.measurement.run(measCat=sourceCat, exposure=exposure, exposureId=idGenerator.catalog_id)
383
384 return pipeBase.Struct(
385 sourceCat=sourceCat,
386 )
387
388
389class PeekDonutTaskConfig(pexConfig.Config):
390 """Config class for the PeekDonutTask."""
391
392 peek = pexConfig.ConfigurableField(
393 target=PeekTask,
394 doc="Peek configuration",
395 ) # type: ignore
396 resolution = pexConfig.Field(
397 dtype=float,
398 default=16.0,
399 doc="Target number of pixels for a binned donut",
400 ) # type: ignore
401 binSizeMax = pexConfig.Field(
402 dtype=int,
403 default=10,
404 doc="Maximum binning factor for donut mode",
405 ) # type: ignore
406
407 def setDefaults(self) -> None:
408 super().setDefaults()
409 # Donuts are big even when binned.
410 self.peek.installPsf.fwhm = 10.0
411 self.peek.installPsf.width = 31
412 # Use DonutPSF if not overridden
413 self.peek.doInstallPsf = False
414
415
416class PeekDonutTask(pipeBase.Task):
417 """Peek at a donut exposure.
418
419 The main modification for donuts is to aggressively bin the image to reduce
420 the size of sources (donuts) from ~100 pixels or more to ~10 pixels. This
421 greatly increases the speed and detection capabilities of PeekTask with
422 little loss of accuracy for centroids.
423 """
424
425 ConfigClass = PeekDonutTaskConfig
426 config: PeekDonutTaskConfig
427 peek: PeekTask
428 _DefaultName = "peekDonut"
429
430 def __init__(self, config: Any, **kwargs: Any):
431 super().__init__(config=config, **kwargs)
432 self.makeSubtask("peek")
433
434 def run(
435 self, exposure: afwImage.Exposure, donutDiameter: float, binSize: int | None = None
436 ) -> pipeBase.Struct:
437 """Peek at donut exposure.
438
439 Parameters
440 ----------
441 exposure : `lsst.afw.image.Exposure`
442 Exposure at which to peek.
443 donutDiameter : `float`
444 Donut diameter in pixels.
445 binSize : `int`, optional
446 Binning factor for exposure. Default is None, which will use the
447 resolution config value to determine the binSize.
448
449 Returns
450 -------
451 result : `pipeBase.Struct`
452 Result of donut peeking.
453 Struct containing:
454 - mode : `str`
455 Peek mode that was run.
456 - binSize : `int`
457 Binning factor used.
458 - binnedSourceCat : `lsst.afw.table.SourceCatalog`
459 Source catalog from the binned exposure.
460 """
461 if binSize is None:
462 binSize = int(
463 np.floor(
464 np.clip(
465 donutDiameter / self.config.resolution,
466 1,
467 self.config.binSizeMax,
468 )
469 )
470 )
471 binnedDonutDiameter = donutDiameter / binSize
472 psf = DonutPsf(
473 binnedDonutDiameter * 1.5, binnedDonutDiameter * 0.5, binnedDonutDiameter * 0.5 * 0.3525
474 )
475
476 # Note that SourceDetectionTask will convolve with a _Gaussian
477 # approximation to the PSF_ anyway, so we don't really need to be
478 # precise with the PSF unless this changes. PSFs that approach the
479 # size of the image, however, can cause problems with the detection
480 # convolution algorithm, so we limit the size.
481 sigma = psf.computeShape(psf.getAveragePosition()).getDeterminantRadius()
482 factor = 8 * sigma / (min(exposure.getDimensions()) / binSize)
483
484 if factor > 1:
485 psf = DonutPsf(
486 binnedDonutDiameter * 1.5 / factor,
487 binnedDonutDiameter * 0.5 / factor,
488 binnedDonutDiameter * 0.5 * 0.3525 / factor,
489 )
490 exposure.setPsf(psf)
491
492 peekResult = self.peek.run(exposure, binSize)
493
494 return pipeBase.Struct(
495 mode="donut",
496 binSize=binSize,
497 binnedSourceCat=peekResult.sourceCat,
498 )
499
500 def getGoodSources(self, binnedSourceCat: afwTable.SourceCatalog) -> np.ndarray:
501 """Perform any filtering on the source catalog.
502
503 Parameters
504 ----------
505 binnedSourceCat : `lsst.afw.table.SourceCatalog`
506 Source catalog from the binned exposure.
507
508 Returns
509 -------
510 goodSourceMask : `numpy.ndarray`
511 Boolean array indicating which sources are good.
512 """
513 # Perform any filtering on the source catalog
514 goodSourceMask = np.ones(len(binnedSourceCat), dtype=bool)
515 return goodSourceMask
516
517
518class PeekPhotoTaskConfig(pexConfig.Config):
519 """Config class for the PeekPhotoTask."""
520
521 peek = pexConfig.ConfigurableField(
522 target=PeekTask,
523 doc="Peek configuration",
524 ) # type: ignore
525 binSize: pexConfig.Field[int] = pexConfig.Field(
526 dtype=int,
527 default=2,
528 doc="Binning factor for exposure",
529 )
530
531 def setDefaults(self) -> None:
532 super().setDefaults()
533 # Use a lower detection threshold in photo mode to go a bit fainter.
534 self.peek.detection.includeThresholdMultiplier = 1.0
535 self.peek.detection.thresholdValue = 10.0
536 self.peek.detection.minPixels = 10
537
538
539class PeekPhotoTask(pipeBase.Task):
540 """Peek at a photo (imaging) exposure.
541
542 For photo mode, we keep a relatively small detection threshold value, so we
543 can detect faint sources to use for image quality assessment.
544 """
545
546 ConfigClass = PeekPhotoTaskConfig
547 config: PeekPhotoTaskConfig
548 peek: PeekTask
549 _DefaultName = "peekPhoto"
550
551 def __init__(self, config: Any, **kwargs: Any):
552 super().__init__(config=config, **kwargs)
553 self.makeSubtask("peek")
554
555 def run(self, exposure: afwImage.Exposure, binSize: int | None = None) -> pipeBase.Struct:
556 """Peek at donut exposure.
557
558 Parameters
559 ----------
560 exposure : `lsst.afw.image.Exposure`
561 Exposure at which to peek.
562 binSize : `int`, optional
563 Binning factor for exposure. Default is None, which will use the
564 binning factor from the config.
565
566 Returns
567 -------
568 result : `pipeBase.Struct`
569 Result of photo peeking.
570 Struct containing:
571 - mode : `str`
572 Peek mode that was run.
573 - binSize : `int`
574 Binning factor used.
575 - binnedSourceCat : `lsst.afw.table.SourceCatalog`
576 Source catalog from the binned exposure.
577 """
578 if binSize is None:
579 binSize = self.config.binSize
580
581 peekResult = self.peek.run(exposure, binSize)
582
583 return pipeBase.Struct(
584 mode="photo",
585 binSize=binSize,
586 binnedSourceCat=peekResult.sourceCat,
587 )
588
589 def getGoodSources(self, binnedSourceCat: afwTable.SourceCatalog) -> np.ndarray:
590 """Perform any filtering on the source catalog.
591
592 Parameters
593 ----------
594 binnedSourceCat : `lsst.afw.table.SourceCatalog`
595 Source catalog from the binned exposure.
596
597 Returns
598 -------
599 goodSourceMask : `numpy.ndarray`
600 Boolean array indicating which sources are good.
601 """
602 # Perform any filtering on the source catalog
603 goodSourceMask = np.ones(len(binnedSourceCat), dtype=bool)
604 return goodSourceMask
605
606
607class PeekSpecTaskConfig(pexConfig.Config):
608 """Config class for the PeekSpecTask."""
609
610 peek = pexConfig.ConfigurableField(
611 target=PeekTask,
612 doc="Peek configuration",
613 ) # type: ignore
614 binSize: pexConfig.Field[int] = pexConfig.Field(
615 dtype=int,
616 default=2,
617 doc="binning factor for exposure",
618 )
619 maxFootprintAspectRatio: pexConfig.Field[int] = pexConfig.Field(
620 dtype=float,
621 default=10.0,
622 doc="Maximum detection footprint aspect ratio to consider as 0th order" " (non-dispersed) light.",
623 )
624
625 def setDefaults(self) -> None:
626 super().setDefaults()
627 # Use bright threshold
628 self.peek.detection.includeThresholdMultiplier = 1.0
629 self.peek.detection.thresholdValue = 500.0
630 # Use a large radius aperture flux for spectra to better identify the
631 # brightest source, which for spectra often has a saturated core.
632 self.peek.measurement.slots.apFlux = "base_CircularApertureFlux_70_0"
633 # Also allow a larger distance to peak for centroiding in case there's
634 # a relatively large saturated region.
635 self.peek.measurement.plugins["base_SdssCentroid"].maxDistToPeak = 15.0
636
637
638class PeekSpecTask(pipeBase.Task):
639 """Peek at a spectroscopic exposure.
640
641 For spec mode, we dramatically increase the detection threshold to avoid
642 creating blends with the long spectra objects that appear in these images.
643 We also change the default aperture flux slot to a larger aperture, which
644 helps overcome challenges with lost flux in the interpolated cores of
645 saturated objects.
646 """
647
648 ConfigClass = PeekSpecTaskConfig
649 config: PeekSpecTaskConfig
650 peek: PeekTask
651 _DefaultName = "peekSpec"
652
653 def __init__(self, config: Any, **kwargs: Any):
654 super().__init__(config=config, **kwargs)
655 self.makeSubtask("peek")
656
657 def run(self, exposure: afwImage.Exposure, binSize: int | None = None) -> pipeBase.Struct:
658 """Peek at spectroscopic exposure.
659
660 Parameters
661 ----------
662 exposure : `lsst.afw.image.Exposure`
663 Exposure at which to peek.
664 binSize : `int`, optional
665 Binning factor for exposure. Default is None, which will use the
666 binning factor from the config.
667
668 Returns
669 -------
670 result : `pipeBase.Struct`
671 Result of spec peeking.
672 Struct containing:
673 - mode : `str`
674 Peek mode that was run.
675 - binSize : `int`
676 Binning factor used.
677 - binnedSourceCat : `lsst.afw.table.SourceCatalog`
678 Source catalog from the binned exposure.
679 """
680 if binSize is None:
681 binSize = self.config.binSize
682
683 peekResult = self.peek.run(exposure, binSize)
684
685 return pipeBase.Struct(
686 mode="spec",
687 binSize=binSize,
688 binnedSourceCat=peekResult.sourceCat,
689 )
690
691 def getGoodSources(self, binnedSourceCat: afwTable.SourceCatalog) -> np.ndarray:
692 """Perform any filtering on the source catalog.
693
694 Parameters
695 ----------
696 binnedSourceCat : `lsst.afw.table.SourceCatalog`
697 Source catalog from the binned exposure.
698
699 Returns
700 -------
701 goodSourceMask : `numpy.ndarray`
702 Boolean array indicating which sources are good.
703 """
704 # Perform any filtering on the source catalog
705 goodSourceMask = np.ones(len(binnedSourceCat), dtype=bool)
706 fpShapes = [src.getFootprint().getShape() for src in binnedSourceCat]
707 # Filter out likely spectrum detections
708 goodSourceMask &= np.array(
709 [sh.getIyy() < self.config.maxFootprintAspectRatio * sh.getIxx() for sh in fpShapes],
710 dtype=np.bool_,
711 )
712 return goodSourceMask
713
714
715class PeekExposureTaskConfig(pexConfig.Config):
716 """Config class for the PeekExposureTask."""
717
718 donutThreshold: pexConfig.Field[float] = pexConfig.Field(
719 dtype=float,
720 default=50.0,
721 doc="Size threshold in pixels for when to switch to donut mode.",
722 )
723 doPhotoFallback: pexConfig.Field[bool] = pexConfig.Field(
724 dtype=bool,
725 default=True,
726 doc="If True, fall back to photo mode if spec mode fails.",
727 )
728 donut = pexConfig.ConfigurableField(
729 target=PeekDonutTask,
730 doc="PeekDonut task",
731 ) # type: ignore
732 photo = pexConfig.ConfigurableField(
733 target=PeekPhotoTask,
734 doc="PeekPhoto task",
735 ) # type: ignore
736 spec = pexConfig.ConfigurableField(
737 target=PeekSpecTask,
738 doc="PeekSpec task",
739 ) # type: ignore
740
741
742class PeekExposureTask(pipeBase.Task):
743 """Peek at exposure to quickly detect and measure both the brightest
744 source in the image, and a set of sources representative of the
745 exposure's overall image quality.
746
747 Parameters
748 ----------
749 config : `lsst.summit.utils.peekExposure.PeekExposureTaskConfig`
750 Configuration for the task.
751 display : `lsst.afw.display.Display`, optional
752 For displaying the exposure and sources.
753
754 Notes
755 -----
756 The basic philosophy of PeekExposureTask is to:
757 1) Classify exposures based on metadata into 'donut', 'spec', or 'photo'.
758 2) Run PeekTask on the exposure through a wrapper with class specific
759 modifications.
760 3) Try only to branch in the code based on the metadata, and not on the
761 data itself. This avoids problematic discontinuities in measurements.
762
763 The main knobs we fiddle with based on the classification are:
764 - Detection threshold
765 - Minimum number of pixels for a detection
766 - Binning of the image
767 - Installed PSF size
768 """
769
770 ConfigClass = PeekExposureTaskConfig
771 config: PeekExposureTaskConfig
772 donut: PeekDonutTask
773 photo: PeekPhotoTask
774 spec: PeekSpecTask
775 _DefaultName = "peekExposureTask"
776
777 def __init__(self, config: Any, *, display: Any = None, **kwargs: Any):
778 super().__init__(config=config, **kwargs)
779
780 self.makeSubtask("donut")
781 self.makeSubtask("photo")
782 self.makeSubtask("spec")
783
784 self._display = display
785
786 def getDonutDiameter(self, exposure: afwImage.Exposure) -> float:
787 """Estimate donut diameter from exposure metadata.
788
789 Parameters
790 ----------
791 exposure : `lsst.afw.image.Exposure`
792 Exposure to estimate donut diameter for.
793
794 Returns
795 -------
796 donutDiameter : `float`
797 Estimated donut diameter in pixels.
798 """
799 visitInfo = exposure.getInfo().getVisitInfo()
800 focusZ = visitInfo.focusZ
801 instrumentLabel = visitInfo.instrumentLabel
802
803 match instrumentLabel:
804 case "LATISS":
805 focusZ *= 41 # magnification factor
806 fratio = 18.0
807 case "LSSTCam" | "LSSTComCam" | "LSSTComCamSim":
808 fratio = 1.234
809 case _:
810 raise ValueError(f"Unknown instrument label: {instrumentLabel}")
811 # AuxTel/ComCam/LSSTCam all have 10 micron pixels (= 10e-3 mm)
812 donutDiameter = abs(focusZ) / fratio / 10e-3
813 self.log.info(f"{focusZ=} mm")
814 self.log.info(f"donutDiameter = {donutDiameter} pixels")
815 return donutDiameter
816
817 def run(
818 self,
819 exposure: afwImage.Exposure,
820 *,
821 doDisplay: bool = False,
822 doDisplayIndices: bool = False,
823 mode: str = "auto",
824 binSize: int | None = None,
825 donutDiameter: float | None = None,
826 ) -> pipeBase.Struct:
827 """
828 Parameters
829 ----------
830 exposure : `lsst.afw.image.Exposure`
831 Exposure at which to peek.
832 doDisplay : `bool`, optional
833 Display the exposure and sources? Default False. (Requires
834 display to have been passed to task constructor)
835 doDisplayIndices : `bool`, optional
836 Display the source indices? Default False. (Requires display to
837 have been passed to task constructor)
838 mode : {'auto', 'donut', 'spec', 'photo'}, optional
839 Mode to run in. Default 'auto'.
840 binSize : `int`, optional
841 Binning factor for exposure. Default is None, which let's subtasks
842 control rebinning directly.
843 donutDiameter : `float`, optional
844 Donut diameter in pixels. Default is None, which will estimate the
845 donut diameter from the exposure metadata.
846
847 Returns
848 -------
849 result : `pipeBase.Struct`
850 Result of the peek.
851 Struct containing:
852 - mode : `str`
853 Peek mode that was run.
854 - binSize : `int`
855 Binning factor used.
856 - binnedSourceCat : `lsst.afw.table.SourceCatalog`
857 Source catalog from the binned exposure.
858 - table : `astropy.table.Table`
859 Curated source table in unbinned coordinates.
860 - brightestIdx : `int`
861 Index of brightest source in source catalog.
862 - brightestCentroid : `lsst.geom.Point2D`
863 Brightest source centroid in unbinned pixel coords.
864 - brightestPixelShape : `lsst.afw.geom.Quadrupole`
865 Brightest source shape in unbinned pixel coords.
866 - brightestEquatorialShape : `lsst.afw.geom.Quadrupole`
867 Brightest source shape in equitorial coordinates (arcsec).
868 - brightestAltAzShape : `lsst.afw.geom.Quadrupole`
869 Brightest source shape in alt/az coordinates (arcsec).
870 - psfPixelShape : `lsst.afw.geom.Quadrupole`
871 Estimated PSF shape in unbinned pixel coords.
872 - psfEquatorialShape : `lsst.afw.geom.Quadrupole`
873 Estimated PSF shape in equitorial coordinates (arcsec).
874 - psfAltAzShape : `lsst.afw.geom.Quadrupole`
875 Estimated PSF shape in alt/az coordinates (arcsec).
876 - pixelMedian : `float`
877 Median estimate of entire image.
878 - pixelMode : `float`
879 Mode estimate of entire image.
880 """
881 # Make a copy so the original image is unmodified.
882 exposure = exposure.clone()
883 try:
884 result = self._run(exposure, doDisplay, doDisplayIndices, mode, binSize, donutDiameter)
885 except Exception as e:
886 self.log.warning(f"Peek failed: {e}")
887 result = pipeBase.Struct(
888 mode="failed",
889 binSize=0,
890 binnedSourceCat=None,
891 table=None,
892 brightestIdx=0,
893 brightestCentroid=Point2D(np.nan, np.nan),
894 brightestPixelShape=Quadrupole(np.nan, np.nan, np.nan),
895 brightestEquatorialShape=Quadrupole(np.nan, np.nan, np.nan),
896 brightestAltAzShape=Quadrupole(np.nan, np.nan, np.nan),
897 psfPixelShape=Quadrupole(np.nan, np.nan, np.nan),
898 psfEquatorialShape=Quadrupole(np.nan, np.nan, np.nan),
899 psfAltAzShape=Quadrupole(np.nan, np.nan, np.nan),
900 pixelMedian=np.nan,
901 pixelMode=np.nan,
902 )
903 return result
904
905 def _run(
906 self,
907 exposure: afwImage.Exposure,
908 doDisplay: bool,
909 doDisplayIndices: bool,
910 mode: str,
911 binSize: int | None,
912 donutDiameter: float | None,
913 ) -> pipeBase.Struct:
914 """The actual run method, called by run()."""
915 # If image is ~large, then use a subsampling of the image for
916 # speedy median/mode estimates.
917 arr = exposure.getMaskedImage().getImage().array
918 sampling = 1
919 if arr.size > 250_000:
920 sampling = int(np.floor(np.sqrt(arr.size / 250_000)))
921 pixelMedian = np.nanmedian(arr[::sampling, ::sampling])
922 pixelMode = _estimateMode(arr[::sampling, ::sampling])
923
924 if donutDiameter is None:
925 donutDiameter = self.getDonutDiameter(exposure)
926
927 mode, binSize, binnedSourceCat = self.runPeek(exposure, mode, donutDiameter, binSize)
928
929 table = self.transformTable(binSize, binnedSourceCat)
930
931 match mode:
932 case "donut":
933 goodSourceMask = self.donut.getGoodSources(binnedSourceCat)
934 case "spec":
935 goodSourceMask = self.spec.getGoodSources(binnedSourceCat)
936 case "photo":
937 goodSourceMask = self.photo.getGoodSources(binnedSourceCat)
938
939 # prepare output variables
940 maxFluxIdx, brightCentroid, brightShape = self.getBrightest(binnedSourceCat, binSize, goodSourceMask)
941 psfShape = self.getPsfShape(binnedSourceCat, binSize, goodSourceMask)
942
943 equatorialShapes, altAzShapes = self.transformShapes([brightShape, psfShape], exposure, binSize)
944
945 if doDisplay:
946 self.updateDisplay(exposure, binSize, binnedSourceCat, maxFluxIdx, doDisplayIndices)
947
948 return pipeBase.Struct(
949 mode=mode,
950 binSize=binSize,
951 binnedSourceCat=binnedSourceCat,
952 table=table,
953 brightestIdx=maxFluxIdx,
954 brightestCentroid=brightCentroid,
955 brightestPixelShape=brightShape,
956 brightestEquatorialShape=equatorialShapes[0],
957 brightestAltAzShape=altAzShapes[0],
958 psfPixelShape=psfShape,
959 psfEquatorialShape=equatorialShapes[1],
960 psfAltAzShape=altAzShapes[1],
961 pixelMedian=pixelMedian,
962 pixelMode=pixelMode,
963 )
964
966 self,
967 exposure: afwImage.Exposure,
968 mode: str,
969 donutDiameter: float,
970 binSize: int | None = None,
971 ) -> tuple[str, int, afwTable.SourceCatalog]:
972 """Classify exposure and run appropriate PeekTask wrapper.
973
974 Parameters
975 ----------
976 exposure : `lsst.afw.image.Exposure`
977 Exposure to peek.
978 mode : {'auto', 'donut', 'spec', 'photo'}
979 Mode to run in.
980 donutDiameter : `float`
981 Donut diameter in pixels.
982 binSize : `int`, optional
983 Binning factor for exposure. Default is None, which let's subtasks
984 control rebinning directly.
985
986 Returns
987 -------
988 result : `pipeBase.Struct`
989 Result of the peek.
990 Struct containing:
991 - mode : `str`
992 Peek mode that was run.
993 - binSize : `int`
994 Binning factor used.
995 - binnedSourceCat : `lsst.afw.table.SourceCatalog`
996 Source catalog from the binned exposure.
997 """
998 if mode == "auto":
999 # Note, no attempt to handle dispersed donuts. They'll default to
1000 # donut mode.
1001 if donutDiameter > self.config.donutThreshold:
1002 mode = "donut"
1003 else:
1004 if exposure.getInfo().getVisitInfo().instrumentLabel == "LATISS":
1005 # only LATISS images *can* be dispersed, and isDispersedExp
1006 # only works cleanly for LATISS
1007 mode = "spec" if isDispersedExp(exposure) else "photo"
1008 else:
1009 mode = "photo"
1010
1011 match mode:
1012 case "donut":
1013 result = self.donut.run(exposure, donutDiameter, binSize=binSize)
1014 binSizeOut = result.binSize
1015 case "spec":
1016 result = self.spec.run(exposure, binSize=binSize)
1017 binSizeOut = result.binSize
1018 if len(result.binnedSourceCat) == 0:
1019 self.log.warn("No sources found in spec mode.")
1020 if self.config.doPhotoFallback:
1021 self.log.warn("Falling back to photo mode.")
1022 # Note that spec.run already rebinned the image,
1023 # so we don't need to do it again.
1024 result = self.photo.run(exposure, binSize=1)
1025 case "photo":
1026 result = self.photo.run(exposure, binSize=binSize)
1027 binSizeOut = result.binSize
1028 case _:
1029 raise ValueError(f"Unknown mode {mode}")
1030 return result.mode, binSizeOut, result.binnedSourceCat
1031
1032 def transformTable(self, binSize: int, binnedSourceCat: afwTable.SourceCatalog) -> astropy.table.Table:
1033 """Make an astropy table from the source catalog but with
1034 transformations back to the original unbinned coordinates.
1035
1036 Since there's some ambiguity in the apFlux apertures when binning,
1037 we'll only populate the table with the slots columns (slot_apFlux
1038 doesn't indicate an aperture radius). For simplicity, do the same for
1039 centroids and shapes too.
1040
1041 And since we're only copying over the slots_* columns, we remove the
1042 "slots_" part of the column names and lowercase the first remaining
1043 letter.
1044
1045 Parameters
1046 ----------
1047 binSize : `int`
1048 Binning factor used.
1049 binnedSourceCat : `lsst.afw.table.SourceCatalog`
1050 Source catalog from the binned exposure.
1051
1052 Returns
1053 -------
1054 table : `astropy.table.Table`
1055 Curated source table in unbinned coordinates.
1056 """
1057 table = binnedSourceCat.asAstropy()
1058 cols = [n for n in table.colnames if n.startswith("slot")]
1059 table = table[cols]
1060 if "slot_Centroid_x" in cols:
1061 table["slot_Centroid_x"] = binSize * table["slot_Centroid_x"] + (binSize - 1) / 2
1062 table["slot_Centroid_y"] = binSize * table["slot_Centroid_y"] + (binSize - 1) / 2
1063 if "slot_Shape_x" in cols:
1064 table["slot_Shape_x"] = binSize * table["slot_Shape_x"] + (binSize - 1) / 2
1065 table["slot_Shape_y"] = binSize * table["slot_Shape_y"] + (binSize - 1) / 2
1066 table["slot_Shape_xx"] *= binSize**2
1067 table["slot_Shape_xy"] *= binSize**2
1068 table["slot_Shape_yy"] *= binSize**2
1069 # area and npixels are just confusing when binning, so remove.
1070 if "slot_PsfFlux_area" in cols:
1071 del table["slot_PsfFlux_area"]
1072 if "slot_PsfFlux_npixels" in cols:
1073 del table["slot_PsfFlux_npixels"]
1074
1076 [n for n in table.colnames if n.startswith("slot_")],
1077 [n[5:6].lower() + n[6:] for n in table.colnames if n.startswith("slot_")],
1078 )
1079
1080 return table
1081
1083 self, binnedSourceCat: afwTable.SourceCatalog, binSize: int, goodSourceMask: npt.NDArray[np.bool_]
1084 ) -> tuple[int, geom.Point2D, afwGeom.Quadrupole]:
1085 """Find the brightest source in the catalog.
1086
1087 Parameters
1088 ----------
1089 binnedSourceCat : `lsst.afw.table.SourceCatalog`
1090 Source catalog from the binned exposure.
1091 binSize : `int`
1092 Binning factor used.
1093 goodSourceMask : `numpy.ndarray`
1094 Boolean array indicating which sources are good.
1095
1096 Returns
1097 -------
1098 maxFluxIdx : `int`
1099 Index of the brightest source in the catalog.
1100 brightCentroid : `lsst.geom.Point2D`
1101 Centroid of the brightest source (unbinned coords).
1102 brightShape : `lsst.afw.geom.Quadrupole`
1103 Shape of the brightest source (unbinned coords).
1104 """
1105 fluxes = np.array([source.getApInstFlux() for source in binnedSourceCat])
1106 idxs = np.arange(len(binnedSourceCat))
1107
1108 good = goodSourceMask & np.isfinite(fluxes)
1109
1110 if np.sum(good) == 0:
1111 maxFluxIdx = IDX_SENTINEL
1112 brightCentroid = Point2D(np.nan, np.nan)
1113 brightShape = Quadrupole(np.nan, np.nan, np.nan)
1114 return maxFluxIdx, brightCentroid, brightShape
1115
1116 fluxes = fluxes[good]
1117 idxs = idxs[good]
1118 maxFluxIdx = idxs[np.nanargmax(fluxes)]
1119 brightest = binnedSourceCat[maxFluxIdx]
1120
1121 # Convert binned coordinates back to original unbinned
1122 # coordinates
1123 brightX, brightY = brightest.getCentroid()
1124 brightX = binSize * brightX + (binSize - 1) / 2
1125 brightY = binSize * brightY + (binSize - 1) / 2
1126 brightCentroid = Point2D(brightX, brightY)
1127 brightIXX = brightest.getIxx() * binSize**2
1128 brightIXY = brightest.getIxy() * binSize**2
1129 brightIYY = brightest.getIyy() * binSize**2
1130 brightShape = Quadrupole(brightIXX, brightIYY, brightIXY)
1131
1132 return maxFluxIdx, brightCentroid, brightShape
1133
1135 self, binnedSourceCat: afwTable.SourceCatalog, binSize: int, goodSourceMask: npt.NDArray[np.bool_]
1136 ) -> afwGeom.Quadrupole:
1137 """Estimate the modal PSF shape from the sources.
1138
1139 Parameters
1140 ----------
1141 binnedSourceCat : `lsst.afw.table.SourceCatalog`
1142 Source catalog from the binned exposure.
1143 binSize : `int`
1144 Binning factor used.
1145 goodSourceMask : `numpy.ndarray`
1146 Boolean array indicating which sources are good.
1147
1148 Returns
1149 -------
1150 psfShape : `lsst.afw.geom.Quadrupole`
1151 Estimated PSF shape (unbinned coords).
1152 """
1153 fluxes = np.array([source.getApInstFlux() for source in binnedSourceCat])
1154 idxs = np.arange(len(binnedSourceCat))
1155
1156 good = goodSourceMask & np.isfinite(fluxes)
1157
1158 if np.sum(good) == 0:
1159 return Quadrupole(np.nan, np.nan, np.nan)
1160
1161 fluxes = fluxes[good]
1162 idxs = idxs[good]
1163
1164 psfIXX = _estimateMode(np.array([source.getIxx() for source in binnedSourceCat])[goodSourceMask])
1165 psfIYY = _estimateMode(np.array([source.getIyy() for source in binnedSourceCat])[goodSourceMask])
1166 psfIXY = _estimateMode(np.array([source.getIxy() for source in binnedSourceCat])[goodSourceMask])
1167
1168 return Quadrupole(
1169 psfIXX * binSize**2,
1170 psfIYY * binSize**2,
1171 psfIXY * binSize**2,
1172 )
1173
1175 self, shapes: afwGeom.Quadrupole, exposure: afwImage.Exposure, binSize: int
1176 ) -> tuple[list[afwGeom.Quadrupole], list[afwGeom.Quadrupole]]:
1177 """Transform shapes from x/y pixel coordinates to equitorial and
1178 horizon coordinates.
1179
1180 Parameters
1181 ----------
1182 shapes : `list` of `lsst.afw.geom.Quadrupole`
1183 List of shapes (in pixel coordinates) to transform.
1184 exposure : `lsst.afw.image.Exposure`
1185 Exposure containing WCS and VisitInfo for transformation.
1186 binSize : `int`
1187 Binning factor used.
1188
1189 Returns
1190 -------
1191 equatorialShapes : `list` of `lsst.afw.geom.Quadrupole`
1192 List of shapes transformed to equitorial (North and West)
1193 coordinates. Units are arcseconds.
1194 altAzShapes : `list` of `lsst.afw.geom.Quadrupole`
1195 List of shapes transformed to alt/az coordinates. Units are
1196 arcseconds.
1197 """
1198 pt = Point2D(np.array([*exposure.getBBox().getCenter()]) / binSize)
1199 wcs = exposure.wcs
1200 visitInfo = exposure.info.getVisitInfo()
1201 parAngle = visitInfo.boresightParAngle
1202
1203 equatorialShapes = []
1204 altAzShapes = []
1205 for shape in shapes:
1206 if wcs is None:
1207 equatorialShapes.append(Quadrupole(np.nan, np.nan, np.nan))
1208 altAzShapes.append(Quadrupole(np.nan, np.nan, np.nan))
1209 continue
1210 # The WCS transforms to N (dec) and E (ra), but we want N and W to
1211 # conform with weak-lensing conventions. So we flip the [0]
1212 # component of the transformation.
1213 neTransform = wcs.linearizePixelToSky(pt, arcseconds).getLinear()
1214 nwTransform = LinearTransform(np.array([[-1, 0], [0, 1]]) @ neTransform.getMatrix())
1215 equatorialShapes.append(shape.transform(nwTransform))
1216
1217 # To get from N/W to alt/az, we need to additionally rotate by the
1218 # parallactic angle.
1219 rot = LinearTransform.makeRotation(parAngle).getMatrix()
1220 aaTransform = LinearTransform(nwTransform.getMatrix() @ rot)
1221 altAzShapes.append(shape.transform(aaTransform))
1222
1223 return equatorialShapes, altAzShapes
1224
1226 self,
1227 exposure: afwImage.Exposure,
1228 binSize: int,
1229 binnedSourceCat: afwTable.SourceCatalog,
1230 maxFluxIdx: int,
1231 doDisplayIndices: bool,
1232 ) -> None:
1233 """Update the afwDisplay with the exposure and sources.
1234
1235 Parameters
1236 ----------
1237 exposure : `lsst.afw.image.Exposure`
1238 Exposure to peek.
1239 binSize : `int`
1240 Binning factor used.
1241 binnedSourceCat : `lsst.afw.table.SourceCatalog`
1242 Source catalog from the binned exposure.
1243 maxFluxIdx : `int`
1244 Index of the brightest source in the catalog.
1245 doDisplayIndices : `bool`
1246 Display the source indices?
1247 """
1248 if self._display is None:
1249 raise RuntimeError("Display failed as no display provided during init()")
1250
1251 visitInfo = exposure.info.getVisitInfo()
1252 self._display.mtv(exposure)
1253 wcs = exposure.wcs
1254 if wcs is not None:
1255 plotRose(
1256 self._display,
1257 wcs,
1258 Point2D(200 / binSize, 200 / binSize),
1259 parAng=visitInfo.boresightParAngle,
1260 len=100 / binSize,
1261 )
1262
1263 for idx, source in enumerate(binnedSourceCat):
1264 x, y = source.getCentroid()
1265 sh = source.getShape()
1266 self._display.dot(sh, x, y)
1267 if doDisplayIndices:
1268 self._display.dot(str(idx), x, y)
1269
1270 if maxFluxIdx != IDX_SENTINEL:
1271 self._display.dot(
1272 "+",
1273 *binnedSourceCat[maxFluxIdx].getCentroid(),
1274 ctype=afwDisplay.RED,
1275 size=10,
1276 )
int min
A polymorphic base class for representing an image's Point Spread Function.
Definition Psf.h:82
lsst::geom::Box2I computeBBox(lsst::geom::Point2D position, image::Color color=image::Color()) const
Return the bounding box of the image returned by computeKernelImage()
Definition Psf.cc:127
virtual lsst::geom::Point2D getAveragePosition() const
Return the average position of the stars used to construct the Psf.
Definition Psf.cc:189
A 2-dimensional celestial WCS that transform pixels to ICRS RA/Dec, using the LSST standard for pixel...
Definition SkyWcs.h:117
An ellipse core with quadrupole moments as parameters.
Definition Quadrupole.h:47
Describe the colour of a source.
Definition Color.h:25
A class to contain the data, WCS, and other information needed to describe an image of the sky.
Definition Exposure.h:72
Tag types used to declare specialized field types.
Definition misc.h:31
Class for storing ordered metadata with comments.
A class representing an angle.
Definition Angle.h:128
An integer coordinate rectangle.
Definition Box.h:55
A 2D linear coordinate transformation.
Point in an unspecified spherical coordinate system.
Definition SpherePoint.h:57
float _doComputeApertureFlux(self, float radius, Point2D|None position=None, afwImage.Color|None color=None)
ImageD _doComputeKernelImage(self, Point2D|None position=None, afwImage.Color|None color=None)
DonutPsf __deepcopy__(self, Any|None memo=None)
__init__(self, float size, float outerRad, float innerRad)
Box2I _doComputeBBox(self, Point2D|None position=None, afwImage.Color|None color=None)
Quadrupole _doComputeShape(self, Point2D|None position=None, afwImage.Color|None color=None)
DonutPsf resized(self, float width, float height)
pipeBase.Struct run(self, afwImage.Exposure exposure, float donutDiameter, int|None binSize=None)
__init__(self, Any config, **Any kwargs)
np.ndarray getGoodSources(self, afwTable.SourceCatalog binnedSourceCat)
pipeBase.Struct _run(self, afwImage.Exposure exposure, bool doDisplay, bool doDisplayIndices, str mode, int|None binSize, float|None donutDiameter)
float getDonutDiameter(self, afwImage.Exposure exposure)
tuple[int, geom.Point2D, afwGeom.Quadrupole] getBrightest(self, afwTable.SourceCatalog binnedSourceCat, int binSize, npt.NDArray[np.bool_] goodSourceMask)
pipeBase.Struct run(self, afwImage.Exposure exposure, *bool doDisplay=False, bool doDisplayIndices=False, str mode="auto", int|None binSize=None, float|None donutDiameter=None)
__init__(self, Any config, *Any display=None, **Any kwargs)
afwGeom.Quadrupole getPsfShape(self, afwTable.SourceCatalog binnedSourceCat, int binSize, npt.NDArray[np.bool_] goodSourceMask)
tuple[str, int, afwTable.SourceCatalog] runPeek(self, afwImage.Exposure exposure, str mode, float donutDiameter, int|None binSize=None)
astropy.table.Table transformTable(self, int binSize, afwTable.SourceCatalog binnedSourceCat)
None updateDisplay(self, afwImage.Exposure exposure, int binSize, afwTable.SourceCatalog binnedSourceCat, int maxFluxIdx, bool doDisplayIndices)
tuple[list[afwGeom.Quadrupole], list[afwGeom.Quadrupole]] transformShapes(self, afwGeom.Quadrupole shapes, afwImage.Exposure exposure, int binSize)
np.ndarray getGoodSources(self, afwTable.SourceCatalog binnedSourceCat)
__init__(self, Any config, **Any kwargs)
pipeBase.Struct run(self, afwImage.Exposure exposure, int|None binSize=None)
np.ndarray getGoodSources(self, afwTable.SourceCatalog binnedSourceCat)
pipeBase.Struct run(self, afwImage.Exposure exposure, int|None binSize=None)
__init__(self, Any config, **Any kwargs)
__init__(self, Any|None schema=None, **Any kwargs)
pipeBase.Struct run(self, afwImage.Exposure exposure, int|None binSize=None)
SingleFrameMeasurementTask measurement
std::shared_ptr< ImageT > binImage(ImageT const &inImage, int const binX, int const binY, lsst::afw::math::Property const flags=lsst::afw::math::MEAN)
Definition binImage.cc:44
float _estimateMode(npt.NDArray[np.float64] data, float frac=0.5)
dict roseVectors(afwGeom.SkyWcs wcs, geom.Point2D imagePoint, geom.Angle|None parAng=None)
geom.Extent2D _bearingToUnitVector(afwGeom.SkyWcs wcs, geom.Angle bearing, geom.Point2D imagePoint, geom.SpherePoint|None skyPoint=None)
None plotRose(afwDisplay.Display display, afwGeom.SkyWcs wcs, geom.Point2D imagePoint, geom.Angle|None parAng=None, float len=50)