31from lsst.utils.timer
import timeMethod
32from .isrFunctions
import checkFilter
34afwDisplay.setDefaultMaskTransparency(75)
38 """Produce a new frame number each time"""
47 """Options for measuring fringes on an exposure"""
48 badMaskPlanes =
ListField(dtype=str, default=[
"SAT"], doc=
"Ignore pixels with these masks")
49 stat =
Field(dtype=int, default=
int(afwMath.MEDIAN), doc=
"Statistic to use")
50 clip =
Field(dtype=float, default=3.0, doc=
"Sigma clip threshold")
51 iterations =
Field(dtype=int, default=3, doc=
"Number of fitting iterations")
52 rngSeedOffset =
Field(dtype=int, default=0,
53 doc=
"Offset to the random number generator seed (full seed includes exposure ID)")
57 """Fringe subtraction options"""
59 filters =
ListField(dtype=str, default=[], doc=
"Only fringe-subtract these filters")
61 useFilterAliases =
Field(dtype=bool, default=
False, doc=
"Search filter aliases during check.",
62 deprecated=(
"Removed with no replacement (FilterLabel has no aliases)."
63 "Will be removed after v22."))
64 num =
Field(dtype=int, default=30000, doc=
"Number of fringe measurements")
65 small =
Field(dtype=int, default=3, doc=
"Half-size of small (fringe) measurements (pixels)")
66 large =
Field(dtype=int, default=30, doc=
"Half-size of large (background) measurements (pixels)")
67 iterations =
Field(dtype=int, default=20, doc=
"Number of fitting iterations")
68 clip =
Field(dtype=float, default=3.0, doc=
"Sigma clip threshold")
69 stats =
ConfigField(dtype=FringeStatisticsConfig, doc=
"Statistics for measuring fringes")
70 pedestal =
Field(dtype=bool, default=
False, doc=
"Remove fringe pedestal?")
74 """Task to remove fringes from a science exposure
76 We measure fringe amplitudes at random positions on the science exposure
77 and at the same positions on the (potentially multiple) fringe frames
78 and solve
for the scales simultaneously.
80 ConfigClass = FringeConfig
81 _DefaultName = 'isrFringe'
84 """Read the fringe frame(s), and pack data into a Struct
86 The current implementation assumes only a single fringe frame and
87 will have to be updated to support multi-mode fringe subtraction.
89 This implementation could be optimised by persisting the fringe
94 dataRef : `daf.butler.butlerSubset.ButlerDataRef`
95 Butler reference
for the exposure that will have fringing
97 expId : `int`, optional
98 Exposure id to be fringe corrected, used to set RNG seed.
100 An instance of AssembleCcdTask (
for assembling fringe
105 fringeData : `pipeBase.Struct`
106 Struct containing fringe data:
108 Calibration fringe files containing master fringe frames.
109 - ``seed`` : `int`, optional
110 Seed
for random number generation.
113 fringe = dataRef.get(
"fringe", immediate=
True)
114 except Exception
as e:
115 raise RuntimeError(
"Unable to retrieve fringe for %s: %s" % (dataRef.dataId, e))
117 return self.
loadFringesloadFringes(fringe, expId=expId, assembler=assembler)
120 """Pack the fringe data into a Struct.
122 This method moves the struct parsing code into a butler
123 generation agnostic handler.
127 fringeExp : `lsst.afw.exposure.Exposure`
128 The exposure containing the fringe data.
129 expId : `int`, optional
130 Exposure id to be fringe corrected, used to set RNG seed.
132 An instance of AssembleCcdTask (for assembling fringe
137 fringeData : `pipeBase.Struct`
138 Struct containing fringe data:
140 Calibration fringe files containing master fringe frames.
141 - ``seed`` : `int`, optional
142 Seed
for random number generation.
144 if assembler
is not None:
145 fringeExp = assembler.assembleCcd(fringeExp)
148 seed = self.config.stats.rngSeedOffset
150 print(f
"{self.config.stats.rngSeedOffset} {expId}")
151 seed = self.config.stats.rngSeedOffset + expId
157 return Struct(fringes=fringeExp,
161 def run(self, exposure, fringes, seed=None):
162 """Remove fringes from the provided science exposure.
164 Primary method of FringeTask. Fringes are only subtracted if the
165 science exposure has a filter listed
in the configuration.
170 Science exposure
from which to remove fringes.
172 Calibration fringe files containing master fringe frames.
173 seed : `int`, optional
174 Seed
for random number generation.
178 solution : `np.array`
179 Fringe solution amplitudes
for each input fringe frame.
181 RMS error
for the fit solution
for this exposure.
187 self.log.
info(
"Filter not found in FringeTaskConfig.filters. Skipping fringe correction.")
191 seed = self.config.stats.rngSeedOffset
192 rng = numpy.random.RandomState(seed=seed)
194 if not hasattr(fringes,
'__iter__'):
197 mask = exposure.getMaskedImage().getMask()
198 for fringe
in fringes:
199 fringe.getMaskedImage().getMask().__ior__(mask)
200 if self.config.pedestal:
204 fluxes = numpy.ndarray([self.config.num, len(fringes)])
205 for i, f
in enumerate(fringes):
206 fluxes[:, i] = self.
measureExposuremeasureExposure(f, positions, title=
"Fringe frame")
208 expFringes = self.
measureExposuremeasureExposure(exposure, positions, title=
"Science")
209 solution, rms = self.
solvesolve(expFringes, fluxes)
210 self.
subtractsubtract(exposure, fringes, solution)
212 afwDisplay.Display(frame=
getFrame()).
mtv(exposure, title=
"Fringe subtracted")
217 """Remove fringes from the provided science exposure.
219 Retrieve fringes from butler dataRef provided
and remove
from
220 provided science exposure. Fringes are only subtracted
if the
221 science exposure has a filter listed
in the configuration.
226 Science exposure
from which to remove fringes.
228 Butler reference to the exposure. Used to find
229 appropriate fringe data.
231 An instance of AssembleCcdTask (
for assembling fringe
236 solution : `np.array`
237 Fringe solution amplitudes
for each input fringe frame.
239 RMS error
for the fit solution
for this exposure.
242 self.log.
info(
"Filter not found in FringeTaskConfig.filters. Skipping fringe correction.")
244 fringeStruct = self.
readFringesreadFringes(dataRef, assembler=assembler)
245 return self.
runrun(exposure, **fringeStruct.getDict())
248 """Check whether we should fringe-subtract the science exposure.
253 Exposure to check the filter of.
258 If True, then the exposure has a filter listed
in the
259 configuration,
and should have the fringe applied.
261 return checkFilter(exposure, self.config.filters, log=self.log)
264 """Remove pedestal from fringe exposure.
269 Fringe data to subtract the pedestal value from.
272 stats.setNumSigmaClip(self.config.stats.clip)
273 stats.setNumIter(self.config.stats.iterations)
274 mi = fringe.getMaskedImage()
276 self.log.info("Removing fringe pedestal: %f", pedestal)
280 """Generate a random distribution of positions for measuring fringe
286 Exposure to measure the positions on.
287 rng : `numpy.random.RandomState`
288 Random number generator to use.
292 positions : `numpy.array`
293 Two-dimensional array containing the positions to sample
294 for fringe amplitudes.
296 start = self.config.large
297 num = self.config.num
298 width = exposure.getWidth() - self.config.large
299 height = exposure.getHeight() - self.config.large
300 return numpy.array([rng.randint(start, width, size=num),
301 rng.randint(start, height, size=num)]).swapaxes(0, 1)
305 """Measure fringe amplitudes for an exposure
307 The fringe amplitudes are measured as the statistic within a square
308 aperture. The statistic within a larger aperture are subtracted so
309 as to remove the background.
314 Exposure to measure the positions on.
315 positions : `numpy.array`
316 Two-dimensional array containing the positions to sample
317 for fringe amplitudes.
318 title : `str`, optional
319 Title used
for debug out plots.
323 fringes : `numpy.array`
324 Array of measured exposure values at each of the positions
328 stats.setNumSigmaClip(self.config.stats.clip)
329 stats.setNumIter(self.config.stats.iterations)
330 stats.setAndMask(exposure.getMaskedImage().getMask().getPlaneBitMask(self.config.stats.badMaskPlanes))
332 num = self.config.num
333 fringes = numpy.ndarray(num)
337 small =
measure(exposure.getMaskedImage(), x, y, self.config.small, self.config.stats.stat, stats)
338 large =
measure(exposure.getMaskedImage(), x, y, self.config.large, self.config.stats.stat, stats)
339 fringes[i] = small - large
344 disp = afwDisplay.Display(frame=
getFrame())
345 disp.mtv(exposure, title=title)
347 with disp.Buffering():
348 for x, y
in positions:
349 corners = numpy.array([[-1, -1], [1, -1], [1, 1], [-1, 1], [-1, -1]]) + [[x, y]]
350 disp.line(corners*self.config.small, ctype=afwDisplay.GREEN)
351 disp.line(corners*self.config.large, ctype=afwDisplay.BLUE)
357 """Solve for the scale factors with iterative clipping.
361 science : `numpy.array`
362 Array of measured science image values at each of the
364 fringes : `numpy.array`
365 Array of measured fringe values at each of the positions
370 solution : `np.array`
371 Fringe solution amplitudes for each input fringe frame.
373 RMS error
for the fit solution
for this exposure.
378 origNum = len(science)
380 def emptyResult(msg=""):
381 """Generate an empty result for return to the user
383 There are no good pixels; doesn't matter what we return.
385 self.log.warning("Unable to solve for fringes: no good pixels%s", msg)
388 out = out*len(fringes)
389 return numpy.array(out), numpy.nan
391 good = numpy.where(numpy.logical_and(numpy.isfinite(science), numpy.any(numpy.isfinite(fringes), 1)))
392 science = science[good]
393 fringes = fringes[good]
394 oldNum = len(science)
400 good =
select(science, self.config.clip)
401 for ff
in range(fringes.shape[1]):
402 good &=
select(fringes[:, ff], self.config.clip)
403 science = science[good]
404 fringes = fringes[good]
405 oldNum = len(science)
407 return emptyResult(
" after initial rejection")
409 for i
in range(self.config.iterations):
410 solution = self.
_solve_solve(science, fringes)
411 resid = science - numpy.sum(solution*fringes, 1)
413 good = numpy.logical_not(
abs(resid) > self.config.clip*rms)
414 self.log.
debug(
"Iteration %d: RMS=%f numGood=%d", i, rms, good.sum())
415 self.log.
debug(
"Solution %d: %s", i, solution)
418 return emptyResult(
" after %d rejection iterations" % i)
421 import matplotlib.pyplot
as plot
422 for j
in range(fringes.shape[1]):
426 fig.canvas._tkcanvas._root().lift()
429 ax = fig.add_subplot(1, 1, 1)
430 adjust = science.copy()
431 others =
set(range(fringes.shape[1]))
434 adjust -= solution[k]*fringes[:, k]
435 ax.plot(fringes[:, j], adjust,
'r.')
436 xmin = fringes[:, j].
min()
437 xmax = fringes[:, j].
max()
438 ymin = solution[j]*xmin
439 ymax = solution[j]*xmax
440 ax.plot([xmin, xmax], [ymin, ymax],
'b-')
441 ax.set_title(
"Fringe %d: %f" % (j, solution[j]))
442 ax.set_xlabel(
"Fringe amplitude")
443 ax.set_ylabel(
"Science amplitude")
444 ax.set_autoscale_on(
False)
445 ax.set_xbound(lower=xmin, upper=xmax)
446 ax.set_ybound(lower=ymin, upper=ymax)
449 ans = input(
"Enter or c to continue [chp]").lower()
450 if ans
in (
"",
"c",):
456 print(
"h[elp] c[ontinue] p[db]")
462 good = numpy.where(good)
463 science = science[good]
464 fringes = fringes[good]
467 solution = self.
_solve_solve(science, fringes)
468 self.log.
info(
"Fringe solution: %s RMS: %f Good: %d/%d", solution, rms, len(science), origNum)
471 def _solve(self, science, fringes):
472 """Solve for the scale factors.
476 science : `numpy.array`
477 Array of measured science image values at each of the
479 fringes : `numpy.array`
480 Array of measured fringe values at each of the positions
485 solution : `np.array`
486 Fringe solution amplitudes for each input fringe frame.
488 return afwMath.LeastSquares.fromDesignMatrix(fringes, science,
489 afwMath.LeastSquares.DIRECT_SVD).getSolution()
492 """Subtract the fringes.
497 Science exposure from which to remove fringes.
499 Calibration fringe files containing master fringe frames.
500 solution : `np.array`
501 Fringe solution amplitudes
for each input fringe frame.
506 Raised
if the number of fringe frames does
not match the
507 number of measured amplitudes.
509 if len(solution) != len(fringes):
510 raise RuntimeError(
"Number of fringe frames (%s) != number of scale factors (%s)" %
511 (len(fringes), len(solution)))
513 for s, f
in zip(solution, fringes):
515 f.getMaskedImage().getMask().getArray()[:] = 0
516 science.getMaskedImage().scaledMinus(s, f.getMaskedImage())
520 """Measure a statistic within an aperture
522 @param mi MaskedImage to measure
523 @param x, y Center
for aperture
524 @param size Size of aperture
525 @param statistic Statistic to measure
526 @param stats StatisticsControl object
527 @return Value of statistic within aperture
531 subImage = mi.Factory(mi, bbox, afwImage.LOCAL)
536 """Calculate a robust standard deviation of an array of values
538 @param vector Array of values
539 @return Standard deviation
541 q1, q3 = numpy.percentile(vector, (25, 75))
542 return 0.74*(q3 - q1)
546 """Select values within 'clip' standard deviations of the median
548 Returns a boolean array.
550 q1, q2, q3 = numpy.percentile(vector, (25, 50, 75))
551 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.
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)