Coverage for tests/test_jointcal.py: 18%

283 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-02 00:36 -0700

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 # NOTE: This parquet file is older and uses the earlier 

95 # "capitalize first letter" naming convention for these fields. 

96 self.config.sourceFluxType = "ApFlux_12_0" 

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

98 self.config.doAstrometry = False 

99 self.config.doPhotometry = False 

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

101 

102 def test_make_catalog_schema(self): 

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

104 the schema returned by _make_catalog_schema(). 

105 """ 

106 table = make_schema_table() 

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

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

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

110 

111 def test_extract_detector_catalog_from_visit_catalog(self): 

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

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

114 is correct for each detectior. 

115 """ 

116 detectorId = 56 

117 table = make_schema_table() 

118 catalog = extract_detector_catalog_from_visit_catalog(table, 

119 self.data, 

120 detectorId, 

121 'ccd', 

122 ['Ixx', 'Iyy', 'Ixy'], 

123 self.config.sourceFluxType, 

124 self.jointcal.log) 

125 

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

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

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

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

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

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

132 

133 

134class JointcalTestBase: 

135 def setUp(self): 

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

137 self.ccdImageList = struct.ccdImageList 

138 # so that countStars() returns nonzero results 

139 for ccdImage in self.ccdImageList: 

140 ccdImage.resetCatalogForFit() 

141 

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

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

144 self.goodChi2.chi2 = 200.0 

145 self.goodChi2.ndof = 100 

146 

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

148 self.badChi2.chi2 = 600.0 

149 self.badChi2.ndof = 100 

150 

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

152 self.nanChi2.chi2 = np.nan 

153 self.nanChi2.ndof = 100 

154 

155 self.maxSteps = 20 

156 self.name = "testing" 

157 self.dataName = "fake" 

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

159 

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

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

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

163 

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

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

166 self.associations.getCcdImageList.return_value = self.ccdImageList 

167 

168 # a default config to be modified by individual tests 

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

170 

171 

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

173 def setUp(self): 

174 super().setUp() 

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

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

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

178 self.fitter.computeChi2.return_value = self.goodChi2 

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

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

181 

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

183 

184 def test_iterateFit_success(self): 

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

186 self.maxSteps, self.name, self.whatToFit) 

187 self.assertEqual(chi2, self.goodChi2) 

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

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

190 

191 def test_iterateFit_writeChi2Outer(self): 

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

193 self.maxSteps, self.name, self.whatToFit, 

194 dataName=self.dataName) 

195 self.assertEqual(chi2, self.goodChi2) 

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

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

198 # Default config should not call saveChi2Contributions 

199 self.fitter.saveChi2Contributions.assert_not_called() 

200 

201 def test_iterateFit_failed(self): 

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

203 

204 with self.assertRaises(RuntimeError): 

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

206 self.maxSteps, self.name, self.whatToFit) 

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

208 

209 def test_iterateFit_badFinalChi2(self): 

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

211 self.jointcal.log = log 

212 self.fitter.computeChi2.return_value = self.badChi2 

213 

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

215 self.maxSteps, self.name, self.whatToFit) 

216 self.assertEqual(chi2, self.badChi2) 

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

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

219 

220 def test_iterateFit_exceedMaxSteps(self): 

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

222 self.jointcal.log = log 

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

224 maxSteps = 3 

225 

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

227 maxSteps, self.name, self.whatToFit) 

228 self.assertEqual(chi2, self.goodChi2) 

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

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

231 

232 def test_moderate_chi2_increase(self): 

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

234 steps. 

235 """ 

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

237 chi2_1.chi2 = 100.0 

238 chi2_1.ndof = 100 

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

240 chi2_2.chi2 = 300.0 

241 chi2_2.ndof = 100 

242 

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

244 self.fitter.computeChi2.side_effect = chi2s 

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

246 MinimizeResult.Chi2Increased, 

247 MinimizeResult.Chi2Increased, 

248 MinimizeResult.Converged, 

249 MinimizeResult.Converged] 

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

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

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

253 self.maxSteps, self.name, self.whatToFit) 

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

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

256 

257 def test_large_chi2_increase_fails(self): 

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

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

260 chi2_1.chi2 = 1e11 

261 chi2_1.ndof = 100 

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

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

264 chi2_2.ndof = 100 

265 

266 chi2s = [chi2_1, chi2_1, chi2_2] 

267 self.fitter.computeChi2.side_effect = chi2s 

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

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

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

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

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

273 self.maxSteps, self.name, self.whatToFit) 

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

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

276 

277 def test_invalid_model(self): 

278 self.model.validate.return_value = False 

279 with(self.assertRaises(ValueError)): 

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

281 

282 def test_nonfinite_chi2(self): 

283 self.fitter.computeChi2.return_value = self.nanChi2 

284 with(self.assertRaises(FloatingPointError)): 

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

286 

287 def test_writeChi2(self): 

288 filename = "somefile" 

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

290 writeChi2Name=filename) 

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

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

293 

294 

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

296 

297 def _make_fake_refcat(self): 

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

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

300 flux = 10 

301 radius = 1 * lsst.geom.degrees 

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

303 

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

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

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

307 refObjLoader = mock.Mock(spec=LoadIndexedReferenceObjectsTask) 

308 refObjLoader.loadSkyCircle.return_value = returnStruct 

309 

310 return refObjLoader, center, radius, filter, fakeRefCat 

311 

312 def test_load_reference_catalog(self): 

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

314 

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

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

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

318 

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

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

321 refCat, fluxField = jointcal._load_reference_catalog(refObjLoader, 

322 jointcal.astrometryReferenceSelector, 

323 center, 

324 radius, 

325 filterLabel) 

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

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

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

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

330 self.assertEqual(r1, r2) 

331 

332 def test_load_reference_catalog_subselect(self): 

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

334 with a ridiculous S/N cut. 

335 """ 

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

337 

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

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

340 config.astrometryReferenceSelector.doSignalToNoise = True 

341 config.astrometryReferenceSelector.signalToNoise.minimum = 1e10 

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

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

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

345 

346 refCat, fluxField = jointcal._load_reference_catalog(refObjLoader, 

347 jointcal.astrometryReferenceSelector, 

348 center, 

349 radius, 

350 filterLabel) 

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

352 

353 

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

355 def test_fit_photometry_writeChi2(self): 

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

357 self.config.photometryModel = "constrainedFlux" 

358 self.config.writeChi2FilesOuterLoop = True 

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

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

361 

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

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

364 fitPatch.return_value.computeChi2.return_value = self.goodChi2 

365 fitPatch.return_value.minimize.return_value = MinimizeResult.Converged 

366 

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

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

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

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

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

372 fitPatch.return_value.saveChi2Contributions.assert_has_calls(expected) 

373 

374 def test_fit_astrometry_writeChi2(self): 

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

376 self.config.astrometryModel = "constrained" 

377 self.config.writeChi2FilesOuterLoop = True 

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

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

380 

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

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

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

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

385 with fitPatch as fit, projectorPatch as projector: 

386 fit.return_value.computeChi2.return_value = self.goodChi2 

387 fit.return_value.minimize.return_value = MinimizeResult.Converged 

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

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

390 

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

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

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

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

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

396 fit.return_value.saveChi2Contributions.assert_has_calls(expected) 

397 

398 

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

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

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

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

403 

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

405 (or slightly less than) radius. 

406 """ 

407 maxSeparation = 0*lsst.geom.degrees 

408 for point in points: 

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

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

411 self.assertLess(maxSeparation, radius) 

412 

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

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

415 bounding circle for the input data. 

416 

417 Parameters 

418 ---------- 

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

420 The CcdImages to add to the Associations object. 

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

422 The WCS of each of the above images. 

423 bbox : `lsst.geom.Box2D` 

424 The ccd bounding box of both images. 

425 """ 

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

427 associations = lsst.jointcal.Associations() 

428 associations.addCcdImage(ccdImage1) 

429 associations.addCcdImage(ccdImage2) 

430 associations.computeCommonTangentPoint() 

431 

432 circle = associations.computeBoundingCircle() 

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

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

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

436 for x in bbox.getCorners()] 

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

438 for x in bbox.getCorners()]) 

439 self._checkPointsInCircle(points, center, radius) 

440 

441 def testPoints(self): 

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

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

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

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

446 

447 def testPointsRA0(self): 

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

449 the bug described in DM-19802. 

450 """ 

451 wcs1, wcs2 = make_fake_wcs() 

452 

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

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

455 date=DateTime(date=65321.1), 

456 boresightRaDec=wcs1.getSkyOrigin()) 

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

458 date=DateTime(date=65322.1), 

459 boresightRaDec=wcs1.getSkyOrigin()) 

460 

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

462 fakeVisitInfos=[visitInfo1, visitInfo2]) 

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

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

465 

466 

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

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

469 def test_compute_proper_motion_epoch(self): 

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

471 

472 wcs1, wcs2 = make_fake_wcs() 

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

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

475 boresightRaDec=wcs1.getSkyOrigin()) 

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

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

478 boresightRaDec=wcs2.getSkyOrigin()) 

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

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

481 boresightRaDec=wcs1.getSkyOrigin()) 

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

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

484 boresightRaDec=wcs2.getSkyOrigin()) 

485 

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

487 fakeVisitInfos=[visitInfo1, visitInfo2]) 

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

489 fakeVisitInfos=[visitInfo3, visitInfo4]) 

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

491 associations = lsst.jointcal.Associations() 

492 for ccdImage in ccdImageList: 

493 associations.addCcdImage(ccdImage) 

494 associations.computeCommonTangentPoint() 

495 

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

497 result = jointcal._compute_proper_motion_epoch(ccdImageList) 

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

499 

500 

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

502 pass 

503 

504 

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

506 lsst.utils.tests.init() 

507 unittest.main()