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
« 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/>.
22"""Spatial association for Solar System Objects."""
24__all__ = ["SolarSystemAssociationConfig", "SolarSystemAssociationTask"]
26from astropy import units as u
27from astropy.coordinates import SkyCoord
28from astropy.table import join, Column, Table
30import healpy as hp
31import numpy as np
32from numpy.polynomial.chebyshev import Chebyshev, chebval
33from scipy.spatial import cKDTree
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
41from .ssp.ssobject import DIA_COLUMNS, DIA_DTYPES
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 )
64class SolarSystemAssociationTask(pipeBase.Task):
65 """Associate DIASources into existing SolarSystem Objects.
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"
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.
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
93 Returns
94 -------
95 resultsStruct : `lsst.pipe.base.Struct`
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 """
108 nSolarSystemObjects = len(ssObjects)
109 if nSolarSystemObjects <= 0:
110 return self._return_empty(diaSourceCatalog, ssObjects)
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
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)
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 )))
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
180 marginArcsec = ssObjects["Err(arcsec)"].max()
181 ssObjects['designation'] = ssObjects['MPCORB_unpacked_primary_provisional_designation']
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 ]
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)
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]
214 ssObjects['helioRange'] = (
215 np.sqrt(ssObjects['helio_x']**2 + ssObjects['helio_y']**2
216 + ssObjects['helio_z']**2)
217 )
219 ssObjects['designation'] = ssObjects['ObjID']
221 marginArcsec = 1.0 # TODO: justify
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']
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']
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)
242 maxRadius = np.deg2rad(self.config.maxDistArcSeconds / 3600)
244 # Transform DIA RADEC coordinates to unit sphere xyz for tree building.
245 vectors = self._radec_to_xyz(diaSourceCatalog["ra"],
246 diaSourceCatalog["dec"])
248 # Create KDTree of DIA sources
249 tree = cKDTree(vectors)
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'
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)
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 )
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)
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"])
368 unassociatedObjects = maskedObjects[unAssocObjectMask]
369 unassociatedObjects.remove_columns(columns_to_drop)
370 unassociatedObjects['ra'] = unassociatedObjects['ephRa']
371 unassociatedObjects['dec'] = unassociatedObjects['ephDec']
373 # Add diaSource columns we care about when producing the SSObject table
374 if len(ssSourceData):
376 # Extract only DIA_COLUMNS
377 dia = diaSourceCatalog[DIA_COLUMNS].copy()
379 # Prefix all except diaSourceId
380 for c in DIA_COLUMNS:
381 if c != "diaSourceId":
382 dia.rename_column(c, f"DIA_{c}")
384 # Join on diaSourceId, keeping all rows of ssSourceData
385 ssSourceData = join(ssSourceData, dia, keys="diaSourceId", join_type="left", uniq_col_name="")
387 return pipeBase.Struct(
388 ssoAssocDiaSources=diaSourceCatalog[assocSourceMask],
389 unAssocDiaSources=diaSourceCatalog[~assocSourceMask],
390 nTotalSsObjects=nSolarSystemObjects,
391 nAssociatedSsObjects=nFound,
392 associatedSsSources=ssSourceData,
393 unassociatedSsObjects=unassociatedObjects)
395 def _maskToCcdRegion(self, ssObjects, bbox, wcs, marginArcsec):
396 """Mask the input SolarSystemObjects to only those in the exposure
397 bounding box.
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.
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)
423 return ssObjects[bbox_contains_sky_coords(
424 bbox,
425 wcs,
426 ssObjects['ephRa'],
427 ssObjects['ephDec'],
428 padding)]
430 def _radec_to_xyz(self, ras, decs):
431 """Convert input ra/dec coordinates to spherical unit-vectors.
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.
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))
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)
457 return vectors
459 def _return_empty(self, diaSourceCatalog, emptySolarSystemObjects):
460 """Return a struct with all appropriate empty values for no SSO associations.
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`)
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 )