35 """Produce a new frame number each time"""
41 """Options for measuring fringes on an exposure"""
42 badMaskPlanes = ListField(dtype=str, default=[
"SAT"], doc=
"Ignore pixels with these masks")
43 stat = Field(dtype=int, default=afwMath.MEDIAN, doc=
"Statistic to use")
44 clip = Field(dtype=float, default=3.0, doc=
"Sigma clip threshold")
45 iterations = Field(dtype=int, default=3, doc=
"Number of fitting iterations")
46 rngSeedOffset = Field(dtype=int, default=0,
47 doc=
"Offset to the random number generator seed (full seed includes exposure ID)")
51 """Fringe subtraction options"""
52 filters = ListField(dtype=str, default=[], doc=
"Only fringe-subtract these filters")
53 num = Field(dtype=int, default=30000, doc=
"Number of fringe measurements")
54 small = Field(dtype=int, default=3, doc=
"Half-size of small (fringe) measurements (pixels)")
55 large = Field(dtype=int, default=30, doc=
"Half-size of large (background) measurements (pixels)")
56 iterations = Field(dtype=int, default=20, doc=
"Number of fitting iterations")
57 clip = Field(dtype=float, default=3.0, doc=
"Sigma clip threshold")
58 stats = ConfigField(dtype=FringeStatisticsConfig, doc=
"Statistics for measuring fringes")
59 pedestal = Field(dtype=bool, default=
False, doc=
"Remove fringe pedestal?")
62 """Task to remove fringes from a science exposure
64 We measure fringe amplitudes at random positions on the science exposure
65 and at the same positions on the (potentially multiple) fringe frames
66 and solve for the scales simultaneously.
68 ConfigClass = FringeConfig
71 """Read the fringe frame(s)
73 The current implementation assumes only a single fringe frame and
74 will have to be updated to support multi-mode fringe subtraction.
76 This implementation could be optimised by persisting the fringe
79 @param dataRef Data reference for the science exposure
80 @param assembler An instance of AssembleCcdTask (for assembling fringe frames)
81 @return Struct(fringes: fringe exposure or list of fringe exposures;
82 seed: 32-bit uint derived from ccdExposureId for random number generator
85 fringe = dataRef.get(
"fringe", immediate=
True)
86 except Exception
as e:
87 raise RuntimeError(
"Unable to retrieve fringe for %s: %s" % (dataRef.dataId, e))
88 if assembler
is not None:
89 fringe = assembler.assembleCcd(fringe)
91 seed = self.config.stats.rngSeedOffset + dataRef.get(
"ccdExposureId", immediate=
True)
95 return Struct(fringes = fringe,
99 def run(self, exposure, fringes, seed=None):
100 """Remove fringes from the provided science exposure.
102 Primary method of FringeTask. Fringes are only subtracted if the
103 science exposure has a filter listed in the configuration.
105 @param exposure Science exposure from which to remove fringes
106 @param fringes Exposure or list of Exposures
107 @param seed 32-bit unsigned integer for random number generator
115 if self.config.pedestal:
119 seed = self.config.stats.rngSeedOffset
120 rng = numpy.random.RandomState(seed=seed)
122 if hasattr(fringes,
'__iter__'):
125 fluxes = numpy.ndarray([len(positions), len(fringes)])
126 for i, f
in enumerate(fringes):
131 fluxes = self.
measureExposure(fringes, positions, title=
"Fringe frame")
132 fluxes = fluxes.reshape([len(positions), 1])
135 expFringes = self.
measureExposure(exposure, positions, title=
"Science")
136 solution = self.
solve(expFringes, fluxes)
137 self.
subtract(exposure, fringes, solution)
139 ds9.mtv(exposure, title=
"Fringe subtracted", frame=
getFrame())
143 """Remove fringes from the provided science exposure.
145 Retrieve fringes from butler dataRef provided and remove from
146 provided science exposure.
147 Fringes are only subtracted if the science exposure has a filter
148 listed in the configuration.
150 @param exposure Science exposure from which to remove fringes
151 @param dataRef Data reference for the science exposure
152 @param assembler An instance of AssembleCcdTask (for assembling fringe frames)
156 fringeStruct = self.
readFringes(dataRef, assembler=assembler)
157 self.
run(exposure, **fringeStruct.getDict())
160 """Check whether we should fringe-subtract the science exposure"""
161 return exposure.getFilter().getName()
in self.config.filters
164 """Remove pedestal from fringe exposure"""
166 stats.setNumSigmaClip(self.config.stats.clip)
167 stats.setNumIter(self.config.stats.iterations)
168 mi = fringe.getMaskedImage()
170 self.log.info(
"Removing fringe pedestal: %f" % pedestal)
174 """Generate a random distribution of positions for measuring fringe amplitudes"""
175 start = self.config.large
176 num = self.config.num
177 width = exposure.getWidth() - self.config.large
178 height = exposure.getHeight() - self.config.large
179 return numpy.array([rng.randint(start, width, size=num),
180 rng.randint(start, height, size=num)]).swapaxes(0, 1)
184 """Measure fringe amplitudes for an exposure
186 The fringe amplitudes are measured as the statistic within a square
187 aperture. The statistic within a larger aperture are subtracted so
188 as to remove the background.
190 @param exposure Exposure to measure
191 @param positions Array of (x,y) for fringe measurement
192 @param title Title for display
193 @return Array of fringe measurements
196 stats.setNumSigmaClip(self.config.stats.clip)
197 stats.setNumIter(self.config.stats.iterations)
198 stats.setAndMask(exposure.getMaskedImage().getMask().getPlaneBitMask(self.config.stats.badMaskPlanes))
200 num = self.config.num
201 fringes = numpy.ndarray(num)
205 small =
measure(exposure.getMaskedImage(), x, y, self.config.small, self.config.stats.stat, stats)
206 large =
measure(exposure.getMaskedImage(), x, y, self.config.large, self.config.stats.stat, stats)
207 fringes[i] = small - large
213 ds9.mtv(exposure, frame=frame, title=title)
215 with ds9.Buffering():
216 for x,y
in positions:
217 corners = numpy.array([[-1, -1], [ 1, -1], [ 1, 1], [-1, 1], [-1, -1]]) + [[x, y]]
218 ds9.line(corners * self.config.small, frame=frame, ctype=
"green")
219 ds9.line(corners * self.config.large, frame=frame, ctype=
"blue")
225 """Solve (with iterative clipping) for the scale factors
227 @param science Array of science exposure fringe amplitudes
228 @param fringes Array of arrays of fringe frame fringe amplitudes
229 @return Array of scale factors for the fringe frames
234 origNum = len(science)
236 good = numpy.where(numpy.logical_and(numpy.isfinite(science), numpy.any(numpy.isfinite(fringes), 1)))
237 science = science[good]
238 fringes = fringes[good]
239 oldNum = len(science)
241 for i
in range(self.config.iterations):
242 solution = self.
_solve(science, fringes)
243 resid = science - numpy.sum(solution * fringes, 1)
245 good = numpy.logical_not(abs(resid) > self.config.clip * rms)
246 self.log.logdebug(
"Iteration %d: RMS=%f numGood=%d" % (i, rms, good.sum()))
247 self.log.logdebug(
"Solution %d: %s" % (i, solution))
251 import matplotlib.pyplot
as plot
252 for j
in range(fringes.shape[1]):
256 fig.canvas._tkcanvas._root().lift()
259 ax = fig.add_axes((0.1, 0.1, 0.8, 0.8))
260 adjust = science.copy()
261 others = set(range(fringes.shape[1]))
264 adjust -= solution[k] * fringes[:,k]
265 ax.plot(fringes[:,j], adjust,
'r.')
266 xmin = fringes[:,j].min()
267 xmax = fringes[:,j].max()
268 ymin = solution[j] * xmin
269 ymax = solution[j] * xmax
270 ax.plot([xmin, xmax], [ymin, ymax],
'b-')
271 ax.set_title(
"Fringe %d: %f" % (j, solution[j]))
272 ax.set_xlabel(
"Fringe amplitude")
273 ax.set_ylabel(
"Science amplitude")
274 ax.set_autoscale_on(
False)
275 ax.set_xbound(lower=xmin, upper=xmax)
276 ax.set_ybound(lower=ymin, upper=ymax)
279 ans = raw_input(
"Enter or c to continue [chp]").lower()
280 if ans
in (
"",
"c",):
283 import pdb; pdb.set_trace()
285 print "h[elp] c[ontinue] p[db]"
291 good = numpy.where(good)
292 science = science[good]
293 fringes = fringes[good]
296 solution = self.
_solve(science, fringes)
297 self.log.info(
"Fringe solution: %s RMS: %f Good: %d/%d" % (solution, rms, len(science), origNum))
301 """Solve for the scale factors
303 @param science Array of science exposure fringe amplitudes
304 @param fringes Array of arrays of fringe frame fringe amplitudes
305 @return Array of scale factors for the fringe frames
307 return afwMath.LeastSquares.fromDesignMatrix(fringes, science,
308 afwMath.LeastSquares.DIRECT_SVD).getSolution()
311 """Subtract the fringes
313 @param science Science exposure
314 @param fringes List of fringe frames
315 @param solution Array of scale factors for the fringe frames
317 if len(solution) != len(fringes):
318 raise RuntimeError(
"Number of fringe frames (%s) != number of scale factors (%s)"% \
319 (len(fringes), len(solution)))
321 for s, f
in zip(solution, fringes):
322 science.getMaskedImage().scaledMinus(s, f.getMaskedImage())
325 def measure(mi, x, y, size, statistic, stats):
326 """Measure a statistic within an aperture
328 @param mi MaskedImage to measure
329 @param x, y Center for aperture
330 @param size Size of aperture
331 @param statistic Statistic to measure
332 @param stats StatisticsControl object
333 @return Value of statistic within aperture
336 subImage = mi.Factory(mi, bbox, afwImage.LOCAL)
340 """Calculate a robust standard deviation of an array of values
342 @param vector Array of values
343 @return Standard deviation
346 vector = vector.copy()
348 return 0.74 * (vector[int(0.75 * num)] - vector[int(0.25 * num)])
An integer coordinate rectangle.
Pass parameters to a Statistics objectA class to pass parameters which control how the stats are calc...
Statistics makeStatistics(afwImage::Mask< afwImage::MaskPixel > const &msk, int const flags, StatisticsControl const &sctrl)
Specialization to handle Masks.