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 def run(self, exposure, dataRef, assembler=None):
72 """Remove fringes from the provided science exposure
74 Fringes are only subtracted if the science exposure has a filter
75 listed in the configuration.
77 @param exposure Science exposure from which to remove fringes
78 @param dataRef Data reference for the science exposure
79 @param assembler An instance of AssembleCcdTask (for assembling fringe frames)
86 fringes = self.
readFringes(dataRef, assembler=assembler)
87 expFringes = self.
measureExposure(exposure, fringes.positions, title=
"Science")
88 solution = self.
solve(expFringes, fringes.fluxes)
89 self.
subtract(exposure, fringes.fringes, solution)
91 ds9.mtv(exposure, title=
"Fringe subtracted", frame=
getFrame())
94 """Check whether we should fringe-subtract the science exposure"""
95 return exposure.getFilter().getName()
in self.config.filters
98 """Read the fringe frame(s) and measure fringe amplitudes.
100 The current implementation assumes only a single fringe frame and
101 will have to be updated to support multi-mode fringe subtraction.
103 This implementation could be optimised by persisting the fringe
104 positions and fluxes.
106 @param dataRef Data reference for the science exposure
107 @param assembler An instance of AssembleCcdTask (for assembling fringe frames)
108 @return Struct(fringes: list of fringe frames;
109 fluxes: fringe amplitues;
110 positions: array of (x,y) for fringe amplitude measurements)
112 seed = dataRef.get(
"ccdExposureId", immediate=
True) + self.config.stats.rngSeedOffset
113 rng = numpy.random.RandomState(seed=seed)
116 fringe = dataRef.get(
"fringe", immediate=
True)
117 except Exception
as e:
118 raise RuntimeError(
"Unable to retrieve fringe for %s: %s" % (dataRef.dataId, e))
119 if assembler
is not None:
120 fringe = assembler.assembleCcd(fringe)
122 if self.config.pedestal:
124 stats.setNumSigmaClip(self.config.stats.clip)
125 stats.setNumIter(self.config.stats.iterations)
126 mi = fringe.getMaskedImage()
128 self.log.info(
"Removing fringe pedestal: %f" % pedestal)
134 return Struct(fringes=[fringe],
135 fluxes=fluxes.reshape([len(positions), 1]),
140 """Generate a random distribution of positions for measuring fringe amplitudes"""
141 start = self.config.large
142 num = self.config.num
143 width = exposure.getWidth() - self.config.large
144 height = exposure.getHeight() - self.config.large
145 return numpy.array([rng.randint(start, width, size=num),
146 rng.randint(start, height, size=num)]).swapaxes(0, 1)
150 """Measure fringe amplitudes for an exposure
152 The fringe amplitudes are measured as the statistic within a square
153 aperture. The statistic within a larger aperture are subtracted so
154 as to remove the background.
156 @param exposure Exposure to measure
157 @param positions Array of (x,y) for fringe measurement
158 @param title Title for display
159 @return Array of fringe measurements
162 stats.setNumSigmaClip(self.config.stats.clip)
163 stats.setNumIter(self.config.stats.iterations)
164 stats.setAndMask(exposure.getMaskedImage().getMask().getPlaneBitMask(self.config.stats.badMaskPlanes))
166 num = self.config.num
167 fringes = numpy.ndarray(num)
171 small =
measure(exposure.getMaskedImage(), x, y, self.config.small, self.config.stats.stat, stats)
172 large =
measure(exposure.getMaskedImage(), x, y, self.config.large, self.config.stats.stat, stats)
173 fringes[i] = small - large
179 ds9.mtv(exposure, frame=frame, title=title)
181 with ds9.Buffering():
182 for x,y
in positions:
183 corners = numpy.array([[-1, -1], [ 1, -1], [ 1, 1], [-1, 1], [-1, -1]]) + [[x, y]]
184 ds9.line(corners * self.config.small, frame=frame, ctype=
"green")
185 ds9.line(corners * self.config.large, frame=frame, ctype=
"blue")
191 """Solve (with iterative clipping) for the scale factors
193 @param science Array of science exposure fringe amplitudes
194 @param fringes Array of arrays of fringe frame fringe amplitudes
195 @return Array of scale factors for the fringe frames
200 origNum = len(science)
202 good = numpy.where(numpy.logical_and(numpy.isfinite(science), numpy.any(numpy.isfinite(fringes), 1)))
203 science = science[good]
204 fringes = fringes[good]
205 oldNum = len(science)
207 for i
in range(self.config.iterations):
208 solution = self.
_solve(science, fringes)
209 resid = science - numpy.sum(solution * fringes, 1)
211 good = numpy.logical_not(abs(resid) > self.config.clip * rms)
212 self.log.logdebug(
"Iteration %d: RMS=%f numGood=%d" % (i, rms, good.sum()))
213 self.log.logdebug(
"Solution %d: %s" % (i, solution))
217 import matplotlib.pyplot
as plot
218 for j
in range(fringes.shape[1]):
222 fig.canvas._tkcanvas._root().lift()
225 ax = fig.add_axes((0.1, 0.1, 0.8, 0.8))
226 adjust = science.copy()
227 others = set(range(fringes.shape[1]))
230 adjust -= solution[k] * fringes[:,k]
231 ax.plot(fringes[:,j], adjust,
'r.')
232 xmin = fringes[:,j].
min()
233 xmax = fringes[:,j].
max()
234 ymin = solution[j] * xmin
235 ymax = solution[j] * xmax
236 ax.plot([xmin, xmax], [ymin, ymax],
'b-')
237 ax.set_title(
"Fringe %d: %f" % (j, solution[j]))
238 ax.set_xlabel(
"Fringe amplitude")
239 ax.set_ylabel(
"Science amplitude")
240 ax.set_autoscale_on(
False)
241 ax.set_xbound(lower=xmin, upper=xmax)
242 ax.set_ybound(lower=ymin, upper=ymax)
245 ans = raw_input(
"Enter or c to continue [chp]").lower()
246 if ans
in (
"",
"c",):
249 import pdb; pdb.set_trace()
251 print "h[elp] c[ontinue] p[db]"
257 good = numpy.where(good)
258 science = science[good]
259 fringes = fringes[good]
262 solution = self.
_solve(science, fringes)
263 self.log.info(
"Fringe solution: %s RMS: %f Good: %d/%d" % (solution, rms, len(science), origNum))
267 """Solve for the scale factors
269 @param science Array of science exposure fringe amplitudes
270 @param fringes Array of arrays of fringe frame fringe amplitudes
271 @return Array of scale factors for the fringe frames
273 return afwMath.LeastSquares.fromDesignMatrix(fringes, science,
274 afwMath.LeastSquares.DIRECT_SVD).getSolution()
277 """Subtract the fringes
279 @param science Science exposure
280 @param fringes List of fringe frames
281 @param solution Array of scale factors for the fringe frames
283 for s, f
in zip(solution, fringes):
284 science.getMaskedImage().scaledMinus(s, f.getMaskedImage())
287 def measure(mi, x, y, size, statistic, stats):
288 """Measure a statistic within an aperture
290 @param mi MaskedImage to measure
291 @param x, y Center for aperture
292 @param size Size of aperture
293 @param statistic Statistic to measure
294 @param stats StatisticsControl object
295 @return Value of statistic within aperture
298 subImage = mi.Factory(mi, bbox, afwImage.LOCAL)
302 """Calculate a robust standard deviation of an array of values
304 @param vector Array of values
305 @return Standard deviation
308 vector = vector.copy()
310 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.