LSSTApplications  16.0-10-g0ee56ad+5,16.0-11-ga33d1f2+5,16.0-12-g3ef5c14+3,16.0-12-g71e5ef5+18,16.0-12-gbdf3636+3,16.0-13-g118c103+3,16.0-13-g8f68b0a+3,16.0-15-gbf5c1cb+4,16.0-16-gfd17674+3,16.0-17-g7c01f5c+3,16.0-18-g0a50484+1,16.0-20-ga20f992+8,16.0-21-g0e05fd4+6,16.0-21-g15e2d33+4,16.0-22-g62d8060+4,16.0-22-g847a80f+4,16.0-25-gf00d9b8+1,16.0-28-g3990c221+4,16.0-3-gf928089+3,16.0-32-g88a4f23+5,16.0-34-gd7987ad+3,16.0-37-gc7333cb+2,16.0-4-g10fc685+2,16.0-4-g18f3627+26,16.0-4-g5f3a788+26,16.0-5-gaf5c3d7+4,16.0-5-gcc1f4bb+1,16.0-6-g3b92700+4,16.0-6-g4412fcd+3,16.0-6-g7235603+4,16.0-69-g2562ce1b+2,16.0-8-g14ebd58+4,16.0-8-g2df868b+1,16.0-8-g4cec79c+6,16.0-8-gadf6c7a+1,16.0-8-gfc7ad86,16.0-82-g59ec2a54a+1,16.0-9-g5400cdc+2,16.0-9-ge6233d7+5,master-g2880f2d8cf+3,v17.0.rc1
LSSTDataManagementBasePackage
fringe.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 #
3 # LSST Data Management System
4 # Copyright 2012 LSST Corporation.
5 #
6 # This product includes software developed by the
7 # LSST Project (http://www.lsst.org/).
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 LSST License Statement and
20 # the GNU General Public License along with this program. If not,
21 # see <http://www.lsstcorp.org/LegalNotices/>.
22 #
23 
24 import numpy
25 
26 import lsst.afw.geom as afwGeom
27 import lsst.afw.image as afwImage
28 import lsst.afw.math as afwMath
29 import lsst.afw.display.ds9 as ds9
30 
31 from lsst.pipe.base import Task, Struct, timeMethod
32 from lsst.pex.config import Config, Field, ListField, ConfigField
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 
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 
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 
75  def readFringes(self, dataRef, assembler=None):
76  """Read the fringe frame(s)
77 
78  The current implementation assumes only a single fringe frame and
79  will have to be updated to support multi-mode fringe subtraction.
80 
81  This implementation could be optimised by persisting the fringe
82  positions and fluxes.
83 
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
88  """
89  try:
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)
95 
96  seed = self.config.stats.rngSeedOffset + dataRef.get("ccdExposureId", immediate=True)
97  # Seed for numpy.random.RandomState must be convertable to a 32 bit unsigned integer
98  seed %= 2**32
99 
100  return Struct(fringes=fringe,
101  seed=seed)
102 
103  @timeMethod
104  def run(self, exposure, fringes, seed=None):
105  """Remove fringes from the provided science exposure.
106 
107  Primary method of FringeTask. Fringes are only subtracted if the
108  science exposure has a filter listed in the configuration.
109 
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
113  """
114  import lsstDebug
115  display = lsstDebug.Info(__name__).display
116 
117  if not self.checkFilter(exposure):
118  return
119 
120  if seed is None:
121  seed = self.config.stats.rngSeedOffset
122  rng = numpy.random.RandomState(seed=seed)
123 
124  if not hasattr(fringes, '__iter__'):
125  fringes = [fringes]
126 
127  mask = exposure.getMaskedImage().getMask()
128  for fringe in fringes:
129  fringe.getMaskedImage().getMask().__ior__(mask)
130  if self.config.pedestal:
131  self.removePedestal(fringe)
132 
133  # Placeholder implementation for multiple fringe frames
134  # This needs to be revisited in DM-4441
135  positions = self.generatePositions(fringes[0], rng)
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")
139 
140  expFringes = self.measureExposure(exposure, positions, title="Science")
141  solution = self.solve(expFringes, fluxes)
142  self.subtract(exposure, fringes, solution)
143  if display:
144  ds9.mtv(exposure, title="Fringe subtracted", frame=getFrame())
145 
146  @timeMethod
147  def runDataRef(self, exposure, dataRef, assembler=None):
148  """Remove fringes from the provided science exposure.
149 
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.
154 
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)
158  """
159  if not self.checkFilter(exposure):
160  return
161  fringeStruct = self.readFringes(dataRef, assembler=assembler)
162  self.run(exposure, **fringeStruct.getDict())
163 
164  def checkFilter(self, exposure):
165  """Check whether we should fringe-subtract the science exposure"""
166  return exposure.getFilter().getName() in self.config.filters
167 
168  def removePedestal(self, fringe):
169  """Remove pedestal from fringe exposure"""
170  stats = afwMath.StatisticsControl()
171  stats.setNumSigmaClip(self.config.stats.clip)
172  stats.setNumIter(self.config.stats.iterations)
173  mi = fringe.getMaskedImage()
174  pedestal = afwMath.makeStatistics(mi, afwMath.MEDIAN, stats).getValue()
175  self.log.info("Removing fringe pedestal: %f", pedestal)
176  mi -= pedestal
177 
178  def generatePositions(self, exposure, rng):
179  """Generate a random distribution of positions for measuring fringe amplitudes"""
180  start = self.config.large
181  num = self.config.num
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)
186 
187  @timeMethod
188  def measureExposure(self, exposure, positions, title="Fringe"):
189  """Measure fringe amplitudes for an exposure
190 
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.
194 
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
199  """
200  stats = afwMath.StatisticsControl()
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))
204 
205  num = self.config.num
206  fringes = numpy.ndarray(num)
207 
208  for i in range(num):
209  x, y = positions[i]
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
213 
214  import lsstDebug
215  display = lsstDebug.Info(__name__).display
216  if display:
217  frame = getFrame()
218  ds9.mtv(exposure, frame=frame, title=title)
219  if False:
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")
225 
226  return fringes
227 
228  @timeMethod
229  def solve(self, science, fringes):
230  """Solve (with iterative clipping) for the scale factors
231 
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
235  """
236  import lsstDebug
237  doPlot = lsstDebug.Info(__name__).plot
238 
239  origNum = len(science)
240 
241  def emptyResult(msg=""):
242  """Generate an empty result for return to the user
243 
244  There are no good pixels; doesn't matter what we return.
245  """
246  self.log.warn("Unable to solve for fringes: no good pixels%s", msg)
247  out = [0]
248  if len(fringes) > 1:
249  out = out*len(fringes)
250  return numpy.array(out)
251 
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)
256  if oldNum == 0:
257  return emptyResult()
258 
259  # Up-front rejection to get rid of extreme, potentially troublesome values
260  # (e.g., fringe apertures that fall on objects).
261  good = select(science, self.config.clip)
262  for ff in range(fringes.shape[1]):
263  good &= select(fringes[:, ff], self.config.clip)
264  science = science[good]
265  fringes = fringes[good]
266  oldNum = len(science)
267  if oldNum == 0:
268  return emptyResult(" after initial rejection")
269 
270  for i in range(self.config.iterations):
271  solution = self._solve(science, fringes)
272  resid = science - numpy.sum(solution * fringes, 1)
273  rms = stdev(resid)
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)
277  newNum = good.sum()
278  if newNum == 0:
279  return emptyResult(" after %d rejection iterations" % i)
280 
281  if doPlot:
282  import matplotlib.pyplot as plot
283  for j in range(fringes.shape[1]):
284  fig = plot.figure(j)
285  fig.clf()
286  try:
287  fig.canvas._tkcanvas._root().lift() # == Tk's raise
288  except Exception:
289  pass
290  ax = fig.add_subplot(1, 1, 1)
291  adjust = science.copy()
292  others = set(range(fringes.shape[1]))
293  others.discard(j)
294  for k in others:
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)
308  fig.show()
309  while True:
310  ans = input("Enter or c to continue [chp]").lower()
311  if ans in ("", "c",):
312  break
313  if ans in ("p",):
314  import pdb
315  pdb.set_trace()
316  elif ans in ("h", ):
317  print("h[elp] c[ontinue] p[db]")
318 
319  if newNum == oldNum:
320  # Not gaining
321  break
322  oldNum = newNum
323  good = numpy.where(good)
324  science = science[good]
325  fringes = fringes[good]
326 
327  # Final solution without rejection
328  solution = self._solve(science, fringes)
329  self.log.info("Fringe solution: %s RMS: %f Good: %d/%d", solution, rms, len(science), origNum)
330  return solution
331 
332  def _solve(self, science, fringes):
333  """Solve for the scale factors
334 
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
338  """
339  return afwMath.LeastSquares.fromDesignMatrix(fringes, science,
340  afwMath.LeastSquares.DIRECT_SVD).getSolution()
341 
342  def subtract(self, science, fringes, solution):
343  """Subtract the fringes
344 
345  @param science Science exposure
346  @param fringes List of fringe frames
347  @param solution Array of scale factors for the fringe frames
348  """
349  if len(solution) != len(fringes):
350  raise RuntimeError("Number of fringe frames (%s) != number of scale factors (%s)" %
351  (len(fringes), len(solution)))
352 
353  for s, f in zip(solution, fringes):
354  science.getMaskedImage().scaledMinus(s, f.getMaskedImage())
355 
356 
357 def measure(mi, x, y, size, statistic, stats):
358  """Measure a statistic within an aperture
359 
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
366  """
367  bbox = afwGeom.Box2I(afwGeom.Point2I(int(x) - size, int(y - size)), afwGeom.Extent2I(2*size, 2*size))
368  subImage = mi.Factory(mi, bbox, afwImage.LOCAL)
369  return afwMath.makeStatistics(subImage, statistic, stats).getValue()
370 
371 
372 def stdev(vector):
373  """Calculate a robust standard deviation of an array of values
374 
375  @param vector Array of values
376  @return Standard deviation
377  """
378  q1, q3 = numpy.percentile(vector, (25, 75))
379  return 0.74*(q3-q1)
380 
381 
382 def select(vector, clip):
383  """Select values within 'clip' standard deviations of the median
384 
385  Returns a boolean array.
386  """
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)
Definition: fringe.py:147
Angle abs(Angle const &a)
Definition: Angle.h:106
def run(self, exposure, fringes, seed=None)
Definition: fringe.py:104
def checkFilter(self, exposure)
Definition: fringe.py:164
def subtract(self, science, fringes, solution)
Definition: fringe.py:342
def _solve(self, science, fringes)
Definition: fringe.py:332
daf::base::PropertySet * set
Definition: fits.cc:832
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:372
Pass parameters to a Statistics object.
Definition: Statistics.h:93
def generatePositions(self, exposure, rng)
Definition: fringe.py:178
def measureExposure(self, exposure, positions, title="Fringe")
Definition: fringe.py:188
int max
def removePedestal(self, fringe)
Definition: fringe.py:168
def readFringes(self, dataRef, assembler=None)
Definition: fringe.py:75
def select(vector, clip)
Definition: fringe.py:382
def measure(mi, x, y, size, statistic, stats)
Definition: fringe.py:357
An integer coordinate rectangle.
Definition: Box.h:54
def solve(self, science, fringes)
Definition: fringe.py:229