Coverage for python/lsst/ap/association/skyBotEphemerisQuery.py: 39%
66 statements
« prev ^ index » next coverage.py v7.2.4, created at 2023-04-29 04:06 -0700
« prev ^ index » next coverage.py v7.2.4, created at 2023-04-29 04:06 -0700
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"""Solar System Object Query to Skybot in place of a internal Rubin solar
23system object caching/retrieval code.
25Will compute the location for of known SSObjects within a visit. This code
26blocks on web requests, so should not be used as part of any real-time or
27time-sensitive system. Use in a larger pipeline at your own risk.
28"""
30__all__ = ["SkyBotEphemerisQueryConfig", "SkyBotEphemerisQueryTask"]
33from hashlib import blake2b
34import pandas as pd
35import requests
36from io import StringIO
38from astropy.coordinates import Angle
39from astropy import units as u
41from lsst.daf.base import DateTime
42import lsst.pex.config as pexConfig
43import lsst.pipe.base as pipeBase
44from lsst.utils.timer import timeMethod
46from lsst.pipe.base import PipelineTask, PipelineTaskConfig, PipelineTaskConnections
47import lsst.pipe.base.connectionTypes as connTypes
49# Enforce an error for unsafe column/array value setting in pandas.
50pd.options.mode.chained_assignment = 'raise'
53class SkyBotEphemerisQueryConnections(PipelineTaskConnections,
54 dimensions=("instrument",
55 "visit")):
56 visitInfos = connTypes.Input(
57 doc="Information defining the visit on a per detector basis.",
58 name="raw.visitInfo",
59 storageClass="VisitInfo",
60 dimensions=("instrument", "exposure", "detector"),
61 deferLoad=True,
62 multiple=True
63 )
64 ssObjects = connTypes.Output(
65 doc="Solar System objects observable in this visit retrieved from "
66 "SkyBoy",
67 name="visitSsObjects",
68 storageClass="DataFrame",
69 dimensions=("instrument", "visit"),
70 )
73class SkyBotEphemerisQueryConfig(
74 PipelineTaskConfig,
75 pipelineConnections=SkyBotEphemerisQueryConnections):
76 observerCode = pexConfig.Field(
77 dtype=str,
78 doc="IAU Minor Planet Center observer code for queries "
79 "(Rubin Obs./LSST default is I11)",
80 default='I11'
81 )
82 queryRadiusDegrees = pexConfig.Field(
83 dtype=float,
84 doc="On sky radius for Ephemeris cone search. Also limits sky "
85 "position error in ephemeris query. Defaults to the radius of "
86 "Rubin Obs FoV in degrees",
87 default=1.75)
90class SkyBotEphemerisQueryTask(PipelineTask):
91 """Tasks to query the SkyBot service and retrieve the solar system objects
92 that are observable within the input visit.
93 """
94 ConfigClass = SkyBotEphemerisQueryConfig
95 _DefaultName = "SkyBotEphemerisQuery"
97 def runQuantum(self, butlerQC, inputRefs, outputRefs):
98 inputs = butlerQC.get(inputRefs)
99 inputs["visit"] = butlerQC.quantum.dataId["visit"]
101 outputs = self.run(**inputs)
103 butlerQC.put(outputs, outputRefs)
105 @timeMethod
106 def run(self, visitInfos, visit):
107 """Parse the information on the current visit and retrieve the
108 observable solar system objects from SkyBot.
110 Parameters
111 ----------
112 visitInfos : `list` of `lsst.daf.butler.DeferredDatasetHandle`
113 Set of visitInfos for raws covered by this visit/exposure. We
114 only use the first instance to retrieve the exposure boresight.
115 visit : `int`
116 Id number of the visit being run.
118 Returns
119 -------
120 result : `lsst.pipe.base.Struct`
121 Results struct with components:
123 - ``ssObjects``: `pandas.DataFrame`
124 DataFrame containing Solar System Objects in field of view as
125 retrieved by SkyBot. The columns are as follows; for more
126 details see
127 https://ssp.imcce.fr/webservices/skybot/api/conesearch/#output-results
129 ``"Num"``
130 object number (`int`, optional)
131 ``"Name"``
132 object name (`str`)
133 ``"RA(h)"``
134 RA in HMS (`str`)
135 ``"DE(deg)"``
136 DEC in DMS (`str`)
137 ``"Class"``
138 Minor planet classification (`str`)
139 ``"Mv"``
140 visual magnitude (`float`)
141 ``"Err(arcsec)"``
142 position error (`float`)
143 ``"d(arcsec)"``
144 distance from exposure boresight (`float`)?
145 ``"dRA(arcsec/h)"``
146 proper motion in RA (`float`)
147 ``"dDEC(arcsec/h)"``
148 proper motion in DEC (`float`)
149 ``"Dg(ua)"``
150 geocentric distance (`float`)
151 ``"Dh(ua)"``
152 heliocentric distance (`float`)
153 ``"Phase(deg)"``
154 phase angle (`float`)
155 ``"SunElong(deg)"``
156 solar elongation (`float`)
157 ``"ra"``
158 RA in decimal degrees (`float`)
159 ``"decl"``
160 DEC in decimal degrees (`float`)
161 ``"ssObjectId"``
162 unique minor planet ID for internal use (`int`). Shared
163 across catalogs; the pair ``(ssObjectId, visitId)`` is
164 globally unique.
165 ``"visitId"``
166 a copy of ``visit`` (`int`)
167 """
168 # Grab the visitInfo from the raw to get the information needed on the
169 # full visit.
170 visitInfo = visitInfos[0].get()
172 # Midpoint time of the exposure in JD
173 expMidPointEPOCH = visitInfo.date.get(system=DateTime.JD, scale=DateTime.UTC)
175 # Boresight of the exposure on sky.
176 expCenter = visitInfo.boresightRaDec
178 # Skybot service query
179 skybotSsObjects = self._skybotConeSearch(
180 expCenter,
181 expMidPointEPOCH,
182 self.config.queryRadiusDegrees)
184 # Add the visit as an extra column.
185 skybotSsObjects['visitId'] = visit
187 return pipeBase.Struct(
188 ssObjects=skybotSsObjects,
189 )
191 def _skybotConeSearch(self, expCenter, epochJD, queryRadius):
192 """Query IMCCE SkyBot ephemeris service for cone search using the
193 exposure boresight.
195 Parameters
196 ----------
197 expCenter : `lsst.geom.SpherePoint`
198 Center of Exposure RADEC [deg]
199 epochJD : `float`
200 Mid point JD of exposure, in UTC [EPOCH].
201 queryRadius : `float`
202 Radius of the cone search in degrees.
204 Returns
205 -------
206 dfSSO : `pandas.DataFrame`
207 DataFrame with Solar System Object information and RA/DEC position
208 within the visit.
209 """
211 fieldRA = expCenter.getRa().asDegrees()
212 fieldDec = expCenter.getDec().asDegrees()
213 observerMPCId = self.config.observerCode
214 radius = queryRadius
215 orbitUncertaintyFilter = queryRadius
217 # TODO: DM-31866
218 q = ['http://vo.imcce.fr/webservices/skybot/skybotconesearch_query.php?']
219 q.append('-ra=' + str(fieldRA))
220 q.append('&-dec=' + str(fieldDec))
221 q.append('&-rd=' + str(radius))
222 q.append('&-ep=' + str(epochJD))
223 q.append('&-loc=' + observerMPCId)
224 q.append('&-filter=' + str(orbitUncertaintyFilter))
225 q.append('&-objFilter=111&-refsys=EQJ2000&-output=obs&-mime=text')
226 query = ''.join(q)
228 result = requests.request("GET", query)
229 dfSSO = pd.read_csv(StringIO(result.text), sep='|', skiprows=2)
230 if len(dfSSO) > 0:
231 # Data has leading and trailing spaces hence the strip.
232 columns = [col.strip() for col in dfSSO.columns]
233 coldict = dict(zip(dfSSO.columns, columns))
234 dfSSO.rename(columns=coldict, inplace=True)
235 # Data returned in hourangle format.
236 dfSSO["ra"] = Angle(dfSSO["RA(h)"], unit=u.hourangle).deg
237 dfSSO["decl"] = Angle(dfSSO["DE(deg)"], unit=u.deg).deg
238 # SkyBot returns a string name for the object. To store the id in
239 # the Apdb we convert this string to an int by hashing the object
240 # name. This is a stop gap until such a time as the Rubin
241 # Ephemeris system exists and we create our own Ids. Use blake2b
242 # is it can produce hashes that can fit in a 64bit int.
243 dfSSO["ssObjectId"] = [
244 int(blake2b(bytes(name, "utf-8"), digest_size=7).hexdigest(),
245 base=16)
246 for name in dfSSO["Name"]
247 ]
248 else:
249 self.log.info("No Solar System objects found for visit.")
250 return pd.DataFrame()
252 nFound = len(dfSSO)
253 self.log.info("%d Solar System Objects in visit", nFound)
255 return dfSSO