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 expId : `int`, optional
97 Exposure id to be fringe corrected, used to set RNG seed.
98 assembler : `lsst.ip.isr.AssembleCcdTask`, optional
99 An instance of AssembleCcdTask (for assembling fringe
104 fringeData : `pipeBase.Struct`
105 Struct containing fringe data:
106 - ``fringes`` : `lsst.afw.image.Exposure` or `list` thereof
107 Calibration fringe files containing master fringe frames.
108 - ``seed`` : `int`, optional
109 Seed for random number generation.
112 fringe = dataRef.get(
"fringe", immediate=
True)
113 except Exception
as e:
114 raise RuntimeError(
"Unable to retrieve fringe for %s: %s" % (dataRef.dataId, e))
116 return self.
loadFringesloadFringes(fringe, expId=expId, assembler=assembler)
119 """Pack the fringe data into a Struct.
121 This method moves the struct parsing code into a butler
122 generation agnostic handler.
126 fringeExp : `lsst.afw.exposure.Exposure`
127 The exposure containing the fringe data.
128 expId : `int`, optional
129 Exposure id to be fringe corrected, used to set RNG seed.
130 assembler : `lsst.ip.isr.AssembleCcdTask`, optional
131 An instance of AssembleCcdTask (for assembling fringe
136 fringeData : `pipeBase.Struct`
137 Struct containing fringe data:
138 - ``fringes`` : `lsst.afw.image.Exposure` or `list` thereof
139 Calibration fringe files containing master fringe frames.
140 - ``seed`` : `int`, optional
141 Seed for random number generation.
143 if assembler
is not None:
144 fringeExp = assembler.assembleCcd(fringeExp)
147 seed = self.config.stats.rngSeedOffset
149 print(f
"{self.config.stats.rngSeedOffset} {expId}")
150 seed = self.config.stats.rngSeedOffset + expId
155 return Struct(fringes=fringeExp,
159 def run(self, exposure, fringes, seed=None):
160 """Remove fringes from the provided science exposure.
162 Primary method of FringeTask. Fringes are only subtracted if the
163 science exposure has a filter listed in the configuration.
167 exposure : `lsst.afw.image.Exposure`
168 Science exposure from which to remove fringes.
169 fringes : `lsst.afw.image.Exposure` or `list` thereof
170 Calibration fringe files containing master fringe frames.
171 seed : `int`, optional
172 Seed for random number generation.
176 solution : `np.array`
177 Fringe solution amplitudes for each input fringe frame.
179 RMS error for the fit solution for this exposure.
185 self.log.
info(
"Filter not found in FringeTaskConfig.filters. Skipping fringe correction.")
189 seed = self.config.stats.rngSeedOffset
190 rng = numpy.random.RandomState(seed=seed)
192 if not hasattr(fringes,
'__iter__'):
195 mask = exposure.getMaskedImage().getMask()
196 for fringe
in fringes:
197 fringe.getMaskedImage().getMask().__ior__(mask)
198 if self.config.pedestal:
202 fluxes = numpy.ndarray([self.config.num, len(fringes)])
203 for i, f
in enumerate(fringes):
204 fluxes[:, i] = self.
measureExposuremeasureExposure(f, positions, title=
"Fringe frame")
206 expFringes = self.
measureExposuremeasureExposure(exposure, positions, title=
"Science")
207 solution, rms = self.
solvesolve(expFringes, fluxes)
208 self.
subtractsubtract(exposure, fringes, solution)
210 afwDisplay.Display(frame=
getFrame()).
mtv(exposure, title=
"Fringe subtracted")
215 """Remove fringes from the provided science exposure.
217 Retrieve fringes from butler dataRef provided and remove from
218 provided science exposure. Fringes are only subtracted if the
219 science exposure has a filter listed in the configuration.
223 exposure : `lsst.afw.image.Exposure`
224 Science exposure from which to remove fringes.
225 dataRef : `daf.persistence.butlerSubset.ButlerDataRef`
226 Butler reference to the exposure. Used to find
227 appropriate fringe data.
228 assembler : `lsst.ip.isr.AssembleCcdTask`, optional
229 An instance of AssembleCcdTask (for assembling fringe
234 solution : `np.array`
235 Fringe solution amplitudes for each input fringe frame.
237 RMS error for the fit solution for this exposure.
240 self.log.
info(
"Filter not found in FringeTaskConfig.filters. Skipping fringe correction.")
242 fringeStruct = self.
readFringesreadFringes(dataRef, assembler=assembler)
243 return self.
runrun(exposure, **fringeStruct.getDict())
246 """Check whether we should fringe-subtract the science exposure.
250 exposure : `lsst.afw.image.Exposure`
251 Exposure to check the filter of.
256 If True, then the exposure has a filter listed in the
257 configuration, and should have the fringe applied.
259 return checkFilter(exposure, self.config.filters, log=self.log)
262 """Remove pedestal from fringe exposure.
266 fringe : `lsst.afw.image.Exposure`
267 Fringe data to subtract the pedestal value from.
270 stats.setNumSigmaClip(self.config.stats.clip)
271 stats.setNumIter(self.config.stats.iterations)
272 mi = fringe.getMaskedImage()
274 self.log.
info(
"Removing fringe pedestal: %f", pedestal)
278 """Generate a random distribution of positions for measuring fringe amplitudes.
282 exposure : `lsst.afw.image.Exposure`
283 Exposure to measure the positions on.
284 rng : `numpy.random.RandomState`
285 Random number generator to use.
289 positions : `numpy.array`
290 Two-dimensional array containing the positions to sample
291 for fringe amplitudes.
293 start = self.config.large
294 num = self.config.num
295 width = exposure.getWidth() - self.config.large
296 height = exposure.getHeight() - self.config.large
297 return numpy.array([rng.randint(start, width, size=num),
298 rng.randint(start, height, size=num)]).swapaxes(0, 1)
302 """Measure fringe amplitudes for an exposure
304 The fringe amplitudes are measured as the statistic within a square
305 aperture. The statistic within a larger aperture are subtracted so
306 as to remove the background.
310 exposure : `lsst.afw.image.Exposure`
311 Exposure to measure the positions on.
312 positions : `numpy.array`
313 Two-dimensional array containing the positions to sample
314 for fringe amplitudes.
315 title : `str`, optional
316 Title used for debug out plots.
320 fringes : `numpy.array`
321 Array of measured exposure values at each of the positions
325 stats.setNumSigmaClip(self.config.stats.clip)
326 stats.setNumIter(self.config.stats.iterations)
327 stats.setAndMask(exposure.getMaskedImage().getMask().getPlaneBitMask(self.config.stats.badMaskPlanes))
329 num = self.config.num
330 fringes = numpy.ndarray(num)
334 small =
measure(exposure.getMaskedImage(), x, y, self.config.small, self.config.stats.stat, stats)
335 large =
measure(exposure.getMaskedImage(), x, y, self.config.large, self.config.stats.stat, stats)
336 fringes[i] = small - large
341 disp = afwDisplay.Display(frame=
getFrame())
342 disp.mtv(exposure, title=title)
344 with disp.Buffering():
345 for x, y
in positions:
346 corners = numpy.array([[-1, -1], [1, -1], [1, 1], [-1, 1], [-1, -1]]) + [[x, y]]
347 disp.line(corners*self.config.small, ctype=afwDisplay.GREEN)
348 disp.line(corners*self.config.large, ctype=afwDisplay.BLUE)
354 """Solve for the scale factors with iterative clipping.
358 science : `numpy.array`
359 Array of measured science image values at each of the
361 fringes : `numpy.array`
362 Array of measured fringe values at each of the positions
367 solution : `np.array`
368 Fringe solution amplitudes for each input fringe frame.
370 RMS error for the fit solution for this exposure.
375 origNum = len(science)
377 def emptyResult(msg=""):
378 """Generate an empty result for return to the user
380 There are no good pixels; doesn't matter what we return.
382 self.log.
warning(
"Unable to solve for fringes: no good pixels%s", msg)
385 out = out*len(fringes)
386 return numpy.array(out), numpy.nan
388 good = numpy.where(numpy.logical_and(numpy.isfinite(science), numpy.any(numpy.isfinite(fringes), 1)))
389 science = science[good]
390 fringes = fringes[good]
391 oldNum = len(science)
397 good =
select(science, self.config.clip)
398 for ff
in range(fringes.shape[1]):
399 good &=
select(fringes[:, ff], self.config.clip)
400 science = science[good]
401 fringes = fringes[good]
402 oldNum = len(science)
404 return emptyResult(
" after initial rejection")
406 for i
in range(self.config.iterations):
407 solution = self.
_solve_solve(science, fringes)
408 resid = science - numpy.sum(solution*fringes, 1)
410 good = numpy.logical_not(
abs(resid) > self.config.clip*rms)
411 self.log.
debug(
"Iteration %d: RMS=%f numGood=%d", i, rms, good.sum())
412 self.log.
debug(
"Solution %d: %s", i, solution)
415 return emptyResult(
" after %d rejection iterations" % i)
418 import matplotlib.pyplot
as plot
419 for j
in range(fringes.shape[1]):
423 fig.canvas._tkcanvas._root().lift()
426 ax = fig.add_subplot(1, 1, 1)
427 adjust = science.copy()
428 others =
set(range(fringes.shape[1]))
431 adjust -= solution[k]*fringes[:, k]
432 ax.plot(fringes[:, j], adjust,
'r.')
433 xmin = fringes[:, j].
min()
434 xmax = fringes[:, j].
max()
435 ymin = solution[j]*xmin
436 ymax = solution[j]*xmax
437 ax.plot([xmin, xmax], [ymin, ymax],
'b-')
438 ax.set_title(
"Fringe %d: %f" % (j, solution[j]))
439 ax.set_xlabel(
"Fringe amplitude")
440 ax.set_ylabel(
"Science amplitude")
441 ax.set_autoscale_on(
False)
442 ax.set_xbound(lower=xmin, upper=xmax)
443 ax.set_ybound(lower=ymin, upper=ymax)
446 ans = input(
"Enter or c to continue [chp]").lower()
447 if ans
in (
"",
"c",):
453 print(
"h[elp] c[ontinue] p[db]")
459 good = numpy.where(good)
460 science = science[good]
461 fringes = fringes[good]
464 solution = self.
_solve_solve(science, fringes)
465 self.log.
info(
"Fringe solution: %s RMS: %f Good: %d/%d", solution, rms, len(science), origNum)
468 def _solve(self, science, fringes):
469 """Solve for the scale factors.
473 science : `numpy.array`
474 Array of measured science image values at each of the
476 fringes : `numpy.array`
477 Array of measured fringe values at each of the positions
482 solution : `np.array`
483 Fringe solution amplitudes for each input fringe frame.
485 return afwMath.LeastSquares.fromDesignMatrix(fringes, science,
486 afwMath.LeastSquares.DIRECT_SVD).getSolution()
489 """Subtract the fringes.
493 science : `lsst.afw.image.Exposure`
494 Science exposure from which to remove fringes.
495 fringes : `lsst.afw.image.Exposure` or `list` thereof
496 Calibration fringe files containing master fringe frames.
497 solution : `np.array`
498 Fringe solution amplitudes for each input fringe frame.
503 Raised if the number of fringe frames does not match the
504 number of measured amplitudes.
506 if len(solution) != len(fringes):
507 raise RuntimeError(
"Number of fringe frames (%s) != number of scale factors (%s)" %
508 (len(fringes), len(solution)))
510 for s, f
in zip(solution, fringes):
512 f.getMaskedImage().getMask().getArray()[:] = 0
513 science.getMaskedImage().scaledMinus(s, f.getMaskedImage())
516 def measure(mi, x, y, size, statistic, stats):
517 """Measure a statistic within an aperture
519 @param mi MaskedImage to measure
520 @param x, y Center for aperture
521 @param size Size of aperture
522 @param statistic Statistic to measure
523 @param stats StatisticsControl object
524 @return Value of statistic within aperture
528 subImage = mi.Factory(mi, bbox, afwImage.LOCAL)
533 """Calculate a robust standard deviation of an array of values
535 @param vector Array of values
536 @return Standard deviation
538 q1, q3 = numpy.percentile(vector, (25, 75))
539 return 0.74*(q3 - q1)
543 """Select values within 'clip' standard deviations of the median
545 Returns a boolean array.
547 q1, q2, q3 = numpy.percentile(vector, (25, 50, 75))
548 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 measureExposure(self, exposure, positions, title="Fringe")
def loadFringes(self, fringeExp, expId=None, 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)
def readFringes(self, dataRef, expId=None, assembler=None)
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)