lsst.pipe.tasks g0e6ec694fa+43f7a4e833
Loading...
Searching...
No Matches
ssoAssociation.py
Go to the documentation of this file.
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 )
_return_empty(self, diaSourceCatalog, emptySolarSystemObjects, source_column)
run(self, diaSourceCatalog, ssObjects, visitInfo, bbox, wcs)
_maskToCcdRegion(self, ssObjects, bbox, wcs, marginArcsec)