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

197 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2025-12-20 01:16 -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"""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 # TODO DM-53699: the source_column should be made consistent, and 

109 # should match the definition in sdm_schemas. 

110 source_columns = DIA_COLUMNS.copy() 

111 if 'diaSourceId' in diaSourceCatalog.columns: 

112 source_column = 'diaSourceId' 

113 else: 

114 source_column = 'id' 

115 source_columns[source_columns.index('diaSourceId')] = source_column 

116 nSolarSystemObjects = len(ssObjects) 

117 if nSolarSystemObjects <= 0: 

118 return self._return_empty(diaSourceCatalog, ssObjects, source_column=source_column) 

119 

120 exposure_midpoint = visitInfo.date.toAstropy() 

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

122 ref_time = exposure_midpoint.tai.mjd - ssObjects["tmin"].value[0] # all tmin should be identical 

123 ssObjects['obs_position'] = [ 

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

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

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

127 for row in ssObjects] 

128 ssObjects['obs_velocity'] = [ 

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

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

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

132 for row in ssObjects] 

133 ssObjects['obj_position'] = [ 

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

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

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

137 for row in ssObjects] 

138 ssObjects['obj_velocity'] = [ 

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

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

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

142 for row in ssObjects] 

143 vector = np.vstack(ssObjects['obj_position'].value - ssObjects['obs_position'].value) 

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

145 ssObjects['ephRa'] = ras 

146 ssObjects['ephDec'] = decs 

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

148 ssObjects['obs_position_z'] = ssObjects['obs_position'].value.T 

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

150 ssObjects['helio_z'] = ssObjects['obj_position'].value.T 

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

152 ssObjects['obs_velocity_z'] = ssObjects['obs_velocity'].value.T 

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

154 ssObjects['helio_vz'] = ssObjects['obj_velocity'].value.T 

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

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

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

158 ) 

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

160 np.array(list(ssObjects['topocentric_position'].value)).T 

161 ) 

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

163 np.array(list(ssObjects['topocentric_velocity'].value)).T 

164 ) 

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

166 ssObjects['helio_vz'] = np.array(list(ssObjects['obj_velocity'].value)).T 

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

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

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

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

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

172 ))) 

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

174 # Fix in DM-53463 

175 ssObjects['RARateCosDec_deg_day'] = 0 

176 ssObjects['DecRate_deg_day'] = 0 

177 ssObjects['trailedSourceMagTrue'] = 0 

178 ssObjects['RangeRate_LTC_km_s'] = 0 

179 

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

181 

182 columns_to_drop = [ 

183 "obs_position", "obs_velocity", "obj_position", "obj_velocity", "topocentric_position", 

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

185 "obj_z_poly", "associated" 

186 ] 

187 

188 else: # Sorcha ephemerides 

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

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

191 ssObjects.rename_columns( 

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

193 'Obj_Sun_y_LTC_km', 'Obj_Sun_z_LTC_km', 'Obj_Sun_vx_LTC_km_s', 'Obj_Sun_vy_LTC_km_s', 

194 'Obj_Sun_vz_LTC_km_s'], 

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

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

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

198 ssObjects['helioRange'] = ( 

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

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

201 ) 

202 for substring1, substring2 in [('x', 'x_km'), ('y', 'y_km'), ('z', 'z_km'), 

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

204 topoName = 'topo_' + substring1 

205 helioName = 'helio_' + substring1 

206 obsName = 'Obs_Sun_' + substring2 

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

208 

209 marginArcsec = 1.0 # TODO: justify 

210 

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

212 'Obs_Sun_x_km', 'Obs_Sun_y_km', 'Obs_Sun_z_km', 

213 'Obs_Sun_vx_km_s', 'Obs_Sun_vy_km_s', 'Obs_Sun_vz_km_s', 

214 '__index_level_0__'] 

215 

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

217 'helio_vy', 'helio_vz', 'topo_x', 'topo_y', 

218 'topo_z', 'topo_vx', 'topo_vy', 'topo_vz'] 

219 

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

221 

222 maskedObjects = self._maskToCcdRegion( 

223 ssObjects, 

224 bbox, 

225 wcs, 

226 marginArcsec).copy() 

227 nSolarSystemObjects = len(maskedObjects) 

228 if nSolarSystemObjects <= 0: 

229 return self._return_empty(diaSourceCatalog, maskedObjects, source_column=source_column) 

230 

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

232 

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

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

235 diaSourceCatalog["dec"]) 

236 

237 # Create KDTree of DIA sources 

238 tree = cKDTree(vectors) 

239 

240 nFound = 0 

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

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

243 # fancier later. 

244 ssSourceData, ssObjectIds, prov_ids = [], [], [] 

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

246 diaSourceCatalog["ssObjectId"] = 0 

247 maskedObjects['associated'] = False 

248 

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

250 nearby_obj_source_pairs = [] 

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

252 ssoVect = self._radec_to_xyz(ra, dec) 

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

254 if not np.isfinite(dist): 

255 continue 

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

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

258 nearby_obj_source_pairs = sorted(nearby_obj_source_pairs) 

259 

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

261 # Skipping already-associated sources and objects. 

262 used_src_indices, used_obj_indices = set(), set() 

263 maskedObjects['associated'] = False 

264 for dist, obj_idx, src_idx in nearby_obj_source_pairs: 

265 if src_idx in used_src_indices or obj_idx in used_obj_indices: 

266 continue 

267 maskedObject = maskedObjects[obj_idx] 

268 used_src_indices.add(src_idx) 

269 used_obj_indices.add(obj_idx) 

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

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

272 all_cols = ( 

273 ["ObjID", "phaseAngle", "helioRange", "topoRange"] + stateVectorColumns + mpcorbColumns 

274 + ["ephRa", "ephDec", "RARateCosDec_deg_day", 

275 "DecRate_deg_day", "trailedSourceMagTrue", "RangeRate_LTC_km_s"] 

276 ) 

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

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

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

280 dia_id = diaSourceCatalog[src_idx][source_column] 

281 ras.append(dia_ra) 

282 decs.append(dia_dec) 

283 dia_ids.append(dia_id) 

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

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

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

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

288 nFound = len(ras) 

289 

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

291 self.metadata['nAssociatedSsObjects'] = nFound 

292 self.metadata['nExpectedSsObjects'] = nSolarSystemObjects 

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

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

295 ssSourceData = np.array(ssSourceData) 

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

297 colnames += stateVectorColumns + mpcorbColumns 

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

299 ssSourceData = Table(ssSourceData, names=colnames, dtype=[str] + [np.float64] * (len(colnames) - 1)) 

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

301 ssSourceData["ra"] = ras 

302 ssSourceData["dec"] = decs 

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

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

305 ssSourceData["ephOffsetRa"] = ephOffsetRa 

306 ssSourceData["ephOffsetDec"] = ephOffsetDec 

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

308 ssSourceData[source_column] = dia_ids 

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

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

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

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

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

314 ssSourceData['designation'] = prov_ids 

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

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

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

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

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

320 + ssSourceData['topo_vy'] ** 2 

321 + ssSourceData['topo_vy'] ** 2) 

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

323 + ssSourceData['helio_vy'] ** 2 

324 + ssSourceData['helio_vy'] ** 2) 

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

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

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

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

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

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

331 ssSourceData['diaDistanceRank'] = 1 

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

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

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

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

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

337 + sun_obs_z * ssSourceData['topo_z']) 

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

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

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

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

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

343 / ssSourceData["helioRange"]) 

344 

345 for distanceName in ['helio_x', 'helio_y', 'helio_z', 'topo_x', 'topo_y', 'topo_z', 

346 'helioRange', 'topoRange']: 

347 ssSourceData[distanceName] = ssSourceData[distanceName].astype(np.float64) 

348 ssSourceData[distanceName] *= (u.km).to(u.AU) 

349 unassociatedObjects = maskedObjects[unAssocObjectMask] 

350 unassociatedObjects.remove_columns(columns_to_drop) 

351 unassociatedObjects['ra'] = unassociatedObjects['ephRa'] 

352 unassociatedObjects['dec'] = unassociatedObjects['ephDec'] 

353 

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

355 if len(ssSourceData): 

356 

357 # Extract only DIA_COLUMNS 

358 dia = diaSourceCatalog[source_columns].copy() 

359 

360 # Prefix all except diaSourceId 

361 for c in source_columns: 

362 if c != source_column: 

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

364 

365 # Join on diaSourceId, keeping all rows of ssSourceData 

366 ssSourceData = join(ssSourceData, dia, keys=source_column, join_type="left", uniq_col_name="") 

367 

368 return pipeBase.Struct( 

369 ssoAssocDiaSources=diaSourceCatalog[assocSourceMask], 

370 unAssocDiaSources=diaSourceCatalog[~assocSourceMask], 

371 nTotalSsObjects=nSolarSystemObjects, 

372 nAssociatedSsObjects=nFound, 

373 associatedSsSources=ssSourceData, 

374 unassociatedSsObjects=unassociatedObjects) 

375 

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

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

378 bounding box. 

379 

380 Parameters 

381 ---------- 

382 ssObjects : `astropy.table.Table` 

383 SolarSystemObjects to mask to ``exposure``. 

384 bbox : 

385 Exposure bbox used for masking 

386 wcs : 

387 Exposure wcs used for masking 

388 marginArcsec : `float` 

389 Maximum possible matching radius to pad onto the exposure bounding 

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

391 be used. 

392 

393 Returns 

394 ------- 

395 maskedSolarSystemObjects : `astropy.table.Table` 

396 Set of SolarSystemObjects contained within the exposure bounds. 

397 """ 

398 if len(ssObjects) == 0: 

399 return ssObjects 

400 padding = min( 

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

402 self.config.maxPixelMargin) 

403 

404 return ssObjects[bbox_contains_sky_coords( 

405 bbox, 

406 wcs, 

407 ssObjects['ephRa'].value * u.degree, 

408 ssObjects['ephDec'].value * u.degree, 

409 padding)] 

410 

411 def _radec_to_xyz(self, ras, decs): 

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

413 

414 Parameters 

415 ---------- 

416 ras : `array-like` 

417 RA coordinates of objects in degrees. 

418 decs : `array-like` 

419 DEC coordinates of objects in degrees. 

420 

421 Returns 

422 ------- 

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

424 Output unit-vectors 

425 """ 

426 ras = np.radians(ras) 

427 decs = np.radians(decs) 

428 try: 

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

430 except TypeError: 

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

432 

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

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

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

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

437 

438 return vectors 

439 

440 def _return_empty(self, diaSourceCatalog, emptySolarSystemObjects, source_column): 

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

442 

443 Parameters 

444 ---------- 

445 diaSourceCatalog : `astropy.table.Table` 

446 Used for column names 

447 emptySolarSystemObjects : `astropy.table.Table` 

448 Used for column names. 

449 Returns 

450 ------- 

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

452 Results struct with components. 

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

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

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

456 - ``nAssociatedSsObjects`` : Zero. 

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

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

459 

460 

461 Raises 

462 ------ 

463 RuntimeError 

464 Raised if duplicate DiaObjects or duplicate DiaSources are found. 

465 """ 

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

467 # TODO DM-53699: these column names and dtypes should match the 

468 # definitions in sdm_schemas. 

469 dia_columns = [col for col in DIA_COLUMNS if not col == source_column] 

470 dia_dtypes = [d[0] for d in zip(DIA_DTYPES, DIA_COLUMNS) if not d[1] == source_column] 

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

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

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

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

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

476 'topoRangeRate', source_column, 'ssObjectId', 'diaDistanceRank'] + dia_columns 

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

478 return pipeBase.Struct( 

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

480 unAssocDiaSources=diaSourceCatalog, 

481 nTotalSsObjects=0, 

482 nAssociatedSsObjects=0, 

483 associatedSsSources=Table(names=names, 

484 dtype=dtypes), 

485 unassociatedSsObjects=Table(names=emptySolarSystemObjects.columns) 

486 )