Coverage for tests/test_dcrModel.py : 12%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# This file is part of ip_diffim.
2#
3# LSST Data Management System
4# This product includes software developed by the
5# LSST Project (http://www.lsst.org/).
6# See COPYRIGHT file at the top of the source tree.
7#
8# This program is free software: you can redistribute it and/or modify
9# it under the terms of the GNU General Public License as published by
10# the Free Software Foundation, either version 3 of the License, or
11# (at your option) any later version.
12#
13# This program is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the LSST License Statement and
19# the GNU General Public License along with this program. If not,
20# see <https://www.lsstcorp.org/LegalNotices/>.
22from astropy import units as u
23from astropy.coordinates import SkyCoord, EarthLocation, Angle
24from astropy.time import Time
25import numpy as np
26from scipy import ndimage
27import unittest
29from astro_metadata_translator import makeObservationInfo
30from lsst.afw.coord.refraction import differentialRefraction
31import lsst.afw.geom as afwGeom
32import lsst.afw.image as afwImage
33import lsst.afw.image.utils as afwImageUtils
34import lsst.afw.math as afwMath
35import lsst.geom as geom
36from lsst.geom import arcseconds, degrees, radians, arcminutes
37from lsst.ip.diffim.dcrModel import (DcrModel, calculateDcr, calculateImageParallacticAngle,
38 applyDcr, wavelengthGenerator)
39from lsst.obs.base import MakeRawVisitInfoViaObsInfo
40from lsst.meas.algorithms.testUtils import plantSources
41import lsst.utils.tests
44# Our calculation of hour angle and parallactic angle ignore precession
45# and nutation, so calculations depending on these are not precise. DM-20133
46coordinateTolerance = 1.*arcminutes
49class DcrModelTestTask(lsst.utils.tests.TestCase):
50 """A test case for the DCR-aware image coaddition algorithm.
52 Attributes
53 ----------
54 bbox : `lsst.afw.geom.Box2I`
55 Bounding box of the test model.
56 bufferSize : `int`
57 Distance from the inner edge of the bounding box
58 to avoid placing test sources in the model images.
59 dcrNumSubfilters : int
60 Number of sub-filters used to model chromatic effects within a band.
61 lambdaEff : `float`
62 Effective wavelength of the full band.
63 lambdaMax : `float`
64 Maximum wavelength where the relative throughput
65 of the band is greater than 1%.
66 lambdaMin : `float`
67 Minimum wavelength where the relative throughput
68 of the band is greater than 1%.
69 mask : `lsst.afw.image.Mask`
70 Reference mask of the unshifted model.
71 """
73 def setUp(self):
74 """Define the filter, DCR parameters, and the bounding box for the tests.
75 """
76 self.rng = np.random.RandomState(5)
77 self.nRandIter = 10 # Number of iterations to repeat each test with random numbers.
78 self.dcrNumSubfilters = 3
79 self.lambdaEff = 476.31 # Use LSST g band values for the test.
80 self.lambdaMin = 405.
81 self.lambdaMax = 552.
82 self.bufferSize = 5
83 xSize = 40
84 ySize = 42
85 x0 = 12345
86 y0 = 67890
87 self.bbox = geom.Box2I(geom.Point2I(x0, y0), geom.Extent2I(xSize, ySize))
89 def makeTestImages(self, seed=5, nSrc=5, psfSize=2., noiseLevel=5.,
90 detectionSigma=5., sourceSigma=20., fluxRange=2.):
91 """Make reproduceable PSF-convolved masked images for testing.
93 Parameters
94 ----------
95 seed : `int`, optional
96 Seed value to initialize the random number generator.
97 nSrc : `int`, optional
98 Number of sources to simulate.
99 psfSize : `float`, optional
100 Width of the PSF of the simulated sources, in pixels.
101 noiseLevel : `float`, optional
102 Standard deviation of the noise to add to each pixel.
103 detectionSigma : `float`, optional
104 Threshold amplitude of the image to set the "DETECTED" mask.
105 sourceSigma : `float`, optional
106 Average amplitude of the simulated sources,
107 relative to ``noiseLevel``
108 fluxRange : `float`, optional
109 Range in flux amplitude of the simulated sources.
111 Returns
112 -------
113 modelImages : `list` of `lsst.afw.image.Image`
114 A list of images, each containing the model for one subfilter
115 """
116 rng = np.random.RandomState(seed)
117 x0, y0 = self.bbox.getBegin()
118 xSize, ySize = self.bbox.getDimensions()
119 xLoc = rng.rand(nSrc)*(xSize - 2*self.bufferSize) + self.bufferSize + x0
120 yLoc = rng.rand(nSrc)*(ySize - 2*self.bufferSize) + self.bufferSize + y0
121 modelImages = []
123 imageSum = np.zeros((ySize, xSize))
124 for subfilter in range(self.dcrNumSubfilters):
125 flux = (rng.rand(nSrc)*(fluxRange - 1.) + 1.)*sourceSigma*noiseLevel
126 sigmas = [psfSize for src in range(nSrc)]
127 coordList = list(zip(xLoc, yLoc, flux, sigmas))
128 model = plantSources(self.bbox, 10, 0, coordList, addPoissonNoise=False)
129 model.image.array += rng.rand(ySize, xSize)*noiseLevel
130 imageSum += model.image.array
131 model.mask.addMaskPlane("CLIPPED")
132 modelImages.append(model.image)
133 maskVals = np.zeros_like(imageSum)
134 maskVals[imageSum > detectionSigma*noiseLevel] = afwImage.Mask.getPlaneBitMask('DETECTED')
135 model.mask.array[:] = maskVals
136 self.mask = model.mask
137 return modelImages
139 def prepareStats(self):
140 """Make a simple statistics object for testing.
142 Returns
143 -------
144 statsCtrl : `lsst.afw.math.StatisticsControl`
145 Statistics control object for coaddition.
146 """
147 statsCtrl = afwMath.StatisticsControl()
148 statsCtrl.setNumSigmaClip(5)
149 statsCtrl.setNumIter(3)
150 statsCtrl.setNanSafe(True)
151 statsCtrl.setWeighted(True)
152 statsCtrl.setCalcErrorFromInputVariance(False)
153 return statsCtrl
155 def makeDummyWcs(self, rotAngle, pixelScale, crval, flipX=True):
156 """Make a World Coordinate System object for testing.
158 Parameters
159 ----------
160 rotAngle : `lsst.geom.Angle`
161 rotation of the CD matrix, East from North
162 pixelScale : `lsst.geom.Angle`
163 Pixel scale of the projection.
164 crval : `lsst.afw.geom.SpherePoint`
165 Coordinates of the reference pixel of the wcs.
166 flipX : `bool`, optional
167 Flip the direction of increasing Right Ascension.
169 Returns
170 -------
171 `lsst.afw.geom.skyWcs.SkyWcs`
172 A wcs that matches the inputs.
173 """
174 crpix = geom.Box2D(self.bbox).getCenter()
175 cdMatrix = afwGeom.makeCdMatrix(scale=pixelScale, orientation=rotAngle, flipX=flipX)
176 wcs = afwGeom.makeSkyWcs(crpix=crpix, crval=crval, cdMatrix=cdMatrix)
177 return wcs
179 def makeDummyVisitInfo(self, azimuth, elevation, exposureId=12345, randomizeTime=False):
180 """Make a self-consistent visitInfo object for testing.
182 Parameters
183 ----------
184 azimuth : `lsst.geom.Angle`
185 Azimuth angle of the simulated observation.
186 elevation : `lsst.geom.Angle`
187 Elevation angle of the simulated observation.
188 exposureId : `int`, optional
189 Unique integer identifier for this observation.
190 randomizeTime : `bool`, optional
191 Add a random offset to the observation time.
193 Returns
194 -------
195 `lsst.afw.image.VisitInfo`
196 VisitInfo for the exposure.
197 """
198 lsstLat = -30.244639*u.degree
199 lsstLon = -70.749417*u.degree
200 lsstAlt = 2663.*u.m
201 lsstTemperature = 20.*u.Celsius
202 lsstHumidity = 40. # in percent
203 lsstPressure = 73892.*u.pascal
204 loc = EarthLocation(lat=lsstLat,
205 lon=lsstLon,
206 height=lsstAlt)
207 airmass = 1.0/np.sin(elevation.asDegrees())
209 time = Time(2000.0, format="jyear", scale="tt")
210 if randomizeTime:
211 # Pick a random date and time within a 20-year span
212 time += 20*u.year*self.rng.rand()
213 altaz = SkyCoord(alt=elevation.asDegrees(), az=azimuth.asDegrees(),
214 unit='deg', obstime=time, frame='altaz', location=loc)
215 obsInfo = makeObservationInfo(location=loc,
216 detector_exposure_id=exposureId,
217 datetime_begin=time,
218 datetime_end=time,
219 boresight_airmass=airmass,
220 boresight_rotation_angle=Angle(0.*u.degree),
221 boresight_rotation_coord='sky',
222 temperature=lsstTemperature,
223 pressure=lsstPressure,
224 relative_humidity=lsstHumidity,
225 tracking_radec=altaz.icrs,
226 altaz_begin=altaz,
227 observation_type='science',
228 )
229 visitInfo = MakeRawVisitInfoViaObsInfo.observationInfo2visitInfo(obsInfo)
230 return visitInfo
232 def testDummyVisitInfo(self):
233 """Verify the implementation of the visitInfo used for tests.
234 """
235 azimuth = 0*degrees
236 for testIter in range(self.nRandIter):
237 # Restrict to 45 < elevation < 85 degrees
238 elevation = (45. + self.rng.rand()*40.)*degrees
239 visitInfo = self.makeDummyVisitInfo(azimuth, elevation)
240 dec = visitInfo.getBoresightRaDec().getLatitude()
241 lat = visitInfo.getObservatory().getLatitude()
242 # An observation made with azimuth=0 should be pointed to the North
243 # So the RA should be North of the telescope's latitude
244 self.assertGreater(dec.asDegrees(), lat.asDegrees())
246 # The hour angle should be zero for azimuth=0
247 HA = visitInfo.getBoresightHourAngle()
248 refHA = 0.*degrees
249 self.assertAnglesAlmostEqual(HA, refHA, maxDiff=coordinateTolerance)
250 # If the observation is North of the telescope's latitude, the
251 # direction to zenith should be along the -y axis
252 # with a parallactic angle of 180 degrees
253 parAngle = visitInfo.getBoresightParAngle()
254 refParAngle = 180.*degrees
255 self.assertAnglesAlmostEqual(parAngle, refParAngle, maxDiff=coordinateTolerance)
257 def testDcrCalculation(self):
258 """Test that the shift in pixels due to DCR is consistently computed.
260 The shift is compared to pre-computed values.
261 """
262 dcrNumSubfilters = 3
263 afwImageUtils.defineFilter("gTest", self.lambdaEff,
264 lambdaMin=self.lambdaMin, lambdaMax=self.lambdaMax)
265 filterInfo = afwImage.Filter("gTest")
266 rotAngle = 0.*degrees
267 azimuth = 30.*degrees
268 elevation = 65.*degrees
269 pixelScale = 0.2*arcseconds
270 visitInfo = self.makeDummyVisitInfo(azimuth, elevation)
271 wcs = self.makeDummyWcs(rotAngle, pixelScale, crval=visitInfo.getBoresightRaDec())
272 dcrShift = calculateDcr(visitInfo, wcs, filterInfo, dcrNumSubfilters)
273 # Compare to precomputed values.
274 refShift = [(-0.5575567724366292, -0.2704095599533037),
275 (0.001961910992342903, 0.000951507567181944),
276 (0.40402552599550073, 0.19594841296051665)]
277 for shiftOld, shiftNew in zip(refShift, dcrShift):
278 self.assertFloatsAlmostEqual(shiftOld[1], shiftNew[1], rtol=1e-6, atol=1e-8)
279 self.assertFloatsAlmostEqual(shiftOld[0], shiftNew[0], rtol=1e-6, atol=1e-8)
281 def testCoordinateTransformDcrCalculation(self):
282 """Check the DCR calculation using astropy coordinate transformations.
284 Astmospheric refraction causes sources to appear closer to zenith than
285 they really are. An alternate calculation of the shift due to DCR is to
286 transform the pixel coordinates to altitude and azimuth, add the DCR
287 amplitude to the altitude, and transform back to pixel coordinates.
288 """
289 afwImageUtils.defineFilter("gTest", self.lambdaEff,
290 lambdaMin=self.lambdaMin, lambdaMax=self.lambdaMax)
291 filterInfo = afwImage.Filter("gTest")
292 pixelScale = 0.2*arcseconds
293 doFlip = [False, True]
295 for testIter in range(self.nRandIter):
296 rotAngle = 360.*self.rng.rand()*degrees
297 azimuth = 360.*self.rng.rand()*degrees
298 elevation = (45. + self.rng.rand()*40.)*degrees # Restrict to 45 < elevation < 85 degrees
299 visitInfo = self.makeDummyVisitInfo(azimuth, elevation)
300 for flip in doFlip:
301 # Repeat the calculation for both WCS orientations
302 wcs = self.makeDummyWcs(rotAngle, pixelScale, crval=visitInfo.getBoresightRaDec(), flipX=flip)
303 dcrShifts = calculateDcr(visitInfo, wcs, filterInfo, self.dcrNumSubfilters)
304 refShifts = calculateAstropyDcr(visitInfo, wcs, filterInfo, self.dcrNumSubfilters)
305 for refShift, dcrShift in zip(refShifts, dcrShifts):
306 # Use a fairly loose tolerance, since 1% of a pixel is good enough agreement.
307 self.assertFloatsAlmostEqual(refShift[1], dcrShift[1], rtol=1e-2, atol=1e-2)
308 self.assertFloatsAlmostEqual(refShift[0], dcrShift[0], rtol=1e-2, atol=1e-2)
310 def testDcrSubfilterOrder(self):
311 """Test that the bluest subfilter always has the largest DCR amplitude.
312 """
313 dcrNumSubfilters = 3
314 afwImageUtils.defineFilter("gTest", self.lambdaEff,
315 lambdaMin=self.lambdaMin, lambdaMax=self.lambdaMax)
316 filterInfo = afwImage.Filter("gTest")
317 pixelScale = 0.2*arcseconds
318 for testIter in range(self.nRandIter):
319 rotAngle = 360.*self.rng.rand()*degrees
320 azimuth = 360.*self.rng.rand()*degrees
321 elevation = (45. + self.rng.rand()*40.)*degrees # Restrict to 45 < elevation < 85 degrees
322 visitInfo = self.makeDummyVisitInfo(azimuth, elevation)
323 wcs = self.makeDummyWcs(rotAngle, pixelScale, crval=visitInfo.getBoresightRaDec())
324 dcrShift = calculateDcr(visitInfo, wcs, filterInfo, dcrNumSubfilters)
325 # First check that the blue subfilter amplitude is greater than the red subfilter
326 rotation = calculateImageParallacticAngle(visitInfo, wcs).asRadians()
327 ampShift = [dcr[1]*np.sin(rotation) + dcr[0]*np.cos(rotation) for dcr in dcrShift]
328 self.assertGreater(ampShift[0], 0.) # The blue subfilter should be shifted towards zenith
329 self.assertLess(ampShift[2], 0.) # The red subfilter should be shifted away from zenith
330 # The absolute amplitude of the blue subfilter should also
331 # be greater than that of the red subfilter
332 self.assertGreater(np.abs(ampShift[0]), np.abs(ampShift[2]))
334 def testApplyDcr(self):
335 """Test that the image transformation reduces to a simple shift.
336 """
337 dxVals = [-2, 1, 0, 1, 2]
338 dyVals = [-2, 1, 0, 1, 2]
339 x0 = 13
340 y0 = 27
341 inputImage = afwImage.MaskedImageF(self.bbox)
342 image = inputImage.image.array
343 image[y0, x0] = 1.
344 for dx in dxVals:
345 for dy in dyVals:
346 shift = (dy, dx)
347 shiftedImage = applyDcr(image, shift, useInverse=False)
348 # Create a blank reference image, and add the fake point source at the shifted location.
349 refImage = afwImage.MaskedImageF(self.bbox)
350 refImage.image.array[y0 + dy, x0 + dx] = 1.
351 self.assertFloatsAlmostEqual(shiftedImage, refImage.image.array, rtol=1e-12, atol=1e-12)
353 def testRotationAngle(self):
354 """Test that the sky rotation angle is consistently computed.
356 The rotation is compared to pre-computed values.
357 """
358 cdRotAngle = 0.*degrees
359 azimuth = 130.*degrees
360 elevation = 70.*degrees
361 pixelScale = 0.2*arcseconds
362 visitInfo = self.makeDummyVisitInfo(azimuth, elevation)
363 wcs = self.makeDummyWcs(cdRotAngle, pixelScale, crval=visitInfo.getBoresightRaDec())
364 rotAngle = calculateImageParallacticAngle(visitInfo, wcs)
365 refAngle = -1.0848032636337364*radians
366 self.assertAnglesAlmostEqual(refAngle, rotAngle)
368 def testRotationSouthZero(self):
369 """Test that an observation pointed due South has zero rotation angle.
371 An observation pointed South and on the meridian should have zenith
372 directly to the North, and a parallactic angle of zero.
373 """
374 refAngle = 0.*degrees
375 azimuth = 180.*degrees # Telescope is pointed South
376 pixelScale = 0.2*arcseconds
377 for testIter in range(self.nRandIter):
378 # Any additional arbitrary rotation should fall out of the calculation
379 cdRotAngle = 360*self.rng.rand()*degrees
380 elevation = (45. + self.rng.rand()*40.)*degrees # Restrict to 45 < elevation < 85 degrees
381 visitInfo = self.makeDummyVisitInfo(azimuth, elevation)
382 wcs = self.makeDummyWcs(cdRotAngle, pixelScale, crval=visitInfo.getBoresightRaDec(), flipX=True)
383 rotAngle = calculateImageParallacticAngle(visitInfo, wcs)
384 self.assertAnglesAlmostEqual(refAngle - cdRotAngle, rotAngle, maxDiff=coordinateTolerance)
386 def testRotationFlipped(self):
387 """Check the interpretation of rotations in the WCS.
388 """
389 doFlip = [False, True]
390 for testIter in range(self.nRandIter):
391 # Any additional arbitrary rotation should fall out of the calculation
392 cdRotAngle = 360*self.rng.rand()*degrees
393 # Make the telescope be pointed South, so that the parallactic angle is zero.
394 azimuth = 180.*degrees
395 elevation = (45. + self.rng.rand()*40.)*degrees # Restrict to 45 < elevation < 85 degrees
396 pixelScale = 0.2*arcseconds
397 visitInfo = self.makeDummyVisitInfo(azimuth, elevation)
398 for flip in doFlip:
399 wcs = self.makeDummyWcs(cdRotAngle, pixelScale,
400 crval=visitInfo.getBoresightRaDec(),
401 flipX=flip)
402 rotAngle = calculateImageParallacticAngle(visitInfo, wcs)
403 if flip:
404 rotAngle *= -1
405 self.assertAnglesAlmostEqual(cdRotAngle, rotAngle, maxDiff=coordinateTolerance)
407 def testConditionDcrModelNoChange(self):
408 """Conditioning should not change the model if it equals the reference.
409 """
410 modelImages = self.makeTestImages()
411 dcrModels = DcrModel(modelImages=modelImages, mask=self.mask)
412 newModels = [model.clone() for model in dcrModels]
413 dcrModels.conditionDcrModel(newModels, self.bbox, gain=1.)
414 for refModel, newModel in zip(dcrModels, newModels):
415 self.assertFloatsAlmostEqual(refModel.array, newModel.array)
417 def testConditionDcrModelNoChangeHighGain(self):
418 """Conditioning should not change the model if it equals the reference.
419 """
420 modelImages = self.makeTestImages()
421 dcrModels = DcrModel(modelImages=modelImages, mask=self.mask)
422 newModels = [model.clone() for model in dcrModels]
423 dcrModels.conditionDcrModel(newModels, self.bbox, gain=3.)
424 for refModel, newModel in zip(dcrModels, newModels):
425 self.assertFloatsAlmostEqual(refModel.array, newModel.array)
427 def testConditionDcrModelWithChange(self):
428 """Verify conditioning when the model changes by a known amount.
429 """
430 modelImages = self.makeTestImages()
431 dcrModels = DcrModel(modelImages=modelImages, mask=self.mask)
432 newModels = [model.clone() for model in dcrModels]
433 for model in newModels:
434 model.array[:] *= 3.
435 dcrModels.conditionDcrModel(newModels, self.bbox, gain=1.)
436 for refModel, newModel in zip(dcrModels, newModels):
437 refModel.array[:] *= 2.
438 self.assertFloatsAlmostEqual(refModel.array, newModel.array)
440 def testRegularizationSmallClamp(self):
441 """Test that large variations between model planes are reduced.
443 This also tests that noise-like pixels are not regularized.
444 """
445 clampFrequency = 2
446 regularizationWidth = 2
447 fluxRange = 10.
448 modelImages = self.makeTestImages(fluxRange=fluxRange)
449 dcrModels = DcrModel(modelImages=modelImages, mask=self.mask)
450 newModels = [model.clone() for model in dcrModels]
451 templateImage = dcrModels.getReferenceImage(self.bbox)
453 statsCtrl = self.prepareStats()
454 dcrModels.regularizeModelFreq(newModels, self.bbox, statsCtrl, clampFrequency, regularizationWidth)
455 for model, refModel in zip(newModels, dcrModels):
456 # Make sure the test parameters do reduce the outliers
457 self.assertGreater(np.max(refModel.array - templateImage),
458 np.max(model.array - templateImage))
459 highThreshold = templateImage*clampFrequency
460 highPix = model.array > highThreshold
461 highPix = ndimage.morphology.binary_opening(highPix, iterations=regularizationWidth)
462 self.assertFalse(np.all(highPix))
463 lowThreshold = templateImage/clampFrequency
464 lowPix = model.array < lowThreshold
465 lowPix = ndimage.morphology.binary_opening(lowPix, iterations=regularizationWidth)
466 self.assertFalse(np.all(lowPix))
468 def testRegularizationSidelobes(self):
469 """Test that artificial chromatic sidelobes are suppressed.
470 """
471 clampFrequency = 2.
472 regularizationWidth = 2
473 noiseLevel = 0.1
474 sourceAmplitude = 100.
475 modelImages = self.makeTestImages(seed=5, nSrc=5, psfSize=3., noiseLevel=noiseLevel,
476 detectionSigma=5., sourceSigma=sourceAmplitude, fluxRange=2.)
477 templateImage = np.mean([model.array for model in modelImages], axis=0)
478 sidelobeImages = self.makeTestImages(seed=5, nSrc=5, psfSize=1.5, noiseLevel=noiseLevel/10.,
479 detectionSigma=5., sourceSigma=sourceAmplitude*5., fluxRange=2.)
480 statsCtrl = self.prepareStats()
481 signList = [-1., 0., 1.]
482 sidelobeShift = (0., 4.)
483 for model, sidelobe, sign in zip(modelImages, sidelobeImages, signList):
484 sidelobe.array *= sign
485 model.array += applyDcr(sidelobe.array, sidelobeShift, useInverse=False)
486 model.array += applyDcr(sidelobe.array, sidelobeShift, useInverse=True)
488 dcrModels = DcrModel(modelImages=modelImages, mask=self.mask)
489 refModels = [dcrModels[subfilter].clone() for subfilter in range(self.dcrNumSubfilters)]
491 dcrModels.regularizeModelFreq(modelImages, self.bbox, statsCtrl, clampFrequency,
492 regularizationWidth=regularizationWidth)
493 for model, refModel, sign in zip(modelImages, refModels, signList):
494 # Make sure the test parameters do reduce the outliers
495 self.assertGreater(np.sum(np.abs(refModel.array - templateImage)),
496 np.sum(np.abs(model.array - templateImage)))
498 def testRegularizeModelIter(self):
499 """Test that large amplitude changes between iterations are restricted.
501 This also tests that noise-like pixels are not regularized.
502 """
503 modelClampFactor = 2.
504 regularizationWidth = 2
505 subfilter = 0
506 dcrModels = DcrModel(modelImages=self.makeTestImages())
507 oldModel = dcrModels[0]
508 xSize, ySize = self.bbox.getDimensions()
509 newModel = oldModel.clone()
510 newModel.array[:] += self.rng.rand(ySize, xSize)*np.max(oldModel.array)
511 newModelRef = newModel.clone()
513 dcrModels.regularizeModelIter(subfilter, newModel, self.bbox, modelClampFactor, regularizationWidth)
515 # Make sure the test parameters do reduce the outliers
516 self.assertGreater(np.max(newModelRef.array),
517 np.max(newModel.array - oldModel.array))
518 # Check that all of the outliers are clipped
519 highThreshold = oldModel.array*modelClampFactor
520 highPix = newModel.array > highThreshold
521 highPix = ndimage.morphology.binary_opening(highPix, iterations=regularizationWidth)
522 self.assertFalse(np.all(highPix))
523 lowThreshold = oldModel.array/modelClampFactor
524 lowPix = newModel.array < lowThreshold
525 lowPix = ndimage.morphology.binary_opening(lowPix, iterations=regularizationWidth)
526 self.assertFalse(np.all(lowPix))
528 def testIterateModel(self):
529 """Test that the DcrModel is iterable, and has the right values.
530 """
531 testModels = self.makeTestImages()
532 refVals = [np.sum(model.array) for model in testModels]
533 dcrModels = DcrModel(modelImages=testModels)
534 for refVal, model in zip(refVals, dcrModels):
535 self.assertFloatsEqual(refVal, np.sum(model.array))
536 # Negative indices are allowed, so check that those return models from the end.
537 self.assertFloatsEqual(refVals[-1], np.sum(dcrModels[-1].array))
540def calculateAstropyDcr(visitInfo, wcs, filterInfo, dcrNumSubfilters):
541 """Calculate the DCR shift using astropy coordinate transformations.
543 Parameters
544 ----------
545 visitInfo : `lsst.afw.image.VisitInfo`
546 VisitInfo for the exposure.
547 wcs : `lsst.afw.geom.skyWcs.SkyWcs`
548 A wcs that matches the inputs.
549 filterInfo : `lsst.afw.image.Filter`
550 The filter definition, set in the current instruments' obs package.
551 dcrNumSubfilters : `int`
552 Number of sub-filters used to model chromatic effects within a band.
554 Returns
555 -------
556 dcrShift : `tuple` of two `float`
557 The 2D shift due to DCR, in pixels.
558 Uses numpy axes ordering (Y, X).
559 """
560 elevation = visitInfo.getBoresightAzAlt().getLatitude()
561 azimuth = visitInfo.getBoresightAzAlt().getLongitude()
562 lambdaEff = filterInfo.getFilterProperty().getLambdaEff()
563 loc = EarthLocation(lat=visitInfo.getObservatory().getLatitude().asDegrees()*u.degree,
564 lon=visitInfo.getObservatory().getLongitude().asDegrees()*u.degree,
565 height=visitInfo.getObservatory().getElevation()*u.m)
566 date = visitInfo.getDate()
567 time = Time(date.get(date.MJD, date.TAI), format='mjd', location=loc, scale='tai')
568 altaz = SkyCoord(alt=elevation.asDegrees(), az=azimuth.asDegrees(),
569 unit='deg', obstime=time, frame='altaz', location=loc)
570 # The DCR calculations are performed at the boresight
571 ra0 = altaz.icrs.ra.degree*degrees
572 dec0 = altaz.icrs.dec.degree*degrees
573 x0, y0 = wcs.skyToPixel(geom.SpherePoint(ra0, dec0))
574 dcrShift = []
575 # We divide the filter into "subfilters" with the full wavelength range
576 # divided into equal sub-ranges.
577 for wl0, wl1 in wavelengthGenerator(filterInfo, dcrNumSubfilters):
578 # Note that diffRefractAmp can be negative,
579 # since it is relative to the midpoint of the full band
580 diffRefractAmp0 = differentialRefraction(wavelength=wl0, wavelengthRef=lambdaEff,
581 elevation=elevation,
582 observatory=visitInfo.getObservatory(),
583 weather=visitInfo.getWeather())
584 diffRefractAmp1 = differentialRefraction(wavelength=wl1, wavelengthRef=lambdaEff,
585 elevation=elevation,
586 observatory=visitInfo.getObservatory(),
587 weather=visitInfo.getWeather())
588 diffRefractAmp = (diffRefractAmp0 + diffRefractAmp1)/2.
590 elevation1 = elevation + diffRefractAmp
591 altaz = SkyCoord(alt=elevation1.asDegrees(), az=azimuth.asDegrees(),
592 unit='deg', obstime=time, frame='altaz', location=loc)
593 ra1 = altaz.icrs.ra.degree*degrees
594 dec1 = altaz.icrs.dec.degree*degrees
595 x1, y1 = wcs.skyToPixel(geom.SpherePoint(ra1, dec1))
596 dcrShift.append((y1-y0, x1-x0))
597 return dcrShift
600class MyMemoryTestCase(lsst.utils.tests.MemoryTestCase):
601 pass
604def setup_module(module):
605 lsst.utils.tests.init()
608if __name__ == "__main__": 608 ↛ 609line 608 didn't jump to line 609, because the condition on line 608 was never true
609 lsst.utils.tests.init()
610 unittest.main()