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
76 """Read the fringe frame(s) 78 The current implementation assumes only a single fringe frame and 79 will have to be updated to support multi-mode fringe subtraction. 81 This implementation could be optimised by persisting the fringe 84 @param dataRef Data reference for the science exposure 85 @param assembler An instance of AssembleCcdTask (for assembling fringe frames) 86 @return Struct(fringes: fringe exposure or list of fringe exposures; 87 seed: 32-bit uint derived from ccdExposureId for random number generator 90 fringe = dataRef.get(
"fringe", immediate=
True)
91 except Exception
as e:
92 raise RuntimeError(
"Unable to retrieve fringe for %s: %s" % (dataRef.dataId, e))
93 if assembler
is not None:
94 fringe = assembler.assembleCcd(fringe)
96 seed = self.
config.stats.rngSeedOffset + dataRef.get(
"ccdExposureId", immediate=
True)
100 return Struct(fringes=fringe,
104 def run(self, exposure, fringes, seed=None):
105 """Remove fringes from the provided science exposure. 107 Primary method of FringeTask. Fringes are only subtracted if the 108 science exposure has a filter listed in the configuration. 110 @param exposure Science exposure from which to remove fringes 111 @param fringes Exposure or list of Exposures 112 @param seed 32-bit unsigned integer for random number generator 121 seed = self.
config.stats.rngSeedOffset
122 rng = numpy.random.RandomState(seed=seed)
124 if not hasattr(fringes,
'__iter__'):
127 mask = exposure.getMaskedImage().getMask()
128 for fringe
in fringes:
129 fringe.getMaskedImage().getMask().__ior__(mask)
136 fluxes = numpy.ndarray([self.
config.num, len(fringes)])
137 for i, f
in enumerate(fringes):
138 fluxes[:, i] = self.
measureExposure(f, positions, title=
"Fringe frame")
140 expFringes = self.
measureExposure(exposure, positions, title=
"Science")
141 solution = self.
solve(expFringes, fluxes)
142 self.
subtract(exposure, fringes, solution)
144 ds9.mtv(exposure, title=
"Fringe subtracted", frame=
getFrame())
148 """Remove fringes from the provided science exposure. 150 Retrieve fringes from butler dataRef provided and remove from 151 provided science exposure. 152 Fringes are only subtracted if the science exposure has a filter 153 listed in the configuration. 155 @param exposure Science exposure from which to remove fringes 156 @param dataRef Data reference for the science exposure 157 @param assembler An instance of AssembleCcdTask (for assembling fringe frames) 161 fringeStruct = self.
readFringes(dataRef, assembler=assembler)
162 self.
run(exposure, **fringeStruct.getDict())
165 """Check whether we should fringe-subtract the science exposure""" 169 """Remove pedestal from fringe exposure""" 171 stats.setNumSigmaClip(self.
config.stats.clip)
172 stats.setNumIter(self.
config.stats.iterations)
173 mi = fringe.getMaskedImage()
175 self.
log.
info(
"Removing fringe pedestal: %f", pedestal)
179 """Generate a random distribution of positions for measuring fringe amplitudes""" 182 width = exposure.getWidth() - self.
config.large
183 height = exposure.getHeight() - self.
config.large
184 return numpy.array([rng.randint(start, width, size=num),
185 rng.randint(start, height, size=num)]).swapaxes(0, 1)
189 """Measure fringe amplitudes for an exposure 191 The fringe amplitudes are measured as the statistic within a square 192 aperture. The statistic within a larger aperture are subtracted so 193 as to remove the background. 195 @param exposure Exposure to measure 196 @param positions Array of (x,y) for fringe measurement 197 @param title Title for display 198 @return Array of fringe measurements 201 stats.setNumSigmaClip(self.
config.stats.clip)
202 stats.setNumIter(self.
config.stats.iterations)
203 stats.setAndMask(exposure.getMaskedImage().getMask().getPlaneBitMask(self.
config.stats.badMaskPlanes))
206 fringes = numpy.ndarray(num)
210 small =
measure(exposure.getMaskedImage(), x, y, self.
config.small, self.
config.stats.stat, stats)
211 large =
measure(exposure.getMaskedImage(), x, y, self.
config.large, self.
config.stats.stat, stats)
212 fringes[i] = small - large
218 ds9.mtv(exposure, frame=frame, title=title)
220 with ds9.Buffering():
221 for x, y
in positions:
222 corners = numpy.array([[-1, -1], [1, -1], [1, 1], [-1, 1], [-1, -1]]) + [[x, y]]
223 ds9.line(corners * self.
config.small, frame=frame, ctype=
"green")
224 ds9.line(corners * self.
config.large, frame=frame, ctype=
"blue")
230 """Solve (with iterative clipping) for the scale factors 232 @param science Array of science exposure fringe amplitudes 233 @param fringes Array of arrays of fringe frame fringe amplitudes 234 @return Array of scale factors for the fringe frames 239 origNum = len(science)
241 def emptyResult(msg=""):
242 """Generate an empty result for return to the user 244 There are no good pixels; doesn't matter what we return. 246 self.
log.
warn(
"Unable to solve for fringes: no good pixels%s", msg)
249 out = out*len(fringes)
250 return numpy.array(out)
252 good = numpy.where(numpy.logical_and(numpy.isfinite(science), numpy.any(numpy.isfinite(fringes), 1)))
253 science = science[good]
254 fringes = fringes[good]
255 oldNum = len(science)
262 for ff
in range(fringes.shape[1]):
264 science = science[good]
265 fringes = fringes[good]
266 oldNum = len(science)
268 return emptyResult(
" after initial rejection")
270 for i
in range(self.
config.iterations):
271 solution = self.
_solve(science, fringes)
272 resid = science - numpy.sum(solution * fringes, 1)
274 good = numpy.logical_not(
abs(resid) > self.
config.clip * rms)
275 self.
log.
debug(
"Iteration %d: RMS=%f numGood=%d", i, rms, good.sum())
276 self.
log.
debug(
"Solution %d: %s", i, solution)
279 return emptyResult(
" after %d rejection iterations" % i)
282 import matplotlib.pyplot
as plot
283 for j
in range(fringes.shape[1]):
287 fig.canvas._tkcanvas._root().lift()
290 ax = fig.add_subplot(1, 1, 1)
291 adjust = science.copy()
292 others =
set(range(fringes.shape[1]))
295 adjust -= solution[k] * fringes[:, k]
296 ax.plot(fringes[:, j], adjust,
'r.')
297 xmin = fringes[:, j].
min()
298 xmax = fringes[:, j].
max()
299 ymin = solution[j] * xmin
300 ymax = solution[j] * xmax
301 ax.plot([xmin, xmax], [ymin, ymax],
'b-')
302 ax.set_title(
"Fringe %d: %f" % (j, solution[j]))
303 ax.set_xlabel(
"Fringe amplitude")
304 ax.set_ylabel(
"Science amplitude")
305 ax.set_autoscale_on(
False)
306 ax.set_xbound(lower=xmin, upper=xmax)
307 ax.set_ybound(lower=ymin, upper=ymax)
310 ans = input(
"Enter or c to continue [chp]").lower()
311 if ans
in (
"",
"c",):
317 print(
"h[elp] c[ontinue] p[db]")
323 good = numpy.where(good)
324 science = science[good]
325 fringes = fringes[good]
328 solution = self.
_solve(science, fringes)
329 self.
log.
info(
"Fringe solution: %s RMS: %f Good: %d/%d", solution, rms, len(science), origNum)
332 def _solve(self, science, fringes):
333 """Solve for the scale factors 335 @param science Array of science exposure fringe amplitudes 336 @param fringes Array of arrays of fringe frame fringe amplitudes 337 @return Array of scale factors for the fringe frames 339 return afwMath.LeastSquares.fromDesignMatrix(fringes, science,
340 afwMath.LeastSquares.DIRECT_SVD).getSolution()
343 """Subtract the fringes 345 @param science Science exposure 346 @param fringes List of fringe frames 347 @param solution Array of scale factors for the fringe frames 349 if len(solution) != len(fringes):
350 raise RuntimeError(
"Number of fringe frames (%s) != number of scale factors (%s)" %
351 (len(fringes), len(solution)))
353 for s, f
in zip(solution, fringes):
354 science.getMaskedImage().scaledMinus(s, f.getMaskedImage())
357 def measure(mi, x, y, size, statistic, stats):
358 """Measure a statistic within an aperture 360 @param mi MaskedImage to measure 361 @param x, y Center for aperture 362 @param size Size of aperture 363 @param statistic Statistic to measure 364 @param stats StatisticsControl object 365 @return Value of statistic within aperture 368 subImage = mi.Factory(mi, bbox, afwImage.LOCAL)
373 """Calculate a robust standard deviation of an array of values 375 @param vector Array of values 376 @return Standard deviation 378 q1, q3 = numpy.percentile(vector, (25, 75))
383 """Select values within 'clip' standard deviations of the median 385 Returns a boolean array. 387 q1, q2, q3 = numpy.percentile(vector, (25, 50, 75))
388 return numpy.abs(vector - q2) < clip*0.74*(q3 - q1)
def runDataRef(self, exposure, dataRef, assembler=None)
Angle abs(Angle const &a)
def run(self, exposure, fringes, seed=None)
def checkFilter(self, exposure)
def subtract(self, science, fringes, solution)
def _solve(self, science, fringes)
daf::base::PropertySet * set
Statistics makeStatistics(lsst::afw::math::MaskedVector< EntryT > const &mv, std::vector< WeightPixel > const &vweights, int const flags, StatisticsControl const &sctrl=StatisticsControl())
The makeStatistics() overload to handle lsst::afw::math::MaskedVector<>
Pass parameters to a Statistics object.
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)
An integer coordinate rectangle.
def solve(self, science, fringes)