32 afwDisplay.setDefaultMaskTransparency(75)
36 """Produce a new frame number each time""" 45 """Options for measuring fringes on an exposure""" 46 badMaskPlanes = ListField(dtype=str, default=[
"SAT"], doc=
"Ignore pixels with these masks")
47 stat = Field(dtype=int, default=int(afwMath.MEDIAN), doc=
"Statistic to use")
48 clip = Field(dtype=float, default=3.0, doc=
"Sigma clip threshold")
49 iterations = Field(dtype=int, default=3, doc=
"Number of fitting iterations")
50 rngSeedOffset = Field(dtype=int, default=0,
51 doc=
"Offset to the random number generator seed (full seed includes exposure ID)")
55 """Fringe subtraction options""" 56 filters = ListField(dtype=str, default=[], doc=
"Only fringe-subtract these filters")
57 num = Field(dtype=int, default=30000, doc=
"Number of fringe measurements")
58 small = Field(dtype=int, default=3, doc=
"Half-size of small (fringe) measurements (pixels)")
59 large = Field(dtype=int, default=30, doc=
"Half-size of large (background) measurements (pixels)")
60 iterations = Field(dtype=int, default=20, doc=
"Number of fitting iterations")
61 clip = Field(dtype=float, default=3.0, doc=
"Sigma clip threshold")
62 stats = ConfigField(dtype=FringeStatisticsConfig, doc=
"Statistics for measuring fringes")
63 pedestal = Field(dtype=bool, default=
False, doc=
"Remove fringe pedestal?")
67 """Task to remove fringes from a science exposure 69 We measure fringe amplitudes at random positions on the science exposure 70 and at the same positions on the (potentially multiple) fringe frames 71 and solve for the scales simultaneously. 73 ConfigClass = FringeConfig
74 _DefaultName =
'isrFringe' 77 """Read the fringe frame(s) 79 The current implementation assumes only a single fringe frame and 80 will have to be updated to support multi-mode fringe subtraction. 82 This implementation could be optimised by persisting the fringe 85 @param dataRef Data reference for the science exposure 86 @param assembler An instance of AssembleCcdTask (for assembling fringe frames) 87 @return Struct(fringes: fringe exposure or list of fringe exposures; 88 seed: 32-bit uint derived from ccdExposureId for random number generator 91 fringe = dataRef.get(
"fringe", immediate=
True)
92 except Exception
as e:
93 raise RuntimeError(
"Unable to retrieve fringe for %s: %s" % (dataRef.dataId, e))
94 if assembler
is not None:
95 fringe = assembler.assembleCcd(fringe)
97 seed = self.config.stats.rngSeedOffset + dataRef.get(
"ccdExposureId", immediate=
True)
101 return Struct(fringes=fringe,
105 def run(self, exposure, fringes, seed=None):
106 """Remove fringes from the provided science exposure. 108 Primary method of FringeTask. Fringes are only subtracted if the 109 science exposure has a filter listed in the configuration. 111 @param exposure Science exposure from which to remove fringes 112 @param fringes Exposure or list of Exposures 113 @param seed 32-bit unsigned integer for random number generator 122 seed = self.config.stats.rngSeedOffset
123 rng = numpy.random.RandomState(seed=seed)
125 if not hasattr(fringes,
'__iter__'):
128 mask = exposure.getMaskedImage().getMask()
129 for fringe
in fringes:
130 fringe.getMaskedImage().getMask().__ior__(mask)
131 if self.config.pedestal:
137 fluxes = numpy.ndarray([self.config.num, len(fringes)])
138 for i, f
in enumerate(fringes):
139 fluxes[:, i] = self.
measureExposure(f, positions, title=
"Fringe frame")
141 expFringes = self.
measureExposure(exposure, positions, title=
"Science")
142 solution = self.
solve(expFringes, fluxes)
143 self.
subtract(exposure, fringes, solution)
145 afwDisplay.Display(frame=
getFrame()).mtv(exposure, title=
"Fringe subtracted")
149 """Remove fringes from the provided science exposure. 151 Retrieve fringes from butler dataRef provided and remove from 152 provided science exposure. 153 Fringes are only subtracted if the science exposure has a filter 154 listed in the configuration. 156 @param exposure Science exposure from which to remove fringes 157 @param dataRef Data reference for the science exposure 158 @param assembler An instance of AssembleCcdTask (for assembling fringe frames) 162 fringeStruct = self.
readFringes(dataRef, assembler=assembler)
163 self.
run(exposure, **fringeStruct.getDict())
166 """Check whether we should fringe-subtract the science exposure""" 167 return exposure.getFilter().getName()
in self.config.filters
170 """Remove pedestal from fringe exposure""" 171 stats = afwMath.StatisticsControl()
172 stats.setNumSigmaClip(self.config.stats.clip)
173 stats.setNumIter(self.config.stats.iterations)
174 mi = fringe.getMaskedImage()
175 pedestal = afwMath.makeStatistics(mi, afwMath.MEDIAN, stats).getValue()
176 self.log.info(
"Removing fringe pedestal: %f", pedestal)
180 """Generate a random distribution of positions for measuring fringe amplitudes""" 181 start = self.config.large
182 num = self.config.num
183 width = exposure.getWidth() - self.config.large
184 height = exposure.getHeight() - self.config.large
185 return numpy.array([rng.randint(start, width, size=num),
186 rng.randint(start, height, size=num)]).swapaxes(0, 1)
190 """Measure fringe amplitudes for an exposure 192 The fringe amplitudes are measured as the statistic within a square 193 aperture. The statistic within a larger aperture are subtracted so 194 as to remove the background. 196 @param exposure Exposure to measure 197 @param positions Array of (x,y) for fringe measurement 198 @param title Title for display 199 @return Array of fringe measurements 201 stats = afwMath.StatisticsControl()
202 stats.setNumSigmaClip(self.config.stats.clip)
203 stats.setNumIter(self.config.stats.iterations)
204 stats.setAndMask(exposure.getMaskedImage().getMask().getPlaneBitMask(self.config.stats.badMaskPlanes))
206 num = self.config.num
207 fringes = numpy.ndarray(num)
211 small =
measure(exposure.getMaskedImage(), x, y, self.config.small, self.config.stats.stat, stats)
212 large =
measure(exposure.getMaskedImage(), x, y, self.config.large, self.config.stats.stat, stats)
213 fringes[i] = small - large
218 disp = afwDisplay.Display(frame=
getFrame())
219 disp.mtv(exposure, title=title)
221 with disp.Buffering():
222 for x, y
in positions:
223 corners = numpy.array([[-1, -1], [1, -1], [1, 1], [-1, 1], [-1, -1]]) + [[x, y]]
224 disp.line(corners*self.config.small, ctype=afwDisplay.GREEN)
225 disp.line(corners*self.config.large, ctype=afwDisplay.BLUE)
231 """Solve (with iterative clipping) for the scale factors 233 @param science Array of science exposure fringe amplitudes 234 @param fringes Array of arrays of fringe frame fringe amplitudes 235 @return Array of scale factors for the fringe frames 240 origNum = len(science)
242 def emptyResult(msg=""):
243 """Generate an empty result for return to the user 245 There are no good pixels; doesn't matter what we return. 247 self.log.warn(
"Unable to solve for fringes: no good pixels%s", msg)
250 out = out*len(fringes)
251 return numpy.array(out)
253 good = numpy.where(numpy.logical_and(numpy.isfinite(science), numpy.any(numpy.isfinite(fringes), 1)))
254 science = science[good]
255 fringes = fringes[good]
256 oldNum = len(science)
262 good =
select(science, self.config.clip)
263 for ff
in range(fringes.shape[1]):
264 good &=
select(fringes[:, ff], self.config.clip)
265 science = science[good]
266 fringes = fringes[good]
267 oldNum = len(science)
269 return emptyResult(
" after initial rejection")
271 for i
in range(self.config.iterations):
272 solution = self.
_solve(science, fringes)
273 resid = science - numpy.sum(solution*fringes, 1)
275 good = numpy.logical_not(abs(resid) > self.config.clip*rms)
276 self.log.debug(
"Iteration %d: RMS=%f numGood=%d", i, rms, good.sum())
277 self.log.debug(
"Solution %d: %s", i, solution)
280 return emptyResult(
" after %d rejection iterations" % i)
283 import matplotlib.pyplot
as plot
284 for j
in range(fringes.shape[1]):
288 fig.canvas._tkcanvas._root().lift()
291 ax = fig.add_subplot(1, 1, 1)
292 adjust = science.copy()
293 others = set(range(fringes.shape[1]))
296 adjust -= solution[k]*fringes[:, k]
297 ax.plot(fringes[:, j], adjust,
'r.')
298 xmin = fringes[:, j].min()
299 xmax = fringes[:, j].max()
300 ymin = solution[j]*xmin
301 ymax = solution[j]*xmax
302 ax.plot([xmin, xmax], [ymin, ymax],
'b-')
303 ax.set_title(
"Fringe %d: %f" % (j, solution[j]))
304 ax.set_xlabel(
"Fringe amplitude")
305 ax.set_ylabel(
"Science amplitude")
306 ax.set_autoscale_on(
False)
307 ax.set_xbound(lower=xmin, upper=xmax)
308 ax.set_ybound(lower=ymin, upper=ymax)
311 ans = input(
"Enter or c to continue [chp]").lower()
312 if ans
in (
"",
"c",):
318 print(
"h[elp] c[ontinue] p[db]")
324 good = numpy.where(good)
325 science = science[good]
326 fringes = fringes[good]
329 solution = self.
_solve(science, fringes)
330 self.log.info(
"Fringe solution: %s RMS: %f Good: %d/%d", solution, rms, len(science), origNum)
333 def _solve(self, science, fringes):
334 """Solve for the scale factors 336 @param science Array of science exposure fringe amplitudes 337 @param fringes Array of arrays of fringe frame fringe amplitudes 338 @return Array of scale factors for the fringe frames 340 return afwMath.LeastSquares.fromDesignMatrix(fringes, science,
341 afwMath.LeastSquares.DIRECT_SVD).getSolution()
344 """Subtract the fringes 346 @param science Science exposure 347 @param fringes List of fringe frames 348 @param solution Array of scale factors for the fringe frames 350 if len(solution) != len(fringes):
351 raise RuntimeError(
"Number of fringe frames (%s) != number of scale factors (%s)" %
352 (len(fringes), len(solution)))
354 for s, f
in zip(solution, fringes):
355 science.getMaskedImage().scaledMinus(s, f.getMaskedImage())
358 def measure(mi, x, y, size, statistic, stats):
359 """Measure a statistic within an aperture 361 @param mi MaskedImage to measure 362 @param x, y Center for aperture 363 @param size Size of aperture 364 @param statistic Statistic to measure 365 @param stats StatisticsControl object 366 @return Value of statistic within aperture 370 subImage = mi.Factory(mi, bbox, afwImage.LOCAL)
371 return afwMath.makeStatistics(subImage, statistic, stats).getValue()
375 """Calculate a robust standard deviation of an array of values 377 @param vector Array of values 378 @return Standard deviation 380 q1, q3 = numpy.percentile(vector, (25, 75))
381 return 0.74*(q3 - q1)
385 """Select values within 'clip' standard deviations of the median 387 Returns a boolean array. 389 q1, q2, q3 = numpy.percentile(vector, (25, 50, 75))
390 return numpy.abs(vector - q2) < clip*0.74*(q3 - q1)
def runDataRef(self, exposure, dataRef, assembler=None)
def run(self, exposure, fringes, seed=None)
def checkFilter(self, exposure)
def subtract(self, science, fringes, solution)
def _solve(self, science, fringes)
def generatePositions(self, exposure, rng)
def measureExposure(self, exposure, positions, title="Fringe")
def removePedestal(self, fringe)
def readFringes(self, dataRef, assembler=None)
def measure(mi, x, y, size, statistic, stats)
def solve(self, science, fringes)