LSSTApplications  1.1.2+25,10.0+13,10.0+132,10.0+133,10.0+224,10.0+41,10.0+8,10.0-1-g0f53050+14,10.0-1-g4b7b172+19,10.0-1-g61a5bae+98,10.0-1-g7408a83+3,10.0-1-gc1e0f5a+19,10.0-1-gdb4482e+14,10.0-11-g3947115+2,10.0-12-g8719d8b+2,10.0-15-ga3f480f+1,10.0-2-g4f67435,10.0-2-gcb4bc6c+26,10.0-28-gf7f57a9+1,10.0-3-g1bbe32c+14,10.0-3-g5b46d21,10.0-4-g027f45f+5,10.0-4-g86f66b5+2,10.0-4-gc4fccf3+24,10.0-40-g4349866+2,10.0-5-g766159b,10.0-5-gca2295e+25,10.0-6-g462a451+1
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  @timeMethod
71  def run(self, exposure, dataRef, assembler=None):
72  """Remove fringes from the provided science exposure
73 
74  Fringes are only subtracted if the science exposure has a filter
75  listed in the configuration.
76 
77  @param exposure Science exposure from which to remove fringes
78  @param dataRef Data reference for the science exposure
79  @param assembler An instance of AssembleCcdTask (for assembling fringe frames)
80  """
81  import lsstDebug
82  display = lsstDebug.Info(__name__).display
83 
84  if not self.checkFilter(exposure):
85  return
86  fringes = self.readFringes(dataRef, assembler=assembler)
87  expFringes = self.measureExposure(exposure, fringes.positions, title="Science")
88  solution = self.solve(expFringes, fringes.fluxes)
89  self.subtract(exposure, fringes.fringes, solution)
90  if display:
91  ds9.mtv(exposure, title="Fringe subtracted", frame=getFrame())
92 
93  def checkFilter(self, exposure):
94  """Check whether we should fringe-subtract the science exposure"""
95  return exposure.getFilter().getName() in self.config.filters
96 
97  def readFringes(self, dataRef, assembler=None):
98  """Read the fringe frame(s) and measure fringe amplitudes.
99 
100  The current implementation assumes only a single fringe frame and
101  will have to be updated to support multi-mode fringe subtraction.
102 
103  This implementation could be optimised by persisting the fringe
104  positions and fluxes.
105 
106  @param dataRef Data reference for the science exposure
107  @param assembler An instance of AssembleCcdTask (for assembling fringe frames)
108  @return Struct(fringes: list of fringe frames;
109  fluxes: fringe amplitues;
110  positions: array of (x,y) for fringe amplitude measurements)
111  """
112  seed = dataRef.get("ccdExposureId", immediate=True) + self.config.stats.rngSeedOffset
113  rng = numpy.random.RandomState(seed=seed)
114 
115  try:
116  fringe = dataRef.get("fringe", immediate=True)
117  except Exception as e:
118  raise RuntimeError("Unable to retrieve fringe for %s: %s" % (dataRef.dataId, e))
119  if assembler is not None:
120  fringe = assembler.assembleCcd(fringe)
121 
122  if self.config.pedestal:
123  stats = afwMath.StatisticsControl()
124  stats.setNumSigmaClip(self.config.stats.clip)
125  stats.setNumIter(self.config.stats.iterations)
126  mi = fringe.getMaskedImage()
127  pedestal = afwMath.makeStatistics(mi, afwMath.MEDIAN, stats).getValue()
128  self.log.info("Removing fringe pedestal: %f" % pedestal)
129  mi -= pedestal
130 
131  positions = self.generatePositions(fringe, rng)
132  fluxes = self.measureExposure(fringe, positions, title="Fringe frame")
133 
134  return Struct(fringes=[fringe],
135  fluxes=fluxes.reshape([len(positions), 1]),
136  positions=positions
137  )
138 
139  def generatePositions(self, exposure, rng):
140  """Generate a random distribution of positions for measuring fringe amplitudes"""
141  start = self.config.large
142  num = self.config.num
143  width = exposure.getWidth() - self.config.large
144  height = exposure.getHeight() - self.config.large
145  return numpy.array([rng.randint(start, width, size=num),
146  rng.randint(start, height, size=num)]).swapaxes(0, 1)
147 
148  @timeMethod
149  def measureExposure(self, exposure, positions, title="Fringe"):
150  """Measure fringe amplitudes for an exposure
151 
152  The fringe amplitudes are measured as the statistic within a square
153  aperture. The statistic within a larger aperture are subtracted so
154  as to remove the background.
155 
156  @param exposure Exposure to measure
157  @param positions Array of (x,y) for fringe measurement
158  @param title Title for display
159  @return Array of fringe measurements
160  """
161  stats = afwMath.StatisticsControl()
162  stats.setNumSigmaClip(self.config.stats.clip)
163  stats.setNumIter(self.config.stats.iterations)
164  stats.setAndMask(exposure.getMaskedImage().getMask().getPlaneBitMask(self.config.stats.badMaskPlanes))
165 
166  num = self.config.num
167  fringes = numpy.ndarray(num)
168 
169  for i in range(num):
170  x, y = positions[i]
171  small = measure(exposure.getMaskedImage(), x, y, self.config.small, self.config.stats.stat, stats)
172  large = measure(exposure.getMaskedImage(), x, y, self.config.large, self.config.stats.stat, stats)
173  fringes[i] = small - large
174 
175  import lsstDebug
176  display = lsstDebug.Info(__name__).display
177  if display:
178  frame = getFrame()
179  ds9.mtv(exposure, frame=frame, title=title)
180  if False:
181  with ds9.Buffering():
182  for x,y in positions:
183  corners = numpy.array([[-1, -1], [ 1, -1], [ 1, 1], [-1, 1], [-1, -1]]) + [[x, y]]
184  ds9.line(corners * self.config.small, frame=frame, ctype="green")
185  ds9.line(corners * self.config.large, frame=frame, ctype="blue")
186 
187  return fringes
188 
189  @timeMethod
190  def solve(self, science, fringes):
191  """Solve (with iterative clipping) for the scale factors
192 
193  @param science Array of science exposure fringe amplitudes
194  @param fringes Array of arrays of fringe frame fringe amplitudes
195  @return Array of scale factors for the fringe frames
196  """
197  import lsstDebug
198  doPlot = lsstDebug.Info(__name__).plot
199 
200  origNum = len(science)
201 
202  good = numpy.where(numpy.logical_and(numpy.isfinite(science), numpy.any(numpy.isfinite(fringes), 1)))
203  science = science[good]
204  fringes = fringes[good]
205  oldNum = len(science)
206 
207  for i in range(self.config.iterations):
208  solution = self._solve(science, fringes)
209  resid = science - numpy.sum(solution * fringes, 1)
210  rms = stdev(resid)
211  good = numpy.logical_not(abs(resid) > self.config.clip * rms)
212  self.log.logdebug("Iteration %d: RMS=%f numGood=%d" % (i, rms, good.sum()))
213  self.log.logdebug("Solution %d: %s" % (i, solution))
214  newNum = good.sum()
215 
216  if doPlot:
217  import matplotlib.pyplot as plot
218  for j in range(fringes.shape[1]):
219  fig = plot.figure(j)
220  fig.clf()
221  try:
222  fig.canvas._tkcanvas._root().lift() # == Tk's raise
223  except:
224  pass
225  ax = fig.add_axes((0.1, 0.1, 0.8, 0.8))
226  adjust = science.copy()
227  others = set(range(fringes.shape[1]))
228  others.discard(j)
229  for k in others:
230  adjust -= solution[k] * fringes[:,k]
231  ax.plot(fringes[:,j], adjust, 'r.')
232  xmin = fringes[:,j].min()
233  xmax = fringes[:,j].max()
234  ymin = solution[j] * xmin
235  ymax = solution[j] * xmax
236  ax.plot([xmin, xmax], [ymin, ymax], 'b-')
237  ax.set_title("Fringe %d: %f" % (j, solution[j]))
238  ax.set_xlabel("Fringe amplitude")
239  ax.set_ylabel("Science amplitude")
240  ax.set_autoscale_on(False)
241  ax.set_xbound(lower=xmin, upper=xmax)
242  ax.set_ybound(lower=ymin, upper=ymax)
243  fig.show()
244  while True:
245  ans = raw_input("Enter or c to continue [chp]").lower()
246  if ans in ("", "c",):
247  break
248  if ans in ("p",):
249  import pdb; pdb.set_trace()
250  elif ans in ("h", ):
251  print "h[elp] c[ontinue] p[db]"
252 
253  if newNum == oldNum:
254  # Not gaining
255  break
256  oldNum = newNum
257  good = numpy.where(good)
258  science = science[good]
259  fringes = fringes[good]
260 
261  # Final solution without rejection
262  solution = self._solve(science, fringes)
263  self.log.info("Fringe solution: %s RMS: %f Good: %d/%d" % (solution, rms, len(science), origNum))
264  return solution
265 
266  def _solve(self, science, fringes):
267  """Solve for the scale factors
268 
269  @param science Array of science exposure fringe amplitudes
270  @param fringes Array of arrays of fringe frame fringe amplitudes
271  @return Array of scale factors for the fringe frames
272  """
273  return afwMath.LeastSquares.fromDesignMatrix(fringes, science,
274  afwMath.LeastSquares.DIRECT_SVD).getSolution()
275 
276  def subtract(self, science, fringes, solution):
277  """Subtract the fringes
278 
279  @param science Science exposure
280  @param fringes List of fringe frames
281  @param solution Array of scale factors for the fringe frames
282  """
283  for s, f in zip(solution, fringes):
284  science.getMaskedImage().scaledMinus(s, f.getMaskedImage())
285 
286 
287 def measure(mi, x, y, size, statistic, stats):
288  """Measure a statistic within an aperture
289 
290  @param mi MaskedImage to measure
291  @param x, y Center for aperture
292  @param size Size of aperture
293  @param statistic Statistic to measure
294  @param stats StatisticsControl object
295  @return Value of statistic within aperture
296  """
297  bbox = afwGeom.Box2I(afwGeom.Point2I(int(x) - size, int(y - size)), afwGeom.Extent2I(2*size, 2*size))
298  subImage = mi.Factory(mi, bbox, afwImage.LOCAL)
299  return afwMath.makeStatistics(subImage, statistic, stats).getValue()
300 
301 def stdev(vector):
302  """Calculate a robust standard deviation of an array of values
303 
304  @param vector Array of values
305  @return Standard deviation
306  """
307  num = len(vector)
308  vector = vector.copy()
309  vector.sort()
310  return 0.74 * (vector[int(0.75 * num)] - vector[int(0.25 * num)])
311 
An integer coordinate rectangle.
Definition: Box.h:53
double min
Definition: attributes.cc:216
double max
Definition: attributes.cc:218
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:1023