25 "attachTransmissionCurve",
27 "brighterFatterCorrection",
36 "illuminationCorrection",
37 "interpolateDefectList",
38 "interpolateFromMask",
40 "saturationCorrection",
42 "transposeMaskedImage",
43 "trimToMatchCalibBBox",
45 "widenSaturationTrails",
60from contextlib
import contextmanager
62from .defects
import Defects
66 """Make a double Gaussian PSF.
71 FWHM of double Gaussian smoothing kernel.
76 The created smoothing kernel.
78 ksize = 4*int(fwhm) + 1
79 return measAlg.DoubleGaussianPsf(ksize, ksize, fwhm/(2*math.sqrt(2*math.log(2))))
83 """Make a transposed copy of a masked image.
93 The transposed copy of the input image.
95 transposed = maskedImage.Factory(lsst.geom.Extent2I(maskedImage.getHeight(), maskedImage.getWidth()))
96 transposed.getImage().getArray()[:] = maskedImage.getImage().getArray().T
97 transposed.getMask().getArray()[:] = maskedImage.getMask().getArray().T
98 transposed.getVariance().getArray()[:] = maskedImage.getVariance().getArray().T
103 """Interpolate over defects specified in a defect list.
109 defectList : `lsst.meas.algorithms.Defects`
110 List of defects to interpolate over.
112 FWHM of double Gaussian smoothing kernel.
113 fallbackValue : scalar, optional
114 Fallback value if an interpolated value cannot be determined.
115 If
None, then the clipped mean of the image
is used.
118 if fallbackValue
is None:
120 if 'INTRP' not in maskedImage.getMask().getMaskPlaneDict():
121 maskedImage.getMask().addMaskPlane(
'INTRP')
122 measAlg.interpolateOverDefects(maskedImage, psf, defectList, fallbackValue,
True)
126def makeThresholdMask(maskedImage, threshold, growFootprints=1, maskName='SAT'):
127 """Mask pixels based on threshold detection.
132 Image to process. Only the mask plane is updated.
135 growFootprints : scalar, optional
136 Number of pixels to grow footprints of detected regions.
137 maskName : str, optional
138 Mask plane name,
or list of names to convert
142 defectList : `lsst.meas.algorithms.Defects`
143 Defect list constructed
from pixels above the threshold.
146 thresh = afwDetection.Threshold(threshold)
147 fs = afwDetection.FootprintSet(maskedImage, thresh)
149 if growFootprints > 0:
150 fs = afwDetection.FootprintSet(fs, rGrow=growFootprints, isotropic=
False)
151 fpList = fs.getFootprints()
154 mask = maskedImage.getMask()
155 bitmask = mask.getPlaneBitMask(maskName)
156 afwDetection.setMaskFromFootprintList(mask, fpList, bitmask)
158 return Defects.fromFootprintList(fpList)
161def growMasks(mask, radius=0, maskNameList=['BAD'], maskValue="BAD"):
162 """Grow a mask by an amount and add to the requested plane.
167 Mask image to process.
169 Amount to grow the mask.
170 maskNameList : `str` or `list` [`str`]
171 Mask names that should be grown.
173 Mask plane to assign the newly masked pixels to.
176 thresh = afwDetection.Threshold(mask.getPlaneBitMask(maskNameList), afwDetection.Threshold.BITMASK)
177 fpSet = afwDetection.FootprintSet(mask, thresh)
178 fpSet = afwDetection.FootprintSet(fpSet, rGrow=radius, isotropic=
False)
179 fpSet.setMask(mask, maskValue)
183 maskNameList=['SAT'], fallbackValue=None):
184 """Interpolate over defects identified by a particular set of mask planes.
191 FWHM of double Gaussian smoothing kernel.
192 growSaturatedFootprints : scalar, optional
193 Number of pixels to grow footprints for saturated pixels.
194 maskNameList : `List` of `str`, optional
196 fallbackValue : scalar, optional
197 Value of last resort
for interpolation.
199 mask = maskedImage.getMask()
201 if growSaturatedFootprints > 0
and "SAT" in maskNameList:
205 growMasks(mask, radius=growSaturatedFootprints, maskNameList=[
'SAT'], maskValue=
"SAT")
207 thresh = afwDetection.Threshold(mask.getPlaneBitMask(maskNameList), afwDetection.Threshold.BITMASK)
208 fpSet = afwDetection.FootprintSet(mask, thresh)
209 defectList = Defects.fromFootprintList(fpSet.getFootprints())
218 """Mark saturated pixels and optionally interpolate over them
225 Saturation level used as the detection threshold.
227 FWHM of double Gaussian smoothing kernel.
228 growFootprints : scalar, optional
229 Number of pixels to grow footprints of detected regions.
230 interpolate : Bool, optional
231 If
True, saturated pixels are interpolated over.
232 maskName : str, optional
234 fallbackValue : scalar, optional
235 Value of last resort
for interpolation.
237 defectList = makeThresholdMask(
238 maskedImage=maskedImage,
239 threshold=saturation,
240 growFootprints=growFootprints,
250 """Compute number of edge trim pixels to match the calibration data.
252 Use the dimension difference between the raw exposure and the
253 calibration exposure to compute the edge trim pixels. This trim
254 is applied symmetrically,
with the same number of pixels masked on
262 Calibration image to draw new bounding box
from.
267 ``rawMaskedImage`` trimmed to the appropriate size.
272 Raised
if ``rawMaskedImage`` cannot be symmetrically trimmed to
273 match ``calibMaskedImage``.
275 nx, ny = rawMaskedImage.getBBox().getDimensions() - calibMaskedImage.getBBox().getDimensions()
277 raise RuntimeError(
"Raw and calib maskedImages are trimmed differently in X and Y.")
279 raise RuntimeError(
"Calibration maskedImage is trimmed unevenly in X.")
281 raise RuntimeError(
"Calibration maskedImage is larger than raw data.")
285 replacementMaskedImage = rawMaskedImage[nEdge:-nEdge, nEdge:-nEdge, afwImage.LOCAL]
286 SourceDetectionTask.setEdgeBits(
288 replacementMaskedImage.getBBox(),
289 rawMaskedImage.getMask().getPlaneBitMask(
"EDGE")
292 replacementMaskedImage = rawMaskedImage
294 return replacementMaskedImage
298 """Apply bias correction in place.
303 Image to process. The image is modified by this method.
305 Bias image of the same size
as ``maskedImage``
306 trimToFit : `Bool`, optional
307 If
True, raw data
is symmetrically trimmed to match
313 Raised
if ``maskedImage``
and ``biasMaskedImage`` do
not have
320 if maskedImage.getBBox(afwImage.LOCAL) != biasMaskedImage.getBBox(afwImage.LOCAL):
321 raise RuntimeError(
"maskedImage bbox %s != biasMaskedImage bbox %s" %
322 (maskedImage.getBBox(afwImage.LOCAL), biasMaskedImage.getBBox(afwImage.LOCAL)))
323 maskedImage -= biasMaskedImage
326def darkCorrection(maskedImage, darkMaskedImage, expScale, darkScale, invert=False, trimToFit=False):
327 """Apply dark correction in place.
332 Image to process. The image is modified by this method.
334 Dark image of the same size
as ``maskedImage``.
336 Dark exposure time
for ``maskedImage``.
338 Dark exposure time
for ``darkMaskedImage``.
339 invert : `Bool`, optional
340 If
True, re-add the dark to an already corrected image.
341 trimToFit : `Bool`, optional
342 If
True, raw data
is symmetrically trimmed to match
348 Raised
if ``maskedImage``
and ``darkMaskedImage`` do
not have
353 The dark correction
is applied by calculating:
354 maskedImage -= dark * expScaling / darkScaling
359 if maskedImage.getBBox(afwImage.LOCAL) != darkMaskedImage.getBBox(afwImage.LOCAL):
360 raise RuntimeError(
"maskedImage bbox %s != darkMaskedImage bbox %s" %
361 (maskedImage.getBBox(afwImage.LOCAL), darkMaskedImage.getBBox(afwImage.LOCAL)))
363 scale = expScale / darkScale
365 maskedImage.scaledMinus(scale, darkMaskedImage)
367 maskedImage.scaledPlus(scale, darkMaskedImage)
371 """Set the variance plane based on the image plane.
376 Image to process. The variance plane is modified.
378 The amplifier gain
in electrons/ADU.
380 The amplifier read nmoise
in ADU/pixel.
382 var = maskedImage.getVariance()
383 var[:] = maskedImage.getImage()
388def flatCorrection(maskedImage, flatMaskedImage, scalingType, userScale=1.0, invert=False, trimToFit=False):
389 """Apply flat correction in place.
394 Image to process. The image is modified.
396 Flat image of the same size
as ``maskedImage``
398 Flat scale computation method. Allowed values are
'MEAN',
400 userScale : scalar, optional
401 Scale to use
if ``scalingType=
'USER'``.
402 invert : `Bool`, optional
403 If
True, unflatten an already flattened image.
404 trimToFit : `Bool`, optional
405 If
True, raw data
is symmetrically trimmed to match
411 Raised
if ``maskedImage``
and ``flatMaskedImage`` do
not have
412 the same size
or if ``scalingType``
is not an allowed value.
417 if maskedImage.getBBox(afwImage.LOCAL) != flatMaskedImage.getBBox(afwImage.LOCAL):
418 raise RuntimeError(
"maskedImage bbox %s != flatMaskedImage bbox %s" %
419 (maskedImage.getBBox(afwImage.LOCAL), flatMaskedImage.getBBox(afwImage.LOCAL)))
425 if scalingType
in (
'MEAN',
'MEDIAN'):
428 elif scalingType ==
'USER':
429 flatScale = userScale
431 raise RuntimeError(
'%s : %s not implemented' % (
"flatCorrection", scalingType))
434 maskedImage.scaledDivides(1.0/flatScale, flatMaskedImage)
436 maskedImage.scaledMultiplies(1.0/flatScale, flatMaskedImage)
440 """Apply illumination correction in place.
445 Image to process. The image is modified.
447 Illumination correction image of the same size
as ``maskedImage``.
449 Scale factor
for the illumination correction.
450 trimToFit : `Bool`, optional
451 If
True, raw data
is symmetrically trimmed to match
457 Raised
if ``maskedImage``
and ``illumMaskedImage`` do
not have
463 if maskedImage.getBBox(afwImage.LOCAL) != illumMaskedImage.getBBox(afwImage.LOCAL):
464 raise RuntimeError(
"maskedImage bbox %s != illumMaskedImage bbox %s" %
465 (maskedImage.getBBox(afwImage.LOCAL), illumMaskedImage.getBBox(afwImage.LOCAL)))
467 maskedImage.scaledDivides(1.0/illumScale, illumMaskedImage)
471 """Apply brighter fatter correction in place for the image.
476 Exposure to have brighter-fatter correction applied. Modified
478 kernel : `numpy.ndarray`
479 Brighter-fatter kernel to apply.
481 Number of correction iterations to run.
483 Convergence threshold in terms of the sum of absolute
484 deviations between an iteration
and the previous one.
486 If
True, then the exposure values are scaled by the gain prior
488 gains : `dict` [`str`, `float`]
489 A dictionary, keyed by amplifier name, of the gains to use.
490 If gains
is None, the nominal gains
in the amplifier object are used.
495 Final difference between iterations achieved
in correction.
497 Number of iterations used to calculate correction.
501 This correction takes a kernel that has been derived
from flat
502 field images to redistribute the charge. The gradient of the
503 kernel
is the deflection field due to the accumulated charge.
505 Given the original image I(x)
and the kernel K(x) we can compute
506 the corrected image Ic(x) using the following equation:
508 Ic(x) = I(x) + 0.5*d/dx(I(x)*d/dx(int( dy*K(x-y)*I(y))))
510 To evaluate the derivative term we expand it
as follows:
512 0.5 * ( d/dx(I(x))*d/dx(int(dy*K(x-y)*I(y)))
513 + I(x)*d^2/dx^2(int(dy* K(x-y)*I(y))) )
515 Because we use the measured counts instead of the incident counts
516 we apply the correction iteratively to reconstruct the original
517 counts
and the correction. We stop iterating when the summed
518 difference between the current corrected image
and the one
from
519 the previous iteration
is below the threshold. We do
not require
520 convergence because the number of iterations
is too large a
521 computational cost. How we define the threshold still needs to be
522 evaluated, the current default was shown to work reasonably well
523 on a small set of images. For more information on the method see
524 DocuShare Document-19407.
526 The edges
as defined by the kernel are
not corrected because they
527 have spurious values due to the convolution.
529 image = exposure.getMaskedImage().getImage()
532 with gainContext(exposure, image, applyGain, gains):
534 kLx = numpy.shape(kernel)[0]
535 kLy = numpy.shape(kernel)[1]
536 kernelImage = afwImage.ImageD(kLx, kLy)
537 kernelImage.getArray()[:, :] = kernel
538 tempImage = image.clone()
540 nanIndex = numpy.isnan(tempImage.getArray())
541 tempImage.getArray()[nanIndex] = 0.
543 outImage = afwImage.ImageF(image.getDimensions())
544 corr = numpy.zeros_like(image.getArray())
545 prev_image = numpy.zeros_like(image.getArray())
559 for iteration
in range(maxIter):
562 tmpArray = tempImage.getArray()
563 outArray = outImage.getArray()
565 with numpy.errstate(invalid=
"ignore", over=
"ignore"):
567 gradTmp = numpy.gradient(tmpArray[startY:endY, startX:endX])
568 gradOut = numpy.gradient(outArray[startY:endY, startX:endX])
569 first = (gradTmp[0]*gradOut[0] + gradTmp[1]*gradOut[1])[1:-1, 1:-1]
572 diffOut20 = numpy.diff(outArray, 2, 0)[startY:endY, startX + 1:endX - 1]
573 diffOut21 = numpy.diff(outArray, 2, 1)[startY + 1:endY - 1, startX:endX]
574 second = tmpArray[startY + 1:endY - 1, startX + 1:endX - 1]*(diffOut20 + diffOut21)
576 corr[startY + 1:endY - 1, startX + 1:endX - 1] = 0.5*(first + second)
578 tmpArray[:, :] = image.getArray()[:, :]
579 tmpArray[nanIndex] = 0.
580 tmpArray[startY:endY, startX:endX] += corr[startY:endY, startX:endX]
583 diff = numpy.sum(numpy.abs(prev_image - tmpArray))
587 prev_image[:, :] = tmpArray[:, :]
589 image.getArray()[startY + 1:endY - 1, startX + 1:endX - 1] += \
590 corr[startY + 1:endY - 1, startX + 1:endX - 1]
592 return diff, iteration
596def gainContext(exp, image, apply, gains=None):
597 """Context manager that applies and removes gain.
602 Exposure to apply/remove gain.
604 Image to apply/remove gain.
606 If True, apply
and remove the amplifier gain.
607 gains : `dict` [`str`, `float`]
608 A dictionary, keyed by amplifier name, of the gains to use.
609 If gains
is None, the nominal gains
in the amplifier object are used.
614 Exposure
with the gain applied.
618 if gains
and apply
is True:
619 ampNames = [amp.getName()
for amp
in exp.getDetector()]
620 for ampName
in ampNames:
621 if ampName
not in gains.keys():
622 raise RuntimeError(f
"Gains provided to gain context, but no entry found for amp {ampName}")
625 ccd = exp.getDetector()
627 sim = image.Factory(image, amp.getBBox())
629 gain = gains[amp.getName()]
638 ccd = exp.getDetector()
640 sim = image.Factory(image, amp.getBBox())
642 gain = gains[amp.getName()]
649 sensorTransmission=None, atmosphereTransmission=None):
650 """Attach a TransmissionCurve to an Exposure, given separate curves for
651 different components.
656 Exposure object to modify by attaching the product of all given
657 ``TransmissionCurves`` in post-assembly trimmed detector coordinates.
658 Must have a valid ``Detector`` attached that matches the detector
659 associated
with sensorTransmission.
661 A ``TransmissionCurve`` that represents the throughput of the optics,
662 to be evaluated
in focal-plane coordinates.
664 A ``TransmissionCurve`` that represents the throughput of the filter
665 itself, to be evaluated
in focal-plane coordinates.
667 A ``TransmissionCurve`` that represents the throughput of the sensor
668 itself, to be evaluated
in post-assembly trimmed detector coordinates.
670 A ``TransmissionCurve`` that represents the throughput of the
671 atmosphere, assumed to be spatially constant.
676 The TransmissionCurve attached to the exposure.
680 All ``TransmissionCurve`` arguments are optional;
if none are provided, the
681 attached ``TransmissionCurve`` will have unit transmission everywhere.
683 combined = afwImage.TransmissionCurve.makeIdentity()
684 if atmosphereTransmission
is not None:
685 combined *= atmosphereTransmission
686 if opticsTransmission
is not None:
687 combined *= opticsTransmission
688 if filterTransmission
is not None:
689 combined *= filterTransmission
690 detector = exposure.getDetector()
691 fpToPix = detector.getTransform(fromSys=camGeom.FOCAL_PLANE,
692 toSys=camGeom.PIXELS)
693 combined = combined.transformedBy(fpToPix)
694 if sensorTransmission
is not None:
695 combined *= sensorTransmission
696 exposure.getInfo().setTransmissionCurve(combined)
700def applyGains(exposure, normalizeGains=False, ptcGains=None):
701 """Scale an exposure by the amplifier gains.
706 Exposure to process. The image is modified.
707 normalizeGains : `Bool`, optional
708 If
True, then amplifiers are scaled to force the median of
709 each amplifier to equal the median of those medians.
710 ptcGains : `dict`[`str`], optional
711 Dictionary keyed by amp name containing the PTC gains.
713 ccd = exposure.getDetector()
714 ccdImage = exposure.getMaskedImage()
718 sim = ccdImage.Factory(ccdImage, amp.getBBox())
720 sim *= ptcGains[amp.getName()]
725 medians.append(numpy.median(sim.getImage().getArray()))
728 median = numpy.median(numpy.array(medians))
729 for index, amp
in enumerate(ccd):
730 sim = ccdImage.Factory(ccdImage, amp.getBBox())
731 if medians[index] != 0.0:
732 sim *= median/medians[index]
736 """Grow the saturation trails by an amount dependent on the width of the
742 Mask which will have the saturated areas grown.
746 for i
in range(1, 6):
748 for i
in range(6, 8):
750 for i
in range(8, 10):
754 if extraGrowMax <= 0:
757 saturatedBit = mask.getPlaneBitMask(
"SAT")
759 xmin, ymin = mask.getBBox().getMin()
760 width = mask.getWidth()
762 thresh = afwDetection.Threshold(saturatedBit, afwDetection.Threshold.BITMASK)
763 fpList = afwDetection.FootprintSet(mask, thresh).getFootprints()
766 for s
in fp.getSpans():
767 x0, x1 = s.getX0(), s.getX1()
769 extraGrow = extraGrowDict.get(x1 - x0 + 1, extraGrowMax)
772 x0 -= xmin + extraGrow
773 x1 -= xmin - extraGrow
780 mask.array[y, x0:x1+1] |= saturatedBit
784 """Set all BAD areas of the chip to the average of the rest of the exposure
789 Exposure to mask. The exposure mask is modified.
790 badStatistic : `str`, optional
791 Statistic to use to generate the replacement value
from the
792 image data. Allowed values are
'MEDIAN' or 'MEANCLIP'.
796 badPixelCount : scalar
797 Number of bad pixels masked.
798 badPixelValue : scalar
799 Value substituted
for bad pixels.
804 Raised
if `badStatistic`
is not an allowed value.
806 if badStatistic ==
"MEDIAN":
807 statistic = afwMath.MEDIAN
808 elif badStatistic ==
"MEANCLIP":
809 statistic = afwMath.MEANCLIP
811 raise RuntimeError(
"Impossible method %s of bad region correction" % badStatistic)
813 mi = exposure.getMaskedImage()
815 BAD = mask.getPlaneBitMask(
"BAD")
816 INTRP = mask.getPlaneBitMask(
"INTRP")
819 sctrl.setAndMask(BAD)
822 maskArray = mask.getArray()
823 imageArray = mi.getImage().getArray()
824 badPixels = numpy.logical_and((maskArray & BAD) > 0, (maskArray & INTRP) == 0)
825 imageArray[:] = numpy.where(badPixels, value, imageArray)
827 return badPixels.sum(), value
830def checkFilter(exposure, filterList, log):
831 """Check to see if an exposure is in a filter specified by a list.
833 The goal of this is to provide a unified filter checking interface
834 for all filter dependent stages.
840 filterList : `list` [`str`]
841 List of physical_filter names to check.
842 log : `logging.Logger`
843 Logger to handle messages.
848 True if the exposure
's filter is contained in the list.
850 if len(filterList) == 0:
852 thisFilter = exposure.getFilter()
853 if thisFilter
is None:
854 log.warning(
"No FilterLabel attached to this exposure!")
858 if thisPhysicalFilter
in filterList:
860 elif thisFilter.bandLabel
in filterList:
862 log.warning(
"Physical filter (%s) should be used instead of band %s for filter configurations"
863 " (%s)", thisPhysicalFilter, thisFilter.bandLabel, filterList)
870 """Get the physical filter label associated with the given filterLabel.
872 If ``filterLabel`` is `
None`
or there
is no physicalLabel attribute
873 associated
with the given ``filterLabel``, the returned label will be
880 physical filter label.
881 log : `logging.Logger`
882 Logger to handle messages.
886 physicalFilter : `str`
887 The value returned by the physicalLabel attribute of ``filterLabel``
if
888 it exists, otherwise set to \
"Unknown\".
890 if filterLabel
is None:
891 physicalFilter =
"Unknown"
892 log.warning(
"filterLabel is None. Setting physicalFilter to \"Unknown\".")
895 physicalFilter = filterLabel.physicalLabel
897 log.warning(
"filterLabel has no physicalLabel attribute. Setting physicalFilter to \"Unknown\".")
898 physicalFilter =
"Unknown"
899 return physicalFilter
902def countMaskedPixels(maskedIm, maskPlane):
903 """Count the number of pixels in a given mask plane.
908 Masked image to examine.
910 Name of the mask plane to examine.
915 Number of pixels in the requested mask plane.
917 maskBit = maskedIm.mask.getPlaneBitMask(maskPlane)
918 nPix = numpy.where(numpy.bitwise_and(maskedIm.mask.array, maskBit))[0].flatten().size
A class to contain the data, WCS, and other information needed to describe an image of the sky.
A group of labels for a filter in an exposure or coadd.
A class to represent a 2-dimensional array of pixels.
Represent a 2-dimensional array of bitmask pixels.
A class to manipulate images, masks, and variance as a single object.
A spatially-varying transmission curve as a function of wavelength.
Parameters to control convolution.
A kernel created from an Image.
Pass parameters to a Statistics object.
Represent a Psf as a circularly symmetrical double Gaussian.
Statistics makeStatistics(lsst::afw::image::Image< Pixel > const &img, lsst::afw::image::Mask< image::MaskPixel > const &msk, int const flags, StatisticsControl const &sctrl=StatisticsControl())
Handle a watered-down front-end to the constructor (no variance)
void convolve(OutImageT &convolvedImage, InImageT const &inImage, KernelT const &kernel, ConvolutionControl const &convolutionControl=ConvolutionControl())
Convolve an Image or MaskedImage with a Kernel, setting pixels of an existing output image.
Property stringToStatisticsProperty(std::string const property)
Conversion function to switch a string to a Property (see Statistics.h)
def biasCorrection(maskedImage, biasMaskedImage, trimToFit=False)
def growMasks(mask, radius=0, maskNameList=['BAD'], maskValue="BAD")
def darkCorrection(maskedImage, darkMaskedImage, expScale, darkScale, invert=False, trimToFit=False)
def saturationCorrection(maskedImage, saturation, fwhm, growFootprints=1, interpolate=True, maskName='SAT', fallbackValue=None)
def interpolateDefectList(maskedImage, defectList, fwhm, fallbackValue=None)
def illuminationCorrection(maskedImage, illumMaskedImage, illumScale, trimToFit=True)
def brighterFatterCorrection(exposure, kernel, maxIter, threshold, applyGain, gains=None)
def setBadRegions(exposure, badStatistic="MEDIAN")
def applyGains(exposure, normalizeGains=False, ptcGains=None)
def getPhysicalFilter(filterLabel, log)
def trimToMatchCalibBBox(rawMaskedImage, calibMaskedImage)
def updateVariance(maskedImage, gain, readNoise)
def interpolateFromMask(maskedImage, fwhm, growSaturatedFootprints=1, maskNameList=['SAT'], fallbackValue=None)
def transposeMaskedImage(maskedImage)
def flatCorrection(maskedImage, flatMaskedImage, scalingType, userScale=1.0, invert=False, trimToFit=False)
def attachTransmissionCurve(exposure, opticsTransmission=None, filterTransmission=None, sensorTransmission=None, atmosphereTransmission=None)
def widenSaturationTrails(mask)