lsst.ip.isr  18.1.0-14-gd04256d+7
isrMock.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 copy
23 import numpy as np
24 import tempfile
25 
26 import lsst.geom
27 import lsst.afw.geom as afwGeom
28 import lsst.afw.image as afwImage
29 import lsst.afw.math as afwMath
30 import lsst.afw.cameraGeom.utils as afwUtils
31 import lsst.afw.cameraGeom.testUtils as afwTestUtils
32 from lsst.meas.algorithms import Defects
33 import lsst.pex.config as pexConfig
34 import lsst.pipe.base as pipeBase
35 from .crosstalk import X_FLIP, Y_FLIP
36 
37 __all__ = ["IsrMockConfig", "IsrMock", "RawMock", "TrimmedRawMock", "RawDictMock",
38  "CalibratedRawMock", "MasterMock",
39  "BiasMock", "DarkMock", "FlatMock", "FringeMock", "UntrimmedFringeMock",
40  "BfKernelMock", "DefectMock", "CrosstalkCoeffMock", "TransmissionMock",
41  "DataRefMock"]
42 
43 
44 class IsrMockConfig(pexConfig.Config):
45  """Configuration parameters for isrMock.
46 
47  These parameters produce generic fixed position signals from
48  various sources, and combine them in a way that matches how those
49  signals are combined to create real data. The camera used is the
50  test camera defined by the afwUtils code.
51  """
52  # Detector parameters. "Exposure" parameters.
53  isLsstLike = pexConfig.Field(
54  dtype=bool,
55  default=True,
56  doc="If True, products have one raw image per amplifier, otherwise, one raw image per detector.",
57  )
58  isTrimmed = pexConfig.Field(
59  dtype=bool,
60  default=True,
61  doc="If True, amplifiers have been trimmed and mosaicked to remove regions outside the data BBox.",
62  )
63  detectorIndex = pexConfig.Field(
64  dtype=int,
65  default=20,
66  doc="Index for the detector to use. The default value uses a standard 2x4 array of amps.",
67  )
68  rngSeed = pexConfig.Field(
69  dtype=int,
70  default=20000913,
71  doc="Seed for random number generator used to add noise.",
72  )
73  # TODO: DM-18345 Check that mocks scale correctly when gain != 1.0
74  gain = pexConfig.Field(
75  dtype=float,
76  default=1.0,
77  doc="Gain for simulated data in e^-/DN.",
78  )
79  readNoise = pexConfig.Field(
80  dtype=float,
81  default=5.0,
82  doc="Read noise of the detector in e-.",
83  )
84  expTime = pexConfig.Field(
85  dtype=float,
86  default=5.0,
87  doc="Exposure time for simulated data.",
88  )
89 
90  # Signal parameters
91  skyLevel = pexConfig.Field(
92  dtype=float,
93  default=1000.0,
94  doc="Background contribution to be generated from 'the sky' in DN.",
95  )
96  sourceFlux = pexConfig.ListField(
97  dtype=float,
98  default=[45000.0],
99  doc="Peak flux level (in DN) of simulated 'astronomical sources'.",
100  )
101  sourceAmp = pexConfig.ListField(
102  dtype=int,
103  default=[0],
104  doc="Amplifier to place simulated 'astronomical sources'.",
105  )
106  sourceX = pexConfig.ListField(
107  dtype=float,
108  default=[50.0],
109  doc="Peak position (in amplifier coordinates) of simulated 'astronomical sources'.",
110  )
111  sourceY = pexConfig.ListField(
112  dtype=float,
113  default=[25.0],
114  doc="Peak position (in amplifier coordinates) of simulated 'astronomical sources'.",
115  )
116  overscanScale = pexConfig.Field(
117  dtype=float,
118  default=100.0,
119  doc="Amplitude (in DN) of the ramp function to add to overscan data.",
120  )
121  biasLevel = pexConfig.Field(
122  dtype=float,
123  default=8000.0,
124  doc="Background contribution to be generated from the bias offset in DN.",
125  )
126  darkRate = pexConfig.Field(
127  dtype=float,
128  default=5.0,
129  doc="Background level contribution (in e-/s) to be generated from dark current.",
130  )
131  darkTime = pexConfig.Field(
132  dtype=float,
133  default=5.0,
134  doc="Exposure time for the dark current contribution.",
135  )
136  flatDrop = pexConfig.Field(
137  dtype=float,
138  default=0.1,
139  doc="Fractional flux drop due to flat from center to edge of detector along x-axis.",
140  )
141  fringeScale = pexConfig.ListField(
142  dtype=float,
143  default=[200.0],
144  doc="Peak fluxes for the components of the fringe ripple in DN.",
145  )
146  fringeX0 = pexConfig.ListField(
147  dtype=float,
148  default=[-100],
149  doc="Center position for the fringe ripples.",
150  )
151  fringeY0 = pexConfig.ListField(
152  dtype=float,
153  default=[-0],
154  doc="Center position for the fringe ripples.",
155  )
156 
157  # Inclusion parameters
158  doAddSky = pexConfig.Field(
159  dtype=bool,
160  default=True,
161  doc="Apply 'sky' signal to output image.",
162  )
163  doAddSource = pexConfig.Field(
164  dtype=bool,
165  default=True,
166  doc="Add simulated source to output image.",
167  )
168  doAddCrosstalk = pexConfig.Field(
169  dtype=bool,
170  default=False,
171  doc="Apply simulated crosstalk to output image. This cannot be corrected by ISR, "
172  "as detector.hasCrosstalk()==False.",
173  )
174  doAddOverscan = pexConfig.Field(
175  dtype=bool,
176  default=True,
177  doc="If untrimmed, add overscan ramp to overscan and data regions.",
178  )
179  doAddBias = pexConfig.Field(
180  dtype=bool,
181  default=True,
182  doc="Add bias signal to data.",
183  )
184  doAddDark = pexConfig.Field(
185  dtype=bool,
186  default=True,
187  doc="Add dark signal to data.",
188  )
189  doAddFlat = pexConfig.Field(
190  dtype=bool,
191  default=True,
192  doc="Add flat signal to data.",
193  )
194  doAddFringe = pexConfig.Field(
195  dtype=bool,
196  default=True,
197  doc="Add fringe signal to data.",
198  )
199 
200  # Datasets to create and return instead of generating an image.
201  doTransmissionCurve = pexConfig.Field(
202  dtype=bool,
203  default=False,
204  doc="Return a simulated transmission curve.",
205  )
206  doDefects = pexConfig.Field(
207  dtype=bool,
208  default=False,
209  doc="Return a simulated defect list.",
210  )
211  doBrighterFatter = pexConfig.Field(
212  dtype=bool,
213  default=False,
214  doc="Return a simulated brighter-fatter kernel.",
215  )
216  doCrosstalkCoeffs = pexConfig.Field(
217  dtype=bool,
218  default=False,
219  doc="Return the matrix of crosstalk coefficients.",
220  )
221  doDataRef = pexConfig.Field(
222  dtype=bool,
223  default=False,
224  doc="Return a simulated gen2 butler dataRef.",
225  )
226  doGenerateImage = pexConfig.Field(
227  dtype=bool,
228  default=False,
229  doc="Return the generated output image if True.",
230  )
231  doGenerateData = pexConfig.Field(
232  dtype=bool,
233  default=False,
234  doc="Return a non-image data structure if True.",
235  )
236  doGenerateAmpDict = pexConfig.Field(
237  dtype=bool,
238  default=False,
239  doc="Return a dict of exposure amplifiers instead of an afwImage.Exposure.",
240  )
241 
242 
243 class IsrMock(pipeBase.Task):
244  """Class to generate consistent mock images for ISR testing.
245 
246  ISR testing currently relies on one-off fake images that do not
247  accurately mimic the full set of detector effects. This class
248  uses the test camera/detector/amplifier structure defined in
249  `lsst.afw.cameraGeom.testUtils` to avoid making the test data
250  dependent on any of the actual obs package formats.
251  """
252  ConfigClass = IsrMockConfig
253  _DefaultName = "isrMock"
254 
255  def __init__(self, **kwargs):
256  super().__init__(**kwargs)
257  self.rng = np.random.RandomState(self.config.rngSeed)
258  self.crosstalkCoeffs = np.array([[0.0, 0.0, 0.0, 0.0, 0.0, -1e-3, 0.0, 0.0],
259  [1e-2, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
260  [1e-2, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
261  [1e-2, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
262  [1e-2, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
263  [1e-2, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
264  [1e-2, 0.0, 0.0, 2.2e-2, 0.0, 0.0, 0.0, 0.0],
265  [1e-2, 5e-3, 5e-4, 3e-3, 4e-2, 5e-3, 5e-3, 0.0]])
266 
267  self.bfKernel = np.array([[1., 4., 7., 4., 1.],
268  [4., 16., 26., 16., 4.],
269  [7., 26., 41., 26., 7.],
270  [4., 16., 26., 16., 4.],
271  [1., 4., 7., 4., 1.]]) / 273.0
272 
273  def run(self):
274  """Generate a mock ISR product, and return it.
275 
276  Returns
277  -------
278  image : `lsst.afw.image.Exposure`
279  Simulated ISR image with signals added.
280  dataProduct :
281  Simulated ISR data products.
282  None :
283  Returned if no valid configuration was found.
284 
285  Raises
286  ------
287  RuntimeError
288  Raised if both doGenerateImage and doGenerateData are specified.
289  """
290  if self.config.doGenerateImage and self.config.doGenerateData:
291  raise RuntimeError("Only one of doGenerateImage and doGenerateData may be specified.")
292  elif self.config.doGenerateImage:
293  return self.makeImage()
294  elif self.config.doGenerateData:
295  return self.makeData()
296  else:
297  return None
298 
299  def makeData(self):
300  """Generate simulated ISR data.
301 
302  Currently, only the class defined crosstalk coefficient
303  matrix, brighter-fatter kernel, a constant unity transmission
304  curve, or a simple single-entry defect list can be generated.
305 
306  Returns
307  -------
308  dataProduct :
309  Simulated ISR data product.
310  """
311  if sum(map(bool, [self.config.doBrighterFatter,
312  self.config.doDefects,
313  self.config.doTransmissionCurve,
314  self.config.doCrosstalkCoeffs])) != 1:
315  raise RuntimeError("Only one data product can be generated at a time.")
316  elif self.config.doBrighterFatter is True:
317  return self.makeBfKernel()
318  elif self.config.doDefects is True:
319  return self.makeDefectList()
320  elif self.config.doTransmissionCurve is True:
321  return self.makeTransmissionCurve()
322  elif self.config.doCrosstalkCoeffs is True:
323  return self.crosstalkCoeffs
324  else:
325  return None
326 
327  def makeBfKernel(self):
328  """Generate a simple Gaussian brighter-fatter kernel.
329 
330  Returns
331  -------
332  kernel : `numpy.ndarray`
333  Simulated brighter-fatter kernel.
334  """
335  return self.bfKernel
336 
337  def makeDefectList(self):
338  """Generate a simple single-entry defect list.
339 
340  Returns
341  -------
342  defectList : `lsst.meas.algorithms.Defects`
343  Simulated defect list
344  """
345  return Defects([lsst.geom.Box2I(lsst.geom.Point2I(0, 0),
346  lsst.geom.Extent2I(40, 50))])
347 
349  """Generate the simulated crosstalk coefficients.
350 
351  Returns
352  -------
353  coeffs : `numpy.ndarray`
354  Simulated crosstalk coefficients.
355  """
356 
357  return self.crosstalkCoeffs
358 
360  """Generate a simulated flat transmission curve.
361 
362  Returns
363  -------
364  transmission : `lsst.afw.image.TransmissionCurve`
365  Simulated transmission curve.
366  """
367 
368  return afwImage.TransmissionCurve.makeIdentity()
369 
370  def makeImage(self):
371  """Generate a simulated ISR image.
372 
373  Returns
374  -------
375  exposure : `lsst.afw.image.Exposure` or `dict`
376  Simulated ISR image data.
377 
378  Notes
379  -----
380  This method currently constructs a "raw" data image by:
381  * Generating a simulated sky with noise
382  * Adding a single Gaussian "star"
383  * Adding the fringe signal
384  * Multiplying the frame by the simulated flat
385  * Adding dark current (and noise)
386  * Adding a bias offset (and noise)
387  * Adding an overscan gradient parallel to the pixel y-axis
388  * Simulating crosstalk by adding a scaled version of each
389  amplifier to each other amplifier.
390 
391  The exposure with image data constructed this way is in one of
392  three formats.
393  * A single image, with overscan and prescan regions retained
394  * A single image, with overscan and prescan regions trimmed
395  * A `dict`, containing the amplifer data indexed by the
396  amplifier name.
397 
398  The nonlinearity, CTE, and brighter fatter are currently not
399  implemented.
400 
401  Note that this method generates an image in the reverse
402  direction as the ISR processing, as the output image here has
403  had a series of instrument effects added to an idealized
404  exposure.
405  """
406  exposure = self.getExposure()
407 
408  for idx, amp in enumerate(exposure.getDetector()):
409  bbox = None
410  if self.config.isTrimmed is True:
411  bbox = amp.getBBox()
412  else:
413  bbox = amp.getRawDataBBox()
414 
415  ampData = exposure.image[bbox]
416 
417  if self.config.doAddSky is True:
418  self.amplifierAddNoise(ampData, self.config.skyLevel, np.sqrt(self.config.skyLevel))
419 
420  if self.config.doAddSource is True:
421  for sourceAmp, sourceFlux, sourceX, sourceY in zip(self.config.sourceAmp,
422  self.config.sourceFlux,
423  self.config.sourceX,
424  self.config.sourceY):
425  if idx == sourceAmp:
426  self.amplifierAddSource(ampData, sourceFlux, sourceX, sourceY)
427 
428  if self.config.doAddFringe is True:
429  self.amplifierAddFringe(amp, ampData, np.array(self.config.fringeScale),
430  x0=np.array(self.config.fringeX0),
431  y0=np.array(self.config.fringeY0))
432 
433  if self.config.doAddFlat is True:
434  if ampData.getArray().sum() == 0.0:
435  self.amplifierAddNoise(ampData, 1.0, 0.0)
436  u0 = exposure.getDimensions().getX()
437  v0 = exposure.getDimensions().getY()
438  self.amplifierMultiplyFlat(amp, ampData, self.config.flatDrop, u0=u0, v0=v0)
439 
440  if self.config.doAddDark is True:
441  self.amplifierAddNoise(ampData,
442  self.config.darkRate * self.config.darkTime / self.config.gain,
443  np.sqrt(self.config.darkRate *
444  self.config.darkTime / self.config.gain))
445 
446  if self.config.doAddCrosstalk is True:
447 
448  for idxS, ampS in enumerate(exposure.getDetector()):
449  for idxT, ampT in enumerate(exposure.getDetector()):
450  ampDataS = exposure.image[ampS.getBBox() if self.config.isTrimmed
451  else ampS.getRawDataBBox()]
452  ampDataT = exposure.image[ampT.getBBox() if self.config.isTrimmed
453  else ampT.getRawDataBBox()]
454  ampDataS = afwMath.flipImage(ampDataS,
455  (X_FLIP[ampS.getReadoutCorner()] ^
456  X_FLIP[ampT.getReadoutCorner()]),
457  (Y_FLIP[ampS.getReadoutCorner()] ^
458  Y_FLIP[ampT.getReadoutCorner()]))
459  self.amplifierAddCT(ampDataS, ampDataT, self.crosstalkCoeffs[idxT][idxS])
460 
461  for amp in exposure.getDetector():
462  bbox = None
463  if self.config.isTrimmed is True:
464  bbox = amp.getBBox()
465  else:
466  bbox = amp.getRawDataBBox()
467 
468  ampData = exposure.image[bbox]
469 
470  if self.config.doAddBias is True:
471  self.amplifierAddNoise(ampData, self.config.biasLevel,
472  self.config.readNoise / self.config.gain)
473 
474  if self.config.doAddOverscan is True:
475  oscanBBox = amp.getRawHorizontalOverscanBBox()
476  oscanData = exposure.image[oscanBBox]
477  self.amplifierAddNoise(oscanData, self.config.biasLevel,
478  self.config.readNoise / self.config.gain)
479 
480  self.amplifierAddYGradient(ampData, -1.0 * self.config.overscanScale,
481  1.0 * self.config.overscanScale)
482  self.amplifierAddYGradient(oscanData, -1.0 * self.config.overscanScale,
483  1.0 * self.config.overscanScale)
484 
485  if self.config.doGenerateAmpDict is True:
486  expDict = dict()
487  for amp in exposure.getDetector():
488  expDict[amp.getName()] = exposure
489  return expDict
490  else:
491  return exposure
492 
493  # afw primatives to construct the image structure
494  def getCamera(self):
495  """Construct a test camera object.
496 
497  Returns
498  -------
499  camera : `lsst.afw.cameraGeom.camera`
500  Test camera.
501  """
502  cameraWrapper = afwTestUtils.CameraWrapper(self.config.isLsstLike)
503  camera = cameraWrapper.camera
504  return camera
505 
506  def getExposure(self):
507  """Construct a test exposure.
508 
509  The test exposure has a simple WCS set, as well as a list of
510  unlikely header keywords that can be removed during ISR
511  processing to exercise that code.
512 
513  Returns
514  -------
515  exposure : `lsst.afw.exposure.Exposure`
516  Construct exposure containing masked image of the
517  appropriate size.
518  """
519  camera = self.getCamera()
520  detector = camera[self.config.detectorIndex]
521  image = afwUtils.makeImageFromCcd(detector,
522  isTrimmed=self.config.isTrimmed,
523  showAmpGain=False,
524  rcMarkSize=0,
525  binSize=1,
526  imageFactory=afwImage.ImageF)
527 
528  var = afwImage.ImageF(image.getDimensions())
529  mask = afwImage.Mask(image.getDimensions())
530  image.assign(0.0)
531 
532  maskedImage = afwImage.makeMaskedImage(image, mask, var)
533  exposure = afwImage.makeExposure(maskedImage)
534  exposure.setDetector(detector)
535  exposure.setWcs(self.getWcs())
536 
537  visitInfo = afwImage.VisitInfo(exposureTime=self.config.expTime, darkTime=self.config.darkTime)
538  exposure.getInfo().setVisitInfo(visitInfo)
539 
540  metadata = exposure.getMetadata()
541  metadata.add("SHEEP", 7.3, "number of sheep on farm")
542  metadata.add("MONKEYS", 155, "monkeys per tree")
543  metadata.add("VAMPIRES", 4, "How scary are vampires.")
544 
545  for amp in exposure.getDetector():
546  amp.setLinearityCoeffs((0., 1., 0., 0.))
547  amp.setLinearityType("Polynomial")
548  amp.setGain(self.config.gain)
549 
550  exposure.image.array[:] = np.zeros(exposure.getImage().getDimensions()).transpose()
551  exposure.mask.array[:] = np.zeros(exposure.getMask().getDimensions()).transpose()
552  exposure.variance.array[:] = np.zeros(exposure.getVariance().getDimensions()).transpose()
553 
554  return exposure
555 
556  def getWcs(self):
557  """Construct a dummy WCS object.
558 
559  Taken from the deprecated ip_isr/examples/exampleUtils.py.
560 
561  This is not guaranteed, given the distortion and pixel scale
562  listed in the afwTestUtils camera definition.
563 
564  Returns
565  -------
566  wcs : `lsst.afw.geom.SkyWcs`
567  Test WCS transform.
568  """
569  return afwGeom.makeSkyWcs(crpix=lsst.geom.Point2D(0.0, 100.0),
570  crval=lsst.geom.SpherePoint(45.0, 25.0, lsst.geom.degrees),
571  cdMatrix=afwGeom.makeCdMatrix(scale=1.0*lsst.geom.degrees))
572 
573  def localCoordToExpCoord(self, ampData, x, y):
574  """Convert between a local amplifier coordinate and the full
575  exposure coordinate.
576 
577  Parameters
578  ----------
579  ampData : `lsst.afw.image.ImageF`
580  Amplifier image to use for conversions.
581  x : `int`
582  X-coordinate of the point to transform.
583  y : `int`
584  Y-coordinate of the point to transform.
585 
586  Returns
587  -------
588  u : `int`
589  Transformed x-coordinate.
590  v : `int`
591  Transformed y-coordinate.
592 
593  Notes
594  -----
595  The output is transposed intentionally here, to match the
596  internal transpose between numpy and afw.image coordinates.
597  """
598  u = x + ampData.getBBox().getBeginX()
599  v = y + ampData.getBBox().getBeginY()
600 
601  return (v, u)
602 
603  # Simple data values.
604  def amplifierAddNoise(self, ampData, mean, sigma):
605  """Add Gaussian noise to an amplifier's image data.
606 
607  This method operates in the amplifier coordinate frame.
608 
609  Parameters
610  ----------
611  ampData : `lsst.afw.image.ImageF`
612  Amplifier image to operate on.
613  mean : `float`
614  Mean value of the Gaussian noise.
615  sigma : `float`
616  Sigma of the Gaussian noise.
617  """
618  ampArr = ampData.array
619  ampArr[:] = ampArr[:] + self.rng.normal(mean, sigma,
620  size=ampData.getDimensions()).transpose()
621 
622  def amplifierAddYGradient(self, ampData, start, end):
623  """Add a y-axis linear gradient to an amplifier's image data.
624 
625  This method operates in the amplifier coordinate frame.
626 
627  Parameters
628  ----------
629  ampData : `lsst.afw.image.ImageF`
630  Amplifier image to operate on.
631  start : `float`
632  Start value of the gradient (at y=0).
633  end : `float`
634  End value of the gradient (at y=ymax).
635  """
636  nPixY = ampData.getDimensions().getY()
637  ampArr = ampData.array
638  ampArr[:] = ampArr[:] + (np.interp(range(nPixY), (0, nPixY - 1), (start, end)).reshape(nPixY, 1) +
639  np.zeros(ampData.getDimensions()).transpose())
640 
641  def amplifierAddSource(self, ampData, scale, x0, y0):
642  """Add a single Gaussian source to an amplifier.
643 
644  This method operates in the amplifier coordinate frame.
645 
646  Parameters
647  ----------
648  ampData : `lsst.afw.image.ImageF`
649  Amplifier image to operate on.
650  scale : `float`
651  Peak flux of the source to add.
652  x0 : `float`
653  X-coordinate of the source peak.
654  y0 : `float`
655  Y-coordinate of the source peak.
656  """
657  for x in range(0, ampData.getDimensions().getX()):
658  for y in range(0, ampData.getDimensions().getY()):
659  ampData.array[y][x] = (ampData.array[y][x] +
660  scale * np.exp(-0.5 * ((x - x0)**2 + (y - y0)**2) / 3.0**2))
661 
662  def amplifierAddCT(self, ampDataSource, ampDataTarget, scale):
663  """Add a scaled copy of an amplifier to another, simulating crosstalk.
664 
665  This method operates in the amplifier coordinate frame.
666 
667  Parameters
668  ----------
669  ampDataSource : `lsst.afw.image.ImageF`
670  Amplifier image to add scaled copy from.
671  ampDataTarget : `lsst.afw.image.ImageF`
672  Amplifier image to add scaled copy to.
673  scale : `float`
674  Flux scale of the copy to add to the target.
675 
676  Notes
677  -----
678  This simulates simple crosstalk between amplifiers.
679  """
680  ampDataTarget.array[:] = (ampDataTarget.array[:] +
681  scale * ampDataSource.array[:])
682 
683  # Functional form data values.
684  def amplifierAddFringe(self, amp, ampData, scale, x0=100, y0=0):
685  """Add a fringe-like ripple pattern to an amplifier's image data.
686 
687  Parameters
688  ----------
689  amp : `~lsst.afw.ampInfo.AmpInfoRecord`
690  Amplifier to operate on. Needed for amp<->exp coordinate transforms.
691  ampData : `lsst.afw.image.ImageF`
692  Amplifier image to operate on.
693  scale : `numpy.array` or `float`
694  Peak intensity scaling for the ripple.
695  x0 : `numpy.array` or `float`, optional
696  Fringe center
697  y0 : `numpy.array` or `float`, optional
698  Fringe center
699 
700  Notes
701  -----
702  This uses an offset sinc function to generate a ripple
703  pattern. True fringes have much finer structure, but this
704  pattern should be visually identifiable. The (x, y)
705  coordinates are in the frame of the amplifier, and (u, v) in
706  the frame of the full trimmed image.
707  """
708  for x in range(0, ampData.getDimensions().getX()):
709  for y in range(0, ampData.getDimensions().getY()):
710  (u, v) = self.localCoordToExpCoord(amp, x, y)
711  ampData.getArray()[y][x] = np.sum((ampData.getArray()[y][x] +
712  scale *
713  np.sinc(((u - x0) / 50)**2 +
714  ((v - y0) / 50)**2)))
715 
716  def amplifierMultiplyFlat(self, amp, ampData, fracDrop, u0=100.0, v0=100.0):
717  """Multiply an amplifier's image data by a flat-like pattern.
718 
719  Parameters
720  ----------
721  amp : `lsst.afw.ampInfo.AmpInfoRecord`
722  Amplifier to operate on. Needed for amp<->exp coordinate transforms.
723  ampData : `lsst.afw.image.ImageF`
724  Amplifier image to operate on.
725  fracDrop : `float`
726  Fractional drop from center to edge of detector along x-axis.
727  u0 : `float`
728  Peak location in detector coordinates.
729  v0 : `float`
730  Peak location in detector coordinates.
731 
732  Notes
733  -----
734  This uses a 2-d Gaussian to simulate an illumination pattern
735  that falls off towards the edge of the detector. The (x, y)
736  coordinates are in the frame of the amplifier, and (u, v) in
737  the frame of the full trimmed image.
738  """
739  if fracDrop >= 1.0:
740  raise RuntimeError("Flat fractional drop cannot be greater than 1.0")
741 
742  sigma = u0 / np.sqrt(-2.0 * np.log(fracDrop))
743 
744  for x in range(0, ampData.getDimensions().getX()):
745  for y in range(0, ampData.getDimensions().getY()):
746  (u, v) = self.localCoordToExpCoord(amp, x, y)
747  f = np.exp(-0.5 * ((u - u0)**2 + (v - v0)**2) / sigma**2)
748  ampData.array[y][x] = (ampData.array[y][x] * f)
749 
750 
752  """Generate a raw exposure suitable for ISR.
753  """
754  def __init__(self, **kwargs):
755  super().__init__(**kwargs)
756  self.config.isTrimmed = False
757  self.config.doGenerateImage = True
758  self.config.doGenerateAmpDict = False
759  self.config.doAddOverscan = True
760  self.config.doAddSky = True
761  self.config.doAddSource = True
762  self.config.doAddCrosstalk = False
763  self.config.doAddBias = True
764  self.config.doAddDark = True
765 
766 
768  """Generate a trimmed raw exposure.
769  """
770  def __init__(self, **kwargs):
771  super().__init__(**kwargs)
772  self.config.isTrimmed = True
773  self.config.doAddOverscan = False
774 
775 
777  """Generate a trimmed raw exposure.
778  """
779  def __init__(self, **kwargs):
780  super().__init__(**kwargs)
781  self.config.isTrimmed = True
782  self.config.doGenerateImage = True
783  self.config.doAddOverscan = False
784  self.config.doAddSky = True
785  self.config.doAddSource = True
786  self.config.doAddCrosstalk = False
787 
788  self.config.doAddBias = False
789  self.config.doAddDark = False
790  self.config.doAddFlat = False
791  self.config.doAddFringe = True
792 
793  self.config.biasLevel = 0.0
794  self.config.readNoise = 10.0
795 
796 
798  """Generate a raw exposure dict suitable for ISR.
799  """
800  def __init__(self, **kwargs):
801  super().__init__(**kwargs)
802  self.config.doGenerateAmpDict = True
803 
804 
806  """Parent class for those that make master calibrations.
807  """
808  def __init__(self, **kwargs):
809  super().__init__(**kwargs)
810  self.config.isTrimmed = True
811  self.config.doGenerateImage = True
812  self.config.doAddOverscan = False
813  self.config.doAddSky = False
814  self.config.doAddSource = False
815  self.config.doAddCrosstalk = False
816 
817  self.config.doAddBias = False
818  self.config.doAddDark = False
819  self.config.doAddFlat = False
820  self.config.doAddFringe = False
821 
822 
824  """Simulated master bias calibration.
825  """
826  def __init__(self, **kwargs):
827  super().__init__(**kwargs)
828  self.config.doAddBias = True
829  self.config.readNoise = 10.0
830 
831 
833  """Simulated master dark calibration.
834  """
835  def __init__(self, **kwargs):
836  super().__init__(**kwargs)
837  self.config.doAddDark = True
838  self.config.darkTime = 1.0
839 
840 
842  """Simulated master flat calibration.
843  """
844  def __init__(self, **kwargs):
845  super().__init__(**kwargs)
846  self.config.doAddFlat = True
847 
848 
850  """Simulated master fringe calibration.
851  """
852  def __init__(self, **kwargs):
853  super().__init__(**kwargs)
854  self.config.doAddFringe = True
855 
856 
858  """Simulated untrimmed master fringe calibration.
859  """
860  def __init__(self, **kwargs):
861  super().__init__(**kwargs)
862  self.config.isTrimmed = False
863 
864 
866  """Simulated brighter-fatter kernel.
867  """
868  def __init__(self, **kwargs):
869  super().__init__(**kwargs)
870  self.config.doGenerateImage = False
871  self.config.doGenerateData = True
872  self.config.doBrighterFatter = True
873  self.config.doDefects = False
874  self.config.doCrosstalkCoeffs = False
875  self.config.doTransmissionCurve = False
876 
877 
879  """Simulated defect list.
880  """
881  def __init__(self, **kwargs):
882  super().__init__(**kwargs)
883  self.config.doGenerateImage = False
884  self.config.doGenerateData = True
885  self.config.doBrighterFatter = False
886  self.config.doDefects = True
887  self.config.doCrosstalkCoeffs = False
888  self.config.doTransmissionCurve = False
889 
890 
892  """Simulated crosstalk coefficient matrix.
893  """
894  def __init__(self, **kwargs):
895  super().__init__(**kwargs)
896  self.config.doGenerateImage = False
897  self.config.doGenerateData = True
898  self.config.doBrighterFatter = False
899  self.config.doDefects = False
900  self.config.doCrosstalkCoeffs = True
901  self.config.doTransmissionCurve = False
902 
903 
905  """Simulated transmission curve.
906  """
907  def __init__(self, **kwargs):
908  super().__init__(**kwargs)
909  self.config.doGenerateImage = False
910  self.config.doGenerateData = True
911  self.config.doBrighterFatter = False
912  self.config.doDefects = False
913  self.config.doCrosstalkCoeffs = False
914  self.config.doTransmissionCurve = True
915 
916 
917 class DataRefMock(object):
918  """Simulated gen2 butler data ref.
919 
920  Currently only supports get and put operations, which are most
921  likely to be called for data in ISR processing.
922 
923  """
924  dataId = "isrMock Fake Data"
925  darkval = 2. # e-/sec
926  oscan = 250. # DN
927  gradient = .10
928  exptime = 15.0 # seconds
929  darkexptime = 15.0 # seconds
930 
931  def __init__(self, **kwargs):
932  if 'config' in kwargs.keys():
933  self.config = kwargs['config']
934  else:
935  self.config = None
936 
937  def expectImage(self):
938  if self.config is None:
939  self.config = IsrMockConfig()
940  self.config.doGenerateImage = True
941  self.config.doGenerateData = False
942 
943  def expectData(self):
944  if self.config is None:
945  self.config = IsrMockConfig()
946  self.config.doGenerateImage = False
947  self.config.doGenerateData = True
948 
949  def get(self, dataType, **kwargs):
950  """Return an appropriate data product.
951 
952  Parameters
953  ----------
954  dataType : `str`
955  Type of data product to return.
956 
957  Returns
958  -------
959  mock : IsrMock.run() result
960  The output product.
961  """
962  if "_filename" in dataType:
963  self.expectData()
964  return tempfile.mktemp(), "mock"
965  elif 'transmission_' in dataType:
966  self.expectData()
967  return TransmissionMock(config=self.config).run()
968  elif dataType == 'ccdExposureId':
969  self.expectData()
970  return 20090913
971  elif dataType == 'camera':
972  self.expectData()
973  return IsrMock(config=self.config).getCamera()
974  elif dataType == 'raw':
975  self.expectImage()
976  return RawMock(config=self.config).run()
977  elif dataType == 'bias':
978  self.expectImage()
979  return BiasMock(config=self.config).run()
980  elif dataType == 'dark':
981  self.expectImage()
982  return DarkMock(config=self.config).run()
983  elif dataType == 'flat':
984  self.expectImage()
985  return FlatMock(config=self.config).run()
986  elif dataType == 'fringe':
987  self.expectImage()
988  return FringeMock(config=self.config).run()
989  elif dataType == 'defects':
990  self.expectData()
991  return DefectMock(config=self.config).run()
992  elif dataType == 'bfKernel':
993  self.expectData()
994  return BfKernelMock(config=self.config).run()
995  elif dataType == 'linearizer':
996  return None
997  elif dataType == 'crosstalkSources':
998  return None
999  else:
1000  raise RuntimeError("ISR DataRefMock cannot return %s.", dataType)
1001 
1002  def put(self, exposure, filename):
1003  """Write an exposure to a FITS file.
1004 
1005  Parameters
1006  ----------
1007  exposure : `lsst.afw.image.Exposure`
1008  Image data to write out.
1009  filename : `str`
1010  Base name of the output file.
1011  """
1012  exposure.writeFits(filename+".fits")
1013 
1014 
1015 class FringeDataRefMock(object):
1016  """Simulated gen2 butler data ref.
1017 
1018  Currently only supports get and put operations, which are most
1019  likely to be called for data in ISR processing.
1020 
1021  """
1022  dataId = "isrMock Fake Data"
1023  darkval = 2. # e-/sec
1024  oscan = 250. # DN
1025  gradient = .10
1026  exptime = 15 # seconds
1027  darkexptime = 40. # seconds
1028 
1029  def __init__(self, **kwargs):
1030  if 'config' in kwargs.keys():
1031  self.config = kwargs['config']
1032  else:
1033  self.config = IsrMockConfig()
1034  self.config.isTrimmed = True
1035  self.config.doAddFringe = True
1036  self.config.readNoise = 10.0
1037 
1038  def get(self, dataType, **kwargs):
1039  """Return an appropriate data product.
1040 
1041  Parameters
1042  ----------
1043  dataType : `str`
1044  Type of data product to return.
1045 
1046  Returns
1047  -------
1048  mock : IsrMock.run() result
1049  The output product.
1050  """
1051  if "_filename" in dataType:
1052  return tempfile.mktemp(), "mock"
1053  elif 'transmission_' in dataType:
1054  return TransmissionMock(config=self.config).run()
1055  elif dataType == 'ccdExposureId':
1056  return 20090913
1057  elif dataType == 'camera':
1058  return IsrMock(config=self.config).getCamera()
1059  elif dataType == 'raw':
1060  return CalibratedRawMock(config=self.config).run()
1061  elif dataType == 'bias':
1062  return BiasMock(config=self.config).run()
1063  elif dataType == 'dark':
1064  return DarkMock(config=self.config).run()
1065  elif dataType == 'flat':
1066  return FlatMock(config=self.config).run()
1067  elif dataType == 'fringe':
1068  fringes = []
1069  configCopy = copy.deepcopy(self.config)
1070  for scale, x, y in zip(self.config.fringeScale, self.config.fringeX0, self.config.fringeY0):
1071  configCopy.fringeScale = [1.0]
1072  configCopy.fringeX0 = [x]
1073  configCopy.fringeY0 = [y]
1074  fringes.append(FringeMock(config=configCopy).run())
1075  return fringes
1076  elif dataType == 'defects':
1077  return DefectMock(config=self.config).run()
1078  elif dataType == 'bfKernel':
1079  return BfKernelMock(config=self.config).run()
1080  elif dataType == 'linearizer':
1081  return None
1082  elif dataType == 'crosstalkSources':
1083  return None
1084  else:
1085  return None
1086 
1087  def put(self, exposure, filename):
1088  """Write an exposure to a FITS file.
1089 
1090  Parameters
1091  ----------
1092  exposure : `lsst.afw.image.Exposure`
1093  Image data to write out.
1094  filename : `str`
1095  Base name of the output file.
1096  """
1097  exposure.writeFits(filename+".fits")
def amplifierAddNoise(self, ampData, mean, sigma)
Definition: isrMock.py:604
def __init__(self, kwargs)
Definition: isrMock.py:844
def __init__(self, kwargs)
Definition: isrMock.py:754
def amplifierAddSource(self, ampData, scale, x0, y0)
Definition: isrMock.py:641
def __init__(self, kwargs)
Definition: isrMock.py:255
def __init__(self, kwargs)
Definition: isrMock.py:808
def amplifierAddYGradient(self, ampData, start, end)
Definition: isrMock.py:622
def put(self, exposure, filename)
Definition: isrMock.py:1002
def get(self, dataType, kwargs)
Definition: isrMock.py:1038
def amplifierAddFringe(self, amp, ampData, scale, x0=100, y0=0)
Definition: isrMock.py:684
def __init__(self, kwargs)
Definition: isrMock.py:931
def __init__(self, kwargs)
Definition: isrMock.py:800
def localCoordToExpCoord(self, ampData, x, y)
Definition: isrMock.py:573
def amplifierMultiplyFlat(self, amp, ampData, fracDrop, u0=100.0, v0=100.0)
Definition: isrMock.py:716
def __init__(self, kwargs)
Definition: isrMock.py:881
def get(self, dataType, kwargs)
Definition: isrMock.py:949
def put(self, exposure, filename)
Definition: isrMock.py:1087
def __init__(self, kwargs)
Definition: isrMock.py:868
def __init__(self, kwargs)
Definition: isrMock.py:835
def __init__(self, kwargs)
Definition: isrMock.py:852
def amplifierAddCT(self, ampDataSource, ampDataTarget, scale)
Definition: isrMock.py:662
def __init__(self, kwargs)
Definition: isrMock.py:826