Coverage for tests/utils_tests.py: 93%

115 statements  

« prev     ^ index     » next       coverage.py v7.14.3, created at 2026-07-01 09:20 +0000

1# This file is part of ap_association. 

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 

22"""Helper functions for tests of DIA catalogs, including generating mock 

23catalogs for simulated APDB access. 

24""" 

25import astropy.units 

26import pandas as pd 

27import numpy as np 

28 

29from lsst.afw.cameraGeom.testUtils import DetectorWrapper 

30from lsst.afw.coord import Observatory 

31import lsst.afw.geom as afwGeom 

32import lsst.afw.image as afwImage 

33from lsst.daf.base import DateTime, PropertySet 

34import lsst.daf.butler as dafButler 

35import lsst.geom 

36import lsst.meas.algorithms as measAlg 

37from lsst.pipe.base.utils import RegionTimeInfo 

38import lsst.sphgeom 

39 

40from lsst.ap.association.utils import getRegion 

41 

42 

43def makeDiaObjects(nObjects, exposure, rng, startId=1): 

44 """Make a test set of DiaObjects. 

45 

46 Parameters 

47 ---------- 

48 nObjects : `int` 

49 Number of objects to create. 

50 exposure : `lsst.afw.image.Exposure` 

51 Exposure to create objects over. 

52 rng : `numpy.random.Generator` 

53 A NumPy random number generator initialized with a fixed seed for reproducibility. 

54 

55 Returns 

56 ------- 

57 diaObjects : `pandas.DataFrame` 

58 DiaObjects generated across the exposure. 

59 """ 

60 bbox = lsst.geom.Box2D(exposure.getBBox()) 

61 rand_x = rng.uniform(bbox.getMinX(), bbox.getMaxX(), size=nObjects) 

62 rand_y = rng.uniform(bbox.getMinY(), bbox.getMaxY(), size=nObjects) 

63 

64 data = [] 

65 for idx, (x, y) in enumerate(zip(rand_x, rand_y)): 

66 coord = exposure.wcs.pixelToSky(x, y) 

67 newObject = {"ra": coord.getRa().asDegrees(), 

68 "dec": coord.getDec().asDegrees(), 

69 "diaObjectId": idx + startId, 

70 "nDiaSources": 5} 

71 for f in ["u", "g", "r", "i", "z", "y"]: 

72 newObject["%s_psfFluxNdata" % f] = 0 

73 data.append(newObject) 

74 

75 return pd.DataFrame(data=data).set_index("diaObjectId", drop=False) 

76 

77 

78def makeDiaSources(nSources, diaObjectIds, exposure, rng, randomizeObjects=False, flagList=None, startId=1): 

79 """Make a test set of DiaSources. 

80 

81 Parameters 

82 ---------- 

83 nSources : `int` 

84 Number of sources to create. 

85 diaObjectIds : `numpy.ndarray` 

86 Integer Ids of diaobjects to "associate" with the DiaSources. 

87 exposure : `lsst.afw.image.Exposure` 

88 Exposure to create sources over. 

89 rng : `numpy.random.Generator` 

90 A NumPy random number generator initialized with a fixed seed for reproducibility. 

91 randomizeObjects : `bool`, optional 

92 If True, randomly draw from `diaObjectIds` to generate the ids in the 

93 output catalog, otherwise just iterate through them, repeating as 

94 necessary to get nSources objectIds. 

95 flagList : `list` of `str`, optional 

96 Optional list of names of flag columns to add to the DiaSource table. 

97 

98 Returns 

99 ------- 

100 diaSources : `pandas.DataFrame` 

101 DiaSources generated across the exposure. 

102 """ 

103 bbox = lsst.geom.Box2D(exposure.getBBox()) 

104 rand_x = rng.uniform(bbox.getMinX(), bbox.getMaxX(), size=nSources) 

105 rand_y = rng.uniform(bbox.getMinY(), bbox.getMaxY(), size=nSources) 

106 diaObjectIds = diaObjectIds.astype(np.int64) 

107 if randomizeObjects: 

108 objectIds = diaObjectIds[rng.integers(len(diaObjectIds), size=nSources)] 

109 else: 

110 objectIds = diaObjectIds[[i % len(diaObjectIds) for i in range(nSources)]] 

111 

112 midpointMjdTai = exposure.visitInfo.date.get(system=DateTime.MJD) 

113 

114 data = [] 

115 flags = {} 

116 if flagList is not None: 

117 for flag in flagList: 

118 flags[flag] = False 

119 for idx, (x, y, objId) in enumerate(zip(rand_x, rand_y, objectIds)): 

120 coord = exposure.wcs.pixelToSky(x, y) 

121 # Put together the minimum values for the alert. 

122 diaSource = {"ra": coord.getRa().asDegrees(), 

123 "dec": coord.getDec().asDegrees(), 

124 "x": x, 

125 "y": y, 

126 "visit": exposure.visitInfo.id, 

127 "detector": exposure.detector.getId(), 

128 "timeProcessedMjdTai": DateTime.now().get(system=DateTime.MJD, 

129 scale=DateTime.TAI), 

130 "diaObjectId": objId, 

131 "ssObjectId": 0, 

132 "parentDiaSourceId": 0, 

133 "diaSourceId": idx + startId, 

134 "midpointMjdTai": midpointMjdTai + 1.0 * idx, 

135 "band": exposure.getFilter().bandLabel, 

136 "psfNdata": 0, 

137 "bboxSize": 23, 

138 "trailNdata": 0, 

139 "dipoleNdata": 0} 

140 data.append(diaSource | flags) 

141 

142 return pd.DataFrame(data=data).set_index(["diaObjectId", "band", "diaSourceId"], drop=False) 

143 

144 

145def makeSolarSystemSources(nSources, diaObjectIds, exposure, rng, randomizeObjects=False, startId=1): 

146 """Make a test set of solar system sources. 

147 

148 Parameters 

149 ---------- 

150 nSources : `int` 

151 Number of sources to create. 

152 diaObjectIds : `numpy.ndarray` 

153 Integer Ids of diaobjects to "associate" with the DiaSources. 

154 exposure : `lsst.afw.image.Exposure` 

155 Exposure to create sources over. 

156 rng : `numpy.random.Generator` 

157 A NumPy random number generator initialized with a fixed seed for reproducibility. 

158 randomizeObjects : `bool`, optional 

159 If True, randomly draw from `diaObjectIds` to generate the ids in the 

160 output catalog, otherwise just iterate through them, repeating as 

161 necessary to get nSources objectIds. 

162 

163 Returns 

164 ------- 

165 solarSystemSources : `pandas.DataFrame` 

166 Solar system sources generated across the exposure. 

167 """ 

168 solarSystemSources = makeDiaSources(nSources, diaObjectIds, exposure, rng, 

169 randomizeObjects=False, 

170 startId=startId) 

171 solarSystemSources["ssObjectId"] = rng.integers(0, high=2**63-1, size=nSources) 

172 solarSystemSources["Err(arcsec)"] = rng.uniform(0.2, 0.4, size=nSources) 

173 

174 return solarSystemSources 

175 

176 

177def makeDiaForcedSources(nForcedSources, diaObjectIds, exposure, rng, randomizeObjects=False, startId=1): 

178 """Make a test set of DiaSources. 

179 

180 Parameters 

181 ---------- 

182 nForcedSources : `int` 

183 Number of sources to create. 

184 diaObjectIds : `numpy.ndarray` 

185 Integer Ids of diaobjects to "associate" with the DiaSources. 

186 exposure : `lsst.afw.image.Exposure` 

187 Exposure to create sources over. 

188 rng : `numpy.random.Generator` 

189 A NumPy random number generator initialized with a fixed seed for reproducibility. 

190 randomizeObjects : `bool`, optional 

191 If True, randomly draw from `diaObjectIds` to generate the ids in the 

192 output catalog, otherwise just iterate through them. 

193 

194 Returns 

195 ------- 

196 diaForcedSources : `pandas.DataFrame` 

197 DiaForcedSources generated across the exposure. 

198 """ 

199 midpointMjdTai = exposure.visitInfo.date.get(system=DateTime.MJD) 

200 visit = exposure.visitInfo.id 

201 detector = exposure.detector.getId() 

202 if randomizeObjects: 202 ↛ 203line 202 didn't jump to line 203 because the condition on line 202 was never true

203 objectIds = diaObjectIds[rng.randint(len(diaObjectIds), size=nForcedSources)] 

204 else: 

205 objectIds = diaObjectIds[[i % len(diaObjectIds) for i in range(nForcedSources)]] 

206 

207 data = [] 

208 bbox = exposure.getBBox() 

209 

210 for i, objId in enumerate(objectIds): 

211 # Put together the minimum values for the alert. 

212 x = rng.uniform(bbox.minX, bbox.maxX) 

213 y = rng.uniform(bbox.minY, bbox.maxY) 

214 coord = exposure.wcs.pixelToSky(x, y) 

215 data.append({"diaForcedSourceId": i + startId, 

216 "visit": visit + i, 

217 "detector": detector, 

218 "diaObjectId": objId, 

219 "ra": coord.getRa().asDegrees(), 

220 "dec": coord.getDec().asDegrees(), 

221 "midpointMjdTai": midpointMjdTai + 1.0 * i, 

222 "timeProcessedMjdTai": DateTime.now().get(system=DateTime.MJD, 

223 scale=DateTime.TAI), 

224 "band": exposure.getFilter().bandLabel}) 

225 

226 return pd.DataFrame(data=data).set_index(["diaObjectId", "diaForcedSourceId"], drop=False) 

227 

228 

229def makeExposure(flipX=False, flipY=False): 

230 """Create an exposure and flip the x or y (or both) coordinates. 

231 

232 Returns bounding boxes that are right or left handed around the bounding 

233 polygon. 

234 

235 Parameters 

236 ---------- 

237 flipX : `bool` 

238 Flip the x coordinate in the WCS. 

239 flipY : `bool` 

240 Flip the y coordinate in the WCS. 

241 

242 Returns 

243 ------- 

244 exposure : `lsst.afw.image.Exposure` 

245 Exposure with a valid bounding box and wcs. 

246 """ 

247 metadata = PropertySet() 

248 

249 metadata.set("SIMPLE", "T") 

250 metadata.set("BITPIX", -32) 

251 metadata.set("NAXIS", 2) 

252 metadata.set("NAXIS1", 1024) 

253 metadata.set("NAXIS2", 1153) 

254 metadata.set("RADECSYS", 'FK5') 

255 metadata.set("EQUINOX", 2000.) 

256 ra = 215.604025685476 

257 dec = 53.1595451514076 

258 

259 metadata.setDouble("CRVAL1", ra) 

260 metadata.setDouble("CRVAL2", dec) 

261 metadata.setDouble("CRPIX1", 1109.99981456774) 

262 metadata.setDouble("CRPIX2", 560.018167811613) 

263 metadata.set("CTYPE1", 'RA---SIN') 

264 metadata.set("CTYPE2", 'DEC--SIN') 

265 

266 xFlip = 1 

267 if flipX: 267 ↛ 268line 267 didn't jump to line 268 because the condition on line 267 was never true

268 xFlip = -1 

269 yFlip = 1 

270 if flipY: 270 ↛ 271line 270 didn't jump to line 271 because the condition on line 270 was never true

271 yFlip = -1 

272 metadata.setDouble("CD1_1", xFlip * 5.10808596133527E-05) 

273 metadata.setDouble("CD1_2", yFlip * 1.85579539217196E-07) 

274 metadata.setDouble("CD2_2", yFlip * -5.10281493481982E-05) 

275 metadata.setDouble("CD2_1", xFlip * -8.27440751733828E-07) 

276 

277 wcs = afwGeom.makeSkyWcs(metadata) 

278 exposure = afwImage.makeExposure( 

279 afwImage.makeMaskedImageFromArrays(np.ones((1024, 1153))), wcs) 

280 detector = DetectorWrapper(id=23, bbox=exposure.getBBox()).detector 

281 visit = afwImage.VisitInfo( 

282 exposureTime=200., 

283 date=DateTime("2014-05-13T17:00:00.000000000", 

284 DateTime.Timescale.TAI), 

285 boresightRaDec=lsst.geom.SpherePoint(ra, dec, lsst.geom.degrees), 

286 boresightRotAngle=73.2*lsst.geom.degrees, 

287 rotType=afwImage.RotType.SKY, 

288 observatory=Observatory(11.1*lsst.geom.degrees, 22.2*lsst.geom.degrees, 0.333), 

289 ) 

290 kernel = measAlg.DoubleGaussianPsf(7, 7, 2.0).getKernel() 

291 exposure.setPsf(measAlg.KernelPsf(kernel, exposure.getBBox().getCenter())) 

292 exposure.info.id = 1234 

293 exposure.setDetector(detector) 

294 exposure.info.setVisitInfo(visit) 

295 exposure.setFilter(afwImage.FilterLabel(band='g')) 

296 exposure.setPhotoCalib(afwImage.PhotoCalib(1., 0., exposure.getBBox())) 

297 

298 return exposure 

299 

300 

301def makeRegionTime(exposure=None, begin=None, end=None): 

302 """Make a `RegionTimeInfo` for testing 

303 

304 Parameters 

305 ---------- 

306 exposure : `lsst.afw.image.Exposure`, optional 

307 Exposure to construct a ``RegionTimeInfo`` for. 

308 If None, a default exposure will be created. 

309 begin : `astropy.time.Time`, optional 

310 The start time of the interval. 

311 If `None`, calculate the start time from the midpoint of the exposure 

312 and the exposure length. 

313 end : `astropy.time.Time`, optional 

314 The end time of the interval. 

315 If `None`, calculate the end time from the midpoint of the exposure 

316 and the exposure length. 

317 

318 Returns 

319 ------- 

320 regionTime : `lsst.pipe.base.utils.RegionTimeInfo` 

321 Object containing the spatial region and temporal timespan for an exposure. 

322 """ 

323 if exposure is None: 323 ↛ 324line 323 didn't jump to line 324 because the condition on line 323 was never true

324 exposure = makeExposure() 

325 region = getRegion(exposure) 

326 expTime = exposure.visitInfo.exposureTime*astropy.units.second 

327 # visitInfo time is the midpoint of the exposure. 

328 if begin is None: 328 ↛ 330line 328 didn't jump to line 330 because the condition on line 328 was always true

329 begin = exposure.visitInfo.date.toAstropy() - expTime/2 

330 if end is None: 330 ↛ 332line 330 didn't jump to line 332 because the condition on line 330 was always true

331 end = exposure.visitInfo.date.toAstropy() + expTime/2 

332 timespan = dafButler.Timespan(begin=begin, end=end) 

333 return RegionTimeInfo(region=region, timespan=timespan)