Hide keyboard shortcuts

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/>. 

21 

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 

28 

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 

42 

43 

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 

47 

48 

49class DcrModelTestTask(lsst.utils.tests.TestCase): 

50 """A test case for the DCR-aware image coaddition algorithm. 

51 

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 """ 

72 

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)) 

88 

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. 

92 

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. 

110 

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 = [] 

122 

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 

138 

139 def prepareStats(self): 

140 """Make a simple statistics object for testing. 

141 

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 

154 

155 def makeDummyWcs(self, rotAngle, pixelScale, crval, flipX=True): 

156 """Make a World Coordinate System object for testing. 

157 

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. 

168 

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 

178 

179 def makeDummyVisitInfo(self, azimuth, elevation, exposureId=12345, randomizeTime=False): 

180 """Make a self-consistent visitInfo object for testing. 

181 

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. 

192 

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()) 

208 

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 

231 

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()) 

245 

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) 

256 

257 def testDcrCalculation(self): 

258 """Test that the shift in pixels due to DCR is consistently computed. 

259 

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) 

280 

281 def testCoordinateTransformDcrCalculation(self): 

282 """Check the DCR calculation using astropy coordinate transformations. 

283 

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] 

294 

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) 

309 

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])) 

333 

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) 

352 

353 def testRotationAngle(self): 

354 """Test that the sky rotation angle is consistently computed. 

355 

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) 

367 

368 def testRotationSouthZero(self): 

369 """Test that an observation pointed due South has zero rotation angle. 

370 

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) 

385 

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) 

406 

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) 

416 

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) 

426 

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) 

439 

440 def testRegularizationSmallClamp(self): 

441 """Test that large variations between model planes are reduced. 

442 

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) 

452 

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)) 

467 

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) 

487 

488 dcrModels = DcrModel(modelImages=modelImages, mask=self.mask) 

489 refModels = [dcrModels[subfilter].clone() for subfilter in range(self.dcrNumSubfilters)] 

490 

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))) 

497 

498 def testRegularizeModelIter(self): 

499 """Test that large amplitude changes between iterations are restricted. 

500 

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() 

512 

513 dcrModels.regularizeModelIter(subfilter, newModel, self.bbox, modelClampFactor, regularizationWidth) 

514 

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)) 

527 

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)) 

538 

539 

540def calculateAstropyDcr(visitInfo, wcs, filterInfo, dcrNumSubfilters): 

541 """Calculate the DCR shift using astropy coordinate transformations. 

542 

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. 

553 

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. 

589 

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 

598 

599 

600class MyMemoryTestCase(lsst.utils.tests.MemoryTestCase): 

601 pass 

602 

603 

604def setup_module(module): 

605 lsst.utils.tests.init() 

606 

607 

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()