Coverage for python/lsst/ap/association/skyBotEphemerisQuery.py : 32%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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 numpy as np
35import pandas as pd
36import requests
37from io import StringIO
39from lsst.daf.base import DateTime
40import lsst.pex.config as pexConfig
41import lsst.pipe.base as pipeBase
42from lsst.utils.timer import timeMethod
44from lsst.pipe.base import PipelineTask, PipelineTaskConfig, PipelineTaskConnections
45import lsst.pipe.base.connectionTypes as connTypes
47# Enforce an error for unsafe column/array value setting in pandas.
48pd.options.mode.chained_assignment = 'raise'
51class SkyBotEphemerisQueryConnections(PipelineTaskConnections,
52 dimensions=("instrument",
53 "visit")):
54 visitInfos = connTypes.Input(
55 doc="Information defining the visit on a per detector basis.",
56 name="raw.visitInfo",
57 storageClass="VisitInfo",
58 dimensions=("instrument", "exposure", "detector"),
59 deferLoad=True,
60 multiple=True
61 )
62 ssObjects = connTypes.Output(
63 doc="Solar System objects observable in this visit retrieved from "
64 "SkyBoy",
65 name="visitSsObjects",
66 storageClass="DataFrame",
67 dimensions=("instrument", "visit"),
68 )
71class SkyBotEphemerisQueryConfig(
72 PipelineTaskConfig,
73 pipelineConnections=SkyBotEphemerisQueryConnections):
74 observerCode = pexConfig.Field(
75 dtype=str,
76 doc="IAU Minor Planet Center observer code for queries "
77 "(Rubin Obs./LSST default is I11)",
78 default='I11'
79 )
80 queryRadiusDegrees = pexConfig.Field(
81 dtype=float,
82 doc="On sky radius for Ephemeris cone search. Also limits sky "
83 "position error in ephemeris query. Defaults to the radius of "
84 "Rubin Obs FoV in degrees",
85 default=1.75)
88class SkyBotEphemerisQueryTask(PipelineTask):
89 """Tasks to query the SkyBot service and retrieve the solar system objects
90 that are observable within the input visit.
91 """
92 ConfigClass = SkyBotEphemerisQueryConfig
93 _DefaultName = "SkyBotEphemerisQuery"
95 def runQuantum(self, butlerQC, inputRefs, outputRefs):
96 inputs = butlerQC.get(inputRefs)
97 inputs["visit"] = butlerQC.quantum.dataId["visit"]
99 outputs = self.run(**inputs)
101 butlerQC.put(outputs, outputRefs)
103 @timeMethod
104 def run(self, visitInfos, visit):
105 """Parse the information on the current visit and retrieve the
106 observable solar system objects from SkyBot.
108 Parameters
109 ----------
110 visitInfos : `list` of `lsst.daf.butler.DeferredDatasetHandle`
111 Set of visitInfos for raws covered by this visit/exposure. We
112 only use the first instance to retrieve the exposure boresight.
113 visit : `int`
114 Id number of the visit being run.
116 Returns
117 -------
118 result : `lsst.pipe.base.Struct`
119 Results struct with components:
121 - ``ssObjects``: `pandas.DataFrame`
122 DataFrame containing name, RA/DEC position, position
123 uncertainty, and unique Id of on sky of Solar System Objects in
124 field of view as retrieved by SkyBot.
125 """
126 # Grab the visitInfo from the raw to get the information needed on the
127 # full visit.
128 visitInfo = visitInfos[0].get(
129 datasetType=self.config.connections.visitInfos)
131 # Midpoint time of the exposure in MJD
132 expMidPointEPOCH = visitInfo.date.get(system=DateTime.JD)
134 # Boresight of the exposure on sky.
135 expCenter = visitInfo.boresightRaDec
137 # Skybot service query
138 skybotSsObjects = self._skybotConeSearch(
139 expCenter,
140 expMidPointEPOCH,
141 self.config.queryRadiusDegrees)
143 # Add the visit as an extra column.
144 skybotSsObjects['visitId'] = visit
146 return pipeBase.Struct(
147 ssObjects=skybotSsObjects,
148 )
150 def _skybotConeSearch(self, expCenter, epochJD, queryRadius):
151 """Query IMCCE SkyBot ephemeris service for cone search using the
152 exposure boresight.
154 Parameters
155 ----------
156 expCenter : `lsst.geom.SpherePoint`
157 Center of Exposure RADEC [deg]
158 epochJD : `float`
159 Mid point time of exposure [EPOCH].
160 queryRadius : `float`
161 Radius of the cone search in degrees.
163 Returns
164 -------
165 dfSSO : `pandas.DataFrame`
166 DataFrame with Solar System Object information and RA/DEC position
167 within the visit.
168 """
170 fieldRA = expCenter.getRa().asDegrees()
171 fieldDec = expCenter.getDec().asDegrees()
172 observerMPCId = self.config.observerCode
173 radius = queryRadius
174 orbitUncertaintyFilter = queryRadius
176 # TODO: DM-31866
177 q = ['http://vo.imcce.fr/webservices/skybot/skybotconesearch_query.php?']
178 q.append('-ra=' + str(fieldRA))
179 q.append('&-dec=' + str(fieldDec))
180 q.append('&-rd=' + str(radius))
181 q.append('&-ep=' + str(epochJD))
182 q.append('&-loc=' + observerMPCId)
183 q.append('&-filter=' + str(orbitUncertaintyFilter))
184 q.append('&-objFilter=111&-refsys=EQJ2000&-output=obs&-mime=text')
185 query = ''.join(q)
187 result = requests.request("GET", query)
188 dfSSO = pd.read_csv(StringIO(result.text), sep='|', skiprows=2)
189 if len(dfSSO) > 0:
190 # Data has leading and trailing spaces hence the strip.
191 columns = [col.strip() for col in dfSSO.columns]
192 coldict = dict(zip(dfSSO.columns, columns))
193 dfSSO.rename(columns=coldict, inplace=True)
194 # TODO: DM-31934 Replace custom conversion code with
195 # Astropy.coordinates.
196 dfSSO["ra"] = self._rahms2radeg(dfSSO["RA(h)"])
197 dfSSO["decl"] = self._decdms2decdeg(dfSSO["DE(deg)"])
198 # SkyBot returns a string name for the object. To store the id in
199 # the Apdb we convert this string to an int by hashing the object
200 # name. This is a stop gap until such a time as the Rubin
201 # Ephemeris system exists and we create our own Ids. Use blake2b
202 # is it can produce hashes that can fit in a 64bit int.
203 dfSSO["ssObjectId"] = [
204 int(blake2b(bytes(name, "utf-8"), digest_size=7).hexdigest(),
205 base=16)
206 for name in dfSSO["Name"]
207 ]
208 else:
209 self.log.info("No Solar System objects found for visit.")
210 return pd.DataFrame()
212 nFound = len(dfSSO)
213 self.log.info(f"{nFound} Solar System Objects in visit")
215 return dfSSO
217 def _decdms2decdeg(self, decdms):
218 """Convert Declination from degrees minutes seconds to decimal degrees.
220 Parameters
221 ----------
222 decdms : `list` of `str`
223 Declination string "degrees minutes seconds"
225 Returns
226 -------
227 decdeg : `numpy.ndarray`, (N,)
228 Declination in degrees.
229 """
230 decdeg = np.empty(len(decdms))
231 for idx, dec in enumerate(decdms):
232 deglist = [float(d) for d in dec.split()]
233 decdeg[idx] = deglist[0] + deglist[1]/60 + deglist[2]/3600
234 return decdeg
236 def _rahms2radeg(self, rahms):
237 """Convert Right Ascension from hours minutes seconds to decimal
238 degrees.
240 Parameters
241 ----------
242 rahms : `list` of `str`
243 Declination string "hours minutes seconds"
244 Returns
245 -------
246 radeg : `numpy.ndarray`, (N,)
247 Right Ascension in degrees
248 """
249 radeg = np.empty(len(rahms))
250 for idx, ra in enumerate(rahms):
251 ralist = [float(r) for r in ra.split()]
252 radeg[idx] = (ralist[0]/24 + ralist[1]/1440 + ralist[2]/86400)*360
253 return radeg