Skip to content

Commit f9d15be

Browse files
Jorge Fernandez HernandezJorge Fernandez Hernandez
authored andcommitted
EUCLIDSWRQ-250 new method for the mage Access Protocol (SIAP)
1 parent 5d05a4d commit f9d15be

File tree

5 files changed

+268
-14
lines changed

5 files changed

+268
-14
lines changed

CHANGES.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ gaia
3636
- New datalink DR4 retrieval type RESIDUAL_IMAGE. [#3489]
3737
- The method ``load_data`` parses ecsv files [#3500].
3838

39+
esa.euclid
40+
^^^^^^^^^^
41+
42+
- New method get_sia to access the Simple Image Access Protocol (SIAP) v2.0 [#3506]
43+
3944
esa.hubble
4045
^^^^^^^^^^
4146

astroquery/esa/euclid/__init__.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,22 @@ class Conf(_config.ConfigNamespace):
2525
"ID is given preference. To give name preference, set the value to True:")
2626

2727
ENVIRONMENTS = {'IDR': {'url_server': 'https://easidr.esac.esa.int/', 'main_table': 'catalogue.mer_catalogue',
28-
'main_table_ra_column': 'right_ascension', 'main_table_dec_column': 'declination'},
28+
'main_table_ra_column': 'right_ascension', 'main_table_dec_column': 'declination',
29+
'data_set_release_part1': 'environment', 'data_set_release_part2': 'activity_code',
30+
'data_set_release_part3': 'version'},
2931
'OTF': {'url_server': 'https://easotf.esac.esa.int/', 'main_table': 'catalogue.mer_catalogue',
30-
'main_table_ra_column': 'right_ascension', 'main_table_dec_column': 'declination'},
32+
'main_table_ra_column': 'right_ascension', 'main_table_dec_column': 'declination',
33+
'data_set_release_part1': 'activity_code', 'data_set_release_part2': 'patch_id',
34+
'data_set_release_part3': 'version'},
3135
'REG': {'url_server': 'https://easreg.esac.esa.int/',
3236
'main_table': 'catalogue.mer_final_catalog_fits_file_regreproc1_r2',
33-
'main_table_ra_column': 'right_ascension', 'main_table_dec_column': 'declination'},
37+
'main_table_ra_column': 'right_ascension', 'main_table_dec_column': 'declination',
38+
'data_set_release_part1': 'environment', 'data_set_release_part2': 'activity_code',
39+
'data_set_release_part3': 'version'},
3440
'PDR': {'url_server': 'https://eas.esac.esa.int/', 'main_table': 'catalogue.mer_catalogue',
35-
'main_table_ra_column': 'right_ascension', 'main_table_dec_column': 'declination'}
41+
'main_table_ra_column': 'right_ascension', 'main_table_dec_column': 'declination',
42+
'data_set_release_part1': 'environment', 'data_set_release_part2': 'activity_code',
43+
'data_set_release_part3': 'version'}
3644
}
3745

3846
OBSERVATION_STACK_PRODUCTS = ['DpdNirStackedFrame', 'DpdVisStackedFrame']

astroquery/esa/euclid/core.py

Lines changed: 146 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,13 @@
1111
import pprint
1212
import tarfile
1313
import zipfile
14+
from collections.abc import Iterable
15+
from datetime import datetime
16+
1417
from astropy import units
1518
from astropy import units as u
1619
from astropy.coordinates import Angle
1720
from astropy.units import Quantity
18-
from collections.abc import Iterable
19-
from datetime import datetime
2021
from requests.exceptions import HTTPError
2122

2223
from astroquery import log
@@ -43,7 +44,7 @@ class EuclidClass(TapPlus):
4344
__VALID_DATALINK_RETRIEVAL_TYPES = conf.VALID_DATALINK_RETRIEVAL_TYPES
4445

4546
def __init__(self, *, environment='PDR', tap_plus_conn_handler=None, datalink_handler=None, cutout_handler=None,
46-
verbose=False, show_server_messages=True):
47+
sia_handler=None, verbose=False, show_server_messages=True):
4748
"""Constructor for EuclidClass.
4849
4950
Parameters
@@ -56,6 +57,8 @@ def __init__(self, *, environment='PDR', tap_plus_conn_handler=None, datalink_ha
5657
HTTP(s) connection hander (creator). If no handler is provided, a new one is created.
5758
cutout_handler : cutout connection handler object, optional, default None
5859
HTTP(s) connection hander (creator). If no handler is provided, a new one is created.
60+
sia_handler : siap connection handler object, optional, default None
61+
HTTP(s) connection hander (creator). If no handler is provided, a new one is created.
5962
verbose : bool, optional, default 'True'
6063
flag to display information about the process
6164
show_server_messages : bool, optional, default 'True'
@@ -70,6 +73,9 @@ def __init__(self, *, environment='PDR', tap_plus_conn_handler=None, datalink_ha
7073
self.main_table = conf.ENVIRONMENTS[self.environment]['main_table']
7174
self.main_table_ra = conf.ENVIRONMENTS[self.environment]['main_table_ra_column']
7275
self.main_table_dec = conf.ENVIRONMENTS[self.environment]['main_table_dec_column']
76+
self.dsr_1 = conf.ENVIRONMENTS[self.environment]['data_set_release_part1']
77+
self.dsr_2 = conf.ENVIRONMENTS[self.environment]['data_set_release_part2']
78+
self.dsr_3 = conf.ENVIRONMENTS[self.environment]['data_set_release_part3']
7379

7480
url_server = conf.ENVIRONMENTS[environment]['url_server']
7581

@@ -113,6 +119,20 @@ def __init__(self, *, environment='PDR', tap_plus_conn_handler=None, datalink_ha
113119
else:
114120
self.__euclidcutout = cutout_handler
115121

122+
if sia_handler is None:
123+
self.__euclidsia = TapPlus(url=url_server,
124+
server_context="sas-sia",
125+
tap_context="tap-server",
126+
upload_context="Upload",
127+
table_edit_context="TableTool",
128+
data_context="sia2/query",
129+
datalink_context="datalink",
130+
verbose=verbose,
131+
client_id='ASTROQUERY',
132+
use_names_over_ids=conf.USE_NAMES_OVER_IDS)
133+
else:
134+
self.__euclidsia = sia_handler
135+
116136
if show_server_messages:
117137
self.get_status_messages()
118138

@@ -632,6 +652,8 @@ def login(self, *, user=None, password=None, credentials_file=None, verbose=Fals
632652
self.__eucliddata.login(user=tap_user, password=tap_password, verbose=verbose)
633653
log.info(f"Login to Euclid cutout service: {self.__euclidcutout._TapPlus__getconnhandler().get_host_url()}")
634654
self.__euclidcutout.login(user=tap_user, password=tap_password, verbose=verbose)
655+
log.info(f"Login to Euclid sia service: {self.__euclidsia._TapPlus__getconnhandler().get_host_url()}")
656+
self.__euclidsia.login(user=tap_user, password=tap_password, verbose=verbose)
635657
except HTTPError as err:
636658
log.error('Error logging in data or cutout services: %s' % (str(err)))
637659
log.error("Logging out from TAP server")
@@ -676,6 +698,14 @@ def login_gui(self, verbose=False):
676698
log.error("Logging out from TAP server")
677699
TapPlus.logout(self, verbose=verbose)
678700

701+
try:
702+
log.info(f"Login to Euclid sia server: {self.__euclidsia._TapPlus__getconnhandler().get_host_url()}")
703+
self.__euclidsia.login(user=tap_user, password=tap_password, verbose=verbose)
704+
except HTTPError as err:
705+
log.error('Error logging in sia server: %s' % (str(err)))
706+
log.error("Logging out from TAP server")
707+
TapPlus.logout(self, verbose=verbose)
708+
679709
def logout(self, verbose=False):
680710
"""
681711
Performs a logout
@@ -711,6 +741,12 @@ def logout(self, verbose=False):
711741
except HTTPError as err:
712742
log.error('Error logging out cutout server: %s' % (str(err)))
713743

744+
try:
745+
self.__euclidsia.logout(verbose=verbose)
746+
log.info("Euclid sia server logout OK")
747+
except HTTPError as err:
748+
log.error('Error logging out sia server: %s' % (str(err)))
749+
714750
@staticmethod
715751
def __get_quantity_input(value, msg):
716752
if value is None:
@@ -1235,6 +1271,113 @@ def get_product(self, *, file_name=None, product_id=None, schema='sedm', output_
12351271

12361272
return files
12371273

1274+
def get_sia(self, *, search_type='CIRCLE', ra, dec, radius, calibration=2, instrument='ALL', band=None,
1275+
collection='sedm', dsr_part1=None, dsr_part2=None, dsr_part3=None, output_file=None, verbose=False):
1276+
"""
1277+
Description
1278+
-----------
1279+
1280+
Access the Euclid Observation Images by VO SIAP v2.0. This service will return public images from Calibrated
1281+
and Stacked NISP and VIS images, MER Mosaics from VIS and NISP and Level 1 (RAW) images for NISP and VIS
1282+
1283+
Parameters
1284+
----------
1285+
search_type : str, mandatory, default None
1286+
search region: CIRCLE or BOX
1287+
ra : float (degrees), str or astropy.coordinate, mandatory
1288+
right ascension
1289+
dec : float (degrees), str or astropy.coordinate, mandatory
1290+
declination
1291+
radius : float (degrees), str or astropy.coordinate, mandatory
1292+
search radius of the cutout to generate
1293+
calibration: int, optional, default 2
1294+
calibration level according to ObsCore VO standard: 0 (raw instrumental data), 1 (instrumental data in a
1295+
standard format), 2 (science ready data) or 3 (enhanced data products).
1296+
instrument: str, mandatory, default ALL
1297+
instrument name: ALL, VIS or NISP
1298+
band: str, optional, default None
1299+
filter name only valid if instrument is different from ALL: VIS for instrument VIS or NIR_H, NIR_J, NIR_Y
1300+
or NISP for instrument NISP
1301+
collection : str, mandatory, default sedm
1302+
the name of the data collection
1303+
dsr_part1: str, optional, default None
1304+
the data set release part 1: for OTF environment, the activity code; for REG and IDR, the target environment
1305+
dsr_part2: str, optional, default None
1306+
the data set release part 2: for OTF environment, the patch id (a positive integer); for REG and IDR,
1307+
the activity code
1308+
dsr_part3: str, optional, default None
1309+
the data set release part 3: for OTF, REG and IDR environment, the version (a integer greater than 1)
1310+
output_file : string, optional, default None
1311+
file where the results are saved.
1312+
verbose : bool, optional, default 'False'
1313+
flag to display information about the process
1314+
1315+
Returns
1316+
-------
1317+
A table object or votable file
1318+
"""
1319+
1320+
valid_search_types = {'CIRCLE', 'BOX'}
1321+
valid_calibrations = {0: 'CALIB_ZERO', 1: 'CALIB_ONE', 2: 'CALIB_TWO', 3: 'CALIB_THREE'}
1322+
valid_instruments = {'ALL', 'VIS', 'NISP'}
1323+
valid_band_vis = {'VIS'}
1324+
valid_band_nisp = {'NIR_H', 'NIR_J', 'NIR_Y', 'NISP'}
1325+
1326+
if search_type not in valid_search_types:
1327+
raise ValueError(f"Invalid search tyype {search_type}")
1328+
1329+
if calibration is not None and calibration not in valid_calibrations:
1330+
raise ValueError(f"Invalid calibration {calibration}")
1331+
1332+
if instrument not in valid_instruments:
1333+
raise ValueError(f"Invalid instrument {instrument}")
1334+
1335+
if instrument == 'ALL' and band is not None:
1336+
raise ValueError(f"For instrument {instrument} band must be None")
1337+
1338+
if instrument == 'VIS' and band is not None and band not in valid_band_vis:
1339+
raise ValueError(f"Invalid band {band} for instrument {instrument}")
1340+
1341+
if instrument == 'NISP' and band is not None and band not in valid_band_nisp:
1342+
raise ValueError(f"Invalid band {band} for instrument {instrument}")
1343+
1344+
ra_deg = self.coordinates_degrees(ra)
1345+
dec_deg = self.coordinates_degrees(dec)
1346+
radius_deg = self.coordinates_degrees(radius)
1347+
1348+
params_dict = dict()
1349+
params_dict['TAPCLIENT'] = 'ASTROQUERY'
1350+
params_dict[
1351+
'POS'] = f"{search_type},{ra_deg.to_value(u.deg)},{dec_deg.to_value(u.deg)},{radius_deg.to_value(u.deg)}"
1352+
params_dict['INSTRUMENT'] = instrument
1353+
params_dict['COLLECTION'] = collection
1354+
1355+
if calibration is not None:
1356+
params_dict['CALIB'] = valid_calibrations[calibration]
1357+
1358+
if instrument != 'ALL' and band is not None:
1359+
params_dict['BAND'] = band
1360+
1361+
if dsr_part1 is not None:
1362+
params_dict['DSP1'] = dsr_part1
1363+
1364+
if dsr_part2 is not None:
1365+
params_dict['DSP2'] = dsr_part2
1366+
1367+
if dsr_part3 is not None:
1368+
params_dict['DSP3'] = dsr_part3
1369+
1370+
return self.__euclidsia.load_data(params_dict=params_dict, output_file=output_file, http_method='GET',
1371+
verbose=verbose)
1372+
1373+
def coordinates_degrees(self, coord):
1374+
1375+
if not isinstance(coord, units.Quantity):
1376+
radius_quantity = Quantity(value=coord, unit=u.deg)
1377+
else:
1378+
radius_quantity = coord.to(u.deg)
1379+
return radius_quantity
1380+
12381381
def get_cutout(self, *, file_path=None, instrument=None, id=None, coordinate, radius, output_file=None,
12391382
verbose=False):
12401383
"""

astroquery/esa/euclid/tests/test_euclidtap.py

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,21 @@
66
European Space Astronomy Centre (ESAC)
77
European Space Agency (ESA)
88
"""
9-
import glob
10-
import os
11-
import shutil
12-
from datetime import datetime
13-
from pathlib import Path
14-
from unittest.mock import patch
15-
169
import astropy.units as u
10+
import glob
1711
import numpy as np
12+
import os
1813
import pytest
14+
import shutil
1915
from astropy import coordinates
2016
from astropy.coordinates.sky_coordinate import SkyCoord
2117
from astropy.table import Column, Table
2218
from astropy.units import Quantity
2319
from astropy.utils.data import get_pkg_data_filename
20+
from datetime import datetime
21+
from pathlib import Path
2422
from requests import HTTPError
23+
from unittest.mock import patch
2524

2625
from astroquery.esa.euclid.core import EuclidClass
2726
from astroquery.esa.euclid.core import conf
@@ -43,6 +42,8 @@
4342
TABLE_FILE_NAME = get_pkg_data_filename(os.path.join("data", '1714556098855O-result.vot'), package=package)
4443
TABLE_DATA = Path(TABLE_FILE_NAME).read_text()
4544

45+
TABLE_SIA_FILE_NAME = get_pkg_data_filename(os.path.join("data", 'sia_test.vot'), package=package)
46+
4647
RADIUS = 1 * u.deg
4748
SKYCOORD = SkyCoord(ra=19 * u.deg, dec=20 * u.deg, frame="icrs")
4849

@@ -1433,6 +1434,57 @@ def test_load_async_job(mock_querier_async):
14331434
assert job.jobid == jobid
14341435

14351436

1437+
def test_get_sia(monkeypatch):
1438+
def load_data_monkeypatched(self, params_dict, output_file, http_method, verbose):
1439+
return Table.read(TABLE_SIA_FILE_NAME, format='votable')
1440+
1441+
monkeypatch.setattr(TapPlus, "load_data", load_data_monkeypatched)
1442+
euclid = EuclidClass(show_server_messages=False)
1443+
1444+
table = euclid.get_sia(ra=89.0, dec=-66.0, radius=1.0, verbose=True)
1445+
assert isinstance(table, Table)
1446+
assert table['file_name'][
1447+
0] == 'EUC_MER_BGSUB-MOSAIC-VIS_TILE101007315-D84386_20230826T000856.482420Z_00.00.fits.gz'
1448+
1449+
table = euclid.get_sia(ra=89.0, dec=-66.0, radius=1.0, dsr_part1='CALBLOCK', dsr_part2='PV-023', dsr_part3=1,
1450+
verbose=True)
1451+
assert isinstance(table, Table)
1452+
assert table['file_name'][
1453+
0] == 'EUC_MER_BGSUB-MOSAIC-VIS_TILE101007315-D84386_20230826T000856.482420Z_00.00.fits.gz'
1454+
1455+
1456+
def test_get_sia_exceptions(monkeypatch):
1457+
def load_data_monkeypatch(self, params_dict, output_file, http_method, verbose):
1458+
return Table()
1459+
1460+
monkeypatch.setattr(TapPlus, "load_data", load_data_monkeypatch)
1461+
euclid = EuclidClass(show_server_messages=False)
1462+
1463+
error_message = "Invalid search tyype XX"
1464+
with pytest.raises(ValueError, match=error_message):
1465+
euclid.get_sia(search_type='XX', ra=89.0, dec=-66.0, radius=1.0, verbose=True)
1466+
1467+
error_message = "Invalid instrument XX"
1468+
with pytest.raises(ValueError, match=error_message):
1469+
euclid.get_sia(instrument='XX', ra=89.0, dec=-66.0, radius=1.0, verbose=True)
1470+
1471+
error_message = "For instrument ALL band must be None"
1472+
with pytest.raises(ValueError, match=error_message):
1473+
euclid.get_sia(instrument='ALL', band='XX', ra=89.0, dec=-66.0, radius=1.0, verbose=True)
1474+
1475+
error_message = "Invalid band NIR_H for instrument VIS"
1476+
with pytest.raises(ValueError, match=error_message):
1477+
euclid.get_sia(instrument='VIS', band='NIR_H', ra=89.0, dec=-66.0, radius=1.0, verbose=True)
1478+
1479+
error_message = "Invalid band VIS for instrument NISP"
1480+
with pytest.raises(ValueError, match=error_message):
1481+
euclid.get_sia(instrument='NISP', band='VIS', ra=89.0, dec=-66.0, radius=1.0, verbose=True)
1482+
1483+
error_message = "Invalid calibration 5"
1484+
with pytest.raises(ValueError, match=error_message):
1485+
euclid.get_sia(calibration=5, ra=89.0, dec=-66.0, radius=1.0, verbose=True)
1486+
1487+
14361488
def remove_temp_dir():
14371489
dirs = glob.glob('./temp_*')
14381490
for dir_path in dirs:

0 commit comments

Comments
 (0)