30 from lsst.pex.config
import Config, Field, ListField, ConfigField
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), and pack data into a Struct 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 87 dataRef : `daf.butler.butlerSubset.ButlerDataRef` 88 Butler reference for the exposure that will have fringing 90 assembler : `lsst.ip.isr.AssembleCcdTask`, optional 91 An instance of AssembleCcdTask (for assembling fringe 96 fringeData : `pipeBase.Struct` 97 Struct containing fringe data: 98 - ``fringes`` : `lsst.afw.image.Exposure` or `list` thereof 99 Calibration fringe files containing master fringe frames. 100 - ``seed`` : `int`, optional 101 Seed for random number generation. 104 fringe = dataRef.get(
"fringe", immediate=
True)
105 except Exception
as e:
106 raise RuntimeError(
"Unable to retrieve fringe for %s: %s" % (dataRef.dataId, e))
107 if assembler
is not None:
108 fringe = assembler.assembleCcd(fringe)
110 seed = self.
config.stats.rngSeedOffset + dataRef.get(
"ccdExposureId", immediate=
True)
114 return Struct(fringes=fringe,
118 def run(self, exposure, fringes, seed=None):
119 """Remove fringes from the provided science exposure. 121 Primary method of FringeTask. Fringes are only subtracted if the 122 science exposure has a filter listed in the configuration. 126 exposure : `lsst.afw.image.Exposure` 127 Science exposure from which to remove fringes. 128 fringes : `lsst.afw.image.Exposure` or `list` thereof 129 Calibration fringe files containing master fringe frames. 130 seed : `int`, optional 131 Seed for random number generation. 135 solution : `np.array` 136 Fringe solution amplitudes for each input fringe frame. 138 RMS error for the fit solution for this exposure. 144 self.
log.
info(
"Filter not found in FringeTaskConfig.filters. Skipping fringe correction.")
148 seed = self.
config.stats.rngSeedOffset
149 rng = numpy.random.RandomState(seed=seed)
151 if not hasattr(fringes,
'__iter__'):
154 mask = exposure.getMaskedImage().getMask()
155 for fringe
in fringes:
156 fringe.getMaskedImage().getMask().__ior__(mask)
161 fluxes = numpy.ndarray([self.
config.num, len(fringes)])
162 for i, f
in enumerate(fringes):
163 fluxes[:, i] = self.
measureExposure(f, positions, title=
"Fringe frame")
165 expFringes = self.
measureExposure(exposure, positions, title=
"Science")
166 solution, rms = self.
solve(expFringes, fluxes)
167 self.
subtract(exposure, fringes, solution)
169 afwDisplay.Display(frame=
getFrame()).
mtv(exposure, title=
"Fringe subtracted")
174 """Remove fringes from the provided science exposure. 176 Retrieve fringes from butler dataRef provided and remove from 177 provided science exposure. Fringes are only subtracted if the 178 science exposure has a filter listed in the configuration. 182 exposure : `lsst.afw.image.Exposure` 183 Science exposure from which to remove fringes. 184 dataRef : `daf.persistence.butlerSubset.ButlerDataRef` 185 Butler reference to the exposure. Used to find 186 appropriate fringe data. 187 assembler : `lsst.ip.isr.AssembleCcdTask`, optional 188 An instance of AssembleCcdTask (for assembling fringe 193 solution : `np.array` 194 Fringe solution amplitudes for each input fringe frame. 196 RMS error for the fit solution for this exposure. 199 self.
log.
info(
"Filter not found in FringeTaskConfig.filters. Skipping fringe correction.")
201 fringeStruct = self.
readFringes(dataRef, assembler=assembler)
202 return self.
run(exposure, **fringeStruct.getDict())
205 """Check whether we should fringe-subtract the science exposure. 209 exposure : `lsst.afw.image.Exposure` 210 Exposure to check the filter of. 215 If True, then the exposure has a filter listed in the 216 configuration, and should have the fringe applied. 221 """Remove pedestal from fringe exposure. 225 fringe : `lsst.afw.image.Exposure` 226 Fringe data to subtract the pedestal value from. 229 stats.setNumSigmaClip(self.
config.stats.clip)
230 stats.setNumIter(self.
config.stats.iterations)
231 mi = fringe.getMaskedImage()
233 self.
log.
info(
"Removing fringe pedestal: %f", pedestal)
237 """Generate a random distribution of positions for measuring fringe amplitudes. 241 exposure : `lsst.afw.image.Exposure` 242 Exposure to measure the positions on. 243 rng : `numpy.random.RandomState` 244 Random number generator to use. 248 positions : `numpy.array` 249 Two-dimensional array containing the positions to sample 250 for fringe amplitudes. 254 width = exposure.getWidth() - self.
config.large
255 height = exposure.getHeight() - self.
config.large
256 return numpy.array([rng.randint(start, width, size=num),
257 rng.randint(start, height, size=num)]).swapaxes(0, 1)
261 """Measure fringe amplitudes for an exposure 263 The fringe amplitudes are measured as the statistic within a square 264 aperture. The statistic within a larger aperture are subtracted so 265 as to remove the background. 269 exposure : `lsst.afw.image.Exposure` 270 Exposure to measure the positions on. 271 positions : `numpy.array` 272 Two-dimensional array containing the positions to sample 273 for fringe amplitudes. 274 title : `str`, optional 275 Title used for debug out plots. 279 fringes : `numpy.array` 280 Array of measured exposure values at each of the positions 284 stats.setNumSigmaClip(self.
config.stats.clip)
285 stats.setNumIter(self.
config.stats.iterations)
286 stats.setAndMask(exposure.getMaskedImage().getMask().getPlaneBitMask(self.
config.stats.badMaskPlanes))
289 fringes = numpy.ndarray(num)
293 small =
measure(exposure.getMaskedImage(), x, y, self.
config.small, self.
config.stats.stat, stats)
294 large =
measure(exposure.getMaskedImage(), x, y, self.
config.large, self.
config.stats.stat, stats)
295 fringes[i] = small - large
300 disp = afwDisplay.Display(frame=
getFrame())
301 disp.mtv(exposure, title=title)
303 with disp.Buffering():
304 for x, y
in positions:
305 corners = numpy.array([[-1, -1], [1, -1], [1, 1], [-1, 1], [-1, -1]]) + [[x, y]]
306 disp.line(corners*self.
config.small, ctype=afwDisplay.GREEN)
307 disp.line(corners*self.
config.large, ctype=afwDisplay.BLUE)
313 """Solve for the scale factors with iterative clipping. 317 science : `numpy.array` 318 Array of measured science image values at each of the 320 fringes : `numpy.array` 321 Array of measured fringe values at each of the positions 326 solution : `np.array` 327 Fringe solution amplitudes for each input fringe frame. 329 RMS error for the fit solution for this exposure. 334 origNum = len(science)
336 def emptyResult(msg=""):
337 """Generate an empty result for return to the user 339 There are no good pixels; doesn't matter what we return. 341 self.
log.
warn(
"Unable to solve for fringes: no good pixels%s", msg)
344 out = out*len(fringes)
345 return numpy.array(out), numpy.nan
347 good = numpy.where(numpy.logical_and(numpy.isfinite(science), numpy.any(numpy.isfinite(fringes), 1)))
348 science = science[good]
349 fringes = fringes[good]
350 oldNum = len(science)
357 for ff
in range(fringes.shape[1]):
359 science = science[good]
360 fringes = fringes[good]
361 oldNum = len(science)
363 return emptyResult(
" after initial rejection")
365 for i
in range(self.
config.iterations):
366 solution = self.
_solve(science, fringes)
367 resid = science - numpy.sum(solution*fringes, 1)
369 good = numpy.logical_not(
abs(resid) > self.
config.clip*rms)
370 self.
log.
debug(
"Iteration %d: RMS=%f numGood=%d", i, rms, good.sum())
371 self.
log.
debug(
"Solution %d: %s", i, solution)
374 return emptyResult(
" after %d rejection iterations" % i)
377 import matplotlib.pyplot
as plot
378 for j
in range(fringes.shape[1]):
382 fig.canvas._tkcanvas._root().lift()
385 ax = fig.add_subplot(1, 1, 1)
386 adjust = science.copy()
387 others =
set(range(fringes.shape[1]))
390 adjust -= solution[k]*fringes[:, k]
391 ax.plot(fringes[:, j], adjust,
'r.')
392 xmin = fringes[:, j].
min()
393 xmax = fringes[:, j].
max()
394 ymin = solution[j]*xmin
395 ymax = solution[j]*xmax
396 ax.plot([xmin, xmax], [ymin, ymax],
'b-')
397 ax.set_title(
"Fringe %d: %f" % (j, solution[j]))
398 ax.set_xlabel(
"Fringe amplitude")
399 ax.set_ylabel(
"Science amplitude")
400 ax.set_autoscale_on(
False)
401 ax.set_xbound(lower=xmin, upper=xmax)
402 ax.set_ybound(lower=ymin, upper=ymax)
405 ans = input(
"Enter or c to continue [chp]").lower()
406 if ans
in (
"",
"c",):
412 print(
"h[elp] c[ontinue] p[db]")
418 good = numpy.where(good)
419 science = science[good]
420 fringes = fringes[good]
423 solution = self.
_solve(science, fringes)
424 self.
log.
info(
"Fringe solution: %s RMS: %f Good: %d/%d", solution, rms, len(science), origNum)
427 def _solve(self, science, fringes):
428 """Solve for the scale factors. 432 science : `numpy.array` 433 Array of measured science image values at each of the 435 fringes : `numpy.array` 436 Array of measured fringe values at each of the positions 441 solution : `np.array` 442 Fringe solution amplitudes for each input fringe frame. 444 return afwMath.LeastSquares.fromDesignMatrix(fringes, science,
445 afwMath.LeastSquares.DIRECT_SVD).getSolution()
448 """Subtract the fringes. 452 science : `lsst.afw.image.Exposure` 453 Science exposure from which to remove fringes. 454 fringes : `lsst.afw.image.Exposure` or `list` thereof 455 Calibration fringe files containing master fringe frames. 456 solution : `np.array` 457 Fringe solution amplitudes for each input fringe frame. 462 Raised if the number of fringe frames does not match the 463 number of measured amplitudes. 465 if len(solution) != len(fringes):
466 raise RuntimeError(
"Number of fringe frames (%s) != number of scale factors (%s)" %
467 (len(fringes), len(solution)))
469 for s, f
in zip(solution, fringes):
470 science.getMaskedImage().scaledMinus(s, f.getMaskedImage())
473 def measure(mi, x, y, size, statistic, stats):
474 """Measure a statistic within an aperture 476 @param mi MaskedImage to measure 477 @param x, y Center for aperture 478 @param size Size of aperture 479 @param statistic Statistic to measure 480 @param stats StatisticsControl object 481 @return Value of statistic within aperture 485 subImage = mi.Factory(mi, bbox, afwImage.LOCAL)
490 """Calculate a robust standard deviation of an array of values 492 @param vector Array of values 493 @return Standard deviation 495 q1, q3 = numpy.percentile(vector, (25, 75))
496 return 0.74*(q3 - q1)
500 """Select values within 'clip' standard deviations of the median 502 Returns a boolean array. 504 q1, q2, q3 = numpy.percentile(vector, (25, 50, 75))
505 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)