Coverage for tests/test_gbdesAstrometricFit.py: 9%

602 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-11-20 02:33 -0800

1# This file is part of drp_tasks 

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 os.path 

23import unittest 

24from copy import copy 

25 

26import astropy.table 

27import astropy.time 

28import astropy.units as u 

29import numpy as np 

30import pandas as pd 

31import wcsfit 

32import yaml 

33from astropy.coordinates import Distance, SkyCoord 

34from smatch.matcher import Matcher 

35 

36import lsst.afw.geom as afwgeom 

37import lsst.afw.table as afwTable 

38import lsst.geom 

39import lsst.utils 

40from lsst import sphgeom 

41from lsst.daf.base import PropertyList 

42from lsst.drp.tasks.gbdesAstrometricFit import ( 

43 GbdesAstrometricFitConfig, 

44 GbdesAstrometricFitTask, 

45 GbdesGlobalAstrometricFitConfig, 

46 GbdesGlobalAstrometricFitTask, 

47 calculate_apparent_motion, 

48) 

49from lsst.meas.algorithms import ReferenceObjectLoader 

50from lsst.meas.algorithms.testUtils import MockRefcatDataId 

51from lsst.pipe.base import InMemoryDatasetHandle 

52 

53TESTDIR = os.path.abspath(os.path.dirname(__file__)) 

54 

55 

56class TestGbdesAstrometricFit(lsst.utils.tests.TestCase): 

57 """This class tests `GbdesAstrometricFit` using real `visitSummaryTable`s 

58 from HSC RC2 processing, with simulated sources for those visits. 

59 """ 

60 

61 @classmethod 

62 def setUpClass(cls): 

63 # Set random seed 

64 np.random.seed(1234) 

65 

66 # Make fake data 

67 cls.datadir = os.path.join(TESTDIR, "data") 

68 

69 cls.fieldNumber = 0 

70 cls.nFields = 1 

71 cls.instrumentName = "HSC" 

72 cls.refEpoch = 57205.5 

73 

74 # Make test inputVisitSummary. VisitSummaryTables are taken from 

75 # collection HSC/runs/RC2/w_2022_20/DM-34794 

76 cls.testVisits = [1176, 17900, 17930, 17934] 

77 cls.inputVisitSummary = [] 

78 for testVisit in cls.testVisits: 

79 visSum = afwTable.ExposureCatalog.readFits( 

80 os.path.join(cls.datadir, f"visitSummary_{testVisit}.fits") 

81 ) 

82 cls.inputVisitSummary.append(visSum) 

83 

84 cls.config = GbdesAstrometricFitConfig() 

85 cls.config.systematicError = 0 

86 cls.config.devicePolyOrder = 4 

87 cls.config.exposurePolyOrder = 6 

88 cls.config.fitReserveFraction = 0 

89 cls.config.fitReserveRandomSeed = 1234 

90 cls.config.saveModelParams = True 

91 cls.config.allowSelfMatches = True 

92 cls.config.saveCameraModel = True 

93 cls.task = GbdesAstrometricFitTask(config=cls.config) 

94 

95 ( 

96 cls.exposureInfo, 

97 cls.exposuresHelper, 

98 cls.extensionInfo, 

99 cls.instruments, 

100 ) = cls.task._get_exposure_info(cls.inputVisitSummary, refEpoch=cls.refEpoch) 

101 

102 cls.fields, cls.fieldCenter, cls.fieldRadius = cls.task._prep_sky( 

103 cls.inputVisitSummary, cls.exposureInfo.medianEpoch 

104 ) 

105 

106 # Bounding box of observations: 

107 raMins, raMaxs = [], [] 

108 decMins, decMaxs = [], [] 

109 for visSum in cls.inputVisitSummary: 

110 raMins.append(visSum["raCorners"].min()) 

111 raMaxs.append(visSum["raCorners"].max()) 

112 decMins.append(visSum["decCorners"].min()) 

113 decMaxs.append(visSum["decCorners"].max()) 

114 raMin = min(raMins) 

115 raMax = max(raMaxs) 

116 decMin = min(decMins) 

117 decMax = max(decMaxs) 

118 

119 corners = [ 

120 lsst.geom.SpherePoint(raMin, decMin, lsst.geom.degrees).getVector(), 

121 lsst.geom.SpherePoint(raMax, decMin, lsst.geom.degrees).getVector(), 

122 lsst.geom.SpherePoint(raMax, decMax, lsst.geom.degrees).getVector(), 

123 lsst.geom.SpherePoint(raMin, decMax, lsst.geom.degrees).getVector(), 

124 ] 

125 cls.boundingPolygon = sphgeom.ConvexPolygon(corners) 

126 

127 # Make random set of data in a bounding box determined by input visits 

128 # Make wcs objects for the "true" model 

129 cls.nStars, starIds, starRAs, starDecs = cls._make_simulated_stars( 

130 raMin, raMax, decMin, decMax, cls.config.matchRadius 

131 ) 

132 

133 # Fraction of simulated stars in the reference catalog and science 

134 # exposures 

135 inReferenceFraction = 1 

136 inScienceFraction = 1 

137 

138 # Make a reference catalog and load it into ReferenceObjectLoader 

139 refDataId, deferredRefCat = cls._make_refCat( 

140 starIds, starRAs, starDecs, inReferenceFraction, cls.boundingPolygon 

141 ) 

142 cls.refObjectLoader = ReferenceObjectLoader([refDataId], [deferredRefCat]) 

143 cls.refObjectLoader.config.requireProperMotion = False 

144 cls.refObjectLoader.config.anyFilterMapsToThis = "test_filter" 

145 

146 cls.task.refObjectLoader = cls.refObjectLoader 

147 

148 # Get True WCS for stars: 

149 with open(os.path.join(cls.datadir, "sample_wcs.yaml"), "r") as f: 

150 cls.trueModel = yaml.load(f, Loader=yaml.Loader) 

151 

152 trueWCSs = cls._make_wcs(cls.trueModel, cls.inputVisitSummary) 

153 

154 # Make source catalogs: 

155 cls.inputCatalogRefs = cls._make_sourceCat(starIds, starRAs, starDecs, trueWCSs, inScienceFraction) 

156 

157 cls.colorCatalog = cls._make_colors(starRAs, starDecs) 

158 

159 cls.outputs = cls.task.run( 

160 cls.inputCatalogRefs, 

161 cls.inputVisitSummary, 

162 instrumentName=cls.instrumentName, 

163 refEpoch=cls.refEpoch, 

164 refObjectLoader=cls.refObjectLoader, 

165 ) 

166 

167 @staticmethod 

168 def _make_simulated_stars(raMin, raMax, decMin, decMax, matchRadius, nStars=10000): 

169 """Generate random positions for "stars" in an RA/Dec box. 

170 

171 Parameters 

172 ---------- 

173 raMin : `float` 

174 Minimum RA for simulated stars. 

175 raMax : `float` 

176 Maximum RA for simulated stars. 

177 decMin : `float` 

178 Minimum Dec for simulated stars. 

179 decMax : `float` 

180 Maximum Dec for simulated stars. 

181 matchRadius : `float` 

182 Minimum allowed distance in arcsec between stars. 

183 nStars : `int`, optional 

184 Number of stars to simulate. Final number will be lower if any 

185 too-close stars are dropped. 

186 

187 Returns 

188 ------- 

189 nStars : `int` 

190 Number of stars simulated. 

191 starIds: `np.ndarray` 

192 Unique identification number for stars. 

193 starRAs: `np.ndarray` 

194 Simulated Right Ascensions. 

195 starDecs: `np.ndarray` 

196 Simulated Declination. 

197 """ 

198 starIds = np.arange(nStars) 

199 starRAs = np.random.random(nStars) * (raMax - raMin) + raMin 

200 starDecs = np.random.random(nStars) * (decMax - decMin) + decMin 

201 # Remove neighbors: 

202 with Matcher(starRAs, starDecs) as matcher: 

203 idx = matcher.query_groups(matchRadius / 3600.0, min_match=2) 

204 if len(idx) > 0: 

205 neighbors = np.unique(np.concatenate(idx)) 

206 starRAs = np.delete(starRAs, neighbors) 

207 starDecs = np.delete(starDecs, neighbors) 

208 nStars = len(starRAs) 

209 starIds = np.arange(nStars) 

210 

211 return nStars, starIds, starRAs, starDecs 

212 

213 @classmethod 

214 def _make_refCat(cls, starIds, starRas, starDecs, inReferenceFraction, bounds): 

215 """Make reference catalog from a subset of the simulated data 

216 

217 Parameters 

218 ---------- 

219 starIds : `np.ndarray` [`int`] 

220 Source ids for the simulated stars 

221 starRas : `np.ndarray` [`float`] 

222 RAs of the simulated stars 

223 starDecs : `np.ndarray` [`float`] 

224 Decs of the simulated stars 

225 inReferenceFraction : float 

226 Percentage of simulated stars to include in reference catalog 

227 bounds : `lsst.sphgeom.ConvexPolygon` 

228 Boundary of the reference catalog region 

229 

230 Returns 

231 ------- 

232 refDataId : `lsst.meas.algorithms.testUtils.MockRefcatDataId` 

233 Object that replicates the functionality of a dataId. 

234 deferredRefCat : `lsst.pipe.base.InMemoryDatasetHandle` 

235 Dataset handle for reference catalog. 

236 """ 

237 nRefs = int(cls.nStars * inReferenceFraction) 

238 refStarIndices = np.random.choice(cls.nStars, nRefs, replace=False) 

239 # Make simpleCatalog to hold data, create datasetRef with `region` 

240 # determined by bounding box used in above simulate. 

241 refSchema = afwTable.SimpleTable.makeMinimalSchema() 

242 idKey = refSchema.addField("sourceId", type="I") 

243 fluxKey = refSchema.addField("test_filter_flux", units="nJy", type=np.float64) 

244 raErrKey = refSchema.addField("coord_raErr", units="rad", type=np.float64) 

245 decErrKey = refSchema.addField("coord_decErr", units="rad", type=np.float64) 

246 pmraErrKey = refSchema.addField("pm_raErr", units="rad2 / yr", type=np.float64) 

247 pmdecErrKey = refSchema.addField("pm_decErr", units="rad2 / yr", type=np.float64) 

248 refCat = afwTable.SimpleCatalog(refSchema) 

249 ref_md = PropertyList() 

250 ref_md.set("REFCAT_FORMAT_VERSION", 1) 

251 refCat.table.setMetadata(ref_md) 

252 for i in refStarIndices: 

253 record = refCat.addNew() 

254 record.set(idKey, starIds[i]) 

255 record.setRa(lsst.geom.Angle(starRas[i], lsst.geom.degrees)) 

256 record.setDec(lsst.geom.Angle(starDecs[i], lsst.geom.degrees)) 

257 record.set(fluxKey, 1) 

258 record.set(raErrKey, 0.00001) 

259 record.set(decErrKey, 0.00001) 

260 record.set(pmraErrKey, 1e-9) 

261 record.set(pmdecErrKey, 1e-9) 

262 refDataId = MockRefcatDataId(bounds) 

263 deferredRefCat = InMemoryDatasetHandle(refCat, storageClass="SourceCatalog", htm7="mockRefCat") 

264 

265 return refDataId, deferredRefCat 

266 

267 @classmethod 

268 def _make_sourceCat(cls, starIds, starRas, starDecs, trueWCSs, inScienceFraction): 

269 """Make a `pd.DataFrame` catalog with the columns needed for the 

270 object selector. 

271 

272 Parameters 

273 ---------- 

274 starIds : `np.ndarray` [`int`] 

275 Source ids for the simulated stars 

276 starRas : `np.ndarray` [`float`] 

277 RAs of the simulated stars 

278 starDecs : `np.ndarray` [`float`] 

279 Decs of the simulated stars 

280 trueWCSs : `list` [`lsst.afw.geom.SkyWcs`] 

281 WCS with which to simulate the source pixel coordinates 

282 inReferenceFraction : float 

283 Percentage of simulated stars to include in reference catalog 

284 

285 Returns 

286 ------- 

287 sourceCat : `list` [`lsst.pipe.base.InMemoryDatasetHandle`] 

288 List of reference to source catalogs. 

289 """ 

290 inputCatalogRefs = [] 

291 # Take a subset of the simulated data 

292 # Use true wcs objects to put simulated data into ccds 

293 bbox = lsst.geom.BoxD( 

294 lsst.geom.Point2D( 

295 cls.inputVisitSummary[0][0]["bbox_min_x"], cls.inputVisitSummary[0][0]["bbox_min_y"] 

296 ), 

297 lsst.geom.Point2D( 

298 cls.inputVisitSummary[0][0]["bbox_max_x"], cls.inputVisitSummary[0][0]["bbox_max_y"] 

299 ), 

300 ) 

301 bboxCorners = bbox.getCorners() 

302 cls.inputCatalogRefs = [] 

303 for v, visit in enumerate(cls.testVisits): 

304 nVisStars = int(cls.nStars * inScienceFraction) 

305 visitStarIndices = np.random.choice(cls.nStars, nVisStars, replace=False) 

306 visitStarIds = starIds[visitStarIndices] 

307 visitStarRas = starRas[visitStarIndices] 

308 visitStarDecs = starDecs[visitStarIndices] 

309 sourceCats = [] 

310 for detector in trueWCSs[visit]: 

311 detWcs = detector.getWcs() 

312 detectorId = detector["id"] 

313 radecCorners = detWcs.pixelToSky(bboxCorners) 

314 detectorFootprint = sphgeom.ConvexPolygon([rd.getVector() for rd in radecCorners]) 

315 detectorIndices = detectorFootprint.contains( 

316 (visitStarRas * u.degree).to(u.radian), (visitStarDecs * u.degree).to(u.radian) 

317 ) 

318 nDetectorStars = detectorIndices.sum() 

319 detectorArray = np.ones(nDetectorStars, dtype=bool) * detector["id"] 

320 

321 ones_like = np.ones(nDetectorStars) 

322 zeros_like = np.zeros(nDetectorStars, dtype=bool) 

323 

324 x, y = detWcs.skyToPixelArray( 

325 visitStarRas[detectorIndices], visitStarDecs[detectorIndices], degrees=True 

326 ) 

327 

328 origWcs = (cls.inputVisitSummary[v][cls.inputVisitSummary[v]["id"] == detectorId])[0].getWcs() 

329 inputRa, inputDec = origWcs.pixelToSkyArray(x, y, degrees=True) 

330 

331 sourceDict = {} 

332 sourceDict["detector"] = detectorArray 

333 sourceDict["sourceId"] = visitStarIds[detectorIndices] 

334 sourceDict["x"] = x 

335 sourceDict["y"] = y 

336 sourceDict["xErr"] = 1e-3 * ones_like 

337 sourceDict["yErr"] = 1e-3 * ones_like 

338 sourceDict["inputRA"] = inputRa 

339 sourceDict["inputDec"] = inputDec 

340 sourceDict["trueRA"] = visitStarRas[detectorIndices] 

341 sourceDict["trueDec"] = visitStarDecs[detectorIndices] 

342 for key in ["apFlux_12_0_flux", "apFlux_12_0_instFlux", "ixx", "iyy"]: 

343 sourceDict[key] = ones_like 

344 for key in [ 

345 "pixelFlags_edge", 

346 "pixelFlags_saturated", 

347 "pixelFlags_interpolatedCenter", 

348 "pixelFlags_interpolated", 

349 "pixelFlags_crCenter", 

350 "pixelFlags_bad", 

351 "hsmPsfMoments_flag", 

352 "apFlux_12_0_flag", 

353 "extendedness", 

354 "sizeExtendedness", 

355 "parentSourceId", 

356 "deblend_nChild", 

357 "ixy", 

358 ]: 

359 sourceDict[key] = zeros_like 

360 sourceDict["apFlux_12_0_instFluxErr"] = 1e-2 * ones_like 

361 sourceDict["detect_isPrimary"] = ones_like.astype(bool) 

362 

363 sourceCat = pd.DataFrame(sourceDict) 

364 sourceCats.append(sourceCat) 

365 

366 visitSourceTable = pd.concat(sourceCats) 

367 

368 inputCatalogRef = InMemoryDatasetHandle( 

369 visitSourceTable, storageClass="DataFrame", dataId={"visit": visit} 

370 ) 

371 

372 inputCatalogRefs.append(inputCatalogRef) 

373 

374 return inputCatalogRefs 

375 

376 @classmethod 

377 def _make_wcs(cls, model, inputVisitSummaries): 

378 """Make a `lsst.afw.geom.SkyWcs` from given model parameters 

379 

380 Parameters 

381 ---------- 

382 model : `dict` 

383 Dictionary with WCS model parameters 

384 inputVisitSummaries : `list` [`lsst.afw.table.ExposureCatalog`] 

385 Visit summary catalogs 

386 Returns 

387 ------- 

388 catalogs : `dict` [`int`, `lsst.afw.table.ExposureCatalog`] 

389 Visit summary catalogs with WCS set to input model 

390 """ 

391 

392 # Pixels will need to be rescaled before going into the mappings 

393 xscale = inputVisitSummaries[0][0]["bbox_max_x"] - inputVisitSummaries[0][0]["bbox_min_x"] 

394 yscale = inputVisitSummaries[0][0]["bbox_max_y"] - inputVisitSummaries[0][0]["bbox_min_y"] 

395 

396 catalogs = {} 

397 schema = lsst.afw.table.ExposureTable.makeMinimalSchema() 

398 schema.addField("visit", type="L", doc="Visit number") 

399 for visitSum in inputVisitSummaries: 

400 visit = visitSum[0]["visit"] 

401 visitMapName = f"{visit}/poly" 

402 visitModel = model[visitMapName] 

403 

404 catalog = lsst.afw.table.ExposureCatalog(schema) 

405 catalog.resize(len(visitSum)) 

406 catalog["visit"] = visit 

407 

408 raDec = visitSum[0].getVisitInfo().getBoresightRaDec() 

409 

410 visitMapType = visitModel["Type"] 

411 visitDict = {"Type": visitMapType} 

412 if visitMapType == "Poly": 

413 mapCoefficients = visitModel["XPoly"]["Coefficients"] + visitModel["YPoly"]["Coefficients"] 

414 visitDict["Coefficients"] = mapCoefficients 

415 

416 for d, detector in enumerate(visitSum): 

417 detectorId = detector["id"] 

418 filterName = detector["physical_filter"] 

419 detectorMapName = f"{filterName}/{detectorId}/poly" 

420 detectorModel = model[detectorMapName] 

421 

422 detectorMapType = detectorModel["Type"] 

423 mapDict = {detectorMapName: {"Type": detectorMapType}, visitMapName: visitDict} 

424 if detectorMapType == "Poly": 

425 mapCoefficients = ( 

426 detectorModel["XPoly"]["Coefficients"] + detectorModel["YPoly"]["Coefficients"] 

427 ) 

428 mapDict[detectorMapName]["Coefficients"] = mapCoefficients 

429 

430 outWCS = cls.task._make_afw_wcs( 

431 mapDict, 

432 raDec.getRa(), 

433 raDec.getDec(), 

434 doNormalizePixels=True, 

435 xScale=xscale, 

436 yScale=yscale, 

437 ) 

438 catalog[d].setId(detectorId) 

439 catalog[d].setWcs(outWCS) 

440 

441 catalog.sort() 

442 catalogs[visit] = catalog 

443 

444 return catalogs 

445 

446 @classmethod 

447 def _make_colors(cls, starRas, starDecs): 

448 """Make a catalog with the star magnitudes. 

449 

450 Parameters 

451 ---------- 

452 starRas : `np.ndarray` [`float`] 

453 RAs of the simulated stars 

454 starDecs : `np.ndarray` [`float`] 

455 Decs of the simulated stars 

456 

457 Returns 

458 ------- 

459 colorCatalog : `lsst.afw.table.SimpleCatalog` 

460 Catalog with star magnitudes. 

461 """ 

462 bands = ["g", "r", "i", "z", "y"] 

463 nStars = len(starRas) 

464 

465 # Make a catalog following what is done in `fgcmCal`. 

466 schema = afwTable.SimpleTable.makeMinimalSchema() 

467 schema.addField( 

468 "mag_std_noabs", 

469 type="ArrayF", 

470 doc="Standard magnitude (no absolute calibration)", 

471 size=len(bands), 

472 ) 

473 colorCatalog = afwTable.SimpleCatalog(schema) 

474 colorCatalog.resize(len(starRas)) 

475 colorCatalog["coord_ra"] = (starRas * u.degree + 10 * np.random.randn(nStars) * u.mas).to(u.radian) 

476 colorCatalog["coord_dec"] = (starDecs * u.degree + 10 * np.random.randn(nStars) * u.mas).to(u.radian) 

477 

478 magMin = 19 

479 magMax = 23 

480 for i in range(len(bands)): 

481 colorCatalog["mag_std_noabs"][:, i] = np.random.random(nStars) * (magMax - magMin) + magMin 

482 

483 md = PropertyList() 

484 md.set("BANDS", bands) 

485 colorCatalog.setMetadata(md) 

486 return colorCatalog 

487 

488 def test_get_exposure_info(self): 

489 """Test that information for input exposures is as expected and that 

490 the WCS in the class object gives approximately the same results as the 

491 input `lsst.afw.geom.SkyWcs`. 

492 """ 

493 

494 # The total number of extensions is the number of detectors for each 

495 # visit plus one for the reference catalog for each field. 

496 totalExtensions = sum([len(visSum) for visSum in self.inputVisitSummary]) + self.nFields 

497 

498 self.assertEqual(totalExtensions, len(self.extensionInfo.visit)) 

499 

500 taskVisits = set(self.extensionInfo.visit) 

501 refVisits = np.arange(0, -1 * self.nFields, -1).tolist() 

502 self.assertEqual(taskVisits, set(self.testVisits + refVisits)) 

503 

504 xx = np.linspace(0, 2000, 3) 

505 yy = np.linspace(0, 4000, 6) 

506 xgrid, ygrid = np.meshgrid(xx, yy) 

507 for visSum in self.inputVisitSummary: 

508 visit = visSum[0]["visit"] 

509 for detectorInfo in visSum: 

510 detector = detectorInfo["id"] 

511 extensionIndex = np.flatnonzero( 

512 (self.extensionInfo.visit == visit) & (self.extensionInfo.detector == detector) 

513 )[0] 

514 fitWcs = self.extensionInfo.wcs[extensionIndex] 

515 calexpWcs = detectorInfo.getWcs() 

516 

517 tanPlaneXY = np.array([fitWcs.toWorld(x, y) for (x, y) in zip(xgrid.ravel(), ygrid.ravel())]) 

518 

519 calexpra, calexpdec = calexpWcs.pixelToSkyArray(xgrid.ravel(), ygrid.ravel(), degrees=True) 

520 

521 # The pixel origin maps to a position slightly off from the 

522 # tangent plane origin, so we want to use this value for the 

523 # tangent-plane-to-sky part of the mapping. 

524 tangentPlaneCenter = fitWcs.toWorld( 

525 calexpWcs.getPixelOrigin().getX(), calexpWcs.getPixelOrigin().getY() 

526 ) 

527 tangentPlaneOrigin = lsst.geom.Point2D(tangentPlaneCenter) 

528 skyOrigin = calexpWcs.pixelToSky( 

529 calexpWcs.getPixelOrigin().getX(), calexpWcs.getPixelOrigin().getY() 

530 ) 

531 cdMatrix = afwgeom.makeCdMatrix(1.0 * lsst.geom.degrees, 0.0 * lsst.geom.degrees, True) 

532 iwcToSkyWcs = afwgeom.makeSkyWcs(tangentPlaneOrigin, skyOrigin, cdMatrix) 

533 newRAdeg, newDecdeg = iwcToSkyWcs.pixelToSkyArray( 

534 tanPlaneXY[:, 0], tanPlaneXY[:, 1], degrees=True 

535 ) 

536 

537 np.testing.assert_allclose(calexpra, newRAdeg) 

538 np.testing.assert_allclose(calexpdec, newDecdeg) 

539 

540 def test_refCatLoader(self): 

541 """Test that we can load objects from refCat""" 

542 

543 tmpAssociations = wcsfit.FoFClass( 

544 self.fields, 

545 self.instruments, 

546 self.exposuresHelper, 

547 [self.fieldRadius.asDegrees()], 

548 (self.task.config.matchRadius * u.arcsec).to(u.degree).value, 

549 ) 

550 

551 self.task._load_refcat( 

552 self.refObjectLoader, 

553 self.extensionInfo, 

554 associations=tmpAssociations, 

555 center=self.fieldCenter, 

556 radius=self.fieldRadius, 

557 epoch=self.exposureInfo.medianEpoch, 

558 ) 

559 

560 # We have only loaded one catalog, so getting the 'matches' should just 

561 # return the same objects we put in, except some random objects that 

562 # are too close together. 

563 tmpAssociations.sortMatches(self.fieldNumber, minMatches=1) 

564 

565 nMatches = (np.array(tmpAssociations.sequence) == 0).sum() 

566 

567 self.assertLessEqual(nMatches, self.nStars) 

568 self.assertGreater(nMatches, self.nStars * 0.9) 

569 

570 def test_loading_and_association(self): 

571 """Test that objects can be loaded and correctly associated.""" 

572 # Running `_load_catalogs_and_associate` changes the input WCSs, so 

573 # recalculate them here so that the variables shared among tests are 

574 # not affected. 

575 _, exposuresHelper, extensionInfo, instruments = self.task._get_exposure_info( 

576 self.inputVisitSummary, refEpoch=self.refEpoch 

577 ) 

578 

579 tmpAssociations = wcsfit.FoFClass( 

580 self.fields, 

581 self.instruments, 

582 exposuresHelper, 

583 [self.fieldRadius.asDegrees()], 

584 (self.task.config.matchRadius * u.arcsec).to(u.degree).value, 

585 ) 

586 self.task._load_catalogs_and_associate(tmpAssociations, self.inputCatalogRefs, extensionInfo) 

587 

588 tmpAssociations.sortMatches(self.fieldNumber, minMatches=2) 

589 

590 matchIds = [] 

591 correctMatches = [] 

592 for s, e, o in zip(tmpAssociations.sequence, tmpAssociations.extn, tmpAssociations.obj): 

593 objVisitInd = extensionInfo.visitIndex[e] 

594 objDet = extensionInfo.detector[e] 

595 extnInds = self.inputCatalogRefs[objVisitInd].get()["detector"] == objDet 

596 objInfo = self.inputCatalogRefs[objVisitInd].get()[extnInds].iloc[o] 

597 if s == 0: 

598 if len(matchIds) > 0: 

599 correctMatches.append(len(set(matchIds)) == 1) 

600 matchIds = [] 

601 

602 matchIds.append(objInfo["sourceId"]) 

603 

604 # A few matches may incorrectly associate sources because of the random 

605 # positions 

606 self.assertGreater(sum(correctMatches), len(correctMatches) * 0.95) 

607 

608 def test_make_outputs(self): 

609 """Test that the run method recovers the input model parameters.""" 

610 for v, visit in enumerate(self.testVisits): 

611 visitSummary = self.inputVisitSummary[v] 

612 outputWcsCatalog = self.outputs.outputWcss[visit] 

613 visitSources = self.inputCatalogRefs[v].get() 

614 for d, detectorRow in enumerate(visitSummary): 

615 detectorId = detectorRow["id"] 

616 fitwcs = outputWcsCatalog[d].getWcs() 

617 detSources = visitSources[visitSources["detector"] == detectorId] 

618 fitRA, fitDec = fitwcs.pixelToSkyArray(detSources["x"], detSources["y"], degrees=True) 

619 dRA = fitRA - detSources["trueRA"] 

620 dDec = fitDec - detSources["trueDec"] 

621 # Check that input coordinates match the output coordinates 

622 self.assertAlmostEqual(np.mean(dRA), 0) 

623 self.assertAlmostEqual(np.std(dRA), 0) 

624 self.assertAlmostEqual(np.mean(dDec), 0) 

625 self.assertAlmostEqual(np.std(dDec), 0) 

626 

627 def test_compute_model_params(self): 

628 """Test the optional model parameters and covariance output.""" 

629 modelParams = pd.DataFrame(self.outputs.modelParams) 

630 # Check that DataFrame is the expected size. 

631 shape = modelParams.shape 

632 self.assertEqual(shape[0] + 4, shape[1]) 

633 # Check that covariance matrix is symmetric. 

634 covariance = (modelParams.iloc[:, 4:]).to_numpy() 

635 np.testing.assert_allclose(covariance, covariance.T, atol=1e-18) 

636 

637 def test_run(self): 

638 """Test that run method recovers the input model parameters""" 

639 outputMaps = self.outputs.fitModel.mapCollection.getParamDict() 

640 

641 for v, visit in enumerate(self.testVisits): 

642 visitSummary = self.inputVisitSummary[v] 

643 visitMapName = f"{visit}/poly" 

644 

645 origModel = self.trueModel[visitMapName] 

646 if origModel["Type"] != "Identity": 

647 fitModel = outputMaps[visitMapName] 

648 origXPoly = origModel["XPoly"]["Coefficients"] 

649 origYPoly = origModel["YPoly"]["Coefficients"] 

650 fitXPoly = fitModel[: len(origXPoly)] 

651 fitYPoly = fitModel[len(origXPoly) :] 

652 

653 absDiffX = abs(fitXPoly - origXPoly) 

654 absDiffY = abs(fitYPoly - origYPoly) 

655 # Check that input visit model matches fit 

656 np.testing.assert_array_less(absDiffX, 1e-6) 

657 np.testing.assert_array_less(absDiffY, 1e-6) 

658 for d, detectorRow in enumerate(visitSummary): 

659 detectorId = detectorRow["id"] 

660 filterName = detectorRow["physical_filter"] 

661 detectorMapName = f"{filterName}/{detectorId}/poly" 

662 origModel = self.trueModel[detectorMapName] 

663 if (origModel["Type"] != "Identity") and (v == 0): 

664 fitModel = outputMaps[detectorMapName] 

665 origXPoly = origModel["XPoly"]["Coefficients"] 

666 origYPoly = origModel["YPoly"]["Coefficients"] 

667 fitXPoly = fitModel[: len(origXPoly)] 

668 fitYPoly = fitModel[len(origXPoly) :] 

669 absDiffX = abs(fitXPoly - origXPoly) 

670 absDiffY = abs(fitYPoly - origYPoly) 

671 # Check that input detector model matches fit 

672 np.testing.assert_array_less(absDiffX, 1e-7) 

673 np.testing.assert_array_less(absDiffY, 1e-7) 

674 

675 def test_missingWcs(self): 

676 """Test that task does not fail when the input WCS is None for one 

677 extension and that the fit WCS for that extension returns a finite 

678 result. 

679 """ 

680 # Need to copy the catalogs since they are modified later. 

681 inputVisitSummary = [cat.copy(deep=True) for cat in self.inputVisitSummary] 

682 # Set one WCS to be None 

683 testVisit = 0 

684 testDetector = 20 

685 inputVisitSummary[testVisit][testDetector].setWcs(None) 

686 

687 outputs = self.task.run( 

688 self.inputCatalogRefs, 

689 inputVisitSummary, 

690 instrumentName=self.instrumentName, 

691 refEpoch=self.refEpoch, 

692 refObjectLoader=self.refObjectLoader, 

693 ) 

694 

695 # Check that the fit WCS for the extension with input WCS=None returns 

696 # finite sky values. 

697 testWcs = outputs.outputWcss[self.testVisits[testVisit]][testDetector].getWcs() 

698 testSky = testWcs.pixelToSky(0, 0) 

699 self.assertTrue(testSky.isFinite()) 

700 

701 def test_inputCameraModel(self): 

702 """Test running task with an input camera model, and check that true 

703 object coordinates are recovered. 

704 """ 

705 config = copy(self.config) 

706 config.saveCameraModel = False 

707 config.useInputCameraModel = True 

708 task = GbdesAstrometricFitTask(config=config) 

709 with open(os.path.join(self.datadir, "sample_camera_model.yaml"), "r") as f: 

710 cameraModel = yaml.load(f, Loader=yaml.Loader) 

711 

712 outputs = task.run( 

713 self.inputCatalogRefs, 

714 self.inputVisitSummary, 

715 instrumentName=self.instrumentName, 

716 refObjectLoader=self.refObjectLoader, 

717 inputCameraModel=cameraModel, 

718 ) 

719 

720 # Check that the output WCS is close (not necessarily exactly equal) to 

721 # the result when fitting the full model. 

722 for v, visit in enumerate(self.testVisits): 

723 visitSummary = self.inputVisitSummary[v] 

724 outputWcsCatalog = outputs.outputWcss[visit] 

725 visitSources = self.inputCatalogRefs[v].get() 

726 for d, detectorRow in enumerate(visitSummary): 

727 detectorId = detectorRow["id"] 

728 fitwcs = outputWcsCatalog[d].getWcs() 

729 detSources = visitSources[visitSources["detector"] == detectorId] 

730 fitRA, fitDec = fitwcs.pixelToSkyArray(detSources["x"], detSources["y"], degrees=True) 

731 dRA = fitRA - detSources["trueRA"] 

732 dDec = fitDec - detSources["trueDec"] 

733 # Check that input coordinates match the output coordinates 

734 self.assertAlmostEqual(np.mean(dRA), 0) 

735 self.assertAlmostEqual(np.std(dRA), 0) 

736 self.assertAlmostEqual(np.mean(dDec), 0) 

737 self.assertAlmostEqual(np.std(dDec), 0) 

738 

739 def test_useColor(self): 

740 """Test running task with color catalog and DCR fitting.""" 

741 config = copy(self.config) 

742 config.useColor = True 

743 

744 task = GbdesAstrometricFitTask(config=config) 

745 outputs = task.run( 

746 self.inputCatalogRefs, 

747 self.inputVisitSummary, 

748 instrumentName=self.instrumentName, 

749 refObjectLoader=self.refObjectLoader, 

750 colorCatalog=self.colorCatalog, 

751 ) 

752 self.assertEqual(len(outputs.colorParams["visit"]), len(self.testVisits)) 

753 

754 

755class TestGbdesGlobalAstrometricFit(TestGbdesAstrometricFit): 

756 @classmethod 

757 def setUpClass(cls): 

758 # Set random seed 

759 np.random.seed(1234) 

760 

761 # Fraction of simulated stars in the reference catalog and science 

762 # exposures 

763 inReferenceFraction = 1 

764 inScienceFraction = 1 

765 

766 # Make fake data 

767 cls.datadir = os.path.join(TESTDIR, "data") 

768 

769 cls.nFields = 2 

770 cls.instrumentName = "HSC" 

771 cls.refEpoch = 57205.5 

772 

773 # Make test inputVisitSummary. VisitSummaryTables are taken from 

774 # collection HSC/runs/RC2/w_2022_20/DM-34794 

775 cls.testVisits = [1176, 17900, 17930, 17934, 36434, 36460, 36494, 36446] 

776 cls.inputVisitSummary = [] 

777 for testVisit in cls.testVisits: 

778 visSum = afwTable.ExposureCatalog.readFits( 

779 os.path.join(cls.datadir, f"visitSummary_{testVisit}.fits") 

780 ) 

781 cls.inputVisitSummary.append(visSum) 

782 

783 cls.config = GbdesGlobalAstrometricFitConfig() 

784 cls.config.systematicError = 0 

785 cls.config.devicePolyOrder = 4 

786 cls.config.exposurePolyOrder = 6 

787 cls.config.fitReserveFraction = 0 

788 cls.config.fitReserveRandomSeed = 1234 

789 cls.config.saveModelParams = True 

790 cls.config.saveCameraModel = True 

791 cls.task = GbdesGlobalAstrometricFitTask(config=cls.config) 

792 

793 cls.fields, cls.fieldRegions = cls.task._prep_sky(cls.inputVisitSummary) 

794 

795 ( 

796 cls.exposureInfo, 

797 cls.exposuresHelper, 

798 cls.extensionInfo, 

799 cls.instruments, 

800 ) = cls.task._get_exposure_info(cls.inputVisitSummary, fieldRegions=cls.fieldRegions) 

801 

802 refDataIds, deferredRefCats = [], [] 

803 allStarIds = [] 

804 allStarRAs = [] 

805 allStarDecs = [] 

806 for region in cls.fieldRegions.values(): 

807 # Bounding box of observations: 

808 bbox = region.getBoundingBox() 

809 raMin = bbox.getLon().getA().asDegrees() 

810 raMax = bbox.getLon().getB().asDegrees() 

811 decMin = bbox.getLat().getA().asDegrees() 

812 decMax = bbox.getLat().getB().asDegrees() 

813 

814 # Make random set of data in a bounding box determined by input 

815 # visits 

816 cls.nStars, starIds, starRAs, starDecs = cls._make_simulated_stars( 

817 raMin, raMax, decMin, decMax, cls.config.matchRadius 

818 ) 

819 

820 allStarIds.append(starIds) 

821 allStarRAs.append(starRAs) 

822 allStarDecs.append(starDecs) 

823 

824 corners = [ 

825 lsst.geom.SpherePoint(ra, dec, lsst.geom.degrees).getVector() 

826 for (ra, dec) in zip([raMin, raMax, raMax, raMin], [decMin, decMin, decMax, decMax]) 

827 ] 

828 conv_box = lsst.sphgeom.ConvexPolygon.convexHull(corners) 

829 # Make a reference catalog that will be loaded into 

830 # ReferenceObjectLoader 

831 refDataId, deferredRefCat = cls._make_refCat( 

832 starIds, starRAs, starDecs, inReferenceFraction, conv_box 

833 ) 

834 refDataIds.append(refDataId) 

835 deferredRefCats.append(deferredRefCat) 

836 

837 cls.refObjectLoader = ReferenceObjectLoader(refDataIds, deferredRefCats) 

838 cls.refObjectLoader.config.requireProperMotion = False 

839 cls.refObjectLoader.config.anyFilterMapsToThis = "test_filter" 

840 

841 cls.task.refObjectLoader = cls.refObjectLoader 

842 

843 allRefObjects, allRefCovariances = {}, {} 

844 for f, fieldRegion in cls.fieldRegions.items(): 

845 refObjects, refCovariance = cls.task._load_refcat( 

846 cls.refObjectLoader, cls.extensionInfo, epoch=cls.exposureInfo.medianEpoch, region=fieldRegion 

847 ) 

848 allRefObjects[f] = refObjects 

849 allRefCovariances[f] = refCovariance 

850 cls.refObjects = allRefObjects 

851 

852 # Get True WCS for stars: 

853 with open(os.path.join(cls.datadir, "sample_global_wcs.yaml"), "r") as f: 

854 cls.trueModel = yaml.load(f, Loader=yaml.Loader) 

855 

856 cls.trueWCSs = cls._make_wcs(cls.trueModel, cls.inputVisitSummary) 

857 

858 cls.isolatedStarCatalogs, cls.isolatedStarSources = cls._make_isolatedStars( 

859 allStarIds, allStarRAs, allStarDecs, cls.trueWCSs, inScienceFraction 

860 ) 

861 

862 cls.colorCatalog = cls._make_colors(np.concatenate(allStarRAs), np.concatenate(allStarDecs)) 

863 

864 cls.outputs = cls.task.run( 

865 cls.inputVisitSummary, 

866 cls.isolatedStarSources, 

867 cls.isolatedStarCatalogs, 

868 instrumentName=cls.instrumentName, 

869 refEpoch=cls.refEpoch, 

870 refObjectLoader=cls.refObjectLoader, 

871 ) 

872 

873 @classmethod 

874 def _make_isolatedStars(cls, allStarIds, allStarRAs, allStarDecs, trueWCSs, inScienceFraction): 

875 """Given a subset of the simulated data, make source catalogs and star 

876 catalogs. 

877 

878 This takes the true WCSs to go from the RA and Decs of the simulated 

879 stars to pixel coordinates for a given visit and detector. If those 

880 pixel coordinates are within the bounding box of the detector, the 

881 source and visit information is put in the corresponding catalog of 

882 isolated star sources. 

883 

884 Parameters 

885 ---------- 

886 allStarIds : `np.ndarray` [`int`] 

887 Source ids for the simulated stars 

888 allStarRas : `np.ndarray` [`float`] 

889 RAs of the simulated stars 

890 allStarDecs : `np.ndarray` [`float`] 

891 Decs of the simulated stars 

892 trueWCSs : `list` [`lsst.afw.geom.SkyWcs`] 

893 WCS with which to simulate the source pixel coordinates 

894 inReferenceFraction : float 

895 Percentage of simulated stars to include in reference catalog 

896 

897 Returns 

898 ------- 

899 isolatedStarCatalogRefs : `list` 

900 [`lsst.pipe.base.InMemoryDatasetHandle`] 

901 List of references to isolated star catalogs. 

902 isolatedStarSourceRefs : `list` 

903 [`lsst.pipe.base.InMemoryDatasetHandle`] 

904 List of references to isolated star sources. 

905 """ 

906 bbox = lsst.geom.BoxD( 

907 lsst.geom.Point2D( 

908 cls.inputVisitSummary[0][0]["bbox_min_x"], cls.inputVisitSummary[0][0]["bbox_min_y"] 

909 ), 

910 lsst.geom.Point2D( 

911 cls.inputVisitSummary[0][0]["bbox_max_x"], cls.inputVisitSummary[0][0]["bbox_max_y"] 

912 ), 

913 ) 

914 bboxCorners = bbox.getCorners() 

915 

916 isolatedStarCatalogRefs = [] 

917 isolatedStarSourceRefs = [] 

918 for i in range(len(allStarIds)): 

919 starIds = allStarIds[i] 

920 starRAs = allStarRAs[i] 

921 starDecs = allStarDecs[i] 

922 isolatedStarCatalog = pd.DataFrame({"ra": starRAs, "dec": starDecs}, index=starIds) 

923 isolatedStarCatalogRefs.append( 

924 InMemoryDatasetHandle(isolatedStarCatalog, storageClass="DataFrame", dataId={"tract": 0}) 

925 ) 

926 sourceCats = [] 

927 for v, visit in enumerate(cls.testVisits): 

928 nVisStars = int(cls.nStars * inScienceFraction) 

929 visitStarIndices = np.random.choice(cls.nStars, nVisStars, replace=False) 

930 visitStarIds = starIds[visitStarIndices] 

931 visitStarRas = starRAs[visitStarIndices] 

932 visitStarDecs = starDecs[visitStarIndices] 

933 for detector in trueWCSs[visit]: 

934 detWcs = detector.getWcs() 

935 detectorId = detector["id"] 

936 radecCorners = detWcs.pixelToSky(bboxCorners) 

937 detectorFootprint = sphgeom.ConvexPolygon([rd.getVector() for rd in radecCorners]) 

938 detectorIndices = detectorFootprint.contains( 

939 (visitStarRas * u.degree).to(u.radian), (visitStarDecs * u.degree).to(u.radian) 

940 ) 

941 nDetectorStars = detectorIndices.sum() 

942 detectorArray = np.ones(nDetectorStars, dtype=int) * detector["id"] 

943 visitArray = np.ones(nDetectorStars, dtype=int) * visit 

944 

945 ones_like = np.ones(nDetectorStars) 

946 zeros_like = np.zeros(nDetectorStars, dtype=bool) 

947 

948 x, y = detWcs.skyToPixelArray( 

949 visitStarRas[detectorIndices], visitStarDecs[detectorIndices], degrees=True 

950 ) 

951 

952 origWcs = (cls.inputVisitSummary[v][cls.inputVisitSummary[v]["id"] == detectorId])[ 

953 0 

954 ].getWcs() 

955 inputRa, inputDec = origWcs.pixelToSkyArray(x, y, degrees=True) 

956 

957 sourceDict = {} 

958 sourceDict["detector"] = detectorArray 

959 sourceDict["visit"] = visitArray 

960 sourceDict["obj_index"] = visitStarIds[detectorIndices] 

961 sourceDict["x"] = x 

962 sourceDict["y"] = y 

963 sourceDict["xErr"] = 1e-3 * ones_like 

964 sourceDict["yErr"] = 1e-3 * ones_like 

965 sourceDict["inputRA"] = inputRa 

966 sourceDict["inputDec"] = inputDec 

967 sourceDict["trueRA"] = visitStarRas[detectorIndices] 

968 sourceDict["trueDec"] = visitStarDecs[detectorIndices] 

969 for key in ["apFlux_12_0_flux", "apFlux_12_0_instFlux", "ixx", "iyy"]: 

970 sourceDict[key] = ones_like 

971 for key in [ 

972 "pixelFlags_edge", 

973 "pixelFlags_saturated", 

974 "pixelFlags_interpolatedCenter", 

975 "pixelFlags_interpolated", 

976 "pixelFlags_crCenter", 

977 "pixelFlags_bad", 

978 "hsmPsfMoments_flag", 

979 "apFlux_12_0_flag", 

980 "extendedness", 

981 "parentSourceId", 

982 "deblend_nChild", 

983 "ixy", 

984 ]: 

985 sourceDict[key] = zeros_like 

986 sourceDict["apFlux_12_0_instFluxErr"] = 1e-2 * ones_like 

987 sourceDict["detect_isPrimary"] = ones_like.astype(bool) 

988 

989 sourceCat = pd.DataFrame(sourceDict) 

990 sourceCats.append(sourceCat) 

991 

992 isolatedStarSourceTable = pd.concat(sourceCats, ignore_index=True) 

993 isolatedStarSourceTable = isolatedStarSourceTable.sort_values(by=["obj_index"]) 

994 isolatedStarSourceRefs.append( 

995 InMemoryDatasetHandle(isolatedStarSourceTable, storageClass="DataFrame", dataId={"tract": 0}) 

996 ) 

997 

998 return isolatedStarCatalogRefs, isolatedStarSourceRefs 

999 

1000 def test_loading_and_association(self): 

1001 """Test that associated objects actually correspond to the same 

1002 simulated object.""" 

1003 associations, sourceDict = self.task._associate_from_isolated_sources( 

1004 self.isolatedStarSources, self.isolatedStarCatalogs, self.extensionInfo, self.refObjects 

1005 ) 

1006 

1007 object_groups = np.flatnonzero(np.array(associations.sequence) == 0) 

1008 for i in range(len(object_groups) - 1)[:10]: 

1009 ras, decs = [], [] 

1010 for ind in np.arange(object_groups[i], object_groups[i + 1]): 

1011 visit = self.extensionInfo.visit[associations.extn[ind]] 

1012 detectorInd = self.extensionInfo.detectorIndex[associations.extn[ind]] 

1013 detector = self.extensionInfo.detector[associations.extn[ind]] 

1014 if detectorInd == -1: 

1015 ra = self.refObjects[visit * -1]["ra"][associations.obj[ind]] 

1016 dec = self.refObjects[visit * -1]["dec"][associations.obj[ind]] 

1017 ras.append(ra) 

1018 decs.append(dec) 

1019 else: 

1020 x = sourceDict[visit][detector]["x"][associations.obj[ind]] 

1021 y = sourceDict[visit][detector]["y"][associations.obj[ind]] 

1022 ra, dec = self.trueWCSs[visit][detectorInd].getWcs().pixelToSky(x, y) 

1023 ras.append(ra.asDegrees()) 

1024 decs.append(dec.asDegrees()) 

1025 np.testing.assert_allclose(ras, ras[0]) 

1026 np.testing.assert_allclose(decs, decs[0]) 

1027 

1028 def test_refCatLoader(self): 

1029 """Test loading objects from the refCat in each of the fields.""" 

1030 

1031 for region in self.fieldRegions.values(): 

1032 refCat, refCov = self.task._load_refcat( 

1033 self.refObjectLoader, self.extensionInfo, region=region, epoch=self.exposureInfo.medianEpoch 

1034 ) 

1035 assert len(refCat) > 0 

1036 

1037 def test_make_outputs(self): 

1038 """Test that the run method recovers the input model parameters.""" 

1039 for isolatedStarSourceRef in self.isolatedStarSources: 

1040 iss = isolatedStarSourceRef.get() 

1041 visits = np.unique(iss["visit"]) 

1042 for v, visit in enumerate(visits): 

1043 outputWcsCatalog = self.outputs.outputWcss[visit] 

1044 visitSources = iss[iss["visit"] == visit] 

1045 detectors = outputWcsCatalog["id"] 

1046 for d, detectorId in enumerate(detectors): 

1047 fitwcs = outputWcsCatalog[d].getWcs() 

1048 detSources = visitSources[visitSources["detector"] == detectorId] 

1049 fitRA, fitDec = fitwcs.pixelToSkyArray(detSources["x"], detSources["y"], degrees=True) 

1050 dRA = fitRA - detSources["trueRA"] 

1051 dDec = fitDec - detSources["trueDec"] 

1052 # Check that input coordinates match the output coordinates 

1053 self.assertAlmostEqual(np.mean(dRA), 0) 

1054 self.assertAlmostEqual(np.std(dRA), 0) 

1055 self.assertAlmostEqual(np.mean(dDec), 0) 

1056 self.assertAlmostEqual(np.std(dDec), 0) 

1057 

1058 def test_missingWcs(self): 

1059 """Test that task does not fail when the input WCS is None for one 

1060 extension and that the fit WCS for that extension returns a finite 

1061 result. 

1062 """ 

1063 # Need to copy the catalogs since they are modified later. 

1064 inputVisitSummary = [cat.copy(deep=True) for cat in self.inputVisitSummary] 

1065 # Set one WCS to be None 

1066 testVisit = 0 

1067 testDetector = 20 

1068 inputVisitSummary[testVisit][testDetector].setWcs(None) 

1069 

1070 outputs = self.task.run( 

1071 inputVisitSummary, 

1072 self.isolatedStarSources, 

1073 self.isolatedStarCatalogs, 

1074 instrumentName=self.instrumentName, 

1075 refEpoch=self.refEpoch, 

1076 refObjectLoader=self.refObjectLoader, 

1077 ) 

1078 

1079 # Check that the fit WCS for the extension with input WCS=None returns 

1080 # finite sky values. 

1081 testWcs = outputs.outputWcss[self.testVisits[testVisit]][testDetector].getWcs() 

1082 testSky = testWcs.pixelToSky(0, 0) 

1083 self.assertTrue(testSky.isFinite()) 

1084 

1085 def test_inputCameraModel(self): 

1086 """Test running task with an input camera model, and check that true 

1087 object coordinates are recovered. 

1088 """ 

1089 config = copy(self.config) 

1090 config.saveCameraModel = False 

1091 config.useInputCameraModel = True 

1092 task = GbdesGlobalAstrometricFitTask(config=config) 

1093 with open(os.path.join(self.datadir, "sample_global_camera_model.yaml"), "r") as f: 

1094 cameraModel = yaml.load(f, Loader=yaml.Loader) 

1095 

1096 outputs = task.run( 

1097 self.inputVisitSummary, 

1098 self.isolatedStarSources, 

1099 self.isolatedStarCatalogs, 

1100 instrumentName=self.instrumentName, 

1101 refObjectLoader=self.refObjectLoader, 

1102 inputCameraModel=cameraModel, 

1103 ) 

1104 

1105 # Check that the output WCS is close (not necessarily exactly equal) to 

1106 # the result when fitting the full model. 

1107 for isolatedStarSourceRef in self.isolatedStarSources: 

1108 iss = isolatedStarSourceRef.get() 

1109 visits = np.unique(iss["visit"]) 

1110 for v, visit in enumerate(visits): 

1111 outputWcsCatalog = outputs.outputWcss[visit] 

1112 visitSources = iss[iss["visit"] == visit] 

1113 detectors = outputWcsCatalog["id"] 

1114 for d, detectorId in enumerate(detectors): 

1115 fitwcs = outputWcsCatalog[d].getWcs() 

1116 detSources = visitSources[visitSources["detector"] == detectorId] 

1117 fitRA, fitDec = fitwcs.pixelToSkyArray(detSources["x"], detSources["y"], degrees=True) 

1118 dRA = fitRA - detSources["trueRA"] 

1119 dDec = fitDec - detSources["trueDec"] 

1120 # Check that input coordinates match the output coordinates 

1121 self.assertAlmostEqual(np.mean(dRA), 0, places=6) 

1122 self.assertAlmostEqual(np.std(dRA), 0) 

1123 self.assertAlmostEqual(np.mean(dDec), 0, places=6) 

1124 self.assertAlmostEqual(np.std(dDec), 0) 

1125 

1126 def test_useColor(self): 

1127 """Test running task with color catalog and DCR fitting.""" 

1128 config = copy(self.config) 

1129 config.useColor = True 

1130 

1131 task = GbdesGlobalAstrometricFitTask(config=config) 

1132 

1133 outputs = task.run( 

1134 self.inputVisitSummary, 

1135 self.isolatedStarSources, 

1136 self.isolatedStarCatalogs, 

1137 instrumentName=self.instrumentName, 

1138 refObjectLoader=self.refObjectLoader, 

1139 colorCatalog=self.colorCatalog, 

1140 ) 

1141 self.assertEqual(len(outputs.colorParams["visit"]), len(self.testVisits)) 

1142 

1143 

1144class TestApparentMotion(lsst.utils.tests.TestCase): 

1145 """This class simulates an array of objects with random proper motions and 

1146 parallaxes and compares the results of 

1147 `lsst.drp.tasks.gbdesAstrometricFit.calculate_apparent_motion` against the 

1148 results calculated by astropy.""" 

1149 

1150 def setUp(self): 

1151 np.random.seed(12345) 

1152 

1153 self.ras = (np.random.rand(10) + 150.0) * u.degree 

1154 self.decs = (np.random.rand(10) + 2.5) * u.degree 

1155 

1156 self.pmRAs = (np.random.random(10) * 10) * u.mas / u.yr 

1157 self.pmDecs = (np.random.random(10) * 10) * u.mas / u.yr 

1158 

1159 self.parallaxes = (abs(np.random.random(10) * 5)) * u.mas 

1160 

1161 self.mjds = astropy.time.Time(np.random.rand(10) * 20 + 57000, format="mjd", scale="tai") 

1162 

1163 self.refEpoch = astropy.time.Time(57100, format="mjd", scale="tai") 

1164 

1165 def test_proper_motion(self): 

1166 """Calculate the change in position due to proper motion only for a 

1167 given time change, and compare the results against astropy. 

1168 """ 

1169 # Set parallaxes to zero to test proper motion part by itself. 

1170 parallaxes = np.zeros(len(self.ras)) * u.mas 

1171 

1172 data = astropy.table.Table( 

1173 { 

1174 "ra": self.ras, 

1175 "dec": self.decs, 

1176 "pmRA": self.pmRAs, 

1177 "pmDec": self.pmDecs, 

1178 "parallax": parallaxes, 

1179 "MJD": self.mjds, 

1180 } 

1181 ) 

1182 raCorrection, decCorrection = calculate_apparent_motion(data, self.refEpoch) 

1183 

1184 pmRACosDec = self.pmRAs * np.cos(self.decs) 

1185 coords = SkyCoord( 

1186 ra=self.ras, 

1187 dec=self.decs, 

1188 pm_ra_cosdec=pmRACosDec, 

1189 pm_dec=self.pmDecs, 

1190 obstime=self.refEpoch, 

1191 ) 

1192 

1193 newCoords = coords.apply_space_motion(new_obstime=self.mjds) 

1194 astropyDRA = newCoords.ra.degree - coords.ra.degree 

1195 astropyDDec = newCoords.dec.degree - coords.dec.degree 

1196 

1197 # The proper motion values are on the order of 0-3 mas/yr and the 

1198 # differences are on the scale of 1e-8 mas. 

1199 self.assertFloatsAlmostEqual(astropyDRA, raCorrection.value, rtol=1e-6) 

1200 self.assertFloatsAlmostEqual(astropyDDec, decCorrection.value, rtol=1e-6) 

1201 

1202 def test_parallax(self): 

1203 """Calculate the change in position due to parallax for a given time 

1204 change and compare the results against astropy. Astropy will not give 

1205 the parallax part separate from other sources of space motion, such as 

1206 annual aberration, but it can be obtained by comparing the calculated 

1207 positions of an object with a given parallax and one with a much 

1208 further distance. The result is still only approximately the same, but 

1209 is close enough for accuracy needed here. 

1210 """ 

1211 # Set proper motions to zero to test parallax correction by itself. 

1212 pms = np.zeros(len(self.ras)) * u.mas / u.yr 

1213 

1214 data = astropy.table.Table( 

1215 { 

1216 "ra": self.ras, 

1217 "dec": self.decs, 

1218 "pmRA": pms, 

1219 "pmDec": pms, 

1220 "parallax": self.parallaxes, 

1221 "MJD": self.mjds, 

1222 } 

1223 ) 

1224 raCorrection, decCorrection = calculate_apparent_motion(data, self.refEpoch) 

1225 

1226 coords = SkyCoord( 

1227 ra=self.ras, 

1228 dec=self.decs, 

1229 pm_ra_cosdec=pms, 

1230 pm_dec=pms, 

1231 distance=Distance(parallax=self.parallaxes), 

1232 obstime=self.refEpoch, 

1233 ) 

1234 # Make another set of objects at the same coordinates but a much 

1235 # greater distant in order to isolate parallax when applying space 

1236 # motion. 

1237 refCoords = SkyCoord( 

1238 ra=self.ras, 

1239 dec=self.decs, 

1240 pm_ra_cosdec=pms, 

1241 pm_dec=pms, 

1242 distance=1e10 * Distance(parallax=self.parallaxes), 

1243 obstime=self.refEpoch, 

1244 ) 

1245 

1246 newCoords = coords.apply_space_motion(new_obstime=self.mjds).transform_to("gcrs") 

1247 refNewCoords = refCoords.apply_space_motion(new_obstime=self.mjds).transform_to("gcrs") 

1248 astropyDRA = newCoords.ra.degree - refNewCoords.ra.degree 

1249 astropyDDec = newCoords.dec.degree - refNewCoords.dec.degree 

1250 

1251 # The parallax values are on the scale of 1-3 mas, and the difference 

1252 # between the two calculation methods is ~0.05 mas. 

1253 self.assertFloatsAlmostEqual(astropyDRA, raCorrection.value, rtol=2e-2) 

1254 self.assertFloatsAlmostEqual(astropyDDec, decCorrection.value, rtol=2e-2) 

1255 

1256 

1257def setup_module(module): 

1258 lsst.utils.tests.init() 

1259 

1260 

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

1262 lsst.utils.tests.init() 

1263 unittest.main()