Coverage for tests/test_dcrModel.py: 13%

307 statements  

« prev     ^ index     » next       coverage.py v6.4.2, created at 2022-07-17 02:27 -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/>. 

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

41 

42 

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 

46 

47 

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

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

50 

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

71 

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

86 

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. 

90 

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. 

108 

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

120 

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 

136 

137 def prepareStats(self): 

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

139 

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 

152 

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

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

155 

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. 

166 

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 

176 

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

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

179 

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. 

190 

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

206 

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 

229 

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

243 

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) 

254 

255 def testDcrCalculation(self): 

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

257 

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) 

275 

276 def testCoordinateTransformDcrCalculation(self): 

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

278 

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] 

286 

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) 

305 

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

326 

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) 

345 

346 def testRotationAngle(self): 

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

348 

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) 

360 

361 def testRotationSouthZero(self): 

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

363 

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) 

378 

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) 

399 

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) 

410 

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) 

421 

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) 

435 

436 def testRegularizationSmallClamp(self): 

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

438 

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) 

449 

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

464 

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) 

484 

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

488 

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

495 

496 def testRegularizeModelIter(self): 

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

498 

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

511 

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

513 

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

526 

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

538 

539 

540def calculateAstropyDcr(visitInfo, wcs, effectiveWavelength, bandwidth, 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 dcrNumSubfilters : `int` 

550 Number of sub-filters used to model chromatic effects within a band. 

551 

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. 

586 

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 

595 

596 

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

598 pass 

599 

600 

601def setup_module(module): 

602 lsst.utils.tests.init() 

603 

604 

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