LSSTApplications  8.0.0.0+107,8.0.0.1+13,9.1+18,9.2,master-g084aeec0a4,master-g0aced2eed8+6,master-g15627eb03c,master-g28afc54ef9,master-g3391ba5ea0,master-g3d0fb8ae5f,master-g4432ae2e89+36,master-g5c3c32f3ec+17,master-g60f1e072bb+1,master-g6a3ac32d1b,master-g76a88a4307+1,master-g7bce1f4e06+57,master-g8ff4092549+31,master-g98e65bf68e,master-ga6b77976b1+53,master-gae20e2b580+3,master-gb584cd3397+53,master-gc5448b162b+1,master-gc54cf9771d,master-gc69578ece6+1,master-gcbf758c456+22,master-gcec1da163f+63,master-gcf15f11bcc,master-gd167108223,master-gf44c96c709
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 
47 
48 class FringeConfig(Config):
49  """Fringe subtraction options"""
50  filters = ListField(dtype=str, default=[], doc="Only fringe-subtract these filters")
51  num = Field(dtype=int, default=30000, doc="Number of fringe measurements")
52  small = Field(dtype=int, default=3, doc="Half-size of small (fringe) measurements (pixels)")
53  large = Field(dtype=int, default=30, doc="Half-size of large (background) measurements (pixels)")
54  iterations = Field(dtype=int, default=20, doc="Number of fitting iterations")
55  clip = Field(dtype=float, default=3.0, doc="Sigma clip threshold")
56  stats = ConfigField(dtype=FringeStatisticsConfig, doc="Statistics for measuring fringes")
57  pedestal = Field(dtype=bool, default=False, doc="Remove fringe pedestal?")
58 
59 class FringeTask(Task):
60  """Task to remove fringes from a science exposure
61 
62  We measure fringe amplitudes at random positions on the science exposure
63  and at the same positions on the (potentially multiple) fringe frames
64  and solve for the scales simultaneously.
65  """
66  ConfigClass = FringeConfig
67 
68  @timeMethod
69  def run(self, exposure, dataRef, assembler=None):
70  """Remove fringes from the provided science exposure
71 
72  Fringes are only subtracted if the science exposure has a filter
73  listed in the configuration.
74 
75  @param exposure Science exposure from which to remove fringes
76  @param dataRef Data reference for the science exposure
77  @param assembler An instance of AssembleCcdTask (for assembling fringe frames)
78  """
79  import lsstDebug
80  display = lsstDebug.Info(__name__).display
81 
82  if not self.checkFilter(exposure):
83  return
84  fringes = self.readFringes(dataRef, assembler=assembler)
85  expFringes = self.measureExposure(exposure, fringes.positions, title="Science")
86  solution = self.solve(expFringes, fringes.fluxes)
87  self.subtract(exposure, fringes.fringes, solution)
88  if display:
89  ds9.mtv(exposure, title="Fringe subtracted", frame=getFrame())
90 
91  def checkFilter(self, exposure):
92  """Check whether we should fringe-subtract the science exposure"""
93  return exposure.getFilter().getName() in self.config.filters
94 
95  def readFringes(self, dataRef, assembler=None):
96  """Read the fringe frame(s) and measure fringe amplitudes.
97 
98  The current implementation assumes only a single fringe frame and
99  will have to be updated to support multi-mode fringe subtraction.
100 
101  This implementation could be optimised by persisting the fringe
102  positions and fluxes.
103 
104  @param dataRef Data reference for the science exposure
105  @param assembler An instance of AssembleCcdTask (for assembling fringe frames)
106  @return Struct(fringes: list of fringe frames;
107  fluxes: fringe amplitues;
108  positions: array of (x,y) for fringe amplitude measurements)
109  """
110  fringe = dataRef.get("fringe")
111  if assembler is not None:
112  fringe = assembler.assembleCcd(fringe)
113 
114  if self.config.pedestal:
115  stats = afwMath.StatisticsControl()
116  stats.setNumSigmaClip(self.config.stats.clip)
117  stats.setNumIter(self.config.stats.iterations)
118  mi = fringe.getMaskedImage()
119  pedestal = afwMath.makeStatistics(mi, afwMath.MEDIAN, stats).getValue()
120  self.log.info("Removing fringe pedestal: %f" % pedestal)
121  mi -= pedestal
122 
123  positions = self.generatePositions(fringe)
124  fluxes = self.measureExposure(fringe, positions, title="Fringe frame")
125 
126  return Struct(fringes=[fringe],
127  fluxes=fluxes.reshape([len(positions), 1]),
128  positions=positions
129  )
130 
131  def generatePositions(self, exposure):
132  """Generate a random distribution of positions for measuring fringe amplitudes"""
133  start = self.config.large
134  num = self.config.num
135  width = exposure.getWidth() - self.config.large
136  height = exposure.getHeight() - self.config.large
137  return numpy.array([numpy.random.randint(start, width, size=num),
138  numpy.random.randint(start, height, size=num)]).swapaxes(0, 1)
139 
140  @timeMethod
141  def measureExposure(self, exposure, positions, title="Fringe"):
142  """Measure fringe amplitudes for an exposure
143 
144  The fringe amplitudes are measured as the statistic within a square
145  aperture. The statistic within a larger aperture are subtracted so
146  as to remove the background.
147 
148  @param exposure Exposure to measure
149  @param positions Array of (x,y) for fringe measurement
150  @param title Title for display
151  @return Array of fringe measurements
152  """
153  stats = afwMath.StatisticsControl()
154  stats.setNumSigmaClip(self.config.stats.clip)
155  stats.setNumIter(self.config.stats.iterations)
156  stats.setAndMask(exposure.getMaskedImage().getMask().getPlaneBitMask(self.config.stats.badMaskPlanes))
157 
158  num = self.config.num
159  fringes = numpy.ndarray(num)
160 
161  for i in range(num):
162  x, y = positions[i]
163  small = measure(exposure.getMaskedImage(), x, y, self.config.small, self.config.stats.stat, stats)
164  large = measure(exposure.getMaskedImage(), x, y, self.config.large, self.config.stats.stat, stats)
165  fringes[i] = small - large
166 
167  import lsstDebug
168  display = lsstDebug.Info(__name__).display
169  if display:
170  frame = getFrame()
171  ds9.mtv(exposure, frame=frame, title=title)
172  if False:
173  with ds9.Buffering():
174  for x,y in positions:
175  corners = numpy.array([[-1, -1], [ 1, -1], [ 1, 1], [-1, 1], [-1, -1]]) + [[x, y]]
176  ds9.line(corners * self.config.small, frame=frame, ctype="green")
177  ds9.line(corners * self.config.large, frame=frame, ctype="blue")
178 
179  return fringes
180 
181  @timeMethod
182  def solve(self, science, fringes):
183  """Solve (with iterative clipping) for the scale factors
184 
185  @param science Array of science exposure fringe amplitudes
186  @param fringes Array of arrays of fringe frame fringe amplitudes
187  @return Array of scale factors for the fringe frames
188  """
189  import lsstDebug
190  doPlot = lsstDebug.Info(__name__).plot
191 
192  origNum = len(science)
193 
194  good = numpy.where(numpy.logical_and(numpy.isfinite(science), numpy.any(numpy.isfinite(fringes), 1)))
195  science = science[good]
196  fringes = fringes[good]
197  oldNum = len(science)
198 
199  for i in range(self.config.iterations):
200  solution = self._solve(science, fringes)
201  resid = science - numpy.sum(solution * fringes, 1)
202  rms = stdev(resid)
203  good = numpy.logical_not(abs(resid) > self.config.clip * rms)
204  self.log.logdebug("Iteration %d: RMS=%f numGood=%d" % (i, rms, good.sum()))
205  self.log.logdebug("Solution %d: %s" % (i, solution))
206  newNum = good.sum()
207 
208  if doPlot:
209  import matplotlib.pyplot as plot
210  for j in range(fringes.shape[1]):
211  fig = plot.figure(j)
212  fig.clf()
213  try:
214  fig.canvas._tkcanvas._root().lift() # == Tk's raise
215  except:
216  pass
217  ax = fig.add_axes((0.1, 0.1, 0.8, 0.8))
218  adjust = science.copy()
219  others = set(range(fringes.shape[1]))
220  others.discard(j)
221  for k in others:
222  adjust -= solution[k] * fringes[:,k]
223  ax.plot(fringes[:,j], adjust, 'r.')
224  xmin = fringes[:,j].min()
225  xmax = fringes[:,j].max()
226  ymin = solution[j] * xmin
227  ymax = solution[j] * xmax
228  ax.plot([xmin, xmax], [ymin, ymax], 'b-')
229  ax.set_title("Fringe %d: %f" % (j, solution[j]))
230  ax.set_xlabel("Fringe amplitude")
231  ax.set_ylabel("Science amplitude")
232  ax.set_autoscale_on(False)
233  ax.set_xbound(lower=xmin, upper=xmax)
234  ax.set_ybound(lower=ymin, upper=ymax)
235  fig.show()
236  while True:
237  ans = raw_input("Enter or c to continue [chp]").lower()
238  if ans in ("", "c",):
239  break
240  if ans in ("p",):
241  import pdb; pdb.set_trace()
242  elif ans in ("h", ):
243  print "h[elp] c[ontinue] p[db]"
244 
245  if newNum == oldNum:
246  # Not gaining
247  break
248  oldNum = newNum
249  good = numpy.where(good)
250  science = science[good]
251  fringes = fringes[good]
252 
253  # Final solution without rejection
254  solution = self._solve(science, fringes)
255  self.log.info("Fringe solution: %s RMS: %f Good: %d/%d" % (solution, rms, len(science), origNum))
256  return solution
257 
258  def _solve(self, science, fringes):
259  """Solve for the scale factors
260 
261  @param science Array of science exposure fringe amplitudes
262  @param fringes Array of arrays of fringe frame fringe amplitudes
263  @return Array of scale factors for the fringe frames
264  """
265  return afwMath.LeastSquares.fromDesignMatrix(fringes, science,
266  afwMath.LeastSquares.DIRECT_SVD).getSolution()
267 
268  def subtract(self, science, fringes, solution):
269  """Subtract the fringes
270 
271  @param science Science exposure
272  @param fringes List of fringe frames
273  @param solution Array of scale factors for the fringe frames
274  """
275  for s, f in zip(solution, fringes):
276  science.getMaskedImage().scaledMinus(s, f.getMaskedImage())
277 
278 
279 def measure(mi, x, y, size, statistic, stats):
280  """Measure a statistic within an aperture
281 
282  @param mi MaskedImage to measure
283  @param x, y Center for aperture
284  @param size Size of aperture
285  @param statistic Statistic to measure
286  @param stats StatisticsControl object
287  @return Value of statistic within aperture
288  """
289  bbox = afwGeom.Box2I(afwGeom.Point2I(int(x) - size, int(y - size)), afwGeom.Extent2I(2*size, 2*size))
290  subImage = mi.Factory(mi, bbox, afwImage.LOCAL)
291  return afwMath.makeStatistics(subImage, statistic, stats).getValue()
292 
293 def stdev(vector):
294  """Calculate a robust standard deviation of an array of values
295 
296  @param vector Array of values
297  @return Standard deviation
298  """
299  num = len(vector)
300  vector = vector.copy()
301  vector.sort()
302  return 0.74 * (vector[int(0.75 * num)] - vector[int(0.25 * num)])
303 
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