22__all__ = [
"FringeStatisticsConfig",
"FringeConfig",
"FringeTask"]
33from lsst.utils.timer
import timeMethod
34from .isrFunctions
import checkFilter
36afwDisplay.setDefaultMaskTransparency(75)
40 """Produce a new frame number each time"""
49 """Options for measuring fringes on an exposure"""
50 badMaskPlanes =
ListField(dtype=str, default=[
"SAT"], doc=
"Ignore pixels with these masks")
51 stat =
Field(dtype=int, default=int(afwMath.MEDIAN), doc=
"Statistic to use")
52 clip =
Field(dtype=float, default=3.0, doc=
"Sigma clip threshold")
53 iterations =
Field(dtype=int, default=3, doc=
"Number of fitting iterations")
54 rngSeedOffset =
Field(dtype=int, default=0,
55 doc=
"Offset to the random number generator seed (full seed includes exposure ID)")
59 """Fringe subtraction options"""
61 filters =
ListField(dtype=str, default=[], doc=
"Only fringe-subtract these filters")
63 useFilterAliases =
Field(dtype=bool, default=
False, doc=
"Search filter aliases during check.",
64 deprecated=(
"Removed with no replacement (FilterLabel has no aliases)."
65 "Will be removed after v22."))
66 num =
Field(dtype=int, default=30000, doc=
"Number of fringe measurements")
67 small =
Field(dtype=int, default=3, doc=
"Half-size of small (fringe) measurements (pixels)")
68 large =
Field(dtype=int, default=30, doc=
"Half-size of large (background) measurements (pixels)")
69 iterations =
Field(dtype=int, default=20, doc=
"Number of fitting iterations")
70 clip =
Field(dtype=float, default=3.0, doc=
"Sigma clip threshold")
71 stats =
ConfigField(dtype=FringeStatisticsConfig, doc=
"Statistics for measuring fringes")
72 pedestal =
Field(dtype=bool, default=
False, doc=
"Remove fringe pedestal?")
76 """Task to remove fringes from a science exposure
78 We measure fringe amplitudes at random positions on the science exposure
79 and at the same positions on the (potentially multiple) fringe frames
80 and solve
for the scales simultaneously.
82 ConfigClass = FringeConfig
83 _DefaultName = 'isrFringe'
86 """Pack the fringe data into a Struct.
88 This method moves the struct parsing code into a butler
89 generation agnostic handler.
93 fringeExp : `lsst.afw.exposure.Exposure`
94 The exposure containing the fringe data.
95 expId : `int`, optional
96 Exposure id to be fringe corrected, used to set RNG seed.
98 An instance of AssembleCcdTask (for assembling fringe
103 fringeData : `pipeBase.Struct`
104 Struct containing fringe data:
107 Calibration fringe files containing master fringe frames.
110 Seed
for random number generation. (`int`, optional)
112 if assembler
is not None:
113 fringeExp = assembler.assembleCcd(fringeExp)
116 seed = self.config.stats.rngSeedOffset
118 print(f
"{self.config.stats.rngSeedOffset} {expId}")
119 seed = self.config.stats.rngSeedOffset + expId
125 return Struct(fringes=fringeExp,
129 def run(self, exposure, fringes, seed=None):
130 """Remove fringes from the provided science exposure.
132 Primary method of FringeTask. Fringes are only subtracted if the
133 science exposure has a filter listed
in the configuration.
138 Science exposure
from which to remove fringes.
140 Calibration fringe files containing master fringe frames.
141 seed : `int`, optional
142 Seed
for random number generation.
146 solution : `np.array`
147 Fringe solution amplitudes
for each input fringe frame.
149 RMS error
for the fit solution
for this exposure.
155 self.log.info(
"Filter not found in FringeTaskConfig.filters. Skipping fringe correction.")
159 seed = self.config.stats.rngSeedOffset
160 rng = numpy.random.RandomState(seed=seed)
162 if not hasattr(fringes,
'__iter__'):
165 mask = exposure.getMaskedImage().getMask()
166 for fringe
in fringes:
167 fringe.getMaskedImage().getMask().__ior__(mask)
168 if self.config.pedestal:
172 fluxes = numpy.ndarray([self.config.num, len(fringes)])
173 for i, f
in enumerate(fringes):
174 fluxes[:, i] = self.
measureExposure(f, positions, title=
"Fringe frame")
176 expFringes = self.
measureExposure(exposure, positions, title=
"Science")
177 solution, rms = self.
solve(expFringes, fluxes)
178 self.
subtract(exposure, fringes, solution)
180 afwDisplay.Display(frame=
getFrame()).mtv(exposure, title=
"Fringe subtracted")
183 def checkFilter(self, exposure):
184 """Check whether we should fringe-subtract the science exposure.
189 Exposure to check the filter of.
194 If True, then the exposure has a filter listed
in the
195 configuration,
and should have the fringe applied.
197 return checkFilter(exposure, self.config.filters, log=self.log)
200 """Remove pedestal from fringe exposure.
205 Fringe data to subtract the pedestal value from.
208 stats.setNumSigmaClip(self.config.stats.clip)
209 stats.setNumIter(self.config.stats.iterations)
210 mi = fringe.getMaskedImage()
212 self.log.info("Removing fringe pedestal: %f", pedestal)
216 """Generate a random distribution of positions for measuring fringe
222 Exposure to measure the positions on.
223 rng : `numpy.random.RandomState`
224 Random number generator to use.
228 positions : `numpy.array`
229 Two-dimensional array containing the positions to sample
230 for fringe amplitudes.
232 start = self.config.large
233 num = self.config.num
234 width = exposure.getWidth() - self.config.large
235 height = exposure.getHeight() - self.config.large
236 return numpy.array([rng.randint(start, width, size=num),
237 rng.randint(start, height, size=num)]).swapaxes(0, 1)
241 """Measure fringe amplitudes for an exposure
243 The fringe amplitudes are measured as the statistic within a square
244 aperture. The statistic within a larger aperture are subtracted so
245 as to remove the background.
250 Exposure to measure the positions on.
251 positions : `numpy.array`
252 Two-dimensional array containing the positions to sample
253 for fringe amplitudes.
254 title : `str`, optional
255 Title used
for debug out plots.
259 fringes : `numpy.array`
260 Array of measured exposure values at each of the positions
264 stats.setNumSigmaClip(self.config.stats.clip)
265 stats.setNumIter(self.config.stats.iterations)
266 stats.setAndMask(exposure.getMaskedImage().getMask().getPlaneBitMask(self.config.stats.badMaskPlanes))
268 num = self.config.num
269 fringes = numpy.ndarray(num)
273 small =
measure(exposure.getMaskedImage(), x, y, self.config.small, self.config.stats.stat, stats)
274 large =
measure(exposure.getMaskedImage(), x, y, self.config.large, self.config.stats.stat, stats)
275 fringes[i] = small - large
280 disp = afwDisplay.Display(frame=
getFrame())
281 disp.mtv(exposure, title=title)
283 with disp.Buffering():
284 for x, y
in positions:
285 corners = numpy.array([[-1, -1], [1, -1], [1, 1], [-1, 1], [-1, -1]]) + [[x, y]]
286 disp.line(corners*self.config.small, ctype=afwDisplay.GREEN)
287 disp.line(corners*self.config.large, ctype=afwDisplay.BLUE)
293 """Solve for the scale factors with iterative clipping.
297 science : `numpy.array`
298 Array of measured science image values at each of the
300 fringes : `numpy.array`
301 Array of measured fringe values at each of the positions
306 solution : `np.array`
307 Fringe solution amplitudes for each input fringe frame.
309 RMS error
for the fit solution
for this exposure.
314 origNum = len(science)
316 def emptyResult(msg=""):
317 """Generate an empty result for return to the user
319 There are no good pixels; doesn't matter what we return.
321 self.log.warning("Unable to solve for fringes: no good pixels%s", msg)
324 out = out*len(fringes)
325 return numpy.array(out), numpy.nan
327 good = numpy.where(numpy.logical_and(numpy.isfinite(science), numpy.any(numpy.isfinite(fringes), 1)))
328 science = science[good]
329 fringes = fringes[good]
330 oldNum = len(science)
336 good =
select(science, self.config.clip)
337 for ff
in range(fringes.shape[1]):
338 good &=
select(fringes[:, ff], self.config.clip)
339 science = science[good]
340 fringes = fringes[good]
341 oldNum = len(science)
343 return emptyResult(
" after initial rejection")
345 for i
in range(self.config.iterations):
346 solution = self.
_solve(science, fringes)
347 resid = science - numpy.sum(solution*fringes, 1)
349 good = numpy.logical_not(abs(resid) > self.config.clip*rms)
350 self.log.debug(
"Iteration %d: RMS=%f numGood=%d", i, rms, good.sum())
351 self.log.debug(
"Solution %d: %s", i, solution)
354 return emptyResult(
" after %d rejection iterations" % i)
357 import matplotlib.pyplot
as plot
358 for j
in range(fringes.shape[1]):
362 fig.canvas._tkcanvas._root().lift()
365 ax = fig.add_subplot(1, 1, 1)
366 adjust = science.copy()
367 others =
set(range(fringes.shape[1]))
370 adjust -= solution[k]*fringes[:, k]
371 ax.plot(fringes[:, j], adjust,
'r.')
372 xmin = fringes[:, j].
min()
373 xmax = fringes[:, j].
max()
374 ymin = solution[j]*xmin
375 ymax = solution[j]*xmax
376 ax.plot([xmin, xmax], [ymin, ymax],
'b-')
377 ax.set_title(
"Fringe %d: %f" % (j, solution[j]))
378 ax.set_xlabel(
"Fringe amplitude")
379 ax.set_ylabel(
"Science amplitude")
380 ax.set_autoscale_on(
False)
381 ax.set_xbound(lower=xmin, upper=xmax)
382 ax.set_ybound(lower=ymin, upper=ymax)
385 ans = input(
"Enter or c to continue [chp]").lower()
386 if ans
in (
"",
"c",):
392 print(
"h[elp] c[ontinue] p[db]")
398 good = numpy.where(good)
399 science = science[good]
400 fringes = fringes[good]
403 solution = self.
_solve(science, fringes)
404 self.log.info(
"Fringe solution: %s RMS: %f Good: %d/%d", solution, rms, len(science), origNum)
408 """Solve for the scale factors.
412 science : `numpy.array`
413 Array of measured science image values at each of the
415 fringes : `numpy.array`
416 Array of measured fringe values at each of the positions
421 solution : `np.array`
422 Fringe solution amplitudes for each input fringe frame.
424 return afwMath.LeastSquares.fromDesignMatrix(fringes, science,
425 afwMath.LeastSquares.DIRECT_SVD).getSolution()
428 """Subtract the fringes.
433 Science exposure from which to remove fringes.
435 Calibration fringe files containing master fringe frames.
436 solution : `np.array`
437 Fringe solution amplitudes
for each input fringe frame.
442 Raised
if the number of fringe frames does
not match the
443 number of measured amplitudes.
445 if len(solution) != len(fringes):
446 raise RuntimeError(
"Number of fringe frames (%s) != number of scale factors (%s)" %
447 (len(fringes), len(solution)))
449 for s, f
in zip(solution, fringes):
451 f.getMaskedImage().getMask().getArray()[:] = 0
452 science.getMaskedImage().scaledMinus(s, f.getMaskedImage())
456 """Measure a statistic within an aperture
458 @param mi MaskedImage to measure
459 @param x, y Center
for aperture
460 @param size Size of aperture
461 @param statistic Statistic to measure
462 @param stats StatisticsControl object
463 @return Value of statistic within aperture
467 subImage = mi.Factory(mi, bbox, afwImage.LOCAL)
472 """Calculate a robust standard deviation of an array of values
474 @param vector Array of values
475 @return Standard deviation
477 q1, q3 = numpy.percentile(vector, (25, 75))
478 return 0.74*(q3 - q1)
482 """Select values within 'clip' standard deviations of the median
484 Returns a boolean array.
486 q1, q2, q3 = numpy.percentile(vector, (25, 50, 75))
487 return numpy.abs(vector - q2) < clip*0.74*(q3 - q1)
A class to contain the data, WCS, and other information needed to describe an image of the sky.
Pass parameters to a Statistics object.
An integer coordinate rectangle.
loadFringes(self, fringeExp, expId=None, assembler=None)
checkFilter(self, exposure)
generatePositions(self, exposure, rng)
solve(self, science, fringes)
_solve(self, science, fringes)
subtract(self, science, fringes, solution)
measureExposure(self, exposure, positions, title="Fringe")
removePedestal(self, fringe)
daf::base::PropertySet * set
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)
measure(mi, x, y, size, statistic, stats)