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.
97 assembler : `lsst.ip.isr.AssembleCcdTask`, optional
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.
108 ( : `lsst.afw.image.Exposure` or `list` thereof)
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.
137 exposure : `lsst.afw.image.Exposure`
138 Science exposure from which to remove fringes.
139 fringes : `lsst.afw.image.Exposure` or `list` thereof
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")
200 """Remove pedestal from fringe exposure.
204 fringe : `lsst.afw.image.Exposure`
205 Fringe data to subtract the pedestal value from.
207 stats = afwMath.StatisticsControl()
208 stats.setNumSigmaClip(self.config.stats.clip)
209 stats.setNumIter(self.config.stats.iterations)
210 mi = fringe.getMaskedImage()
211 pedestal = afwMath.makeStatistics(mi, afwMath.MEDIAN, stats).getValue()
212 self.log.info(
"Removing fringe pedestal: %f", pedestal)
216 """Generate a random distribution of positions for measuring fringe
221 exposure : `lsst.afw.image.Exposure`
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.
249 exposure : `lsst.afw.image.Exposure`
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
263 stats = afwMath.StatisticsControl()
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.
432 science : `lsst.afw.image.Exposure`
433 Science exposure from which to remove fringes.
434 fringes : `lsst.afw.image.Exposure` or `list` thereof
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())