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))
111 """Pack the fringe data into a Struct. 113 This method moves the struct parsing code into a butler 114 generation agnostic handler. 118 fringeExp : `lsst.afw.exposure.Exposure` 119 The exposure containing the fringe data. 120 expId : `int`, optional 121 Exposure id to be fringe corrected, used to set RNG seed. 122 assembler : `lsst.ip.isr.AssembleCcdTask`, optional 123 An instance of AssembleCcdTask (for assembling fringe 128 fringeData : `pipeBase.Struct` 129 Struct containing fringe data: 130 - ``fringes`` : `lsst.afw.image.Exposure` or `list` thereof 131 Calibration fringe files containing master fringe frames. 132 - ``seed`` : `int`, optional 133 Seed for random number generation. 135 if assembler
is not None:
136 fringeExp = assembler.assembleCcd(fringeExp)
139 seed = self.
config.stats.rngSeedOffset
141 print(f
"{self.config.stats.rngSeedOffset} {expId}")
142 seed = self.
config.stats.rngSeedOffset + expId
147 return Struct(fringes=fringeExp,
151 def run(self, exposure, fringes, seed=None):
152 """Remove fringes from the provided science exposure. 154 Primary method of FringeTask. Fringes are only subtracted if the 155 science exposure has a filter listed in the configuration. 159 exposure : `lsst.afw.image.Exposure` 160 Science exposure from which to remove fringes. 161 fringes : `lsst.afw.image.Exposure` or `list` thereof 162 Calibration fringe files containing master fringe frames. 163 seed : `int`, optional 164 Seed for random number generation. 168 solution : `np.array` 169 Fringe solution amplitudes for each input fringe frame. 171 RMS error for the fit solution for this exposure. 177 self.
log.
info(
"Filter not found in FringeTaskConfig.filters. Skipping fringe correction.")
181 seed = self.
config.stats.rngSeedOffset
182 rng = numpy.random.RandomState(seed=seed)
184 if not hasattr(fringes,
'__iter__'):
187 mask = exposure.getMaskedImage().getMask()
188 for fringe
in fringes:
189 fringe.getMaskedImage().getMask().__ior__(mask)
194 fluxes = numpy.ndarray([self.
config.num, len(fringes)])
195 for i, f
in enumerate(fringes):
196 fluxes[:, i] = self.
measureExposure(f, positions, title=
"Fringe frame")
198 expFringes = self.
measureExposure(exposure, positions, title=
"Science")
199 solution, rms = self.
solve(expFringes, fluxes)
200 self.
subtract(exposure, fringes, solution)
202 afwDisplay.Display(frame=
getFrame()).
mtv(exposure, title=
"Fringe subtracted")
207 """Remove fringes from the provided science exposure. 209 Retrieve fringes from butler dataRef provided and remove from 210 provided science exposure. Fringes are only subtracted if the 211 science exposure has a filter listed in the configuration. 215 exposure : `lsst.afw.image.Exposure` 216 Science exposure from which to remove fringes. 217 dataRef : `daf.persistence.butlerSubset.ButlerDataRef` 218 Butler reference to the exposure. Used to find 219 appropriate fringe data. 220 assembler : `lsst.ip.isr.AssembleCcdTask`, optional 221 An instance of AssembleCcdTask (for assembling fringe 226 solution : `np.array` 227 Fringe solution amplitudes for each input fringe frame. 229 RMS error for the fit solution for this exposure. 232 self.
log.
info(
"Filter not found in FringeTaskConfig.filters. Skipping fringe correction.")
234 fringeStruct = self.
readFringes(dataRef, assembler=assembler)
235 return self.
run(exposure, **fringeStruct.getDict())
238 """Check whether we should fringe-subtract the science exposure. 242 exposure : `lsst.afw.image.Exposure` 243 Exposure to check the filter of. 248 If True, then the exposure has a filter listed in the 249 configuration, and should have the fringe applied. 254 """Remove pedestal from fringe exposure. 258 fringe : `lsst.afw.image.Exposure` 259 Fringe data to subtract the pedestal value from. 262 stats.setNumSigmaClip(self.
config.stats.clip)
263 stats.setNumIter(self.
config.stats.iterations)
264 mi = fringe.getMaskedImage()
266 self.
log.
info(
"Removing fringe pedestal: %f", pedestal)
270 """Generate a random distribution of positions for measuring fringe amplitudes. 274 exposure : `lsst.afw.image.Exposure` 275 Exposure to measure the positions on. 276 rng : `numpy.random.RandomState` 277 Random number generator to use. 281 positions : `numpy.array` 282 Two-dimensional array containing the positions to sample 283 for fringe amplitudes. 287 width = exposure.getWidth() - self.
config.large
288 height = exposure.getHeight() - self.
config.large
289 return numpy.array([rng.randint(start, width, size=num),
290 rng.randint(start, height, size=num)]).swapaxes(0, 1)
294 """Measure fringe amplitudes for an exposure 296 The fringe amplitudes are measured as the statistic within a square 297 aperture. The statistic within a larger aperture are subtracted so 298 as to remove the background. 302 exposure : `lsst.afw.image.Exposure` 303 Exposure to measure the positions on. 304 positions : `numpy.array` 305 Two-dimensional array containing the positions to sample 306 for fringe amplitudes. 307 title : `str`, optional 308 Title used for debug out plots. 312 fringes : `numpy.array` 313 Array of measured exposure values at each of the positions 317 stats.setNumSigmaClip(self.
config.stats.clip)
318 stats.setNumIter(self.
config.stats.iterations)
319 stats.setAndMask(exposure.getMaskedImage().getMask().getPlaneBitMask(self.
config.stats.badMaskPlanes))
322 fringes = numpy.ndarray(num)
326 small =
measure(exposure.getMaskedImage(), x, y, self.
config.small, self.
config.stats.stat, stats)
327 large =
measure(exposure.getMaskedImage(), x, y, self.
config.large, self.
config.stats.stat, stats)
328 fringes[i] = small - large
333 disp = afwDisplay.Display(frame=
getFrame())
334 disp.mtv(exposure, title=title)
336 with disp.Buffering():
337 for x, y
in positions:
338 corners = numpy.array([[-1, -1], [1, -1], [1, 1], [-1, 1], [-1, -1]]) + [[x, y]]
339 disp.line(corners*self.
config.small, ctype=afwDisplay.GREEN)
340 disp.line(corners*self.
config.large, ctype=afwDisplay.BLUE)
346 """Solve for the scale factors with iterative clipping. 350 science : `numpy.array` 351 Array of measured science image values at each of the 353 fringes : `numpy.array` 354 Array of measured fringe values at each of the positions 359 solution : `np.array` 360 Fringe solution amplitudes for each input fringe frame. 362 RMS error for the fit solution for this exposure. 367 origNum = len(science)
369 def emptyResult(msg=""):
370 """Generate an empty result for return to the user 372 There are no good pixels; doesn't matter what we return. 374 self.
log.
warn(
"Unable to solve for fringes: no good pixels%s", msg)
377 out = out*len(fringes)
378 return numpy.array(out), numpy.nan
380 good = numpy.where(numpy.logical_and(numpy.isfinite(science), numpy.any(numpy.isfinite(fringes), 1)))
381 science = science[good]
382 fringes = fringes[good]
383 oldNum = len(science)
390 for ff
in range(fringes.shape[1]):
392 science = science[good]
393 fringes = fringes[good]
394 oldNum = len(science)
396 return emptyResult(
" after initial rejection")
398 for i
in range(self.
config.iterations):
399 solution = self.
_solve(science, fringes)
400 resid = science - numpy.sum(solution*fringes, 1)
402 good = numpy.logical_not(
abs(resid) > self.
config.clip*rms)
403 self.
log.
debug(
"Iteration %d: RMS=%f numGood=%d", i, rms, good.sum())
404 self.
log.
debug(
"Solution %d: %s", i, solution)
407 return emptyResult(
" after %d rejection iterations" % i)
410 import matplotlib.pyplot
as plot
411 for j
in range(fringes.shape[1]):
415 fig.canvas._tkcanvas._root().lift()
418 ax = fig.add_subplot(1, 1, 1)
419 adjust = science.copy()
420 others =
set(range(fringes.shape[1]))
423 adjust -= solution[k]*fringes[:, k]
424 ax.plot(fringes[:, j], adjust,
'r.')
425 xmin = fringes[:, j].
min()
426 xmax = fringes[:, j].
max()
427 ymin = solution[j]*xmin
428 ymax = solution[j]*xmax
429 ax.plot([xmin, xmax], [ymin, ymax],
'b-')
430 ax.set_title(
"Fringe %d: %f" % (j, solution[j]))
431 ax.set_xlabel(
"Fringe amplitude")
432 ax.set_ylabel(
"Science amplitude")
433 ax.set_autoscale_on(
False)
434 ax.set_xbound(lower=xmin, upper=xmax)
435 ax.set_ybound(lower=ymin, upper=ymax)
438 ans = input(
"Enter or c to continue [chp]").lower()
439 if ans
in (
"",
"c",):
445 print(
"h[elp] c[ontinue] p[db]")
451 good = numpy.where(good)
452 science = science[good]
453 fringes = fringes[good]
456 solution = self.
_solve(science, fringes)
457 self.
log.
info(
"Fringe solution: %s RMS: %f Good: %d/%d", solution, rms, len(science), origNum)
460 def _solve(self, science, fringes):
461 """Solve for the scale factors. 465 science : `numpy.array` 466 Array of measured science image values at each of the 468 fringes : `numpy.array` 469 Array of measured fringe values at each of the positions 474 solution : `np.array` 475 Fringe solution amplitudes for each input fringe frame. 477 return afwMath.LeastSquares.fromDesignMatrix(fringes, science,
478 afwMath.LeastSquares.DIRECT_SVD).getSolution()
481 """Subtract the fringes. 485 science : `lsst.afw.image.Exposure` 486 Science exposure from which to remove fringes. 487 fringes : `lsst.afw.image.Exposure` or `list` thereof 488 Calibration fringe files containing master fringe frames. 489 solution : `np.array` 490 Fringe solution amplitudes for each input fringe frame. 495 Raised if the number of fringe frames does not match the 496 number of measured amplitudes. 498 if len(solution) != len(fringes):
499 raise RuntimeError(
"Number of fringe frames (%s) != number of scale factors (%s)" %
500 (len(fringes), len(solution)))
502 for s, f
in zip(solution, fringes):
503 science.getMaskedImage().scaledMinus(s, f.getMaskedImage())
506 def measure(mi, x, y, size, statistic, stats):
507 """Measure a statistic within an aperture 509 @param mi MaskedImage to measure 510 @param x, y Center for aperture 511 @param size Size of aperture 512 @param statistic Statistic to measure 513 @param stats StatisticsControl object 514 @return Value of statistic within aperture 518 subImage = mi.Factory(mi, bbox, afwImage.LOCAL)
523 """Calculate a robust standard deviation of an array of values 525 @param vector Array of values 526 @return Standard deviation 528 q1, q3 = numpy.percentile(vector, (25, 75))
529 return 0.74*(q3 - q1)
533 """Select values within 'clip' standard deviations of the median 535 Returns a boolean array. 537 q1, q2, q3 = numpy.percentile(vector, (25, 50, 75))
538 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 loadFringes(self, fringeExp, expId=0, assembler=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)