31 from .isrFunctions
import checkFilter
33 afwDisplay.setDefaultMaskTransparency(75)
37 """Produce a new frame number each time"""
46 """Options for measuring fringes on an exposure"""
47 badMaskPlanes =
ListField(dtype=str, default=[
"SAT"], doc=
"Ignore pixels with these masks")
48 stat =
Field(dtype=int, default=int(afwMath.MEDIAN), doc=
"Statistic to use")
49 clip =
Field(dtype=float, default=3.0, doc=
"Sigma clip threshold")
50 iterations =
Field(dtype=int, default=3, doc=
"Number of fitting iterations")
51 rngSeedOffset =
Field(dtype=int, default=0,
52 doc=
"Offset to the random number generator seed (full seed includes exposure ID)")
56 """Fringe subtraction options"""
58 filters =
ListField(dtype=str, default=[], doc=
"Only fringe-subtract these filters")
60 useFilterAliases =
Field(dtype=bool, default=
False, doc=
"Search filter aliases during check.",
61 deprecated=(
"Removed with no replacement (FilterLabel has no aliases)."
62 "Will be removed after v22."))
63 num =
Field(dtype=int, default=30000, doc=
"Number of fringe measurements")
64 small =
Field(dtype=int, default=3, doc=
"Half-size of small (fringe) measurements (pixels)")
65 large =
Field(dtype=int, default=30, doc=
"Half-size of large (background) measurements (pixels)")
66 iterations =
Field(dtype=int, default=20, doc=
"Number of fitting iterations")
67 clip =
Field(dtype=float, default=3.0, doc=
"Sigma clip threshold")
68 stats =
ConfigField(dtype=FringeStatisticsConfig, doc=
"Statistics for measuring fringes")
69 pedestal =
Field(dtype=bool, default=
False, doc=
"Remove fringe pedestal?")
73 """Task to remove fringes from a science exposure
75 We measure fringe amplitudes at random positions on the science exposure
76 and at the same positions on the (potentially multiple) fringe frames
77 and solve for the scales simultaneously.
79 ConfigClass = FringeConfig
80 _DefaultName =
'isrFringe'
83 """Read the fringe frame(s), and pack data into a Struct
85 The current implementation assumes only a single fringe frame and
86 will have to be updated to support multi-mode fringe subtraction.
88 This implementation could be optimised by persisting the fringe
93 dataRef : `daf.butler.butlerSubset.ButlerDataRef`
94 Butler reference for the exposure that will have fringing
96 assembler : `lsst.ip.isr.AssembleCcdTask`, optional
97 An instance of AssembleCcdTask (for assembling fringe
102 fringeData : `pipeBase.Struct`
103 Struct containing fringe data:
104 - ``fringes`` : `lsst.afw.image.Exposure` or `list` thereof
105 Calibration fringe files containing master fringe frames.
106 - ``seed`` : `int`, optional
107 Seed for random number generation.
110 fringe = dataRef.get(
"fringe", immediate=
True)
111 except Exception
as e:
112 raise RuntimeError(
"Unable to retrieve fringe for %s: %s" % (dataRef.dataId, e))
114 return self.
loadFringesloadFringes(fringe, assembler)
117 """Pack the fringe data into a Struct.
119 This method moves the struct parsing code into a butler
120 generation agnostic handler.
124 fringeExp : `lsst.afw.exposure.Exposure`
125 The exposure containing the fringe data.
126 expId : `int`, optional
127 Exposure id to be fringe corrected, used to set RNG seed.
128 assembler : `lsst.ip.isr.AssembleCcdTask`, optional
129 An instance of AssembleCcdTask (for assembling fringe
134 fringeData : `pipeBase.Struct`
135 Struct containing fringe data:
136 - ``fringes`` : `lsst.afw.image.Exposure` or `list` thereof
137 Calibration fringe files containing master fringe frames.
138 - ``seed`` : `int`, optional
139 Seed for random number generation.
141 if assembler
is not None:
142 fringeExp = assembler.assembleCcd(fringeExp)
145 seed = self.
configconfig.stats.rngSeedOffset
147 print(f
"{self.config.stats.rngSeedOffset} {expId}")
148 seed = self.
configconfig.stats.rngSeedOffset + expId
153 return Struct(fringes=fringeExp,
157 def run(self, exposure, fringes, seed=None):
158 """Remove fringes from the provided science exposure.
160 Primary method of FringeTask. Fringes are only subtracted if the
161 science exposure has a filter listed in the configuration.
165 exposure : `lsst.afw.image.Exposure`
166 Science exposure from which to remove fringes.
167 fringes : `lsst.afw.image.Exposure` or `list` thereof
168 Calibration fringe files containing master fringe frames.
169 seed : `int`, optional
170 Seed for random number generation.
174 solution : `np.array`
175 Fringe solution amplitudes for each input fringe frame.
177 RMS error for the fit solution for this exposure.
183 self.
loglog.
info(
"Filter not found in FringeTaskConfig.filters. Skipping fringe correction.")
187 seed = self.
configconfig.stats.rngSeedOffset
188 rng = numpy.random.RandomState(seed=seed)
190 if not hasattr(fringes,
'__iter__'):
193 mask = exposure.getMaskedImage().getMask()
194 for fringe
in fringes:
195 fringe.getMaskedImage().getMask().__ior__(mask)
196 if self.
configconfig.pedestal:
200 fluxes = numpy.ndarray([self.
configconfig.num, len(fringes)])
201 for i, f
in enumerate(fringes):
202 fluxes[:, i] = self.
measureExposuremeasureExposure(f, positions, title=
"Fringe frame")
204 expFringes = self.
measureExposuremeasureExposure(exposure, positions, title=
"Science")
205 solution, rms = self.
solvesolve(expFringes, fluxes)
206 self.
subtractsubtract(exposure, fringes, solution)
208 afwDisplay.Display(frame=
getFrame()).
mtv(exposure, title=
"Fringe subtracted")
213 """Remove fringes from the provided science exposure.
215 Retrieve fringes from butler dataRef provided and remove from
216 provided science exposure. Fringes are only subtracted if the
217 science exposure has a filter listed in the configuration.
221 exposure : `lsst.afw.image.Exposure`
222 Science exposure from which to remove fringes.
223 dataRef : `daf.persistence.butlerSubset.ButlerDataRef`
224 Butler reference to the exposure. Used to find
225 appropriate fringe data.
226 assembler : `lsst.ip.isr.AssembleCcdTask`, optional
227 An instance of AssembleCcdTask (for assembling fringe
232 solution : `np.array`
233 Fringe solution amplitudes for each input fringe frame.
235 RMS error for the fit solution for this exposure.
238 self.
loglog.
info(
"Filter not found in FringeTaskConfig.filters. Skipping fringe correction.")
240 fringeStruct = self.
readFringesreadFringes(dataRef, assembler=assembler)
241 return self.
runrun(exposure, **fringeStruct.getDict())
244 """Check whether we should fringe-subtract the science exposure.
248 exposure : `lsst.afw.image.Exposure`
249 Exposure to check the filter of.
254 If True, then the exposure has a filter listed in the
255 configuration, and should have the fringe applied.
260 """Remove pedestal from fringe exposure.
264 fringe : `lsst.afw.image.Exposure`
265 Fringe data to subtract the pedestal value from.
268 stats.setNumSigmaClip(self.
configconfig.stats.clip)
269 stats.setNumIter(self.
configconfig.stats.iterations)
270 mi = fringe.getMaskedImage()
272 self.
loglog.
info(
"Removing fringe pedestal: %f", pedestal)
276 """Generate a random distribution of positions for measuring fringe amplitudes.
280 exposure : `lsst.afw.image.Exposure`
281 Exposure to measure the positions on.
282 rng : `numpy.random.RandomState`
283 Random number generator to use.
287 positions : `numpy.array`
288 Two-dimensional array containing the positions to sample
289 for fringe amplitudes.
291 start = self.
configconfig.large
292 num = self.
configconfig.num
293 width = exposure.getWidth() - self.
configconfig.large
294 height = exposure.getHeight() - self.
configconfig.large
295 return numpy.array([rng.randint(start, width, size=num),
296 rng.randint(start, height, size=num)]).swapaxes(0, 1)
300 """Measure fringe amplitudes for an exposure
302 The fringe amplitudes are measured as the statistic within a square
303 aperture. The statistic within a larger aperture are subtracted so
304 as to remove the background.
308 exposure : `lsst.afw.image.Exposure`
309 Exposure to measure the positions on.
310 positions : `numpy.array`
311 Two-dimensional array containing the positions to sample
312 for fringe amplitudes.
313 title : `str`, optional
314 Title used for debug out plots.
318 fringes : `numpy.array`
319 Array of measured exposure values at each of the positions
323 stats.setNumSigmaClip(self.
configconfig.stats.clip)
324 stats.setNumIter(self.
configconfig.stats.iterations)
325 stats.setAndMask(exposure.getMaskedImage().getMask().getPlaneBitMask(self.
configconfig.stats.badMaskPlanes))
327 num = self.
configconfig.num
328 fringes = numpy.ndarray(num)
332 small =
measure(exposure.getMaskedImage(), x, y, self.
configconfig.small, self.
configconfig.stats.stat, stats)
333 large =
measure(exposure.getMaskedImage(), x, y, self.
configconfig.large, self.
configconfig.stats.stat, stats)
334 fringes[i] = small - large
339 disp = afwDisplay.Display(frame=
getFrame())
340 disp.mtv(exposure, title=title)
342 with disp.Buffering():
343 for x, y
in positions:
344 corners = numpy.array([[-1, -1], [1, -1], [1, 1], [-1, 1], [-1, -1]]) + [[x, y]]
345 disp.line(corners*self.
configconfig.small, ctype=afwDisplay.GREEN)
346 disp.line(corners*self.
configconfig.large, ctype=afwDisplay.BLUE)
352 """Solve for the scale factors with iterative clipping.
356 science : `numpy.array`
357 Array of measured science image values at each of the
359 fringes : `numpy.array`
360 Array of measured fringe values at each of the positions
365 solution : `np.array`
366 Fringe solution amplitudes for each input fringe frame.
368 RMS error for the fit solution for this exposure.
373 origNum = len(science)
375 def emptyResult(msg=""):
376 """Generate an empty result for return to the user
378 There are no good pixels; doesn't matter what we return.
380 self.
loglog.
warn(
"Unable to solve for fringes: no good pixels%s", msg)
383 out = out*len(fringes)
384 return numpy.array(out), numpy.nan
386 good = numpy.where(numpy.logical_and(numpy.isfinite(science), numpy.any(numpy.isfinite(fringes), 1)))
387 science = science[good]
388 fringes = fringes[good]
389 oldNum = len(science)
396 for ff
in range(fringes.shape[1]):
398 science = science[good]
399 fringes = fringes[good]
400 oldNum = len(science)
402 return emptyResult(
" after initial rejection")
404 for i
in range(self.
configconfig.iterations):
405 solution = self.
_solve_solve(science, fringes)
406 resid = science - numpy.sum(solution*fringes, 1)
408 good = numpy.logical_not(
abs(resid) > self.
configconfig.clip*rms)
409 self.
loglog.
debug(
"Iteration %d: RMS=%f numGood=%d", i, rms, good.sum())
410 self.
loglog.
debug(
"Solution %d: %s", i, solution)
413 return emptyResult(
" after %d rejection iterations" % i)
416 import matplotlib.pyplot
as plot
417 for j
in range(fringes.shape[1]):
421 fig.canvas._tkcanvas._root().lift()
424 ax = fig.add_subplot(1, 1, 1)
425 adjust = science.copy()
426 others =
set(range(fringes.shape[1]))
429 adjust -= solution[k]*fringes[:, k]
430 ax.plot(fringes[:, j], adjust,
'r.')
431 xmin = fringes[:, j].
min()
432 xmax = fringes[:, j].
max()
433 ymin = solution[j]*xmin
434 ymax = solution[j]*xmax
435 ax.plot([xmin, xmax], [ymin, ymax],
'b-')
436 ax.set_title(
"Fringe %d: %f" % (j, solution[j]))
437 ax.set_xlabel(
"Fringe amplitude")
438 ax.set_ylabel(
"Science amplitude")
439 ax.set_autoscale_on(
False)
440 ax.set_xbound(lower=xmin, upper=xmax)
441 ax.set_ybound(lower=ymin, upper=ymax)
444 ans = input(
"Enter or c to continue [chp]").lower()
445 if ans
in (
"",
"c",):
451 print(
"h[elp] c[ontinue] p[db]")
457 good = numpy.where(good)
458 science = science[good]
459 fringes = fringes[good]
462 solution = self.
_solve_solve(science, fringes)
463 self.
loglog.
info(
"Fringe solution: %s RMS: %f Good: %d/%d", solution, rms, len(science), origNum)
466 def _solve(self, science, fringes):
467 """Solve for the scale factors.
471 science : `numpy.array`
472 Array of measured science image values at each of the
474 fringes : `numpy.array`
475 Array of measured fringe values at each of the positions
480 solution : `np.array`
481 Fringe solution amplitudes for each input fringe frame.
483 return afwMath.LeastSquares.fromDesignMatrix(fringes, science,
484 afwMath.LeastSquares.DIRECT_SVD).getSolution()
487 """Subtract the fringes.
491 science : `lsst.afw.image.Exposure`
492 Science exposure from which to remove fringes.
493 fringes : `lsst.afw.image.Exposure` or `list` thereof
494 Calibration fringe files containing master fringe frames.
495 solution : `np.array`
496 Fringe solution amplitudes for each input fringe frame.
501 Raised if the number of fringe frames does not match the
502 number of measured amplitudes.
504 if len(solution) != len(fringes):
505 raise RuntimeError(
"Number of fringe frames (%s) != number of scale factors (%s)" %
506 (len(fringes), len(solution)))
508 for s, f
in zip(solution, fringes):
509 science.getMaskedImage().scaledMinus(s, f.getMaskedImage())
512 def measure(mi, x, y, size, statistic, stats):
513 """Measure a statistic within an aperture
515 @param mi MaskedImage to measure
516 @param x, y Center for aperture
517 @param size Size of aperture
518 @param statistic Statistic to measure
519 @param stats StatisticsControl object
520 @return Value of statistic within aperture
524 subImage = mi.Factory(mi, bbox, afwImage.LOCAL)
529 """Calculate a robust standard deviation of an array of values
531 @param vector Array of values
532 @return Standard deviation
534 q1, q3 = numpy.percentile(vector, (25, 75))
535 return 0.74*(q3 - q1)
539 """Select values within 'clip' standard deviations of the median
541 Returns a boolean array.
543 q1, q2, q3 = numpy.percentile(vector, (25, 50, 75))
544 return numpy.abs(vector - q2) < clip*0.74*(q3 - q1)
Pass parameters to a Statistics object.
An integer coordinate rectangle.
def solve(self, science, fringes)
def readFringes(self, dataRef, assembler=None)
def measureExposure(self, exposure, positions, title="Fringe")
def loadFringes(self, fringeExp, expId=0, assembler=None)
def subtract(self, science, fringes, solution)
def removePedestal(self, fringe)
def runDataRef(self, exposure, dataRef, assembler=None)
def generatePositions(self, exposure, rng)
def run(self, exposure, fringes, seed=None)
def _solve(self, science, fringes)
def checkFilter(self, exposure)
daf::base::PropertySet * set
def mtv(data, frame=None, title="", wcs=None, *args, **kwargs)
Backwards-compatibility support for depersisting the old Calib (FluxMag0/FluxMag0Err) objects.
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)
def measure(mi, x, y, size, statistic, stats)
Angle abs(Angle const &a)