LSSTApplications  17.0+124,17.0+14,17.0+73,18.0.0+37,18.0.0+80,18.0.0-4-g68ffd23+4,18.1.0-1-g0001055+12,18.1.0-1-g03d53ef+5,18.1.0-1-g1349e88+55,18.1.0-1-g2505f39+44,18.1.0-1-g5315e5e+4,18.1.0-1-g5e4b7ea+14,18.1.0-1-g7e8fceb+4,18.1.0-1-g85f8cd4+48,18.1.0-1-g8ff0b9f+4,18.1.0-1-ga2c679d+1,18.1.0-1-gd55f500+35,18.1.0-10-gb58edde+2,18.1.0-11-g0997b02+4,18.1.0-13-gfe4edf0b+12,18.1.0-14-g259bd21+21,18.1.0-19-gdb69f3f+2,18.1.0-2-g5f9922c+24,18.1.0-2-gd3b74e5+11,18.1.0-2-gfbf3545+32,18.1.0-26-g728bddb4+5,18.1.0-27-g6ff7ca9+2,18.1.0-3-g52aa583+25,18.1.0-3-g8ea57af+9,18.1.0-3-gb69f684+42,18.1.0-3-gfcaddf3+6,18.1.0-32-gd8786685a,18.1.0-4-gf3f9b77+6,18.1.0-5-g1dd662b+2,18.1.0-5-g6dbcb01+41,18.1.0-6-gae77429+3,18.1.0-7-g9d75d83+9,18.1.0-7-gae09a6d+30,18.1.0-9-gc381ef5+4,w.2019.45
LSSTDataManagementBasePackage
fringe.py
Go to the documentation of this file.
1 # This file is part of ip_isr.
2 #
3 # Developed for the LSST Data Management System.
4 # This product includes software developed by the LSST Project
5 # (https://www.lsst.org).
6 # See the COPYRIGHT file at the top-level directory of this distribution
7 # for details of code ownership.
8 #
9 # This program is free software: you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation, either version 3 of the License, or
12 # (at your option) any later version.
13 #
14 # This program is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
18 #
19 # You should have received a copy of the GNU General Public License
20 # along with this program. If not, see <https://www.gnu.org/licenses/>.
21 
22 import numpy
23 
24 import lsst.geom
25 import lsst.afw.image as afwImage
26 import lsst.afw.math as afwMath
27 import lsst.afw.display as afwDisplay
28 
29 from lsst.pipe.base import Task, Struct, timeMethod
30 from lsst.pex.config import Config, Field, ListField, ConfigField
31 
32 afwDisplay.setDefaultMaskTransparency(75)
33 
34 
35 def getFrame():
36  """Produce a new frame number each time"""
37  getFrame.frame += 1
38  return getFrame.frame
39 
40 
41 getFrame.frame = 0
42 
43 
44 class FringeStatisticsConfig(Config):
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)")
52 
53 
54 class FringeConfig(Config):
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?")
64 
65 
67  """Task to remove fringes from a science exposure
68 
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.
72  """
73  ConfigClass = FringeConfig
74  _DefaultName = 'isrFringe'
75 
76  def readFringes(self, dataRef, assembler=None):
77  """Read the fringe frame(s), and pack data into a Struct
78 
79  The current implementation assumes only a single fringe frame and
80  will have to be updated to support multi-mode fringe subtraction.
81 
82  This implementation could be optimised by persisting the fringe
83  positions and fluxes.
84 
85  Parameters
86  ----------
87  dataRef : `daf.butler.butlerSubset.ButlerDataRef`
88  Butler reference for the exposure that will have fringing
89  removed.
90  assembler : `lsst.ip.isr.AssembleCcdTask`, optional
91  An instance of AssembleCcdTask (for assembling fringe
92  frames).
93 
94  Returns
95  -------
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.
102  """
103  try:
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)
109 
110  seed = self.config.stats.rngSeedOffset + dataRef.get("ccdExposureId", immediate=True)
111  # Seed for numpy.random.RandomState must be convertable to a 32 bit unsigned integer
112  seed %= 2**32
113 
114  return Struct(fringes=fringe,
115  seed=seed)
116 
117  @timeMethod
118  def run(self, exposure, fringes, seed=None):
119  """Remove fringes from the provided science exposure.
120 
121  Primary method of FringeTask. Fringes are only subtracted if the
122  science exposure has a filter listed in the configuration.
123 
124  Parameters
125  ----------
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.
132 
133  Returns
134  -------
135  solution : `np.array`
136  Fringe solution amplitudes for each input fringe frame.
137  rms : `float`
138  RMS error for the fit solution for this exposure.
139  """
140  import lsstDebug
141  display = lsstDebug.Info(__name__).display
142 
143  if not self.checkFilter(exposure):
144  self.log.info("Filter not found in FringeTaskConfig.filters. Skipping fringe correction.")
145  return
146 
147  if seed is None:
148  seed = self.config.stats.rngSeedOffset
149  rng = numpy.random.RandomState(seed=seed)
150 
151  if not hasattr(fringes, '__iter__'):
152  fringes = [fringes]
153 
154  mask = exposure.getMaskedImage().getMask()
155  for fringe in fringes:
156  fringe.getMaskedImage().getMask().__ior__(mask)
157  if self.config.pedestal:
158  self.removePedestal(fringe)
159 
160  positions = self.generatePositions(fringes[0], rng)
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")
164 
165  expFringes = self.measureExposure(exposure, positions, title="Science")
166  solution, rms = self.solve(expFringes, fluxes)
167  self.subtract(exposure, fringes, solution)
168  if display:
169  afwDisplay.Display(frame=getFrame()).mtv(exposure, title="Fringe subtracted")
170  return solution, rms
171 
172  @timeMethod
173  def runDataRef(self, exposure, dataRef, assembler=None):
174  """Remove fringes from the provided science exposure.
175 
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.
179 
180  Parameters
181  ----------
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
189  frames).
190 
191  Returns
192  -------
193  solution : `np.array`
194  Fringe solution amplitudes for each input fringe frame.
195  rms : `float`
196  RMS error for the fit solution for this exposure.
197  """
198  if not self.checkFilter(exposure):
199  self.log.info("Filter not found in FringeTaskConfig.filters. Skipping fringe correction.")
200  return
201  fringeStruct = self.readFringes(dataRef, assembler=assembler)
202  return self.run(exposure, **fringeStruct.getDict())
203 
204  def checkFilter(self, exposure):
205  """Check whether we should fringe-subtract the science exposure.
206 
207  Parameters
208  ----------
209  exposure : `lsst.afw.image.Exposure`
210  Exposure to check the filter of.
211 
212  Returns
213  -------
214  needsFringe : `bool`
215  If True, then the exposure has a filter listed in the
216  configuration, and should have the fringe applied.
217  """
218  return exposure.getFilter().getName() in self.config.filters
219 
220  def removePedestal(self, fringe):
221  """Remove pedestal from fringe exposure.
222 
223  Parameters
224  ----------
225  fringe : `lsst.afw.image.Exposure`
226  Fringe data to subtract the pedestal value from.
227  """
228  stats = afwMath.StatisticsControl()
229  stats.setNumSigmaClip(self.config.stats.clip)
230  stats.setNumIter(self.config.stats.iterations)
231  mi = fringe.getMaskedImage()
232  pedestal = afwMath.makeStatistics(mi, afwMath.MEDIAN, stats).getValue()
233  self.log.info("Removing fringe pedestal: %f", pedestal)
234  mi -= pedestal
235 
236  def generatePositions(self, exposure, rng):
237  """Generate a random distribution of positions for measuring fringe amplitudes.
238 
239  Parameters
240  ----------
241  exposure : `lsst.afw.image.Exposure`
242  Exposure to measure the positions on.
243  rng : `numpy.random.RandomState`
244  Random number generator to use.
245 
246  Returns
247  -------
248  positions : `numpy.array`
249  Two-dimensional array containing the positions to sample
250  for fringe amplitudes.
251  """
252  start = self.config.large
253  num = self.config.num
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)
258 
259  @timeMethod
260  def measureExposure(self, exposure, positions, title="Fringe"):
261  """Measure fringe amplitudes for an exposure
262 
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.
266 
267  Parameters
268  ----------
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.
276 
277  Returns
278  -------
279  fringes : `numpy.array`
280  Array of measured exposure values at each of the positions
281  supplied.
282  """
283  stats = afwMath.StatisticsControl()
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))
287 
288  num = self.config.num
289  fringes = numpy.ndarray(num)
290 
291  for i in range(num):
292  x, y = positions[i]
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
296 
297  import lsstDebug
298  display = lsstDebug.Info(__name__).display
299  if display:
300  disp = afwDisplay.Display(frame=getFrame())
301  disp.mtv(exposure, title=title)
302  if False:
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)
308 
309  return fringes
310 
311  @timeMethod
312  def solve(self, science, fringes):
313  """Solve for the scale factors with iterative clipping.
314 
315  Parameters
316  ----------
317  science : `numpy.array`
318  Array of measured science image values at each of the
319  positions supplied.
320  fringes : `numpy.array`
321  Array of measured fringe values at each of the positions
322  supplied.
323 
324  Returns
325  -------
326  solution : `np.array`
327  Fringe solution amplitudes for each input fringe frame.
328  rms : `float`
329  RMS error for the fit solution for this exposure.
330  """
331  import lsstDebug
332  doPlot = lsstDebug.Info(__name__).plot
333 
334  origNum = len(science)
335 
336  def emptyResult(msg=""):
337  """Generate an empty result for return to the user
338 
339  There are no good pixels; doesn't matter what we return.
340  """
341  self.log.warn("Unable to solve for fringes: no good pixels%s", msg)
342  out = [0]
343  if len(fringes) > 1:
344  out = out*len(fringes)
345  return numpy.array(out), numpy.nan
346 
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)
351  if oldNum == 0:
352  return emptyResult()
353 
354  # Up-front rejection to get rid of extreme, potentially troublesome values
355  # (e.g., fringe apertures that fall on objects).
356  good = select(science, self.config.clip)
357  for ff in range(fringes.shape[1]):
358  good &= select(fringes[:, ff], self.config.clip)
359  science = science[good]
360  fringes = fringes[good]
361  oldNum = len(science)
362  if oldNum == 0:
363  return emptyResult(" after initial rejection")
364 
365  for i in range(self.config.iterations):
366  solution = self._solve(science, fringes)
367  resid = science - numpy.sum(solution*fringes, 1)
368  rms = stdev(resid)
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)
372  newNum = good.sum()
373  if newNum == 0:
374  return emptyResult(" after %d rejection iterations" % i)
375 
376  if doPlot:
377  import matplotlib.pyplot as plot
378  for j in range(fringes.shape[1]):
379  fig = plot.figure(j)
380  fig.clf()
381  try:
382  fig.canvas._tkcanvas._root().lift() # == Tk's raise
383  except Exception:
384  pass
385  ax = fig.add_subplot(1, 1, 1)
386  adjust = science.copy()
387  others = set(range(fringes.shape[1]))
388  others.discard(j)
389  for k in others:
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)
403  fig.show()
404  while True:
405  ans = input("Enter or c to continue [chp]").lower()
406  if ans in ("", "c",):
407  break
408  if ans in ("p",):
409  import pdb
410  pdb.set_trace()
411  elif ans in ("h", ):
412  print("h[elp] c[ontinue] p[db]")
413 
414  if newNum == oldNum:
415  # Not gaining
416  break
417  oldNum = newNum
418  good = numpy.where(good)
419  science = science[good]
420  fringes = fringes[good]
421 
422  # Final solution without rejection
423  solution = self._solve(science, fringes)
424  self.log.info("Fringe solution: %s RMS: %f Good: %d/%d", solution, rms, len(science), origNum)
425  return solution, rms
426 
427  def _solve(self, science, fringes):
428  """Solve for the scale factors.
429 
430  Parameters
431  ----------
432  science : `numpy.array`
433  Array of measured science image values at each of the
434  positions supplied.
435  fringes : `numpy.array`
436  Array of measured fringe values at each of the positions
437  supplied.
438 
439  Returns
440  -------
441  solution : `np.array`
442  Fringe solution amplitudes for each input fringe frame.
443  """
444  return afwMath.LeastSquares.fromDesignMatrix(fringes, science,
445  afwMath.LeastSquares.DIRECT_SVD).getSolution()
446 
447  def subtract(self, science, fringes, solution):
448  """Subtract the fringes.
449 
450  Parameters
451  ----------
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.
458 
459  Raises
460  ------
461  RuntimeError :
462  Raised if the number of fringe frames does not match the
463  number of measured amplitudes.
464  """
465  if len(solution) != len(fringes):
466  raise RuntimeError("Number of fringe frames (%s) != number of scale factors (%s)" %
467  (len(fringes), len(solution)))
468 
469  for s, f in zip(solution, fringes):
470  science.getMaskedImage().scaledMinus(s, f.getMaskedImage())
471 
472 
473 def measure(mi, x, y, size, statistic, stats):
474  """Measure a statistic within an aperture
475 
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
482  """
483  bbox = lsst.geom.Box2I(lsst.geom.Point2I(int(x) - size, int(y - size)),
484  lsst.geom.Extent2I(2*size, 2*size))
485  subImage = mi.Factory(mi, bbox, afwImage.LOCAL)
486  return afwMath.makeStatistics(subImage, statistic, stats).getValue()
487 
488 
489 def stdev(vector):
490  """Calculate a robust standard deviation of an array of values
491 
492  @param vector Array of values
493  @return Standard deviation
494  """
495  q1, q3 = numpy.percentile(vector, (25, 75))
496  return 0.74*(q3 - q1)
497 
498 
499 def select(vector, clip):
500  """Select values within 'clip' standard deviations of the median
501 
502  Returns a boolean array.
503  """
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)
Definition: fringe.py:173
Angle abs(Angle const &a)
Definition: Angle.h:106
def run(self, exposure, fringes, seed=None)
Definition: fringe.py:118
def checkFilter(self, exposure)
Definition: fringe.py:204
def subtract(self, science, fringes, solution)
Definition: fringe.py:447
def _solve(self, science, fringes)
Definition: fringe.py:427
daf::base::PropertySet * set
Definition: fits.cc:902
int min
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<>
Definition: Statistics.h:520
def stdev(vector)
Definition: fringe.py:489
Pass parameters to a Statistics object.
Definition: Statistics.h:93
def generatePositions(self, exposure, rng)
Definition: fringe.py:236
def measureExposure(self, exposure, positions, title="Fringe")
Definition: fringe.py:260
def getName(self)
Definition: task.py:250
int max
def removePedestal(self, fringe)
Definition: fringe.py:220
def mtv(data, frame=None, title="", wcs=None, args, kwargs)
Definition: ds9.py:93
def readFringes(self, dataRef, assembler=None)
Definition: fringe.py:76
def select(vector, clip)
Definition: fringe.py:499
def measure(mi, x, y, size, statistic, stats)
Definition: fringe.py:473
Backwards-compatibility support for depersisting the old Calib (FluxMag0/FluxMag0Err) objects...
An integer coordinate rectangle.
Definition: Box.h:55
def solve(self, science, fringes)
Definition: fringe.py:312