LSSTApplications  10.0-2-g4f67435,11.0.rc2+1,11.0.rc2+12,11.0.rc2+3,11.0.rc2+4,11.0.rc2+5,11.0.rc2+6,11.0.rc2+7,11.0.rc2+8
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 def getFrame():
35  """Produce a new frame number each time"""
36  getFrame.frame += 1
37  return getFrame.frame
38 getFrame.frame = 0
39 
40 class FringeStatisticsConfig(Config):
41  """Options for measuring fringes on an exposure"""
42  badMaskPlanes = ListField(dtype=str, default=["SAT"], doc="Ignore pixels with these masks")
43  stat = Field(dtype=int, default=afwMath.MEDIAN, doc="Statistic to use")
44  clip = Field(dtype=float, default=3.0, doc="Sigma clip threshold")
45  iterations = Field(dtype=int, default=3, doc="Number of fitting iterations")
46  rngSeedOffset = Field(dtype=int, default=0,
47  doc="Offset to the random number generator seed (full seed includes exposure ID)")
48 
49 
50 class FringeConfig(Config):
51  """Fringe subtraction options"""
52  filters = ListField(dtype=str, default=[], doc="Only fringe-subtract these filters")
53  num = Field(dtype=int, default=30000, doc="Number of fringe measurements")
54  small = Field(dtype=int, default=3, doc="Half-size of small (fringe) measurements (pixels)")
55  large = Field(dtype=int, default=30, doc="Half-size of large (background) measurements (pixels)")
56  iterations = Field(dtype=int, default=20, doc="Number of fitting iterations")
57  clip = Field(dtype=float, default=3.0, doc="Sigma clip threshold")
58  stats = ConfigField(dtype=FringeStatisticsConfig, doc="Statistics for measuring fringes")
59  pedestal = Field(dtype=bool, default=False, doc="Remove fringe pedestal?")
60 
61 class FringeTask(Task):
62  """Task to remove fringes from a science exposure
63 
64  We measure fringe amplitudes at random positions on the science exposure
65  and at the same positions on the (potentially multiple) fringe frames
66  and solve for the scales simultaneously.
67  """
68  ConfigClass = FringeConfig
69 
70  def readFringes(self, dataRef, assembler=None):
71  """Read the fringe frame(s)
72 
73  The current implementation assumes only a single fringe frame and
74  will have to be updated to support multi-mode fringe subtraction.
75 
76  This implementation could be optimised by persisting the fringe
77  positions and fluxes.
78 
79  @param dataRef Data reference for the science exposure
80  @param assembler An instance of AssembleCcdTask (for assembling fringe frames)
81  @return Struct(fringes: fringe exposure or list of fringe exposures;
82  seed: 32-bit uint derived from ccdExposureId for random number generator
83  """
84  try:
85  fringe = dataRef.get("fringe", immediate=True)
86  except Exception as e:
87  raise RuntimeError("Unable to retrieve fringe for %s: %s" % (dataRef.dataId, e))
88  if assembler is not None:
89  fringe = assembler.assembleCcd(fringe)
90 
91  seed = self.config.stats.rngSeedOffset + dataRef.get("ccdExposureId", immediate=True)
92  #Seed for numpy.random.RandomState must be convertable to a 32 bit unsigned integer
93  seed %= 2**32
94 
95  return Struct(fringes = fringe,
96  seed = seed)
97 
98  @timeMethod
99  def run(self, exposure, fringes, seed=None):
100  """Remove fringes from the provided science exposure.
101 
102  Primary method of FringeTask. Fringes are only subtracted if the
103  science exposure has a filter listed in the configuration.
104 
105  @param exposure Science exposure from which to remove fringes
106  @param fringes Exposure or list of Exposures
107  @param seed 32-bit unsigned integer for random number generator
108  """
109  import lsstDebug
110  display = lsstDebug.Info(__name__).display
111 
112  if not self.checkFilter(exposure):
113  return
114 
115  if self.config.pedestal:
116  self.removePedestal(fringes)
117 
118  if seed is None:
119  seed = self.config.stats.rngSeedOffset
120  rng = numpy.random.RandomState(seed=seed)
121 
122  if hasattr(fringes, '__iter__'):
123  #multiple fringe frames (placeholder implementation)
124  positions = self.generatePositions(fringes[0], rng)
125  fluxes = numpy.ndarray([len(positions), len(fringes)])
126  for i, f in enumerate(fringes):
127  fluxes[:,i] = self.measureExposure(f, positions, title="Fringe frame")
128  else:
129  #single fringe frame
130  positions = self.generatePositions(fringes, rng)
131  fluxes = self.measureExposure(fringes, positions, title="Fringe frame")
132  fluxes = fluxes.reshape([len(positions), 1])
133  fringes = [fringes]
134 
135  expFringes = self.measureExposure(exposure, positions, title="Science")
136  solution = self.solve(expFringes, fluxes)
137  self.subtract(exposure, fringes, solution)
138  if display:
139  ds9.mtv(exposure, title="Fringe subtracted", frame=getFrame())
140 
141  @timeMethod
142  def runDataRef(self, exposure, dataRef, assembler=None):
143  """Remove fringes from the provided science exposure.
144 
145  Retrieve fringes from butler dataRef provided and remove from
146  provided science exposure.
147  Fringes are only subtracted if the science exposure has a filter
148  listed in the configuration.
149 
150  @param exposure Science exposure from which to remove fringes
151  @param dataRef Data reference for the science exposure
152  @param assembler An instance of AssembleCcdTask (for assembling fringe frames)
153  """
154  if not self.checkFilter(exposure):
155  return
156  fringeStruct = self.readFringes(dataRef, assembler=assembler)
157  self.run(exposure, **fringeStruct.getDict())
158 
159  def checkFilter(self, exposure):
160  """Check whether we should fringe-subtract the science exposure"""
161  return exposure.getFilter().getName() in self.config.filters
162 
163  def removePedestal(self, fringe):
164  """Remove pedestal from fringe exposure"""
165  stats = afwMath.StatisticsControl()
166  stats.setNumSigmaClip(self.config.stats.clip)
167  stats.setNumIter(self.config.stats.iterations)
168  mi = fringe.getMaskedImage()
169  pedestal = afwMath.makeStatistics(mi, afwMath.MEDIAN, stats).getValue()
170  self.log.info("Removing fringe pedestal: %f" % pedestal)
171  mi -= pedestal
172 
173  def generatePositions(self, exposure, rng):
174  """Generate a random distribution of positions for measuring fringe amplitudes"""
175  start = self.config.large
176  num = self.config.num
177  width = exposure.getWidth() - self.config.large
178  height = exposure.getHeight() - self.config.large
179  return numpy.array([rng.randint(start, width, size=num),
180  rng.randint(start, height, size=num)]).swapaxes(0, 1)
181 
182  @timeMethod
183  def measureExposure(self, exposure, positions, title="Fringe"):
184  """Measure fringe amplitudes for an exposure
185 
186  The fringe amplitudes are measured as the statistic within a square
187  aperture. The statistic within a larger aperture are subtracted so
188  as to remove the background.
189 
190  @param exposure Exposure to measure
191  @param positions Array of (x,y) for fringe measurement
192  @param title Title for display
193  @return Array of fringe measurements
194  """
195  stats = afwMath.StatisticsControl()
196  stats.setNumSigmaClip(self.config.stats.clip)
197  stats.setNumIter(self.config.stats.iterations)
198  stats.setAndMask(exposure.getMaskedImage().getMask().getPlaneBitMask(self.config.stats.badMaskPlanes))
199 
200  num = self.config.num
201  fringes = numpy.ndarray(num)
202 
203  for i in range(num):
204  x, y = positions[i]
205  small = measure(exposure.getMaskedImage(), x, y, self.config.small, self.config.stats.stat, stats)
206  large = measure(exposure.getMaskedImage(), x, y, self.config.large, self.config.stats.stat, stats)
207  fringes[i] = small - large
208 
209  import lsstDebug
210  display = lsstDebug.Info(__name__).display
211  if display:
212  frame = getFrame()
213  ds9.mtv(exposure, frame=frame, title=title)
214  if False:
215  with ds9.Buffering():
216  for x,y in positions:
217  corners = numpy.array([[-1, -1], [ 1, -1], [ 1, 1], [-1, 1], [-1, -1]]) + [[x, y]]
218  ds9.line(corners * self.config.small, frame=frame, ctype="green")
219  ds9.line(corners * self.config.large, frame=frame, ctype="blue")
220 
221  return fringes
222 
223  @timeMethod
224  def solve(self, science, fringes):
225  """Solve (with iterative clipping) for the scale factors
226 
227  @param science Array of science exposure fringe amplitudes
228  @param fringes Array of arrays of fringe frame fringe amplitudes
229  @return Array of scale factors for the fringe frames
230  """
231  import lsstDebug
232  doPlot = lsstDebug.Info(__name__).plot
233 
234  origNum = len(science)
235 
236  good = numpy.where(numpy.logical_and(numpy.isfinite(science), numpy.any(numpy.isfinite(fringes), 1)))
237  science = science[good]
238  fringes = fringes[good]
239  oldNum = len(science)
240 
241  for i in range(self.config.iterations):
242  solution = self._solve(science, fringes)
243  resid = science - numpy.sum(solution * fringes, 1)
244  rms = stdev(resid)
245  good = numpy.logical_not(abs(resid) > self.config.clip * rms)
246  self.log.logdebug("Iteration %d: RMS=%f numGood=%d" % (i, rms, good.sum()))
247  self.log.logdebug("Solution %d: %s" % (i, solution))
248  newNum = good.sum()
249 
250  if doPlot:
251  import matplotlib.pyplot as plot
252  for j in range(fringes.shape[1]):
253  fig = plot.figure(j)
254  fig.clf()
255  try:
256  fig.canvas._tkcanvas._root().lift() # == Tk's raise
257  except:
258  pass
259  ax = fig.add_axes((0.1, 0.1, 0.8, 0.8))
260  adjust = science.copy()
261  others = set(range(fringes.shape[1]))
262  others.discard(j)
263  for k in others:
264  adjust -= solution[k] * fringes[:,k]
265  ax.plot(fringes[:,j], adjust, 'r.')
266  xmin = fringes[:,j].min()
267  xmax = fringes[:,j].max()
268  ymin = solution[j] * xmin
269  ymax = solution[j] * xmax
270  ax.plot([xmin, xmax], [ymin, ymax], 'b-')
271  ax.set_title("Fringe %d: %f" % (j, solution[j]))
272  ax.set_xlabel("Fringe amplitude")
273  ax.set_ylabel("Science amplitude")
274  ax.set_autoscale_on(False)
275  ax.set_xbound(lower=xmin, upper=xmax)
276  ax.set_ybound(lower=ymin, upper=ymax)
277  fig.show()
278  while True:
279  ans = raw_input("Enter or c to continue [chp]").lower()
280  if ans in ("", "c",):
281  break
282  if ans in ("p",):
283  import pdb; pdb.set_trace()
284  elif ans in ("h", ):
285  print "h[elp] c[ontinue] p[db]"
286 
287  if newNum == oldNum:
288  # Not gaining
289  break
290  oldNum = newNum
291  good = numpy.where(good)
292  science = science[good]
293  fringes = fringes[good]
294 
295  # Final solution without rejection
296  solution = self._solve(science, fringes)
297  self.log.info("Fringe solution: %s RMS: %f Good: %d/%d" % (solution, rms, len(science), origNum))
298  return solution
299 
300  def _solve(self, science, fringes):
301  """Solve for the scale factors
302 
303  @param science Array of science exposure fringe amplitudes
304  @param fringes Array of arrays of fringe frame fringe amplitudes
305  @return Array of scale factors for the fringe frames
306  """
307  return afwMath.LeastSquares.fromDesignMatrix(fringes, science,
308  afwMath.LeastSquares.DIRECT_SVD).getSolution()
309 
310  def subtract(self, science, fringes, solution):
311  """Subtract the fringes
312 
313  @param science Science exposure
314  @param fringes List of fringe frames
315  @param solution Array of scale factors for the fringe frames
316  """
317  if len(solution) != len(fringes):
318  raise RuntimeError("Number of fringe frames (%s) != number of scale factors (%s)"% \
319  (len(fringes), len(solution)))
320 
321  for s, f in zip(solution, fringes):
322  science.getMaskedImage().scaledMinus(s, f.getMaskedImage())
323 
324 
325 def measure(mi, x, y, size, statistic, stats):
326  """Measure a statistic within an aperture
327 
328  @param mi MaskedImage to measure
329  @param x, y Center for aperture
330  @param size Size of aperture
331  @param statistic Statistic to measure
332  @param stats StatisticsControl object
333  @return Value of statistic within aperture
334  """
335  bbox = afwGeom.Box2I(afwGeom.Point2I(int(x) - size, int(y - size)), afwGeom.Extent2I(2*size, 2*size))
336  subImage = mi.Factory(mi, bbox, afwImage.LOCAL)
337  return afwMath.makeStatistics(subImage, statistic, stats).getValue()
338 
339 def stdev(vector):
340  """Calculate a robust standard deviation of an array of values
341 
342  @param vector Array of values
343  @return Standard deviation
344  """
345  num = len(vector)
346  vector = vector.copy()
347  vector.sort()
348  return 0.74 * (vector[int(0.75 * num)] - vector[int(0.25 * num)])
349 
An integer coordinate rectangle.
Definition: Box.h:53
Pass parameters to a Statistics objectA class to pass parameters which control how the stats are calc...
Definition: Statistics.h:92
Statistics makeStatistics(afwImage::Mask< afwImage::MaskPixel > const &msk, int const flags, StatisticsControl const &sctrl)
Specialization to handle Masks.
Definition: Statistics.cc:1082