52 """Build an ``exposure`` dimension record from an observation info.
56 obs_info : `astro_metadata_translator.ObservationInfo`
57 Observation info of an image that the image to be converted is
60 Name of the instrument, usually from the quantum data ID.
62 Id of the visit, usually from the quantum data ID. It is also used
63 as the id of the exposure.
64 universe : `lsst.daf.butler.DimensionUniverse`
65 Dimension universe that defines the record's schema, usually
66 ``butlerQC.quantum.dataId.universe``.
70 record : `lsst.daf.butler.DimensionRecord`
71 An ``exposure`` record holding the fields that `lsst.images` reads
72 when it converts a legacy image. Fields it does not read, such as
73 ``dark_time`` and ``target_name``, are `None`.
78 Raised if ``obs_info`` is `None`, which means the image it came from
79 was not in an `lsst.images` format.
83 The instrument and the visit are passed separately because an
84 observation info does not carry them in the form the conversion needs:
85 its ``instrument`` is read from the legacy FITS header rather than from
86 the record, and its ``exposure_id`` is the id that the visit info held.
89 raise ValueError(
"Cannot rebuild an exposure record without an observation info.")
90 azimuth = zenithAngle =
None
91 if obs_info.altaz_begin
is not None:
92 azimuth = obs_info.altaz_begin.az.deg
93 zenithAngle = 90.0 - obs_info.altaz_begin.alt.deg
94 exposureTime = obs_info.exposure_time_requested
95 return universe[
"exposure"].RecordClass(
96 instrument=instrument,
98 obs_id=obs_info.observation_id,
99 group=obs_info.exposure_group,
100 day_obs=obs_info.observing_day,
101 physical_filter=obs_info.physical_filter,
102 exposure_time=
None if exposureTime
is None else exposureTime.to_value(u.s),
103 seq_num=obs_info.observation_counter,
104 seq_start=obs_info.group_counter_start,
105 seq_end=obs_info.group_counter_end,
106 can_see_sky=obs_info.can_see_sky,
108 zenith_angle=zenithAngle,
109 timespan=Timespan(begin=obs_info.datetime_begin, end=obs_info.datetime_end),
114 """Get the PSF matching kernel or the template records of an input
119 image : `lsst.images.DifferenceImage`
120 Input image read by a task in future mode.
121 attribute : {"kernel", "templates"}
122 Name of the attribute to get.
124 Name of the input connection the image was read from.
125 dataId : `lsst.daf.butler.DataCoordinate`
126 Data ID of the quantum, for the error message.
130 value : `lsst.images.convolution_kernels.ConvolutionKernel` or \
131 `list` [`lsst.images.DifferenceImageTemplateInfo`]
132 Value of the attribute.
136 lsst.pipe.base.InvalidQuantumError
137 Raised if the image does not have the attribute, which happens when
138 the input was written as a legacy Exposure and converted on read.
141 return getattr(image, attribute)
142 except AttributeError
as e:
143 what =
"PSF matching kernel" if attribute ==
"kernel" else "template coadd records"
144 raise lsst.pipe.base.InvalidQuantumError(
145 f
"Input {connection!r} for {dataId} has no {what}, which is required for image_type='future'."
146 " This is likely due to reading a legacy Exposure, which lacks this component. To fix,"
147 " reprocess this image starting from `getTemplate`."
152 """Calculate the PSF matching kernel peak offset from the nominal
157 kernel : `~lsst.afw.math.LinearCombinationKernel`
158 The PSF matching kernel to evaluate.
160 The x position on the detector to evaluate the kernel
162 The y position on the detector to evaluate the kernel
163 image : `~lsst.afw.image.ImageD`
164 The image to use as base for computing kernel pixel values
169 The sum of the kernel on the desired location
171 The displacement of the kernel averaged peak, with respect to the
172 center of the extraction of the kernel
174 The displacement of the kernel averaged peak, with respect to the
175 center of the extraction of the kernel
177 The position angle in detector coordinates of the displacement
179 The displacement module of the kernel centroid in pixel units
183 image = afwImage.ImageD(kernel.getDimensions())
186 hsize = kernel.getWidth()//2
187 kernel_sum = kernel.computeImage(image, doNormalize=
False, x=x, y=y)
195 vx = data.sum(axis=0)
197 dx = np.dot(vx, xx) - hsize
199 vy = data.sum(axis=1)
201 dy = np.dot(vy, yy) - hsize
204 pos_angle = np.arctan2(dy, dx)
205 length = np.sqrt(dx**2 + dy**2)
207 return kernel_sum, dx, dy, pos_angle, length
211 """Directly calculate the horizontal and vertical widths
212 of a PSF at half its maximum value.
216 psf : `~lsst.afw.detection.Psf`
217 Point spread function (PSF) to evaluate.
218 average : `bool`, optional
219 Set to return the average width over Y and X axes.
220 position : `~lsst.geom.Point2D`, optional
221 The position at which to evaluate the PSF. If `None`, then the
222 average position is used.
226 psfSize : `float` | `tuple` [`float`]
227 The FWHM of the PSF computed at its average position.
228 Returns the widths along the Y and X axes,
229 or the average of the two if `average` is set.
236 position = psf.getAveragePosition()
237 shape = psf.computeShape(position)
240 return gaussian_sigma_to_fwhm*shape.getTraceRadius()
242 return [gaussian_sigma_to_fwhm*np.sqrt(shape.getIxx()),
243 gaussian_sigma_to_fwhm*np.sqrt(shape.getIyy())]
247 fwhmExposureBuffer: float, fwhmExposureGrid: int) -> float:
248 """Get the mean PSF FWHM by evaluating it on a grid within an exposure.
252 exposure : `~lsst.afw.image.Exposure`
253 The exposure for which the mean FWHM of the PSF is to be computed.
254 The exposure must contain a `psf` attribute.
255 fwhmExposureBuffer : `float`
256 Fractional buffer margin to be left out of all sides of the image
257 during the construction of the grid to compute mean PSF FWHM in an
259 fwhmExposureGrid : `int`
260 Grid size to compute the average PSF FWHM in an exposure (pixels).
265 The mean PSF FWHM on the exposure.
270 Raised if the PSF cannot be computed at any of the grid points.
280 bbox = exposure.getBBox()
281 xmax, ymax = bbox.getMax()
282 xmin, ymin = bbox.getMin()
284 xbuffer = fwhmExposureBuffer*(xmax-xmin)
285 ybuffer = fwhmExposureBuffer*(ymax-ymin)
288 for (x, y)
in itertools.product(np.linspace(xmin+xbuffer, xmax-xbuffer, fwhmExposureGrid),
289 np.linspace(ymin+ybuffer, ymax-ybuffer, fwhmExposureGrid)
293 fwhm =
getPsfFwhm(psf, average=
True, position=pos)
294 except (InvalidParameterError, RangeError):
295 _LOG.debug(
"Unable to compute PSF FWHM at position (%f, %f).", x, y)
301 raise ValueError(
"Unable to compute PSF FWHM at any position on the exposure.")
303 return np.nanmean(width)
307 psfExposureBuffer: float, psfExposureGrid: int) -> afwImage.ImageD:
308 """Get the average PSF by evaluating it on a grid within an exposure.
312 exposure : `~lsst.afw.image.Exposure`
313 The exposure for which the average PSF is to be computed.
314 The exposure must contain a `psf` attribute.
315 psfExposureBuffer : `float`
316 Fractional buffer margin to be left out of all sides of the image
317 during the construction of the grid to compute average PSF in an
319 psfExposureGrid : `int`
320 Grid size to compute the average PSF in an exposure.
324 psfImage : `~lsst.afw.image.Image`
325 The average PSF across the exposure.
330 Raised if the PSF cannot be computed at any of the grid points.
334 `evaluateMeanPsfFwhm`
339 bbox = exposure.getBBox()
340 xmax, ymax = bbox.getMax()
341 xmin, ymin = bbox.getMin()
343 xbuffer = psfExposureBuffer*(xmax-xmin)
344 ybuffer = psfExposureBuffer*(ymax-ymin)
348 for (x, y)
in itertools.product(np.linspace(xmin+xbuffer, xmax-xbuffer, psfExposureGrid),
349 np.linspace(ymin+ybuffer, ymax-ybuffer, psfExposureGrid)
353 singleImage = psf.computeKernelImage(pos)
354 except InvalidParameterError:
355 _LOG.debug(
"Unable to compute PSF image at position (%f, %f).", x, y)
359 psfArray = singleImage.array
361 psfArray += singleImage.array
365 raise ValueError(
"Unable to compute PSF image at any position on the exposure.")
367 psfImage = afwImage.ImageD(psfArray/nImg)
372 """Calculate a robust mean of the variance plane of an exposure.
376 image : `lsst.afw.image.Image`
377 Image or variance plane of an exposure to evaluate.
378 mask : `lsst.afw.image.Mask`
379 Mask plane to use for excluding pixels.
380 statsCtrl : `lsst.afw.math.StatisticsControl`
381 Statistics control object for configuring the calculation.
382 statistic : `lsst.afw.math.Property`, optional
383 The type of statistic to compute. Typical values are
384 ``afwMath.MEANCLIP`` or ``afwMath.STDEVCLIP``.
389 The result of the statistic calculated from the unflagged pixels.
392 return statObj.getValue(statistic)
448 r"""Compute quality metrics (saved to the task metadata) on the
449 difference image, at the locations of detected stars on the science
450 image. This restricts the metric to locations that should be
455 science : `lsst.afw.image.ExposureF`
456 Science exposure that was subtracted.
457 difference : `lsst.afw.image.ExposureF`
458 Result of subtracting template and science.
459 stars : `lsst.afw.table.SourceCatalog`
460 Good calibration sources detected on science image; these
461 footprints are what the metrics are computed on.
465 metrics : `lsst.pipe.base.Struct`
467 ``differenceFootprintRatioMean`` : `float`
468 Mean of the ratio of the absolute value of the difference image
469 (with the mean absolute value of the sky regions on the difference
470 image removed) to the science image, computed in the footprints
471 of stars detected on the science image (the sums below are of the
472 pixels in each star or sky footprint):
473 :math:`\mathrm{mean}_{footprints}((\sum |difference| -
474 \mathrm{mean}(\sum |difference_{sky}|)) / \sum science)`
475 ``differenceFootprintRatioStdev`` : `float`
476 Standard Deviation across footprints of the above ratio.
477 ``differenceFootprintSkyRatioMean`` : `float`
478 Mean of the ratio of the absolute value of sky source regions on
479 the difference image to the science image (the sum below is of the
480 pixels in each sky source footprint):
481 :math:`\mathrm{mean}_{footprints}(\sum |difference_{sky}| / \sum science_{sky})`
482 ``differenceFootprintSkyRatioStdev`` : `float`
483 Standard Deivation across footprints of the above sky ratio.
485 def footprint_mean(sources, sky=0):
486 """Compute ratio of the absolute value of the diffim to the science
487 image, within each source footprint, subtracting the sky from the
488 diffim values if provided.
491 science_footprints = np.zeros(n)
492 difference_footprints = np.zeros(n)
494 for i, record
in enumerate(sources):
495 footprint = record.getFootprint()
496 heavy = afwDetection.makeHeavyFootprint(footprint, science.maskedImage)
497 heavy_diff = afwDetection.makeHeavyFootprint(footprint, difference.maskedImage)
498 science_footprints[i] = abs(heavy.getImageArray()).mean()
499 difference_footprints[i] = abs(heavy_diff.getImageArray()).mean()
500 ratio[i] = abs((difference_footprints[i] - sky)/science_footprints[i])
501 return science_footprints, difference_footprints, ratio
503 if "sky_source" in stars.schema:
504 sky = stars[
"sky_source"]
505 selectStars = stars[~sky]
506 if sky_sources
is None:
507 sky_sources = stars[sky]
511 if sky_sources
is not None and len(sky_sources) > 0:
512 sky_science, sky_difference, sky_ratio = footprint_mean(sky_sources)
513 sky_mean = sky_ratio.mean()
514 sky_std = sky_ratio.std()
515 sky_difference = sky_difference.mean()
519 sky_difference = np.nanmedian(np.abs(difference.image.array))
520 science_footprints, difference_footprints, ratio = footprint_mean(selectStars, sky_difference)
521 return lsst.pipe.base.Struct(differenceFootprintRatioMean=ratio.mean(),
522 differenceFootprintRatioStdev=ratio.std(),
523 differenceFootprintSkyRatioMean=sky_mean,
524 differenceFootprintSkyRatioStdev=sky_std,
529 """Populate a cache of predicted satellite positions in the sattle service.
533 visit_info: `lsst.afw.table.ExposureRecord.visitInfo`
534 Visit info for the science exposure being processed.
536 Set to True if observations are older than the current day.
541 Raised if sattle call does not return success.
544 visit_mjd = visit_info.getDate().toAstropy().mjd
546 exposure_time_days = visit_info.getExposureTime() / 86400.0
547 exposure_end_mjd = visit_mjd + exposure_time_days / 2.0
548 exposure_start_mjd = visit_mjd - exposure_time_days / 2.0
550 boresight_ra = visit_info.boresightRaDec.getRa().asDegrees()
551 boresight_dec = visit_info.boresightRaDec.getDec().asDegrees()
554 f
'{os.getenv("SATTLE_URI_BASE")}/visit_cache',
555 json={
"visit_id": visit_info.getId(),
556 "exposure_start_mjd": exposure_start_mjd,
557 "exposure_end_mjd": exposure_end_mjd,
558 "boresight_ra": boresight_ra,
559 "boresight_dec": boresight_dec,
560 "historical": historical})
566 """Exclude sources that have masked pixels in their footprints.
570 mask : `lsst.afw.image.Mask`
571 The image mask plane to use to reject sources
572 based on the location of their centroid on the ccd.
573 sources : `lsst.afw.table.SourceCatalog`
574 The source catalog to evaluate.
575 excludeMaskPlanes : `list` of `str`
576 List of the names of the mask planes to exclude.
580 good : `numpy.ndarray` of `bool`
581 Array indicating whether each source in the catalog should be
582 kept (True) or rejected (False) based on the value of the
583 mask plane at its location.
585 setExcludeMaskPlanes = [
586 maskPlane
for maskPlane
in excludeMaskPlanes
if maskPlane
in mask.getMaskPlaneDict()
589 excludePixelMask = mask.getPlaneBitMask(setExcludeMaskPlanes)
591 good = np.ones(len(sources), dtype=bool)
592 for i, source
in enumerate(sources):
593 bbox = source.getFootprint().getBBox()
594 if not mask.getBBox().contains(bbox):
599 if (mask.subset(bbox).array & excludePixelMask).any():
Statistics makeStatistics(lsst::afw::image::Image< Pixel > const &img, lsst::afw::image::Mask< image::MaskPixel > const &msk, int const flags, StatisticsControl const &sctrl=StatisticsControl())