Skip to content

Commit 5258370

Browse files
authored
Merge pull request #2268 from esdc-esac-esa-int/hubble_related_members
Hubble related members
2 parents 2b3d954 + 98368e0 commit 5258370

File tree

5 files changed

+436
-109
lines changed

5 files changed

+436
-109
lines changed

CHANGES.rst

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,17 @@
33

44
New Tools and Services
55
----------------------
6+
esa.hubble
7+
^^^^^^^^^^
68

9+
- Added new method ``get_hap_hst_link`` and ``get_member_observations`` to get related observations [#2268]
710

811
Service fixes and enhancements
912
------------------------------
13+
esa.hubble
14+
^^^^^^^^^^
15+
16+
- Changed query_target method to use TAP instead of AIO [#2268]
1017

1118
casda
1219
^^^^^
@@ -1155,4 +1162,4 @@ Infrastructure, Utility and Other Changes and Additions
11551162
0.1 (2013-09-19)
11561163
================
11571164

1158-
- Initial release. Includes features!
1165+
- Initial release. Includes features!

astroquery/esa/hubble/core.py

Lines changed: 148 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,12 @@
1616
1717
1818
"""
19-
from astroquery.utils import commons
2019
from astropy import units
2120
from astropy.coordinates import SkyCoord
2221
from astropy.coordinates import Angle
23-
from astropy.units import Quantity
2422
from astroquery.utils.tap.core import TapPlus
25-
from astroquery.utils.tap.model import modelutils
2623
from astroquery.query import BaseQuery
27-
from astropy.table import Table
28-
from io import BytesIO
2924
import shutil
30-
import os
31-
import json
3225

3326
from . import conf
3427
from astroquery import log
@@ -119,14 +112,105 @@ def download_product(self, observation_id, *, calibration_level=None,
119112

120113
shutil.move(response, filename)
121114

115+
def get_member_observations(self, observation_id):
116+
"""
117+
Returns the related members of simple and composite observations
118+
119+
Parameters
120+
----------
121+
observation_id : str
122+
Observation identifier.
123+
124+
Returns
125+
-------
126+
A list of strings with the observation_id of the associated
127+
observations
128+
"""
129+
observation_type = self.get_observation_type(observation_id)
130+
131+
if 'Composite' in observation_type:
132+
oids = self._select_related_members(observation_id)
133+
elif 'Simple' in observation_type:
134+
oids = self._select_related_composite(observation_id)
135+
else:
136+
raise ValueError("Invalid observation id")
137+
return oids
138+
139+
def get_hap_hst_link(self, observation_id):
140+
"""
141+
Returns the related members of hap and hst observations
142+
143+
Parameters
144+
----------
145+
observation_id : string
146+
id of the observation to be downloaded, mandatory
147+
The identifier of the observation we want to retrieve, regardless
148+
of whether it is simple or composite.
149+
150+
Returns
151+
-------
152+
A list of strings with the observation_id of the associated
153+
observations
154+
"""
155+
observation_type = self.get_observation_type(observation_id)
156+
if 'Composite' in observation_type:
157+
raise ValueError("HAP-HST link is only available for simple observations. Input observation is Composite.")
158+
elif 'HAP' in observation_type:
159+
oids = self._select_related_members(observation_id)
160+
elif 'HST' in observation_type:
161+
query = f"select observation_id from ehst.observation where obs_type='HAP Simple' and members like '%{observation_id}%'"
162+
job = self.query_hst_tap(query=query)
163+
oids = job["observation_id"].pformat(show_name=False)
164+
else:
165+
raise ValueError("Invalid observation id")
166+
return oids
167+
168+
def get_observation_type(self, observation_id):
169+
"""
170+
Returns the type of an observation
171+
172+
Parameters
173+
----------
174+
observation_id : string
175+
id of the observation to be downloaded, mandatory
176+
The identifier of the observation we want to retrieve, regardless
177+
of whether it is simple or composite.
178+
179+
Returns
180+
-------
181+
String with the observation type
182+
"""
183+
if observation_id is None:
184+
raise ValueError("Please input an observation id")
185+
186+
query = f"select obs_type from ehst.observation where observation_id='{observation_id}'"
187+
job = self.query_hst_tap(query=query)
188+
if any(job["obs_type"]):
189+
obs_type = self._get_decoded_string(string=job["obs_type"][0])
190+
else:
191+
raise ValueError("Invalid Observation ID")
192+
return obs_type
193+
194+
def _select_related_members(self, observation_id):
195+
query = f"select members from ehst.observation where observation_id='{observation_id}'"
196+
job = self.query_hst_tap(query=query)
197+
oids = self._get_decoded_string(string=job["members"][0]).replace("caom:HST/", "").split(" ")
198+
return oids
199+
200+
def _select_related_composite(self, observation_id):
201+
query = f"select observation_id from ehst.observation where members like '%{observation_id}%'"
202+
job = self.query_hst_tap(query=query)
203+
oids = job["observation_id"].pformat(show_name=False)
204+
return oids
205+
122206
def __validate_product_type(self, product_type):
123-
if(product_type not in self.product_types):
207+
if (product_type not in self.product_types):
124208
raise ValueError("This product_type is not allowed")
125209

126210
def _get_product_filename(self, product_type, filename):
127-
if(product_type == "PRODUCT"):
211+
if (product_type == "PRODUCT"):
128212
return filename
129-
elif(product_type == "SCIENCE_PRODUCT"):
213+
elif (product_type == "SCIENCE_PRODUCT"):
130214
log.info("This is a SCIENCE_PRODUCT, the filename will be "
131215
"renamed to " + filename + ".fits.gz")
132216
return filename + ".fits.gz"
@@ -263,27 +347,27 @@ def cone_search(self, coordinates, radius, filename=None,
263347
radius_in_grades = radius.to(units.deg).value
264348
ra = coord.ra.deg
265349
dec = coord.dec.deg
266-
query = "select o.observation_id, "\
267-
"o.start_time, o.end_time, o.start_time_mjd, "\
268-
"o.end_time_mjd, o.exposure_duration, o.release_date, "\
269-
"o.run_id, o.program_id, o.set_id, o.collection, "\
270-
"o.members_number, o.instrument_configuration, "\
271-
"o.instrument_name, o.obs_type, o.target_moving, "\
272-
"o.target_name, o.target_description, o.proposal_id, "\
273-
"o.pi_name, prop.title, pl.metadata_provenance, "\
274-
"pl.data_product_type, pl.software_version, pos.ra, "\
275-
"pos.dec, pos.gal_lat, pos.gal_lon, pos.ecl_lat, "\
276-
"pos.ecl_lon, pos.fov_size, en.wave_central, "\
277-
"en.wave_bandwidth, en.wave_max, en.wave_min, "\
278-
"en.filter from ehst.observation o join ehst.proposal "\
279-
"prop on o.proposal_id=prop.proposal_id join ehst.plane "\
280-
"pl on pl.observation_id=o.observation_id join "\
281-
"ehst.position pos on pos.plane_id = pl.plane_id join "\
282-
"ehst.energy en on en.plane_id=pl.plane_id where "\
283-
"pl.main_science_plane='true' and 1=CONTAINS(POINT('ICRS', "\
284-
"pos.ra, pos.dec),CIRCLE('ICRS', {0}, {1}, {2})) order "\
285-
"by prop.proposal_id desc".format(str(ra), str(dec),
286-
str(radius_in_grades))
350+
query = ("select o.observation_id, "
351+
"o.start_time, o.end_time, o.start_time_mjd, "
352+
"o.end_time_mjd, o.exposure_duration, o.release_date, "
353+
"o.run_id, o.program_id, o.set_id, o.collection, "
354+
"o.members_number, o.instrument_configuration, "
355+
"o.instrument_name, o.obs_type, o.target_moving, "
356+
"o.target_name, o.target_description, o.proposal_id, "
357+
"o.pi_name, prop.title, pl.metadata_provenance, "
358+
"pl.data_product_type, pl.software_version, pos.ra, "
359+
"pos.dec, pos.gal_lat, pos.gal_lon, pos.ecl_lat, "
360+
"pos.ecl_lon, pos.fov_size, en.wave_central, "
361+
"en.wave_bandwidth, en.wave_max, en.wave_min, "
362+
"en.filter from ehst.observation o join ehst.proposal "
363+
"prop on o.proposal_id=prop.proposal_id join ehst.plane "
364+
"pl on pl.observation_id=o.observation_id join "
365+
"ehst.position pos on pos.plane_id = pl.plane_id join "
366+
"ehst.energy en on en.plane_id=pl.plane_id where "
367+
"pl.main_science_plane='true' and 1=CONTAINS(POINT('ICRS', "
368+
f"pos.ra, pos.dec),CIRCLE('ICRS', {str(ra)}, {str(dec)}, {str(radius_in_grades)})) order "
369+
"by prop.proposal_id desc")
370+
print("type: " + str(type(query)))
287371
if verbose:
288372
log.info(query)
289373
table = self.query_hst_tap(query=query, async_job=async_job,
@@ -380,19 +464,20 @@ def cone_search_criteria(self, radius, target=None,
380464
raise TypeError("Please use only target or coordinates as"
381465
"parameter.")
382466
if target:
383-
ra, dec = self._query_tap_target(target)
467+
coord = self._query_tap_target(target)
384468
else:
385469
coord = self._getCoordInput(coordinates)
386-
ra = coord.ra.deg
387-
dec = coord.dec.deg
470+
471+
ra = coord.ra.deg
472+
dec = coord.dec.deg
388473

389474
if type(radius) == int or type(radius) == float:
390475
radius_in_grades = Angle(radius, units.arcmin).deg
391476
else:
392477
radius_in_grades = radius.to(units.deg).value
393-
cone_query = "1=CONTAINS(POINT('ICRS', pos.ra, pos.dec),"\
394-
"CIRCLE('ICRS', {0}, {1}, {2}))".\
395-
format(str(ra), str(dec), str(radius_in_grades))
478+
cone_query = "1=CONTAINS(POINT('ICRS', pos.ra, pos.dec)," \
479+
"CIRCLE('ICRS', {0}, {1}, {2}))". \
480+
format(str(ra), str(dec), str(radius_in_grades))
396481
query = "{}{})".format(crit_query, cone_query)
397482
if verbose:
398483
log.info(query)
@@ -415,15 +500,15 @@ def _query_tap_target(self, target):
415500
target_result = target_response.json()['data'][0]
416501
ra = target_result['RA_DEGREES']
417502
dec = target_result['DEC_DEGREES']
418-
return ra, dec
503+
return SkyCoord(ra=ra, dec=dec, unit="deg")
419504
except KeyError as e:
420505
raise ValueError("This target cannot be resolved")
421506

422507
def query_metadata(self, output_format='votable', verbose=False):
423508
return
424509

425-
def query_target(self, name, filename=None, output_format='votable',
426-
verbose=False):
510+
def query_target(self, name, *, filename=None, output_format='votable',
511+
verbose=False, async_job=False, radius=7):
427512
"""
428513
It executes a query over EHST and download the xml with the results.
429514
@@ -439,34 +524,22 @@ def query_target(self, name, filename=None, output_format='votable',
439524
verbose : bool
440525
optional, default 'False'
441526
Flag to display information about the process
527+
async_job : bool, optional, default 'False'
528+
executes the query (job) in asynchronous/synchronous mode (default
529+
synchronous)
530+
radius : int
531+
optional, default 7
532+
radius in arcmin (int, float) or quantity of the cone_search
442533
443534
Returns
444535
-------
445536
Table with the result of the query. It downloads metadata as a file.
446537
"""
538+
coordinates = self._query_tap_target(name)
539+
table = self.cone_search(coordinates, radius, filename=filename, output_format=output_format,
540+
verbose=verbose, async_job=async_job)
447541

448-
params = {"RESOURCE_CLASS": "OBSERVATION",
449-
"USERNAME": "ehst-astroquery",
450-
"SELECTED_FIELDS": "OBSERVATION",
451-
"QUERY": "(TARGET.TARGET_NAME=='" + name + "')",
452-
"RETURN_TYPE": str(output_format)}
453-
response = self._request('GET', self.metadata_url, save=True,
454-
cache=True,
455-
params=params)
456-
457-
if verbose:
458-
log.info(self.metadata_url + "?RESOURCE_CLASS=OBSERVATION&"
459-
"SELECTED_FIELDS=OBSERVATION&QUERY=(TARGET.TARGET_NAME"
460-
"=='" + name + "')&USERNAME=ehst-astroquery&"
461-
"RETURN_TYPE=" + str(output_format))
462-
log.info(self.copying_string.format(filename))
463-
if filename is None:
464-
filename = "target.xml"
465-
466-
shutil.move(response, filename)
467-
468-
return modelutils.read_results_table_from_file(filename,
469-
str(output_format))
542+
return table
470543

471544
def query_hst_tap(self, query, async_job=False, output_file=None,
472545
output_format="votable", verbose=False):
@@ -495,13 +568,12 @@ def query_hst_tap(self, query, async_job=False, output_file=None,
495568
job = self._tap.launch_job_async(query=query,
496569
output_file=output_file,
497570
output_format=output_format,
498-
verbose=False,
499-
dump_to_file=output_file
500-
is not None)
571+
verbose=verbose,
572+
dump_to_file=output_file is not None)
501573
else:
502574
job = self._tap.launch_job(query=query, output_file=output_file,
503575
output_format=output_format,
504-
verbose=False,
576+
verbose=verbose,
505577
dump_to_file=output_file is not None)
506578
table = job.get_results()
507579
return table
@@ -582,9 +654,9 @@ def query_criteria(self, calibration_level=None,
582654
parameters.append("(o.instrument_configuration LIKE '%{}%')"
583655
.format("%' OR o.instrument_configuration "
584656
"LIKE '%".join(filters)))
585-
query = "select o.*, p.calibration_level, p.data_product_type, "\
586-
"pos.ra, pos.dec from ehst.observation AS o JOIN "\
587-
"ehst.plane as p on o.observation_uuid=p.observation_uuid "\
657+
query = "select o.*, p.calibration_level, p.data_product_type, " \
658+
"pos.ra, pos.dec from ehst.observation AS o JOIN " \
659+
"ehst.plane as p on o.observation_uuid=p.observation_uuid " \
588660
"JOIN ehst.position as pos on p.plane_id = pos.plane_id"
589661
if parameters:
590662
query += " where({})".format(" AND ".join(parameters))
@@ -600,7 +672,7 @@ def query_criteria(self, calibration_level=None,
600672

601673
def __get_calibration_level(self, calibration_level):
602674
condition = ""
603-
if(calibration_level is not None):
675+
if (calibration_level is not None):
604676
if isinstance(calibration_level, str):
605677
condition = calibration_level
606678
elif isinstance(calibration_level, int):
@@ -696,5 +768,11 @@ def _getCoordInput(self, value):
696768
else:
697769
return value
698770

771+
def _get_decoded_string(self, string):
772+
try:
773+
return string.decode('utf-8')
774+
except (UnicodeDecodeError, AttributeError):
775+
return string
776+
699777

700778
ESAHubble = ESAHubbleClass()

0 commit comments

Comments
 (0)