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)
135 fluxes = numpy.ndarray([self.
config.num, len(fringes)])
136 for i, f
in enumerate(fringes):
137 fluxes[:, i] = self.
measureExposure(f, positions, title=
"Fringe frame")
139 expFringes = self.
measureExposure(exposure, positions, title=
"Science")
140 solution, rms = self.
solve(expFringes, fluxes)
141 self.
subtract(exposure, fringes, solution)
143 afwDisplay.Display(frame=
getFrame()).
mtv(exposure, title=
"Fringe subtracted")
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 return 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
217 disp = afwDisplay.Display(frame=
getFrame())
218 disp.mtv(exposure, title=title)
220 with disp.Buffering():
221 for x, y
in positions:
222 corners = numpy.array([[-1, -1], [1, -1], [1, 1], [-1, 1], [-1, -1]]) + [[x, y]]
223 disp.line(corners*self.
config.small, ctype=afwDisplay.GREEN)
224 disp.line(corners*self.
config.large, ctype=afwDisplay.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), numpy.nan
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 369 subImage = mi.Factory(mi, bbox, afwImage.LOCAL)
374 """Calculate a robust standard deviation of an array of values 376 @param vector Array of values 377 @return Standard deviation 379 q1, q3 = numpy.percentile(vector, (25, 75))
380 return 0.74*(q3 - q1)
384 """Select values within 'clip' standard deviations of the median 386 Returns a boolean array. 388 q1, q2, q3 = numpy.percentile(vector, (25, 50, 75))
389 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 mtv(data, frame=None, title="", wcs=None, args, kwargs)
def readFringes(self, dataRef, assembler=None)
def measure(mi, x, y, size, statistic, stats)
Backwards-compatibility support for depersisting the old Calib (FluxMag0/FluxMag0Err) objects...
An integer coordinate rectangle.
def solve(self, science, fringes)