Coverage for tests/utils_tests.py: 13%

108 statements  

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

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 

30import lsst.afw.geom as afwGeom 

31import lsst.afw.image as afwImage 

32from lsst.daf.base import DateTime, PropertySet 

33import lsst.daf.butler as dafButler 

34import lsst.geom 

35from lsst.pipe.base.utils import RegionTimeInfo 

36import lsst.sphgeom 

37 

38from lsst.ap.association.utils import getRegion 

39 

40 

41def makeDiaObjects(nObjects, exposure, rng): 

42 """Make a test set of DiaObjects. 

43 

44 Parameters 

45 ---------- 

46 nObjects : `int` 

47 Number of objects to create. 

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

49 Exposure to create objects over. 

50 rng : `numpy.random.Generator` 

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

52 

53 Returns 

54 ------- 

55 diaObjects : `pandas.DataFrame` 

56 DiaObjects generated across the exposure. 

57 """ 

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

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

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

61 

62 data = [] 

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

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

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

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

67 "diaObjectId": idx + 1, 

68 "nDiaSources": 5} 

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

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

71 data.append(newObject) 

72 

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

74 

75 

76def makeDiaSources(nSources, diaObjectIds, exposure, rng, randomizeObjects=False, flagList=None): 

77 """Make a test set of DiaSources. 

78 

79 Parameters 

80 ---------- 

81 nSources : `int` 

82 Number of sources to create. 

83 diaObjectIds : `numpy.ndarray` 

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

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

86 Exposure to create sources over. 

87 rng : `numpy.random.Generator` 

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

89 randomizeObjects : `bool`, optional 

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

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

92 necessary to get nSources objectIds. 

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

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

95 

96 Returns 

97 ------- 

98 diaSources : `pandas.DataFrame` 

99 DiaSources generated across the exposure. 

100 """ 

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

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

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

104 if randomizeObjects: 

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

106 else: 

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

108 

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

110 

111 data = [] 

112 flags = {} 

113 if flagList is not None: 

114 for flag in flagList: 

115 flags[flag] = False 

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

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

118 # Put together the minimum values for the alert. 

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

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

121 "x": x, 

122 "y": y, 

123 "visit": exposure.visitInfo.id, 

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

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

126 scale=DateTime.TAI), 

127 "diaObjectId": objId, 

128 "ssObjectId": 0, 

129 "parentDiaSourceId": 0, 

130 "diaSourceId": idx + 1, 

131 "midpointMjdTai": midpointMjdTai + 1.0 * idx, 

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

133 "psfNdata": 0, 

134 "trailNdata": 0, 

135 "dipoleNdata": 0} 

136 data.append(diaSource | flags) 

137 

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

139 

140 

141def makeSolarSystemSources(nSources, diaObjectIds, exposure, rng, randomizeObjects=False): 

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

143 

144 Parameters 

145 ---------- 

146 nSources : `int` 

147 Number of sources to create. 

148 diaObjectIds : `numpy.ndarray` 

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

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

151 Exposure to create sources over. 

152 rng : `numpy.random.Generator` 

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

154 randomizeObjects : `bool`, optional 

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

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

157 necessary to get nSources objectIds. 

158 

159 Returns 

160 ------- 

161 solarSystemSources : `pandas.DataFrame` 

162 Solar system sources generated across the exposure. 

163 """ 

164 solarSystemSources = makeDiaSources(nSources, diaObjectIds, exposure, rng, randomizeObjects=False) 

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

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

167 

168 return solarSystemSources 

169 

170 

171def makeDiaForcedSources(nForcedSources, diaObjectIds, exposure, rng, randomizeObjects=False): 

172 """Make a test set of DiaSources. 

173 

174 Parameters 

175 ---------- 

176 nForcedSources : `int` 

177 Number of sources to create. 

178 diaObjectIds : `numpy.ndarray` 

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

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

181 Exposure to create sources over. 

182 rng : `numpy.random.Generator` 

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

184 randomizeObjects : `bool`, optional 

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

186 output catalog, otherwise just iterate through them. 

187 

188 Returns 

189 ------- 

190 diaForcedSources : `pandas.DataFrame` 

191 DiaForcedSources generated across the exposure. 

192 """ 

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

194 visit = exposure.visitInfo.id 

195 detector = exposure.detector.getId() 

196 if randomizeObjects: 

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

198 else: 

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

200 

201 data = [] 

202 bbox = exposure.getBBox() 

203 

204 for i, objId in enumerate(objectIds): 

205 # Put together the minimum values for the alert. 

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

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

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

209 data.append({"diaForcedSourceId": i + 1, 

210 "visit": visit + i, 

211 "detector": detector, 

212 "diaObjectId": objId, 

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

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

215 "midpointMjdTai": midpointMjdTai + 1.0 * i, 

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

217 scale=DateTime.TAI), 

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

219 

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

221 

222 

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

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

225 

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

227 polygon. 

228 

229 Parameters 

230 ---------- 

231 flipX : `bool` 

232 Flip the x coordinate in the WCS. 

233 flipY : `bool` 

234 Flip the y coordinate in the WCS. 

235 

236 Returns 

237 ------- 

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

239 Exposure with a valid bounding box and wcs. 

240 """ 

241 metadata = PropertySet() 

242 

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

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

245 metadata.set("NAXIS", 2) 

246 metadata.set("NAXIS1", 1024) 

247 metadata.set("NAXIS2", 1153) 

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

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

250 

251 metadata.setDouble("CRVAL1", 215.604025685476) 

252 metadata.setDouble("CRVAL2", 53.1595451514076) 

253 metadata.setDouble("CRPIX1", 1109.99981456774) 

254 metadata.setDouble("CRPIX2", 560.018167811613) 

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

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

257 

258 xFlip = 1 

259 if flipX: 

260 xFlip = -1 

261 yFlip = 1 

262 if flipY: 

263 yFlip = -1 

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

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

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

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

268 

269 wcs = afwGeom.makeSkyWcs(metadata) 

270 exposure = afwImage.makeExposure( 

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

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

273 visit = afwImage.VisitInfo( 

274 exposureTime=200., 

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

276 DateTime.Timescale.TAI)) 

277 exposure.info.id = 1234 

278 exposure.setDetector(detector) 

279 exposure.info.setVisitInfo(visit) 

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

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

282 

283 return exposure 

284 

285 

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

287 """Make a `RegionTimeInfo` for testing 

288 

289 Parameters 

290 ---------- 

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

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

293 If None, a default exposure will be created. 

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

295 The start time of the interval. 

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

297 and the exposure length. 

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

299 The end time of the interval. 

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

301 and the exposure length. 

302 

303 Returns 

304 ------- 

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

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

307 """ 

308 if exposure is None: 

309 exposure = makeExposure() 

310 region = getRegion(exposure) 

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

312 # visitInfo time is the midpoint of the exposure. 

313 if begin is None: 

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

315 if end is None: 

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

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

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