29from lsst.pipe.base
import Task, Struct
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 """Pack the fringe data into a Struct.
86 This method moves the struct parsing code into a butler
87 generation agnostic handler.
91 fringeExp : `lsst.afw.exposure.Exposure`
92 The exposure containing the fringe data.
93 expId : `int`, optional
94 Exposure id to be fringe corrected, used to set RNG seed.
96 An instance of AssembleCcdTask (for assembling fringe
101 fringeData : `pipeBase.Struct`
102 Struct containing fringe data:
104 Calibration fringe files containing master fringe frames.
105 - ``seed`` : `int`, optional
106 Seed
for random number generation.
108 if assembler
is not None:
109 fringeExp = assembler.assembleCcd(fringeExp)
112 seed = self.config.stats.rngSeedOffset
114 print(f
"{self.config.stats.rngSeedOffset} {expId}")
115 seed = self.config.stats.rngSeedOffset + expId
121 return Struct(fringes=fringeExp,
125 def run(self, exposure, fringes, seed=None):
126 """Remove fringes from the provided science exposure.
128 Primary method of FringeTask. Fringes are only subtracted if the
129 science exposure has a filter listed
in the configuration.
134 Science exposure
from which to remove fringes.
136 Calibration fringe files containing master fringe frames.
137 seed : `int`, optional
138 Seed
for random number generation.
142 solution : `np.array`
143 Fringe solution amplitudes
for each input fringe frame.
145 RMS error
for the fit solution
for this exposure.
151 self.log.info(
"Filter not found in FringeTaskConfig.filters. Skipping fringe correction.")
155 seed = self.config.stats.rngSeedOffset
156 rng = numpy.random.RandomState(seed=seed)
158 if not hasattr(fringes,
'__iter__'):
161 mask = exposure.getMaskedImage().getMask()
162 for fringe
in fringes:
163 fringe.getMaskedImage().getMask().__ior__(mask)
164 if self.config.pedestal:
168 fluxes = numpy.ndarray([self.config.num, len(fringes)])
169 for i, f
in enumerate(fringes):
170 fluxes[:, i] = self.
measureExposure(f, positions, title=
"Fringe frame")
172 expFringes = self.
measureExposure(exposure, positions, title=
"Science")
173 solution, rms = self.
solve(expFringes, fluxes)
174 self.
subtract(exposure, fringes, solution)
176 afwDisplay.Display(frame=
getFrame()).mtv(exposure, title=
"Fringe subtracted")
179 def checkFilter(self, exposure):
180 """Check whether we should fringe-subtract the science exposure.
185 Exposure to check the filter of.
190 If True, then the exposure has a filter listed
in the
191 configuration,
and should have the fringe applied.
193 return checkFilter(exposure, self.config.filters, log=self.log)
196 """Remove pedestal from fringe exposure.
201 Fringe data to subtract the pedestal value from.
203 stats = afwMath.StatisticsControl()
204 stats.setNumSigmaClip(self.config.stats.clip)
205 stats.setNumIter(self.config.stats.iterations)
206 mi = fringe.getMaskedImage()
207 pedestal = afwMath.makeStatistics(mi, afwMath.MEDIAN, stats).getValue()
208 self.log.info("Removing fringe pedestal: %f", pedestal)
212 """Generate a random distribution of positions for measuring fringe
218 Exposure to measure the positions on.
219 rng : `numpy.random.RandomState`
220 Random number generator to use.
224 positions : `numpy.array`
225 Two-dimensional array containing the positions to sample
226 for fringe amplitudes.
228 start = self.config.large
229 num = self.config.num
230 width = exposure.getWidth() - self.config.large
231 height = exposure.getHeight() - self.config.large
232 return numpy.array([rng.randint(start, width, size=num),
233 rng.randint(start, height, size=num)]).swapaxes(0, 1)
237 """Measure fringe amplitudes for an exposure
239 The fringe amplitudes are measured as the statistic within a square
240 aperture. The statistic within a larger aperture are subtracted so
241 as to remove the background.
246 Exposure to measure the positions on.
247 positions : `numpy.array`
248 Two-dimensional array containing the positions to sample
249 for fringe amplitudes.
250 title : `str`, optional
251 Title used
for debug out plots.
255 fringes : `numpy.array`
256 Array of measured exposure values at each of the positions
259 stats = afwMath.StatisticsControl()
260 stats.setNumSigmaClip(self.config.stats.clip)
261 stats.setNumIter(self.config.stats.iterations)
262 stats.setAndMask(exposure.getMaskedImage().getMask().getPlaneBitMask(self.config.stats.badMaskPlanes))
264 num = self.config.num
265 fringes = numpy.ndarray(num)
269 small =
measure(exposure.getMaskedImage(), x, y, self.config.small, self.config.stats.stat, stats)
270 large =
measure(exposure.getMaskedImage(), x, y, self.config.large, self.config.stats.stat, stats)
271 fringes[i] = small - large
276 disp = afwDisplay.Display(frame=
getFrame())
277 disp.mtv(exposure, title=title)
279 with disp.Buffering():
280 for x, y
in positions:
281 corners = numpy.array([[-1, -1], [1, -1], [1, 1], [-1, 1], [-1, -1]]) + [[x, y]]
282 disp.line(corners*self.config.small, ctype=afwDisplay.GREEN)
283 disp.line(corners*self.config.large, ctype=afwDisplay.BLUE)
289 """Solve for the scale factors with iterative clipping.
293 science : `numpy.array`
294 Array of measured science image values at each of the
296 fringes : `numpy.array`
297 Array of measured fringe values at each of the positions
302 solution : `np.array`
303 Fringe solution amplitudes for each input fringe frame.
305 RMS error
for the fit solution
for this exposure.
310 origNum = len(science)
312 def emptyResult(msg=""):
313 """Generate an empty result for return to the user
315 There are no good pixels; doesn't matter what we return.
317 self.log.warning("Unable to solve for fringes: no good pixels%s", msg)
320 out = out*len(fringes)
321 return numpy.array(out), numpy.nan
323 good = numpy.where(numpy.logical_and(numpy.isfinite(science), numpy.any(numpy.isfinite(fringes), 1)))
324 science = science[good]
325 fringes = fringes[good]
326 oldNum = len(science)
332 good =
select(science, self.config.clip)
333 for ff
in range(fringes.shape[1]):
334 good &=
select(fringes[:, ff], self.config.clip)
335 science = science[good]
336 fringes = fringes[good]
337 oldNum = len(science)
339 return emptyResult(
" after initial rejection")
341 for i
in range(self.config.iterations):
342 solution = self.
_solve(science, fringes)
343 resid = science - numpy.sum(solution*fringes, 1)
345 good = numpy.logical_not(abs(resid) > self.config.clip*rms)
346 self.log.debug(
"Iteration %d: RMS=%f numGood=%d", i, rms, good.sum())
347 self.log.debug(
"Solution %d: %s", i, solution)
350 return emptyResult(
" after %d rejection iterations" % i)
353 import matplotlib.pyplot
as plot
354 for j
in range(fringes.shape[1]):
358 fig.canvas._tkcanvas._root().lift()
361 ax = fig.add_subplot(1, 1, 1)
362 adjust = science.copy()
363 others = set(range(fringes.shape[1]))
366 adjust -= solution[k]*fringes[:, k]
367 ax.plot(fringes[:, j], adjust,
'r.')
368 xmin = fringes[:, j].min()
369 xmax = fringes[:, j].max()
370 ymin = solution[j]*xmin
371 ymax = solution[j]*xmax
372 ax.plot([xmin, xmax], [ymin, ymax],
'b-')
373 ax.set_title(
"Fringe %d: %f" % (j, solution[j]))
374 ax.set_xlabel(
"Fringe amplitude")
375 ax.set_ylabel(
"Science amplitude")
376 ax.set_autoscale_on(
False)
377 ax.set_xbound(lower=xmin, upper=xmax)
378 ax.set_ybound(lower=ymin, upper=ymax)
381 ans = input(
"Enter or c to continue [chp]").lower()
382 if ans
in (
"",
"c",):
388 print(
"h[elp] c[ontinue] p[db]")
394 good = numpy.where(good)
395 science = science[good]
396 fringes = fringes[good]
399 solution = self.
_solve(science, fringes)
400 self.log.info(
"Fringe solution: %s RMS: %f Good: %d/%d", solution, rms, len(science), origNum)
403 def _solve(self, science, fringes):
404 """Solve for the scale factors.
408 science : `numpy.array`
409 Array of measured science image values at each of the
411 fringes : `numpy.array`
412 Array of measured fringe values at each of the positions
417 solution : `np.array`
418 Fringe solution amplitudes for each input fringe frame.
420 return afwMath.LeastSquares.fromDesignMatrix(fringes, science,
421 afwMath.LeastSquares.DIRECT_SVD).getSolution()
424 """Subtract the fringes.
429 Science exposure from which to remove fringes.
431 Calibration fringe files containing master fringe frames.
432 solution : `np.array`
433 Fringe solution amplitudes
for each input fringe frame.
438 Raised
if the number of fringe frames does
not match the
439 number of measured amplitudes.
441 if len(solution) != len(fringes):
442 raise RuntimeError(
"Number of fringe frames (%s) != number of scale factors (%s)" %
443 (len(fringes), len(solution)))
445 for s, f
in zip(solution, fringes):
447 f.getMaskedImage().getMask().getArray()[:] = 0
448 science.getMaskedImage().scaledMinus(s, f.getMaskedImage())
452 """Measure a statistic within an aperture
454 @param mi MaskedImage to measure
455 @param x, y Center
for aperture
456 @param size Size of aperture
457 @param statistic Statistic to measure
458 @param stats StatisticsControl object
459 @return Value of statistic within aperture
463 subImage = mi.Factory(mi, bbox, afwImage.LOCAL)
464 return afwMath.makeStatistics(subImage, statistic, stats).getValue()
468 """Calculate a robust standard deviation of an array of values
470 @param vector Array of values
471 @return Standard deviation
473 q1, q3 = numpy.percentile(vector, (25, 75))
474 return 0.74*(q3 - q1)
478 """Select values within 'clip' standard deviations of the median
480 Returns a boolean array.
482 q1, q2, q3 = numpy.percentile(vector, (25, 50, 75))
483 return numpy.abs(vector - q2) < clip*0.74*(q3 - q1)
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 generatePositions(self, exposure, rng)
def run(self, exposure, fringes, seed=None)
def _solve(self, science, fringes)
def checkFilter(self, exposure)
def measure(mi, x, y, size, statistic, stats)