Loading [MathJax]/extensions/tex2jax.js
LSST Applications g0fba68d861+05816baf74,g1ec0fe41b4+f536777771,g1fd858c14a+a9301854fb,g35bb328faa+fcb1d3bbc8,g4af146b050+a5c07d5b1d,g4d2262a081+6e5fcc2a4e,g53246c7159+fcb1d3bbc8,g56a49b3a55+9c12191793,g5a012ec0e7+3632fc3ff3,g60b5630c4e+ded28b650d,g67b6fd64d1+ed4b5058f4,g78460c75b0+2f9a1b4bcd,g786e29fd12+cf7ec2a62a,g8352419a5c+fcb1d3bbc8,g87b7deb4dc+7b42cf88bf,g8852436030+e5453db6e6,g89139ef638+ed4b5058f4,g8e3bb8577d+d38d73bdbd,g9125e01d80+fcb1d3bbc8,g94187f82dc+ded28b650d,g989de1cb63+ed4b5058f4,g9d31334357+ded28b650d,g9f33ca652e+50a8019d8c,gabe3b4be73+1e0a283bba,gabf8522325+fa80ff7197,gb1101e3267+d9fb1f8026,gb58c049af0+f03b321e39,gb665e3612d+2a0c9e9e84,gb89ab40317+ed4b5058f4,gcf25f946ba+e5453db6e6,gd6cbbdb0b4+bb83cc51f8,gdd1046aedd+ded28b650d,gde0f65d7ad+941d412827,ge278dab8ac+d65b3c2b70,ge410e46f29+ed4b5058f4,gf23fb2af72+b7cae620c0,gf5e32f922b+fcb1d3bbc8,gf67bdafdda+ed4b5058f4,w.2025.16
LSST Data Management Base Package
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros Modules Pages
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.table import Table, Column
28from astropy.coordinates import SkyCoord
29import healpy as hp
30import numpy as np
31from numpy.polynomial.chebyshev import Chebyshev, chebval
32from scipy.spatial import cKDTree
33
34from lsst.afw.image.exposure.exposureUtils import bbox_contains_sky_coords
35import lsst.pex.config as pexConfig
36import lsst.pipe.base as pipeBase
37from lsst.utils.timer import timeMethod
38
39
40class SolarSystemAssociationConfig(pexConfig.Config):
41 """Config class for SolarSystemAssociationTask.
42 """
43 maxDistArcSeconds = pexConfig.Field(
44 dtype=float,
45 doc='Maximum distance in arcseconds to test for a DIASource to be a '
46 'match to a SSObject.',
47 default=1.0,
48 )
49 maxPixelMargin = pexConfig.RangeField(
50 doc="Maximum padding to add to the ccd bounding box before masking "
51 "SolarSystem objects to the ccd footprint. The bounding box will "
52 "be padded by the minimum of this number or the max uncertainty "
53 "of the SolarSystemObjects in pixels.",
54 dtype=int,
55 default=100,
56 min=0,
57 )
58
59
60class SolarSystemAssociationTask(pipeBase.Task):
61 """Associate DIASources into existing SolarSystem Objects.
62
63 This task performs the association of detected DIASources in a visit
64 with known solar system objects.
65 """
66 ConfigClass = SolarSystemAssociationConfig
67 _DefaultName = "ssoAssociation"
68
69 @timeMethod
70 def run(self, diaSourceCatalog, solarSystemObjects, visitInfo, bbox, wcs):
71 """Create a searchable tree of unassociated DiaSources and match
72 to the nearest ssoObject.
73
74 Parameters
75 ----------
76 diaSourceCatalog : `astropy.table.Table`
77 Catalog of DiaSources. Modified in place to add ssObjectId to
78 successfully associated DiaSources.
79 solarSystemObjects : `astropy.table.Table`
80 Set of solar system objects that should be within the footprint
81 of the current visit.
82 visitInfo : `lsst.afw.image.VisitInfo`
83 visitInfo of exposure used for exposure time
84 bbox : `lsst.geom.Box2I`
85 bbox of exposure used for masking
86 wcs : `lsst.afw.geom.SkyWcs`
87 wcs of exposure used for masking
88
89 Returns
90 -------
91 resultsStruct : `lsst.pipe.base.Struct`
92
93 - ``ssoAssocDiaSources`` : DiaSources that were associated with
94 solar system objects in this visit. (`astropy.table.Table`)
95 - ``unAssocDiaSources`` : Set of DiaSources that were not
96 associated with any solar system object. (`astropy.table.Table`)
97 - ``nTotalSsObjects`` : Total number of SolarSystemObjects
98 contained in the CCD footprint. (`int`)
99 - ``nAssociatedSsObjects`` : Number of SolarSystemObjects
100 that were associated with DiaSources. (`int`)
101 - ``ssSourceData`` : ssSource table data. (`Astropy.table.Table`)
102 """
103 nSolarSystemObjects = len(solarSystemObjects)
104 if nSolarSystemObjects <= 0:
105 return self._return_empty(diaSourceCatalog, solarSystemObjects)
106
107 mjd_midpoint = visitInfo.date.toAstropy().tai.mjd
108 ref_time = mjd_midpoint - solarSystemObjects["tmin"].value[0] # all tmin should be identical
109
110 solarSystemObjects['obs_position'] = [
111 np.array([chebval(ref_time, row['obs_x_poly']),
112 chebval(ref_time, row['obs_y_poly']),
113 chebval(ref_time, row['obs_z_poly'])])
114 for row in solarSystemObjects]
115 solarSystemObjects['obs_velocity'] = [
116 np.array([chebval(ref_time, Chebyshev(row['obs_x_poly']).deriv().coef),
117 chebval(ref_time, Chebyshev(row['obs_y_poly']).deriv().coef),
118 chebval(ref_time, Chebyshev(row['obs_z_poly']).deriv().coef)])
119 for row in solarSystemObjects]
120 solarSystemObjects['obj_position'] = [
121 np.array([chebval(ref_time, row['obj_x_poly']),
122 chebval(ref_time, row['obj_y_poly']),
123 chebval(ref_time, row['obj_z_poly'])])
124 for row in solarSystemObjects]
125 solarSystemObjects['obj_velocity'] = [
126 np.array([chebval(ref_time, Chebyshev(row['obj_x_poly']).deriv().coef),
127 chebval(ref_time, Chebyshev(row['obj_y_poly']).deriv().coef),
128 chebval(ref_time, Chebyshev(row['obj_z_poly']).deriv().coef)])
129 for row in solarSystemObjects]
130 vector = np.vstack(solarSystemObjects['obj_position'].value
131 - solarSystemObjects['obs_position'].value)
132 ras, decs = np.vstack(hp.vec2ang(vector, lonlat=True))
133 solarSystemObjects['ra'] = ras
134 solarSystemObjects['dec'] = decs
135 solarSystemObjects['obs_position_x'], solarSystemObjects['obs_position_y'], \
136 solarSystemObjects['obs_position_z'] = solarSystemObjects['obs_position'].value.T
137 solarSystemObjects['heliocentricX'], solarSystemObjects['heliocentricY'], \
138 solarSystemObjects['heliocentricZ'] = solarSystemObjects['obj_position'].value.T
139 solarSystemObjects['obs_velocity_x'], solarSystemObjects['obs_velocity_y'], \
140 solarSystemObjects['obs_velocity_z'] = solarSystemObjects['obs_velocity'].value.T
141 solarSystemObjects['heliocentricVX'], solarSystemObjects['heliocentricVY'], \
142 solarSystemObjects['heliocentricVZ'] = solarSystemObjects['obj_velocity'].value.T
143 solarSystemObjects['topocentric_position'], solarSystemObjects['topocentric_velocity'] = (
144 solarSystemObjects['obj_position'] - solarSystemObjects['obs_position'],
145 solarSystemObjects['obj_velocity'] - solarSystemObjects['obs_velocity'],
146 )
147 solarSystemObjects['topocentricX'], solarSystemObjects['topocentricY'], \
148 solarSystemObjects['topocentricZ'] = (
149 np.array(list(solarSystemObjects['topocentric_position'].value)).T
150 )
151 solarSystemObjects['topocentricVX'], solarSystemObjects['topocentricVY'], \
152 solarSystemObjects['topocentricVZ'] = (
153 np.array(list(solarSystemObjects['topocentric_velocity'].value)).T
154 )
155 solarSystemObjects['heliocentricVX'], solarSystemObjects['heliocentricVY'], \
156 solarSystemObjects['heliocentricVZ'] = np.array(list(solarSystemObjects['obj_velocity'].value)).T
157 solarSystemObjects['heliocentricDist'] = np.linalg.norm(solarSystemObjects['obj_position'], axis=1)
158 solarSystemObjects['topocentricDist'] = np.linalg.norm(solarSystemObjects['topocentric_position'],
159 axis=1)
160 solarSystemObjects['phaseAngle'] = np.degrees(np.arccos(np.sum(
161 solarSystemObjects['obj_position'].T * solarSystemObjects['topocentric_position'].T
162 / solarSystemObjects['heliocentricDist'] / solarSystemObjects['topocentricDist'], axis=0
163 )))
164
165 stateVectorColumns = ['heliocentricX', 'heliocentricY', 'heliocentricZ', 'heliocentricVX',
166 'heliocentricVY', 'heliocentricVZ', 'topocentricX', 'topocentricY',
167 'topocentricZ', 'topocentricVX', 'topocentricVY', 'topocentricVZ']
168
169 maskedObjects = self._maskToCcdRegion(
170 solarSystemObjects,
171 bbox,
172 wcs,
173 solarSystemObjects["Err(arcsec)"].max()).copy()
174 nSolarSystemObjects = len(maskedObjects)
175 if nSolarSystemObjects <= 0:
176 return self._return_empty(diaSourceCatalog, maskedObjects)
177
178 maxRadius = np.deg2rad(self.config.maxDistArcSeconds / 3600)
179
180 # Transform DIA RADEC coordinates to unit sphere xyz for tree building.
181 vectors = self._radec_to_xyz(diaSourceCatalog["ra"],
182 diaSourceCatalog["dec"])
183
184 # Create KDTree of DIA sources
185 tree = cKDTree(vectors)
186
187 nFound = 0
188 # Query the KDtree for DIA nearest neighbors to SSOs. Currently only
189 # picks the DiaSource with the shortest distance. We can do something
190 # fancier later.
191 ssSourceData, ssObjectIds = [], []
192 ras, decs, residual_ras, residual_decs, dia_ids = [], [], [], [], []
193 diaSourceCatalog["ssObjectId"] = 0
194 source_column = 'id'
195 maskedObjects['associated'] = False
196 if 'diaSourceId' in diaSourceCatalog.columns:
197 source_column = 'diaSourceId'
198 for ssObject in maskedObjects:
199 index = ssObject.index
200 ssoVect = self._radec_to_xyz(ssObject["ra"], ssObject["dec"])
201 # Which DIA Sources fall within r?
202 dist, idx = tree.query(ssoVect, distance_upper_bound=maxRadius)
203 if len(idx) == 1 and np.isfinite(dist[0]):
204 nFound += 1
205 diaSourceCatalog[idx[0]]["ssObjectId"] = ssObject["ssObjectId"]
206 ssObjectIds.append(ssObject["ssObjectId"])
207 ssSourceData.append(list(ssObject[["phaseAngle", "heliocentricDist",
208 "topocentricDist"] + stateVectorColumns].values()))
209 dia_ra = diaSourceCatalog[idx[0]]["ra"]
210 dia_dec = diaSourceCatalog[idx[0]]["dec"]
211 dia_id = diaSourceCatalog[idx[0]][source_column]
212 ras.append(dia_ra)
213 decs.append(dia_dec)
214 dia_ids.append(dia_id)
215 residual_ras.append(dia_ra - ssObject["ra"])
216 residual_decs.append(dia_dec - ssObject["dec"])
217 maskedObjects['associated'][index] = True
218 else:
219 maskedObjects['associated'][index] = False
220
221 self.log.info("Successfully associated %d / %d SolarSystemObjects.", nFound, nSolarSystemObjects)
222 self.metadata['nAssociatedSsObjects'] = nFound
223 self.metadata['nExpectedSsObjects'] = nSolarSystemObjects
224 assocSourceMask = diaSourceCatalog["ssObjectId"] != 0
225 unAssocObjectMask = np.logical_not(maskedObjects['associated'].value)
226 ssSourceData = np.array(ssSourceData)
227 ssSourceData = Table(ssSourceData,
228 names=[
229 "phaseAngle", "heliocentricDist", "topocentricDist"
230 ] + stateVectorColumns)
231 ssSourceData['ssObjectId'] = Column(data=ssObjectIds, dtype=int)
232 ssSourceData["ra"] = ras
233 ssSourceData["dec"] = decs
234 ssSourceData["residualRa"] = residual_ras
235 ssSourceData["residualDec"] = residual_decs
236 ssSourceData[source_column] = dia_ids
237 coords = SkyCoord(ra=ssSourceData['ra'].value * u.deg, dec=ssSourceData['dec'].value * u.deg)
238 ssSourceData['galacticL'] = coords.galactic.l.deg
239 ssSourceData['galacticB'] = coords.galactic.b.deg
240 ssSourceData['eclipticLambda'] = coords.barycentrictrueecliptic.lon.deg
241 ssSourceData['eclipticBeta'] = coords.barycentrictrueecliptic.lat.deg
242 unassociatedObjects = maskedObjects[unAssocObjectMask]
243 columns_to_drop = [
244 "obs_position", "obs_velocity", "obj_position", "obj_velocity", "topocentric_position",
245 "topocentric_velocity", "obs_x_poly", "obs_y_poly", "obs_z_poly", "obj_x_poly", "obj_y_poly",
246 "obj_z_poly", "associated"
247 ]
248 unassociatedObjects.remove_columns(columns_to_drop)
249 return pipeBase.Struct(
250 ssoAssocDiaSources=diaSourceCatalog[assocSourceMask],
251 unAssocDiaSources=diaSourceCatalog[~assocSourceMask],
252 nTotalSsObjects=nSolarSystemObjects,
253 nAssociatedSsObjects=nFound,
254 associatedSsSources=ssSourceData,
255 unassociatedSsObjects=unassociatedObjects)
256
257 def _maskToCcdRegion(self, solarSystemObjects, bbox, wcs, marginArcsec):
258 """Mask the input SolarSystemObjects to only those in the exposure
259 bounding box.
260
261 Parameters
262 ----------
263 solarSystemObjects : `astropy.table.Table`
264 SolarSystemObjects to mask to ``exposure``.
265 bbox :
266 Exposure bbox used for masking
267 wcs :
268 Exposure wcs used for masking
269 marginArcsec : `float`
270 Maximum possible matching radius to pad onto the exposure bounding
271 box. If greater than ``maxPixelMargin``, ``maxPixelMargin`` will
272 be used.
273
274 Returns
275 -------
276 maskedSolarSystemObjects : `astropy.table.Table`
277 Set of SolarSystemObjects contained within the exposure bounds.
278 """
279 if len(solarSystemObjects) == 0:
280 return solarSystemObjects
281 padding = min(
282 int(np.ceil(marginArcsec / wcs.getPixelScale(bbox.getCenter()).asArcseconds())),
283 self.config.maxPixelMargin)
284
285 return solarSystemObjects[bbox_contains_sky_coords(
286 bbox,
287 wcs,
288 solarSystemObjects['ra'].value * u.degree,
289 solarSystemObjects['dec'].value * u.degree,
290 padding)]
291
292 def _radec_to_xyz(self, ras, decs):
293 """Convert input ra/dec coordinates to spherical unit-vectors.
294
295 Parameters
296 ----------
297 ras : `array-like`
298 RA coordinates of objects in degrees.
299 decs : `array-like`
300 DEC coordinates of objects in degrees.
301
302 Returns
303 -------
304 vectors : `numpy.ndarray`, (N, 3)
305 Output unit-vectors
306 """
307 ras = np.radians(ras)
308 decs = np.radians(decs)
309 try:
310 vectors = np.empty((len(ras), 3))
311 except TypeError:
312 vectors = np.empty((1, 3))
313
314 sin_dec = np.sin(np.pi / 2 - decs)
315 vectors[:, 0] = sin_dec * np.cos(ras)
316 vectors[:, 1] = sin_dec * np.sin(ras)
317 vectors[:, 2] = np.cos(np.pi / 2 - decs)
318
319 return vectors
320
321 def _return_empty(self, diaSourceCatalog, emptySolarSystemObjects):
322 """Return a struct with all appropriate empty values for no SSO associations.
323
324 Parameters
325 ----------
326 diaSourceCatalog : `astropy.table.Table`
327 Used for column names
328 emptySolarSystemObjects : `astropy.table.Table`
329 Used for column names.
330 Returns
331 -------
332 results : `lsst.pipe.base.Struct`
333 Results struct with components.
334 - ``ssoAssocDiaSources`` : Empty. (`astropy.table.Table`)
335 - ``unAssocDiaSources`` : Input DiaSources. (`astropy.table.Table`)
336 - ``nTotalSsObjects`` : Zero. (`int`)
337 - ``nAssociatedSsObjects`` : Zero.
338 - ``associatedSsSources`` : Empty. (`Astropy.table.Table`)
339 - ``unassociatedSsObjects`` : Empty. (`Astropy.table.Table`)
340
341
342 Raises
343 ------
344 RuntimeError
345 Raised if duplicate DiaObjects or duplicate DiaSources are found.
346 """
347 self.log.info("No SolarSystemObjects found in detector bounding box.")
348 return pipeBase.Struct(
349 ssoAssocDiaSources=Table(names=diaSourceCatalog.columns),
350 unAssocDiaSources=diaSourceCatalog,
351 nTotalSsObjects=0,
352 nAssociatedSsObjects=0,
353 associatedSsSources=Table(names=['phaseAngle', 'heliocentricDist', 'topocentricDist',
354 'heliocentricX', 'heliocentricY', 'heliocentricZ',
355 'heliocentricVX', 'heliocentricVY', 'heliocentricVZ',
356 'topocentricX', 'topocentricY', 'topocentricZ',
357 'topocentricVX', 'topocentricVY', 'topocentricVZ',
358 'residualRa', 'residualDec', 'eclipticLambda', 'eclipticBeta',
359 'galacticL', 'galacticB', 'ssObjectId', 'diaSourceId'],
360 dtype=[float] * 21 + [int] * 2),
361 unassociatedSsObjects=Table(names=emptySolarSystemObjects.columns)
362 )
_return_empty(self, diaSourceCatalog, emptySolarSystemObjects)
_maskToCcdRegion(self, solarSystemObjects, bbox, wcs, marginArcsec)
run(self, diaSourceCatalog, solarSystemObjects, visitInfo, bbox, wcs)