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 self.log.debug(
"Seeding with offset %d and ccdExposureId %d.",
119 self.config.stats.rngSeedOffset, expId)
120 seed = self.config.stats.rngSeedOffset + expId
126 return Struct(fringes=fringeExp,
130 def run(self, exposure, fringes, seed=None):
131 """Remove fringes from the provided science exposure.
133 Primary method of FringeTask. Fringes are only subtracted if the
134 science exposure has a filter listed in the configuration.
138 exposure : `lsst.afw.image.Exposure`
139 Science exposure from which to remove fringes.
140 fringes : `lsst.afw.image.Exposure` or `list` thereof
141 Calibration fringe files containing master fringe frames.
142 seed : `int`, optional
143 Seed for random number generation.
147 solution : `np.array`
148 Fringe solution amplitudes for each input fringe frame.
150 RMS error for the fit solution for this exposure.
156 self.log.info(
"Filter not found in FringeTaskConfig.filters. Skipping fringe correction.")
160 seed = self.config.stats.rngSeedOffset
161 rng = numpy.random.RandomState(seed=seed)
163 if not hasattr(fringes,
'__iter__'):
166 mask = exposure.getMaskedImage().getMask()
167 for fringe
in fringes:
168 fringe.getMaskedImage().getMask().__ior__(mask)
169 if self.config.pedestal:
173 fluxes = numpy.ndarray([self.config.num, len(fringes)])
174 for i, f
in enumerate(fringes):
175 fluxes[:, i] = self.
measureExposure(f, positions, title=
"Fringe frame")
177 expFringes = self.
measureExposure(exposure, positions, title=
"Science")
178 solution, rms = self.
solve(expFringes, fluxes)
179 self.
subtract(exposure, fringes, solution)
181 afwDisplay.Display(frame=
getFrame()).mtv(exposure, title=
"Fringe subtracted")
201 """Remove pedestal from fringe exposure.
205 fringe : `lsst.afw.image.Exposure`
206 Fringe data to subtract the pedestal value from.
208 stats = afwMath.StatisticsControl()
209 stats.setNumSigmaClip(self.config.stats.clip)
210 stats.setNumIter(self.config.stats.iterations)
211 mi = fringe.getMaskedImage()
212 pedestal = afwMath.makeStatistics(mi, afwMath.MEDIAN, stats).getValue()
213 self.log.info(
"Removing fringe pedestal: %f", pedestal)
217 """Generate a random distribution of positions for measuring fringe
222 exposure : `lsst.afw.image.Exposure`
223 Exposure to measure the positions on.
224 rng : `numpy.random.RandomState`
225 Random number generator to use.
229 positions : `numpy.array`
230 Two-dimensional array containing the positions to sample
231 for fringe amplitudes.
233 start = self.config.large
234 num = self.config.num
235 width = exposure.getWidth() - self.config.large
236 height = exposure.getHeight() - self.config.large
237 return numpy.array([rng.randint(start, width, size=num),
238 rng.randint(start, height, size=num)]).swapaxes(0, 1)
242 """Measure fringe amplitudes for an exposure
244 The fringe amplitudes are measured as the statistic within a square
245 aperture. The statistic within a larger aperture are subtracted so
246 as to remove the background.
250 exposure : `lsst.afw.image.Exposure`
251 Exposure to measure the positions on.
252 positions : `numpy.array`
253 Two-dimensional array containing the positions to sample
254 for fringe amplitudes.
255 title : `str`, optional
256 Title used for debug out plots.
260 fringes : `numpy.array`
261 Array of measured exposure values at each of the positions
264 stats = afwMath.StatisticsControl()
265 stats.setNumSigmaClip(self.config.stats.clip)
266 stats.setNumIter(self.config.stats.iterations)
267 stats.setAndMask(exposure.getMaskedImage().getMask().getPlaneBitMask(self.config.stats.badMaskPlanes))
269 num = self.config.num
270 fringes = numpy.ndarray(num)
274 small =
measure(exposure.getMaskedImage(), x, y, self.config.small, self.config.stats.stat, stats)
275 large =
measure(exposure.getMaskedImage(), x, y, self.config.large, self.config.stats.stat, stats)
276 fringes[i] = small - large
281 disp = afwDisplay.Display(frame=
getFrame())
282 disp.mtv(exposure, title=title)
284 with disp.Buffering():
285 for x, y
in positions:
286 corners = numpy.array([[-1, -1], [1, -1], [1, 1], [-1, 1], [-1, -1]]) + [[x, y]]
287 disp.line(corners*self.config.small, ctype=afwDisplay.GREEN)
288 disp.line(corners*self.config.large, ctype=afwDisplay.BLUE)
294 """Solve for the scale factors with iterative clipping.
298 science : `numpy.array`
299 Array of measured science image values at each of the
301 fringes : `numpy.array`
302 Array of measured fringe values at each of the positions
307 solution : `np.array`
308 Fringe solution amplitudes for each input fringe frame.
310 RMS error for the fit solution for this exposure.
315 origNum = len(science)
317 def emptyResult(msg=""):
318 """Generate an empty result for return to the user
320 There are no good pixels; doesn't matter what we return.
322 self.log.warning(
"Unable to solve for fringes: no good pixels%s", msg)
325 out = out*len(fringes)
326 return numpy.array(out), numpy.nan
328 good = numpy.where(numpy.logical_and(numpy.isfinite(science), numpy.any(numpy.isfinite(fringes), 1)))
329 science = science[good]
330 fringes = fringes[good]
331 oldNum = len(science)
337 good =
select(science, self.config.clip)
338 for ff
in range(fringes.shape[1]):
339 good &=
select(fringes[:, ff], self.config.clip)
340 science = science[good]
341 fringes = fringes[good]
342 oldNum = len(science)
344 return emptyResult(
" after initial rejection")
346 for i
in range(self.config.iterations):
347 solution = self.
_solve(science, fringes)
348 resid = science - numpy.sum(solution*fringes, 1)
350 good = numpy.logical_not(abs(resid) > self.config.clip*rms)
351 self.log.debug(
"Iteration %d: RMS=%f numGood=%d", i, rms, good.sum())
352 self.log.debug(
"Solution %d: %s", i, solution)
355 return emptyResult(
" after %d rejection iterations" % i)
358 import matplotlib.pyplot
as plot
359 for j
in range(fringes.shape[1]):
363 fig.canvas._tkcanvas._root().lift()
366 ax = fig.add_subplot(1, 1, 1)
367 adjust = science.copy()
368 others = set(range(fringes.shape[1]))
371 adjust -= solution[k]*fringes[:, k]
372 ax.plot(fringes[:, j], adjust,
'r.')
373 xmin = fringes[:, j].min()
374 xmax = fringes[:, j].max()
375 ymin = solution[j]*xmin
376 ymax = solution[j]*xmax
377 ax.plot([xmin, xmax], [ymin, ymax],
'b-')
378 ax.set_title(
"Fringe %d: %f" % (j, solution[j]))
379 ax.set_xlabel(
"Fringe amplitude")
380 ax.set_ylabel(
"Science amplitude")
381 ax.set_autoscale_on(
False)
382 ax.set_xbound(lower=xmin, upper=xmax)
383 ax.set_ybound(lower=ymin, upper=ymax)
386 ans = input(
"Enter or c to continue [chp]").lower()
387 if ans
in (
"",
"c",):
393 print(
"h[elp] c[ontinue] p[db]")
399 good = numpy.where(good)
400 science = science[good]
401 fringes = fringes[good]
404 solution = self.
_solve(science, fringes)
405 self.log.info(
"Fringe solution: %s RMS: %f Good: %d/%d", solution, rms, len(science), origNum)
409 """Solve for the scale factors.
413 science : `numpy.array`
414 Array of measured science image values at each of the
416 fringes : `numpy.array`
417 Array of measured fringe values at each of the positions
422 solution : `np.array`
423 Fringe solution amplitudes for each input fringe frame.
425 return afwMath.LeastSquares.fromDesignMatrix(fringes, science,
426 afwMath.LeastSquares.DIRECT_SVD).getSolution()
429 """Subtract the fringes.
433 science : `lsst.afw.image.Exposure`
434 Science exposure from which to remove fringes.
435 fringes : `lsst.afw.image.Exposure` or `list` thereof
436 Calibration fringe files containing master fringe frames.
437 solution : `np.array`
438 Fringe solution amplitudes for each input fringe frame.
443 Raised if the number of fringe frames does not match the
444 number of measured amplitudes.
446 if len(solution) != len(fringes):
447 raise RuntimeError(
"Number of fringe frames (%s) != number of scale factors (%s)" %
448 (len(fringes), len(solution)))
450 for s, f
in zip(solution, fringes):
452 f.getMaskedImage().getMask().getArray()[:] = 0
453 science.getMaskedImage().scaledMinus(s, f.getMaskedImage())