Coverage for python / lsst / pipe / tasks / ssoAssociation.py: 10%

204 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-05-05 18:38 +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"""Spatial association for Solar System Objects.""" 

23 

24__all__ = ["SolarSystemAssociationConfig", "SolarSystemAssociationTask"] 

25 

26from astropy import units as u 

27from astropy.coordinates import SkyCoord 

28from astropy.table import join, Column, Table 

29 

30import healpy as hp 

31import numpy as np 

32from numpy.polynomial.chebyshev import Chebyshev, chebval 

33from scipy.spatial import cKDTree 

34 

35from lsst.afw.image.exposure.exposureUtils import bbox_contains_sky_coords 

36import lsst.pex.config as pexConfig 

37import lsst.pipe.base as pipeBase 

38from lsst.utils.timer import timeMethod 

39from lsst.pipe.tasks.associationUtils import obj_id_to_ss_object_id 

40 

41from .ssp.ssobject import DIA_COLUMNS, DIA_DTYPES 

42 

43 

44class SolarSystemAssociationConfig(pexConfig.Config): 

45 """Config class for SolarSystemAssociationTask. 

46 """ 

47 maxDistArcSeconds = pexConfig.Field( 

48 dtype=float, 

49 doc='Maximum distance in arcseconds to test for a DIASource to be a ' 

50 'match to a SSObject.', 

51 default=1.0, 

52 ) 

53 maxPixelMargin = pexConfig.RangeField( 

54 doc="Maximum padding to add to the ccd bounding box before masking " 

55 "SolarSystem objects to the ccd footprint. The bounding box will " 

56 "be padded by the minimum of this number or the max uncertainty " 

57 "of the SolarSystemObjects in pixels.", 

58 dtype=int, 

59 default=100, 

60 min=0, 

61 ) 

62 

63 

64class SolarSystemAssociationTask(pipeBase.Task): 

65 """Associate DIASources into existing SolarSystem Objects. 

66 

67 This task performs the association of detected DIASources in a visit 

68 with known solar system objects. 

69 """ 

70 ConfigClass = SolarSystemAssociationConfig 

71 _DefaultName = "ssoAssociation" 

72 

73 @timeMethod 

74 def run(self, diaSourceCatalog, ssObjects, visitInfo, bbox, wcs): 

75 """Create a searchable tree of unassociated DiaSources and match 

76 to the nearest ssoObject. 

77 

78 Parameters 

79 ---------- 

80 diaSourceCatalog : `astropy.table.Table` 

81 Catalog of DiaSources. Modified in place to add ssObjectId to 

82 successfully associated DiaSources. 

83 ssObjects : `astropy.table.Table` 

84 Set of solar system objects that should be within the footprint 

85 of the current visit. 

86 visitInfo : `lsst.afw.image.VisitInfo` 

87 visitInfo of exposure used for exposure time 

88 bbox : `lsst.geom.Box2I` 

89 bbox of exposure used for masking 

90 wcs : `lsst.afw.geom.SkyWcs` 

91 wcs of exposure used for masking 

92 

93 Returns 

94 ------- 

95 resultsStruct : `lsst.pipe.base.Struct` 

96 

97 - ``ssoAssocDiaSources`` : DiaSources that were associated with 

98 solar system objects in this visit. (`astropy.table.Table`) 

99 - ``unAssocDiaSources`` : Set of DiaSources that were not 

100 associated with any solar system object. (`astropy.table.Table`) 

101 - ``nTotalSsObjects`` : Total number of SolarSystemObjects 

102 contained in the CCD footprint. (`int`) 

103 - ``nAssociatedSsObjects`` : Number of SolarSystemObjects 

104 that were associated with DiaSources. (`int`) 

105 - ``ssSourceData`` : ssSource table data. (`Astropy.table.Table`) 

106 """ 

107 

108 nSolarSystemObjects = len(ssObjects) 

109 if nSolarSystemObjects <= 0: 

110 return self._return_empty(diaSourceCatalog, ssObjects) 

111 

112 exposure_midpoint = visitInfo.date.toAstropy() 

113 if 'obs_x_poly' in ssObjects.columns: # mpSky ephemeris 

114 # tmin, tmax are mjd. All tmin should be identical, so just take the first. 

115 ref_time = exposure_midpoint.tai.mjd - ssObjects["tmin"].quantity.to_value(u.d)[0] 

116 # *_poly are Chebyshev polynomials encoding the observer and object positions. 

117 # Positions are received in equatorial, cartesian au. Velocities are in au/d. 

118 ssObjects['obs_position'] = [ 

119 np.array([chebval(ref_time, row['obs_x_poly']), 

120 chebval(ref_time, row['obs_y_poly']), 

121 chebval(ref_time, row['obs_z_poly'])]) 

122 for row in ssObjects] * u.au 

123 ssObjects['obs_velocity'] = [ 

124 np.array([chebval(ref_time, Chebyshev(row['obs_x_poly']).deriv().coef), 

125 chebval(ref_time, Chebyshev(row['obs_y_poly']).deriv().coef), 

126 chebval(ref_time, Chebyshev(row['obs_z_poly']).deriv().coef)]) 

127 for row in ssObjects] * u.au/u.d 

128 ssObjects['obj_position'] = [ 

129 np.array([chebval(ref_time, row['obj_x_poly']), 

130 chebval(ref_time, row['obj_y_poly']), 

131 chebval(ref_time, row['obj_z_poly'])]) 

132 for row in ssObjects] * u.au 

133 ssObjects['obj_velocity'] = [ 

134 np.array([chebval(ref_time, Chebyshev(row['obj_x_poly']).deriv().coef), 

135 chebval(ref_time, Chebyshev(row['obj_y_poly']).deriv().coef), 

136 chebval(ref_time, Chebyshev(row['obj_z_poly']).deriv().coef)]) 

137 for row in ssObjects] * u.au/u.d 

138 

139 vector = np.vstack(ssObjects['obj_position'].quantity.to_value(u.au) 

140 - ssObjects['obs_position'].quantity.to_value(u.au)) 

141 ras, decs = np.vstack(hp.vec2ang(vector, lonlat=True)) 

142 # Angles in degrees 

143 ssObjects['ephRa'] = ras * u.deg 

144 ssObjects['ephDec'] = decs * u.deg 

145 # Positions in au, velocities in km/s. 

146 ssObjects['obs_position_x'], ssObjects['obs_position_y'], \ 

147 ssObjects['obs_position_z'] = ssObjects['obs_position'].quantity.to_value(u.au).T 

148 ssObjects['helio_x'], ssObjects['helio_y'], \ 

149 ssObjects['helio_z'] = ssObjects['obj_position'].quantity.to_value(u.au).T 

150 ssObjects['obs_velocity_x'], ssObjects['obs_velocity_y'], \ 

151 ssObjects['obs_velocity_z'] = ssObjects['obs_velocity'].quantity.to_value(u.km/u.s).T 

152 ssObjects['helio_vx'], ssObjects['helio_vy'], \ 

153 ssObjects['helio_vz'] = ssObjects['obj_velocity'].quantity.to_value(u.km/u.s).T 

154 ssObjects['topocentric_position'], ssObjects['topocentric_velocity'] = ( 

155 ssObjects['obj_position'] - ssObjects['obs_position'], 

156 ssObjects['obj_velocity'] - ssObjects['obs_velocity'], 

157 ) 

158 ssObjects['topo_x'], ssObjects['topo_y'], ssObjects['topo_z'] = ( 

159 np.array(list(ssObjects['topocentric_position'].quantity.to_value(u.au))).T 

160 ) 

161 ssObjects['topo_vx'], ssObjects['topo_vy'], ssObjects['topo_vz'] = ( 

162 np.array(list(ssObjects['topocentric_velocity'].quantity.to_value(u.km/u.s))).T 

163 ) 

164 ssObjects['helioRange'] = np.linalg.norm(ssObjects['obj_position'], axis=1) 

165 ssObjects['topoRange'] = np.linalg.norm(ssObjects['topocentric_position'], axis=1) 

166 

167 # Phase angle in degrees 

168 ssObjects['phaseAngle'] = np.degrees(np.arccos(np.sum( 

169 ssObjects['obj_position'].T * ssObjects['topocentric_position'].T 

170 / ssObjects['helioRange'] / ssObjects['topoRange'], axis=0 

171 ))) 

172 

173 # ssObjects['trailedSourceMagTrue'] should already be filled 

174 # Add other required columns with dummy values until we compute them properly. 

175 # Fix in DM-53463 

176 ssObjects['RARateCosDec_deg_day'] = 0 

177 ssObjects['DecRate_deg_day'] = 0 

178 ssObjects['RangeRate_LTC_km_s'] = 0 

179 

180 marginArcsec = ssObjects["Err(arcsec)"].max() 

181 ssObjects['designation'] = ssObjects['MPCORB_unpacked_primary_provisional_designation'] 

182 

183 columns_to_drop = [ 

184 "obs_position", "obs_velocity", "obj_position", "obj_velocity", "topocentric_position", 

185 "topocentric_velocity", "obs_x_poly", "obs_y_poly", "obs_z_poly", "obj_x_poly", "obj_y_poly", 

186 "obj_z_poly", "associated" 

187 ] 

188 

189 else: # Sorcha ephemerides 

190 if 'trailedSourceMagTrue' not in ssObjects.columns: # Only possible for historical CI ephemerides 

191 ssObjects['trailedSourceMagTrue'] = 0 # Fix in DM-53462 

192 ssObjects.rename_columns( 

193 ['RATrue_deg', 'DecTrue_deg', 'phase_deg', 'Range_LTC_km', 'Obj_Sun_x_LTC_km', 

194 'Obj_Sun_y_LTC_km', 'Obj_Sun_z_LTC_km', 'Obj_Sun_vx_LTC_km_s', 'Obj_Sun_vy_LTC_km_s', 

195 'Obj_Sun_vz_LTC_km_s'], 

196 ['ephRa', 'ephDec', 'phaseAngle', 'topoRange', 'helio_x', 'helio_y', 

197 'helio_z', 'helio_vx', 'helio_vy', 'helio_vz']) 

198 for col in ['topoRange', 'helio_x', 'helio_y', 'helio_z']: 

199 ssObjects[col] = ssObjects[col].quantity.to_value(u.au) 

200 for col in ['Obs_Sun_x_km', 'Obs_Sun_y_km', 'Obs_Sun_z_km']: 

201 ssObjects[col.replace('km', 'au')] = ssObjects[col].quantity.to_value(u.au) 

202 for col in ['helio_vx', 'helio_vy', 'helio_vz', 'Obs_Sun_vx_km_s', 'Obs_Sun_vy_km_s', 

203 'Obs_Sun_vz_km_s']: 

204 ssObjects[col] = ssObjects[col].quantity.to_value(u.km/u.s) 

205 

206 ssObjects['ssObjectId'] = [obj_id_to_ss_object_id(v) for v in ssObjects['packed_desig']] 

207 for substring1, substring2 in [('x', 'x_au'), ('y', 'y_au'), ('z', 'z_au'), 

208 ('vx', 'vx_km_s'), ('vy', 'vy_km_s'), ('vz', 'vz_km_s')]: 

209 topoName = 'topo_' + substring1 

210 helioName = 'helio_' + substring1 

211 obsName = 'Obs_Sun_' + substring2 

212 ssObjects[topoName] = ssObjects[helioName] - ssObjects[obsName] 

213 

214 ssObjects['helioRange'] = ( 

215 np.sqrt(ssObjects['helio_x']**2 + ssObjects['helio_y']**2 

216 + ssObjects['helio_z']**2) 

217 ) 

218 

219 ssObjects['designation'] = ssObjects['ObjID'] 

220 

221 marginArcsec = 1.0 # TODO: justify 

222 

223 columns_to_drop = ['FieldID', 'fieldMJD_TAI', 'fieldJD_TDB', 

224 'Obs_Sun_x_km', 'Obs_Sun_y_km', 'Obs_Sun_z_km', 

225 'Obs_Sun_vx_km_s', 'Obs_Sun_vy_km_s', 'Obs_Sun_vz_km_s', 

226 'Obs_Sun_x_au', 'Obs_Sun_y_au', 'Obs_Sun_z_au'] 

227 

228 stateVectorColumns = ['helio_x', 'helio_y', 'helio_z', 'helio_vx', 

229 'helio_vy', 'helio_vz', 'topo_x', 'topo_y', 

230 'topo_z', 'topo_vx', 'topo_vy', 'topo_vz'] 

231 

232 mpcorbColumns = [col for col in ssObjects.columns if col[:7] == 'MPCORB_'] 

233 maskedObjects = self._maskToCcdRegion( 

234 ssObjects, 

235 bbox, 

236 wcs, 

237 marginArcsec).copy() 

238 nSolarSystemObjects = len(maskedObjects) 

239 if nSolarSystemObjects <= 0: 

240 return self._return_empty(diaSourceCatalog, maskedObjects) 

241 

242 maxRadius = np.deg2rad(self.config.maxDistArcSeconds / 3600) 

243 

244 # Transform DIA RADEC coordinates to unit sphere xyz for tree building. 

245 vectors = self._radec_to_xyz(diaSourceCatalog["ra"], 

246 diaSourceCatalog["dec"]) 

247 

248 # Create KDTree of DIA sources 

249 tree = cKDTree(vectors) 

250 

251 nFound = 0 

252 # Query the KDtree for DIA nearest neighbors to SSOs. Currently only 

253 # picks the DiaSource with the shortest distance. We can do something 

254 # fancier later. 

255 ssSourceData, ssObjectIds, prov_ids = [], [], [] 

256 ras, decs, residual_ras, residual_decs, dia_ids = [], [], [], [], [] 

257 diaSourceCatalog["ssObjectId"] = 0 

258 source_column = 'id' 

259 maskedObjects['associated'] = False 

260 if 'diaSourceId' in diaSourceCatalog.columns: 

261 source_column = 'diaSourceId' 

262 

263 # Find all pairs of a source and an object within maxRadius 

264 nearby_obj_source_pairs = [] 

265 for obj_idx, (ra, dec) in enumerate(zip(maskedObjects["ephRa"].data, maskedObjects["ephDec"].data)): 

266 ssoVect = self._radec_to_xyz(ra, dec) 

267 dist, idx = tree.query(ssoVect, distance_upper_bound=maxRadius) 

268 if not np.isfinite(dist): 

269 continue 

270 for i in range(len(dist)): 

271 nearby_obj_source_pairs.append((dist, obj_idx, idx[i])) 

272 nearby_obj_source_pairs = sorted(nearby_obj_source_pairs) 

273 

274 # From closest to farthest, associate diaSources to SSOs. 

275 # Skipping already-associated sources and objects. 

276 all_cols = ( 

277 ["designation", "phaseAngle", "helioRange", "topoRange"] + stateVectorColumns + mpcorbColumns 

278 + ["ephRa", "ephDec", "RARateCosDec_deg_day", 

279 "DecRate_deg_day", "trailedSourceMagTrue", "RangeRate_LTC_km_s"] 

280 ) 

281 

282 used_src_indices, used_obj_indices = set(), set() 

283 maskedObjects['associated'] = False 

284 for dist, obj_idx, src_idx in nearby_obj_source_pairs: 

285 if src_idx in used_src_indices or obj_idx in used_obj_indices: 

286 continue 

287 maskedObject = maskedObjects[obj_idx] 

288 used_src_indices.add(src_idx) 

289 used_obj_indices.add(obj_idx) 

290 diaSourceCatalog[src_idx]["ssObjectId"] = maskedObject["ssObjectId"] 

291 ssObjectIds.append(maskedObject["ssObjectId"]) 

292 ssSourceData.append(list(maskedObject[all_cols].values())) 

293 dia_ra = diaSourceCatalog[src_idx]["ra"] 

294 dia_dec = diaSourceCatalog[src_idx]["dec"] 

295 dia_id = diaSourceCatalog[src_idx][source_column] 

296 ras.append(dia_ra) 

297 decs.append(dia_dec) 

298 dia_ids.append(dia_id) 

299 residual_ras.append(dia_ra - maskedObject["ephRa"]) 

300 residual_decs.append(dia_dec - maskedObject["ephDec"]) 

301 prov_ids.append(maskedObject['ObjID']) 

302 maskedObjects['associated'][obj_idx] = True 

303 nFound = len(ras) 

304 

305 self.log.info("Successfully associated %d / %d SolarSystemObjects.", nFound, nSolarSystemObjects) 

306 self.metadata['nAssociatedSsObjects'] = nFound 

307 self.metadata['nExpectedSsObjects'] = nSolarSystemObjects 

308 assocSourceMask = diaSourceCatalog["ssObjectId"] != 0 

309 unAssocObjectMask = np.logical_not(maskedObjects['associated'].value) 

310 dtypes = [float if d is np.ma.core.MaskedConstant else type(d) 

311 for d in maskedObjects[0][all_cols].values()] 

312 ssSourceData = np.ma.filled(np.array(ssSourceData), np.nan) 

313 colnames = ["designation", "phaseAngle", "helioRange", "topoRange"] 

314 colnames += stateVectorColumns + mpcorbColumns 

315 colnames += ["ephRa", "ephDec", "ephRateRa", "ephRateDec", "ephVmag", "topoRangeRate"] 

316 ssSourceData = Table(ssSourceData, names=colnames, dtype=dtypes) 

317 if 'MPCORB_created_at' in ssSourceData.columns: 

318 ssSourceData['MPCORB_created_at'] = 0 

319 ssSourceData['MPCORB_updated_at'] = 0 

320 ssSourceData['MPCORB_fitting_datetime'] = 0 

321 for c in ['MPCORB_created_at', 'MPCORB_updated_at', 'MPCORB_fitting_datetime', 

322 'MPCORB_orbit_type_int', 'MPCORB_u_param']: 

323 ssSourceData[c] = ssSourceData[c].astype(np.int64) 

324 ssSourceData['ssObjectId'] = Column(data=ssObjectIds, dtype=int) 

325 ssSourceData["ra"] = ras 

326 ssSourceData["dec"] = decs 

327 ephOffsetRa = np.array(residual_ras * np.cos(np.radians(ssSourceData["dec"]))) * 3600 # in arcsec 

328 ephOffsetDec = np.array(residual_decs) * 3600 # in arcsec 

329 ssSourceData["ephOffsetRa"] = ephOffsetRa 

330 ssSourceData["ephOffsetDec"] = ephOffsetDec 

331 ephOffsetVec = np.array([ephOffsetRa, ephOffsetDec]) 

332 ssSourceData[source_column] = dia_ids 

333 coords = SkyCoord(ra=ssSourceData['ra'] * u.deg, dec=ssSourceData['dec'] * u.deg) 

334 ssSourceData['galLon'] = coords.galactic.l.deg 

335 ssSourceData['galLat'] = coords.galactic.b.deg 

336 ssSourceData['eclLambda'] = coords.barycentrictrueecliptic.lon.deg 

337 ssSourceData['eclBeta'] = coords.barycentrictrueecliptic.lat.deg 

338 ssSourceData['ephRate'] = np.sqrt((ssSourceData['ephRateRa']) ** 2 

339 + (ssSourceData['ephRateDec']) ** 2) 

340 ssSourceData['ephOffset'] = np.sqrt((ssSourceData['ephOffsetRa']) ** 2 

341 + (ssSourceData['ephOffsetDec']) ** 2) 

342 ssSourceData['topo_vtot'] = np.sqrt(ssSourceData['topo_vx'] ** 2 

343 + ssSourceData['topo_vy'] ** 2 

344 + ssSourceData['topo_vz'] ** 2) 

345 ssSourceData['helio_vtot'] = np.sqrt(ssSourceData['helio_vx'] ** 2 

346 + ssSourceData['helio_vy'] ** 2 

347 + ssSourceData['helio_vz'] ** 2) 

348 skyMotionNormal0 = (ssSourceData['ephRateRa']/ssSourceData['ephRate']).data 

349 skyMotionNormal1 = (ssSourceData['ephRateDec']/ssSourceData['ephRate']).data 

350 skyMotionNormal = np.array([skyMotionNormal0, skyMotionNormal1]) 

351 skyMotionOrthogonal = np.array([[0, -1], [1, 0]]) @ skyMotionNormal 

352 ssSourceData['ephOffsetAlongTrack'] = (ephOffsetVec * skyMotionNormal).sum(axis=0) 

353 ssSourceData['ephOffsetCrossTrack'] = (ephOffsetVec * skyMotionOrthogonal).sum(axis=0) 

354 ssSourceData['diaDistanceRank'] = 1 

355 sun_obs_x = ssSourceData['topo_x'] - ssSourceData['helio_x'] 

356 sun_obs_y = ssSourceData['topo_y'] - ssSourceData['helio_y'] 

357 sun_obs_z = ssSourceData['topo_z'] - ssSourceData['helio_z'] 

358 sun_obs_range = np.sqrt(sun_obs_x**2 + sun_obs_y**2 + sun_obs_z**2) 

359 sun_obj_dot = (sun_obs_x * ssSourceData['topo_x'] + sun_obs_y * ssSourceData['topo_y'] 

360 + sun_obs_z * ssSourceData['topo_z']) 

361 ssSourceData["elongation"] = np.degrees(np.arccos(sun_obj_dot 

362 / (sun_obs_range * ssSourceData['topoRange']))) 

363 ssSourceData["helioRangeRate"] = ((ssSourceData["helio_vx"] * ssSourceData["helio_x"] 

364 + ssSourceData["helio_vy"] * ssSourceData["helio_y"] 

365 + ssSourceData["helio_vz"] * ssSourceData["helio_z"]) 

366 / ssSourceData["helioRange"]) 

367 

368 unassociatedObjects = maskedObjects[unAssocObjectMask] 

369 unassociatedObjects.remove_columns(columns_to_drop) 

370 unassociatedObjects['ra'] = unassociatedObjects['ephRa'] 

371 unassociatedObjects['dec'] = unassociatedObjects['ephDec'] 

372 

373 # Add diaSource columns we care about when producing the SSObject table 

374 if len(ssSourceData): 

375 

376 # Extract only DIA_COLUMNS 

377 dia = diaSourceCatalog[DIA_COLUMNS].copy() 

378 

379 # Prefix all except diaSourceId 

380 for c in DIA_COLUMNS: 

381 if c != "diaSourceId": 

382 dia.rename_column(c, f"DIA_{c}") 

383 

384 # Join on diaSourceId, keeping all rows of ssSourceData 

385 ssSourceData = join(ssSourceData, dia, keys="diaSourceId", join_type="left", uniq_col_name="") 

386 

387 return pipeBase.Struct( 

388 ssoAssocDiaSources=diaSourceCatalog[assocSourceMask], 

389 unAssocDiaSources=diaSourceCatalog[~assocSourceMask], 

390 nTotalSsObjects=nSolarSystemObjects, 

391 nAssociatedSsObjects=nFound, 

392 associatedSsSources=ssSourceData, 

393 unassociatedSsObjects=unassociatedObjects) 

394 

395 def _maskToCcdRegion(self, ssObjects, bbox, wcs, marginArcsec): 

396 """Mask the input SolarSystemObjects to only those in the exposure 

397 bounding box. 

398 

399 Parameters 

400 ---------- 

401 ssObjects : `astropy.table.Table` 

402 SolarSystemObjects to mask to ``exposure``. 

403 bbox : 

404 Exposure bbox used for masking 

405 wcs : 

406 Exposure wcs used for masking 

407 marginArcsec : `float` 

408 Maximum possible matching radius to pad onto the exposure bounding 

409 box. If greater than ``maxPixelMargin``, ``maxPixelMargin`` will 

410 be used. 

411 

412 Returns 

413 ------- 

414 maskedSolarSystemObjects : `astropy.table.Table` 

415 Set of SolarSystemObjects contained within the exposure bounds. 

416 """ 

417 if len(ssObjects) == 0: 

418 return ssObjects 

419 padding = min( 

420 int(np.ceil(marginArcsec / wcs.getPixelScale(bbox.getCenter()).asArcseconds())), 

421 self.config.maxPixelMargin) 

422 

423 return ssObjects[bbox_contains_sky_coords( 

424 bbox, 

425 wcs, 

426 ssObjects['ephRa'], 

427 ssObjects['ephDec'], 

428 padding)] 

429 

430 def _radec_to_xyz(self, ras, decs): 

431 """Convert input ra/dec coordinates to spherical unit-vectors. 

432 

433 Parameters 

434 ---------- 

435 ras : `array-like` 

436 RA coordinates of objects in degrees. 

437 decs : `array-like` 

438 DEC coordinates of objects in degrees. 

439 

440 Returns 

441 ------- 

442 vectors : `numpy.ndarray`, (N, 3) 

443 Output unit-vectors 

444 """ 

445 ras = np.radians(ras) 

446 decs = np.radians(decs) 

447 try: 

448 vectors = np.empty((len(ras), 3)) 

449 except TypeError: 

450 vectors = np.empty((1, 3)) 

451 

452 sin_dec = np.sin(np.pi / 2 - decs) 

453 vectors[:, 0] = sin_dec * np.cos(ras) 

454 vectors[:, 1] = sin_dec * np.sin(ras) 

455 vectors[:, 2] = np.cos(np.pi / 2 - decs) 

456 

457 return vectors 

458 

459 def _return_empty(self, diaSourceCatalog, emptySolarSystemObjects): 

460 """Return a struct with all appropriate empty values for no SSO associations. 

461 

462 Parameters 

463 ---------- 

464 diaSourceCatalog : `astropy.table.Table` 

465 Used for column names 

466 emptySolarSystemObjects : `astropy.table.Table` 

467 Used for column names. 

468 Returns 

469 ------- 

470 results : `lsst.pipe.base.Struct` 

471 Results struct with components. 

472 - ``ssoAssocDiaSources`` : Empty. (`astropy.table.Table`) 

473 - ``unAssocDiaSources`` : Input DiaSources. (`astropy.table.Table`) 

474 - ``nTotalSsObjects`` : Zero. (`int`) 

475 - ``nAssociatedSsObjects`` : Zero. 

476 - ``associatedSsSources`` : Empty. (`Astropy.table.Table`) 

477 - ``unassociatedSsObjects`` : Empty. (`Astropy.table.Table`) 

478 

479 

480 Raises 

481 ------ 

482 RuntimeError 

483 Raised if duplicate DiaObjects or duplicate DiaSources are found. 

484 """ 

485 self.log.info("No SolarSystemObjects found in detector bounding box.") 

486 dia_columns = [col for col in DIA_COLUMNS if not col == 'diaSourceId'] 

487 dia_dtypes = [d[0] for d in zip(DIA_DTYPES, DIA_COLUMNS) if not d[1] == 'diaSourceId'] 

488 names = ['designation', 'eclBeta', 'eclLambda', 'ephDec', 'ephOffsetDec', 'ephOffsetRa', 'ephRa', 

489 'galLat', 'galLon', 'elongation', 'ephOffset', 'ephOffsetAlongTrack', 'ephOffsetCrossTrack', 

490 'ephRate', 'ephRateDec', 'ephRateRa', 'ephVmag', 'helio_vtot', 'helio_vx', 'helio_vy', 

491 'helio_vz', 'helio_x', 'helio_y', 'helio_z', 'helioRange', 'helioRangeRate', 'phaseAngle', 

492 'topo_vtot', 'topo_vx', 'topo_vy', 'topo_vz', 'topo_x', 'topo_y', 'topo_z', 'topoRange', 

493 'topoRangeRate', 'diaSourceId', 'ssObjectId', 'diaDistanceRank'] + dia_columns 

494 dtypes = [str] + [float] * 35 + [int] * 3 + dia_dtypes 

495 return pipeBase.Struct( 

496 ssoAssocDiaSources=Table(names=diaSourceCatalog.columns), 

497 unAssocDiaSources=diaSourceCatalog, 

498 nTotalSsObjects=0, 

499 nAssociatedSsObjects=0, 

500 associatedSsSources=Table(names=names, 

501 dtype=dtypes), 

502 unassociatedSsObjects=Table(names=emptySolarSystemObjects.columns) 

503 )