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