Coverage for tests/test_jointcal.py: 24%

Shortcuts 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

284 statements  

1# This file is part of jointcal. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (https://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

9# This program is free software: you can redistribute it and/or modify 

10# it under the terms of the GNU General Public License as published by 

11# the Free Software Foundation, either version 3 of the License, or 

12# (at your option) any later version. 

13# 

14# This program is distributed in the hope that it will be useful, 

15# but WITHOUT ANY WARRANTY; without even the implied warranty of 

16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License 

20# along with this program. If not, see <https://www.gnu.org/licenses/>. 

21 

22import itertools 

23import os.path 

24import unittest 

25from unittest import mock 

26 

27import numpy as np 

28import pyarrow.parquet 

29import astropy.time 

30 

31import lsst.log 

32import lsst.utils 

33 

34import lsst.afw.table 

35import lsst.daf.butler 

36from lsst.daf.base import DateTime 

37import lsst.geom 

38from lsst.meas.algorithms import getRefFluxField, LoadIndexedReferenceObjectsTask, DatasetConfig 

39import lsst.obs.base 

40import lsst.pipe.base 

41import lsst.jointcal 

42from lsst.jointcal.jointcal import make_schema_table, extract_detector_catalog_from_visit_catalog 

43from lsst.jointcal import MinimizeResult 

44import lsst.jointcal.chi2 

45import lsst.jointcal.testUtils 

46 

47 

48# for MemoryTestCase 

49def setup_module(module): 

50 lsst.utils.tests.init() 

51 

52 

53def make_fake_refcat(center, flux, filterName): 

54 """Make a fake reference catalog.""" 

55 schema = LoadIndexedReferenceObjectsTask.makeMinimalSchema([filterName], 

56 addProperMotion=True) 

57 catalog = lsst.afw.table.SimpleCatalog(schema) 

58 record = catalog.addNew() 

59 record.setCoord(center) 

60 record[filterName + '_flux'] = flux 

61 record[filterName + '_fluxErr'] = flux*0.1 

62 record['pm_ra'] = lsst.geom.Angle(1) 

63 record['pm_dec'] = lsst.geom.Angle(2) 

64 record['epoch'] = 65432.1 

65 return catalog 

66 

67 

68def make_fake_wcs(): 

69 """Return two simple SkyWcs objects, with slightly different sky positions. 

70 

71 Use the same pixel origins as the cfht_minimal data, but put the sky origin 

72 at RA=0 

73 """ 

74 crpix = lsst.geom.Point2D(931.517869, 2438.572109) 

75 cd = np.array([[5.19513851e-05, -2.81124812e-07], 

76 [-3.25186974e-07, -5.19112119e-05]]) 

77 crval1 = lsst.geom.SpherePoint(0.01, -0.01, lsst.geom.degrees) 

78 crval2 = lsst.geom.SpherePoint(-0.01, 0.01, lsst.geom.degrees) 

79 wcs1 = lsst.afw.geom.makeSkyWcs(crpix, crval1, cd) 

80 wcs2 = lsst.afw.geom.makeSkyWcs(crpix, crval2, cd) 

81 return wcs1, wcs2 

82 

83 

84class TestJointcalVisitCatalog(lsst.utils.tests.TestCase): 

85 """Tests of jointcal's sourceTable_visit parquet ->single detector afw 

86 table catalog unrolling. 

87 """ 

88 def setUp(self): 

89 filename = os.path.join(os.path.dirname(__file__), 

90 "data/subselected-sourceTable-0034690.parq") 

91 file = pyarrow.parquet.ParquetFile(filename) 

92 self.data = file.read(use_pandas_metadata=True).to_pandas() 

93 self.config = lsst.jointcal.jointcal.JointcalConfig() 

94 # TODO DM-29008: Remove this (to use the new gen3 default) before gen2 removal. 

95 self.config.sourceFluxType = "ApFlux_12_0" 

96 # we don't actually need either fitter to run for these tests 

97 self.config.doAstrometry = False 

98 self.config.doPhotometry = False 

99 self.jointcal = lsst.jointcal.JointcalTask(config=self.config) 

100 

101 def test_make_catalog_schema(self): 

102 """Check that the slot fields required by CcdImage::loadCatalog are in 

103 the schema returned by _make_catalog_schema(). 

104 """ 

105 table = make_schema_table() 

106 self.assertTrue(table.getCentroidSlot().getMeasKey().isValid()) 

107 self.assertTrue(table.getCentroidSlot().getErrKey().isValid()) 

108 self.assertTrue(table.getShapeSlot().getMeasKey().isValid()) 

109 

110 def test_extract_detector_catalog_from_visit_catalog(self): 

111 """Spot check a value output by the script that generated the test 

112 parquet catalog and check that the size of the returned catalog 

113 is correct for each detectior. 

114 """ 

115 detectorId = 56 

116 table = make_schema_table() 

117 catalog = extract_detector_catalog_from_visit_catalog(table, 

118 self.data, 

119 detectorId, 

120 'ccd', 

121 ['Ixx', 'Iyy', 'Ixy'], 

122 self.config.sourceFluxType, 

123 self.jointcal.log) 

124 

125 # The test catalog has a number of elements for each detector equal to the detector id. 

126 self.assertEqual(len(catalog), detectorId) 

127 self.assertIn(29798723617816629, catalog['id']) 

128 matched = catalog[29798723617816629 == catalog['id']] 

129 self.assertEqual(1715.734359473175, matched['slot_Centroid_x']) 

130 self.assertEqual(89.06076509964362, matched['slot_Centroid_y']) 

131 

132 

133class JointcalTestBase: 

134 def setUp(self): 

135 # Ensure that the filter list is reset for each test so that we avoid 

136 # confusion or contamination each time we create a cfht camera below. 

137 lsst.obs.base.FilterDefinitionCollection.reset() 

138 

139 struct = lsst.jointcal.testUtils.createTwoFakeCcdImages(100, 100) 

140 self.ccdImageList = struct.ccdImageList 

141 # so that countStars() returns nonzero results 

142 for ccdImage in self.ccdImageList: 

143 ccdImage.resetCatalogForFit() 

144 

145 self.goodChi2 = lsst.jointcal.chi2.Chi2Statistic() 

146 # chi2/ndof == 2.0 should be non-bad 

147 self.goodChi2.chi2 = 200.0 

148 self.goodChi2.ndof = 100 

149 

150 self.badChi2 = lsst.jointcal.chi2.Chi2Statistic() 

151 self.badChi2.chi2 = 600.0 

152 self.badChi2.ndof = 100 

153 

154 self.nanChi2 = lsst.jointcal.chi2.Chi2Statistic() 

155 self.nanChi2.chi2 = np.nan 

156 self.nanChi2.ndof = 100 

157 

158 self.maxSteps = 20 

159 self.name = "testing" 

160 self.dataName = "fake" 

161 self.whatToFit = "" # unneeded, since we're mocking the fitter 

162 

163 # Mock a Butler so the refObjLoaders have something to call `get()` on. 

164 self.butler = unittest.mock.Mock(spec=lsst.daf.butler.Butler) 

165 self.butler.get.return_value.indexer = DatasetConfig().indexer 

166 

167 # Mock the association manager and give it access to the ccd list above. 

168 self.associations = mock.Mock(spec=lsst.jointcal.Associations) 

169 self.associations.getCcdImageList.return_value = self.ccdImageList 

170 

171 # a default config to be modified by individual tests 

172 self.config = lsst.jointcal.jointcal.JointcalConfig() 

173 

174 

175class TestJointcalIterateFit(JointcalTestBase, lsst.utils.tests.TestCase): 

176 def setUp(self): 

177 super().setUp() 

178 # Mock the fitter and model, so we can force particular 

179 # return values/exceptions. Default to "good" return values. 

180 self.fitter = mock.Mock(spec=lsst.jointcal.PhotometryFit) 

181 self.fitter.computeChi2.return_value = self.goodChi2 

182 self.fitter.minimize.return_value = MinimizeResult.Converged 

183 self.model = mock.Mock(spec=lsst.jointcal.SimpleFluxModel) 

184 

185 self.jointcal = lsst.jointcal.JointcalTask(config=self.config, butler=self.butler) 

186 

187 def test_iterateFit_success(self): 

188 chi2 = self.jointcal._iterate_fit(self.associations, self.fitter, 

189 self.maxSteps, self.name, self.whatToFit) 

190 self.assertEqual(chi2, self.goodChi2) 

191 # Once for the for loop, the second time for the rank update. 

192 self.assertEqual(self.fitter.minimize.call_count, 2) 

193 

194 def test_iterateFit_writeChi2Outer(self): 

195 chi2 = self.jointcal._iterate_fit(self.associations, self.fitter, 

196 self.maxSteps, self.name, self.whatToFit, 

197 dataName=self.dataName) 

198 self.assertEqual(chi2, self.goodChi2) 

199 # Once for the for loop, the second time for the rank update. 

200 self.assertEqual(self.fitter.minimize.call_count, 2) 

201 # Default config should not call saveChi2Contributions 

202 self.fitter.saveChi2Contributions.assert_not_called() 

203 

204 def test_iterateFit_failed(self): 

205 self.fitter.minimize.return_value = MinimizeResult.Failed 

206 

207 with self.assertRaises(RuntimeError): 

208 self.jointcal._iterate_fit(self.associations, self.fitter, 

209 self.maxSteps, self.name, self.whatToFit) 

210 self.assertEqual(self.fitter.minimize.call_count, 1) 

211 

212 def test_iterateFit_badFinalChi2(self): 

213 log = mock.Mock(spec=lsst.log.Log) 

214 self.jointcal.log = log 

215 self.fitter.computeChi2.return_value = self.badChi2 

216 

217 chi2 = self.jointcal._iterate_fit(self.associations, self.fitter, 

218 self.maxSteps, self.name, self.whatToFit) 

219 self.assertEqual(chi2, self.badChi2) 

220 log.info.assert_called_with("%s %s", "Fit completed", self.badChi2) 

221 log.error.assert_called_with("Potentially bad fit: High chi-squared/ndof.") 

222 

223 def test_iterateFit_exceedMaxSteps(self): 

224 log = mock.Mock(spec=lsst.log.Log) 

225 self.jointcal.log = log 

226 self.fitter.minimize.return_value = MinimizeResult.Chi2Increased 

227 maxSteps = 3 

228 

229 chi2 = self.jointcal._iterate_fit(self.associations, self.fitter, 

230 maxSteps, self.name, self.whatToFit) 

231 self.assertEqual(chi2, self.goodChi2) 

232 self.assertEqual(self.fitter.minimize.call_count, maxSteps) 

233 log.error.assert_called_with("testing failed to converge after %s steps" % maxSteps) 

234 

235 def test_moderate_chi2_increase(self): 

236 """DM-25159: warn, but don't fail, on moderate chi2 increases between 

237 steps. 

238 """ 

239 chi2_1 = lsst.jointcal.chi2.Chi2Statistic() 

240 chi2_1.chi2 = 100.0 

241 chi2_1.ndof = 100 

242 chi2_2 = lsst.jointcal.chi2.Chi2Statistic() 

243 chi2_2.chi2 = 300.0 

244 chi2_2.ndof = 100 

245 

246 chi2s = [self.goodChi2, chi2_1, chi2_2, self.goodChi2, self.goodChi2] 

247 self.fitter.computeChi2.side_effect = chi2s 

248 self.fitter.minimize.side_effect = [MinimizeResult.Chi2Increased, 

249 MinimizeResult.Chi2Increased, 

250 MinimizeResult.Chi2Increased, 

251 MinimizeResult.Converged, 

252 MinimizeResult.Converged] 

253 with lsst.log.UsePythonLogging(): # so that assertLogs works with lsst.log 

254 with self.assertLogs("lsst.jointcal", level="WARNING") as logger: 

255 self.jointcal._iterate_fit(self.associations, self.fitter, 

256 self.maxSteps, self.name, self.whatToFit) 

257 msg = "Significant chi2 increase by a factor of 300 / 100 = 3" 

258 self.assertIn(msg, [rec.message for rec in logger.records]) 

259 

260 def test_large_chi2_increase_fails(self): 

261 """DM-25159: fail on large chi2 increases between steps.""" 

262 chi2_1 = lsst.jointcal.chi2.Chi2Statistic() 

263 chi2_1.chi2 = 1e11 

264 chi2_1.ndof = 100 

265 chi2_2 = lsst.jointcal.chi2.Chi2Statistic() 

266 chi2_2.chi2 = 1.123456e13 # to check floating point formatting 

267 chi2_2.ndof = 100 

268 

269 chi2s = [chi2_1, chi2_1, chi2_2] 

270 self.fitter.computeChi2.side_effect = chi2s 

271 self.fitter.minimize.return_value = MinimizeResult.Chi2Increased 

272 with lsst.log.UsePythonLogging(): # so that assertLogs works with lsst.log 

273 with self.assertLogs("lsst.jointcal", level="WARNING") as logger: 

274 with(self.assertRaisesRegex(RuntimeError, "Large chi2 increase")): 

275 self.jointcal._iterate_fit(self.associations, self.fitter, 

276 self.maxSteps, self.name, self.whatToFit) 

277 msg = "Significant chi2 increase by a factor of 1.123e+13 / 1e+11 = 112.3" 

278 self.assertIn(msg, [rec.message for rec in logger.records]) 

279 

280 def test_invalid_model(self): 

281 self.model.validate.return_value = False 

282 with(self.assertRaises(ValueError)): 

283 self.jointcal._logChi2AndValidate(self.associations, self.fitter, self.model, "invalid") 

284 

285 def test_nonfinite_chi2(self): 

286 self.fitter.computeChi2.return_value = self.nanChi2 

287 with(self.assertRaises(FloatingPointError)): 

288 self.jointcal._logChi2AndValidate(self.associations, self.fitter, self.model, "nonfinite") 

289 

290 def test_writeChi2(self): 

291 filename = "somefile" 

292 self.jointcal._logChi2AndValidate(self.associations, self.fitter, self.model, "writeCh2", 

293 writeChi2Name=filename) 

294 # logChi2AndValidate prepends `config.debugOutputPath` to the filename 

295 self.fitter.saveChi2Contributions.assert_called_with("./"+filename+"{type}") 

296 

297 

298class TestJointcalLoadRefCat(JointcalTestBase, lsst.utils.tests.TestCase): 

299 

300 def _make_fake_refcat(self): 

301 """Mock a fake reference catalog and the bits necessary to use it.""" 

302 center = lsst.geom.SpherePoint(30, -30, lsst.geom.degrees) 

303 flux = 10 

304 radius = 1 * lsst.geom.degrees 

305 filter = lsst.afw.image.FilterLabel(band='fake', physical="fake-filter") 

306 

307 fakeRefCat = make_fake_refcat(center, flux, filter.bandLabel) 

308 fluxField = getRefFluxField(fakeRefCat.schema, filter.bandLabel) 

309 returnStruct = lsst.pipe.base.Struct(refCat=fakeRefCat, fluxField=fluxField) 

310 refObjLoader = mock.Mock(spec=LoadIndexedReferenceObjectsTask) 

311 refObjLoader.loadSkyCircle.return_value = returnStruct 

312 

313 return refObjLoader, center, radius, filter, fakeRefCat 

314 

315 def test_load_reference_catalog(self): 

316 refObjLoader, center, radius, filterLabel, fakeRefCat = self._make_fake_refcat() 

317 

318 config = lsst.jointcal.jointcal.JointcalConfig() 

319 config.astrometryReferenceErr = 0.1 # our test refcats don't have coord errors 

320 jointcal = lsst.jointcal.JointcalTask(config=config, butler=self.butler) 

321 

322 # NOTE: we cannot test application of proper motion here, because we 

323 # mock the refObjLoader, so the real loader is never called. 

324 refCat, fluxField = jointcal._load_reference_catalog(refObjLoader, 

325 jointcal.astrometryReferenceSelector, 

326 center, 

327 radius, 

328 filterLabel) 

329 # operator== isn't implemented for Catalogs, so we have to check like 

330 # this, in case the records are copied during load. 

331 self.assertEqual(len(refCat), len(fakeRefCat)) 

332 for r1, r2 in zip(refCat, fakeRefCat): 

333 self.assertEqual(r1, r2) 

334 

335 def test_load_reference_catalog_subselect(self): 

336 """Test that we can select out the one source in the fake refcat 

337 with a ridiculous S/N cut. 

338 """ 

339 refObjLoader, center, radius, filterLabel, fakeRefCat = self._make_fake_refcat() 

340 

341 config = lsst.jointcal.jointcal.JointcalConfig() 

342 config.astrometryReferenceErr = 0.1 # our test refcats don't have coord errors 

343 config.astrometryReferenceSelector.doSignalToNoise = True 

344 config.astrometryReferenceSelector.signalToNoise.minimum = 1e10 

345 config.astrometryReferenceSelector.signalToNoise.fluxField = "fake_flux" 

346 config.astrometryReferenceSelector.signalToNoise.errField = "fake_fluxErr" 

347 jointcal = lsst.jointcal.JointcalTask(config=config, butler=self.butler) 

348 

349 refCat, fluxField = jointcal._load_reference_catalog(refObjLoader, 

350 jointcal.astrometryReferenceSelector, 

351 center, 

352 radius, 

353 filterLabel) 

354 self.assertEqual(len(refCat), 0) 

355 

356 

357class TestJointcalFitModel(JointcalTestBase, lsst.utils.tests.TestCase): 

358 def test_fit_photometry_writeChi2(self): 

359 """Test that we are calling saveChi2 with appropriate file prefixes.""" 

360 self.config.photometryModel = "constrainedFlux" 

361 self.config.writeChi2FilesOuterLoop = True 

362 jointcal = lsst.jointcal.JointcalTask(config=self.config, butler=self.butler) 

363 jointcal.focalPlaneBBox = lsst.geom.Box2D() 

364 

365 # Mock the fitter, so we can pretend it found a good fit 

366 with mock.patch("lsst.jointcal.PhotometryFit", autospect=True) as fitPatch: 

367 fitPatch.return_value.computeChi2.return_value = self.goodChi2 

368 fitPatch.return_value.minimize.return_value = MinimizeResult.Converged 

369 

370 # config.debugOutputPath is prepended to the filenames that go into saveChi2Contributions 

371 expected = ["./photometry_init-ModelVisit_chi2", "./photometry_init-Model_chi2", 

372 "./photometry_init-Fluxes_chi2", "./photometry_init-ModelFluxes_chi2"] 

373 expected = [mock.call(x+"-fake{type}") for x in expected] 

374 jointcal._fit_photometry(self.associations, dataName=self.dataName) 

375 fitPatch.return_value.saveChi2Contributions.assert_has_calls(expected) 

376 

377 def test_fit_astrometry_writeChi2(self): 

378 """Test that we are calling saveChi2 with appropriate file prefixes.""" 

379 self.config.astrometryModel = "constrained" 

380 self.config.writeChi2FilesOuterLoop = True 

381 jointcal = lsst.jointcal.JointcalTask(config=self.config, butler=self.butler) 

382 jointcal.focalPlaneBBox = lsst.geom.Box2D() 

383 

384 # Mock the fitter, so we can pretend it found a good fit 

385 fitPatch = mock.patch("lsst.jointcal.AstrometryFit") 

386 # Mock the projection handler so we don't segfault due to not-fully initialized ccdImages 

387 projectorPatch = mock.patch("lsst.jointcal.OneTPPerVisitHandler") 

388 with fitPatch as fit, projectorPatch as projector: 

389 fit.return_value.computeChi2.return_value = self.goodChi2 

390 fit.return_value.minimize.return_value = MinimizeResult.Converged 

391 # return a real ProjectionHandler to keep ConstrainedAstrometryModel() happy 

392 projector.return_value = lsst.jointcal.IdentityProjectionHandler() 

393 

394 # config.debugOutputPath is prepended to the filenames that go into saveChi2Contributions 

395 expected = ["./astrometry_init-DistortionsVisit_chi2", "./astrometry_init-Distortions_chi2", 

396 "./astrometry_init-Positions_chi2", "./astrometry_init-DistortionsPositions_chi2"] 

397 expected = [mock.call(x+"-fake{type}") for x in expected] 

398 jointcal._fit_astrometry(self.associations, dataName=self.dataName) 

399 fit.return_value.saveChi2Contributions.assert_has_calls(expected) 

400 

401 

402class TestComputeBoundingCircle(lsst.utils.tests.TestCase): 

403 """Tests of Associations.computeBoundingCircle()""" 

404 def _checkPointsInCircle(self, points, center, radius): 

405 """Check that all points are within the (center, radius) circle. 

406 

407 The test is whether the max(points - center) separation is equal to 

408 (or slightly less than) radius. 

409 """ 

410 maxSeparation = 0*lsst.geom.degrees 

411 for point in points: 

412 maxSeparation = max(maxSeparation, center.separation(point)) 

413 self.assertAnglesAlmostEqual(maxSeparation, radius, maxDiff=3*lsst.geom.arcseconds) 

414 self.assertLess(maxSeparation, radius) 

415 

416 def _testPoints(self, ccdImage1, ccdImage2, skyWcs1, skyWcs2, bbox): 

417 """Fill an Associations object and test that it computes the correct 

418 bounding circle for the input data. 

419 

420 Parameters 

421 ---------- 

422 ccdImage1, ccdImage2 : `lsst.jointcal.CcdImage` 

423 The CcdImages to add to the Associations object. 

424 skyWcs1, skyWcs2 : `lsst.afw.geom.SkyWcs` 

425 The WCS of each of the above images. 

426 bbox : `lsst.geom.Box2D` 

427 The ccd bounding box of both images. 

428 """ 

429 lsst.log.setLevel('jointcal', lsst.log.DEBUG) 

430 associations = lsst.jointcal.Associations() 

431 associations.addCcdImage(ccdImage1) 

432 associations.addCcdImage(ccdImage2) 

433 associations.computeCommonTangentPoint() 

434 

435 circle = associations.computeBoundingCircle() 

436 center = lsst.geom.SpherePoint(circle.getCenter()) 

437 radius = lsst.geom.Angle(circle.getOpeningAngle().asRadians(), lsst.geom.radians) 

438 points = [lsst.geom.SpherePoint(skyWcs1.pixelToSky(lsst.geom.Point2D(x))) 

439 for x in bbox.getCorners()] 

440 points.extend([lsst.geom.SpherePoint(skyWcs2.pixelToSky(lsst.geom.Point2D(x))) 

441 for x in bbox.getCorners()]) 

442 self._checkPointsInCircle(points, center, radius) 

443 

444 def testPoints(self): 

445 """Test for points in an "easy" area, far from RA=0 or the poles.""" 

446 struct = lsst.jointcal.testUtils.createTwoFakeCcdImages() 

447 self._testPoints(struct.ccdImageList[0], struct.ccdImageList[1], 

448 struct.skyWcs[0], struct.skyWcs[1], struct.bbox) 

449 

450 def testPointsRA0(self): 

451 """Test for CcdImages crossing RA=0; this demonstrates a fix for 

452 the bug described in DM-19802. 

453 """ 

454 wcs1, wcs2 = make_fake_wcs() 

455 

456 # Put the visit boresights at the WCS origin, for consistency 

457 visitInfo1 = lsst.afw.image.VisitInfo(exposureId=30577512, 

458 date=DateTime(date=65321.1), 

459 boresightRaDec=wcs1.getSkyOrigin()) 

460 visitInfo2 = lsst.afw.image.VisitInfo(exposureId=30621144, 

461 date=DateTime(date=65322.1), 

462 boresightRaDec=wcs1.getSkyOrigin()) 

463 

464 struct = lsst.jointcal.testUtils.createTwoFakeCcdImages(fakeWcses=[wcs1, wcs2], 

465 fakeVisitInfos=[visitInfo1, visitInfo2]) 

466 self._testPoints(struct.ccdImageList[0], struct.ccdImageList[1], 

467 struct.skyWcs[0], struct.skyWcs[1], struct.bbox) 

468 

469 

470class TestJointcalComputePMDate(JointcalTestBase, lsst.utils.tests.TestCase): 

471 """Tests of jointcal._compute_proper_motion_epoch(), using fake dates.""" 

472 def test_compute_proper_motion_epoch(self): 

473 mjds = np.array((65432.1, 66666.0, 55555.0, 44322.2)) 

474 

475 wcs1, wcs2 = make_fake_wcs() 

476 visitInfo1 = lsst.afw.image.VisitInfo(exposureId=30577512, 

477 date=DateTime(date=mjds[0]), 

478 boresightRaDec=wcs1.getSkyOrigin()) 

479 visitInfo2 = lsst.afw.image.VisitInfo(exposureId=30621144, 

480 date=DateTime(date=mjds[1]), 

481 boresightRaDec=wcs2.getSkyOrigin()) 

482 visitInfo3 = lsst.afw.image.VisitInfo(exposureId=30577513, 

483 date=DateTime(date=mjds[2]), 

484 boresightRaDec=wcs1.getSkyOrigin()) 

485 visitInfo4 = lsst.afw.image.VisitInfo(exposureId=30621145, 

486 date=DateTime(date=mjds[3]), 

487 boresightRaDec=wcs2.getSkyOrigin()) 

488 

489 struct1 = lsst.jointcal.testUtils.createTwoFakeCcdImages(fakeWcses=[wcs1, wcs2], 

490 fakeVisitInfos=[visitInfo1, visitInfo2]) 

491 struct2 = lsst.jointcal.testUtils.createTwoFakeCcdImages(fakeWcses=[wcs1, wcs2], 

492 fakeVisitInfos=[visitInfo3, visitInfo4]) 

493 ccdImageList = list(itertools.chain(struct1.ccdImageList, struct2.ccdImageList)) 

494 associations = lsst.jointcal.Associations() 

495 for ccdImage in ccdImageList: 

496 associations.addCcdImage(ccdImage) 

497 associations.computeCommonTangentPoint() 

498 

499 jointcal = lsst.jointcal.JointcalTask(config=self.config, butler=self.butler) 

500 result = jointcal._compute_proper_motion_epoch(ccdImageList) 

501 self.assertEqual(result.jyear, (astropy.time.Time(mjds, format="mjd", scale="tai").jyear).mean()) 

502 

503 

504class MemoryTester(lsst.utils.tests.MemoryTestCase): 

505 pass 

506 

507 

508if __name__ == "__main__": 508 ↛ 509line 508 didn't jump to line 509, because the condition on line 508 was never true

509 lsst.utils.tests.init() 

510 unittest.main()