diff --git a/CHANGES.rst b/CHANGES.rst index 536e238f0a..4400aed1c9 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -96,6 +96,7 @@ svo_fps ^^^^^^^ - Add ``get_filter_metadata`` to allow retrieval of filter metadata. [#3528] +- Add ``get_zeropoint`` to allow retrieval of filter zeropoints and allow kwarg passing to ``get_filter_metadata``. [#3545] heasarc ^^^^^^^ diff --git a/astroquery/svo_fps/core.py b/astroquery/svo_fps/core.py index e8686ada8c..3216600e14 100644 --- a/astroquery/svo_fps/core.py +++ b/astroquery/svo_fps/core.py @@ -23,6 +23,11 @@ QUERY_PARAMETERS.update(("Instrument", "Facility", "PhotSystem", "ID", "PhotCalID", "FORMAT", "VERB")) +ALLOWED_QUERY_PARAMETERS = { + "VERB": {0, 1, 2}, + "FORMAT": {"metadata", None} +} + class SvoFpsClass(BaseQuery): """ @@ -31,21 +36,88 @@ class SvoFpsClass(BaseQuery): SVO_MAIN_URL = conf.base_url TIMEOUT = conf.timeout - def data_from_svo(self, query, *, cache=True, timeout=None, - error_msg='No data found for requested query'): + def data_from_svo(self, + *, + WavelengthRef_min=None, + WavelengthRef_max=None, + WavelengthMean_min=None, + WavelengthMean_max=None, + WavelengthEff_min=None, + WavelengthEff_max=None, + WavelengthMin_min=None, + WavelengthMin_max=None, + WavelengthMax_min=None, + WavelengthMax_max=None, + WidthEff_min=None, + WidthEff_max=None, + FWHM_min=None, + FWHM_max=None, + Instrument=None, + Facility=None, + PhotSystem=None, + ID=None, + PhotCalID=None, + FORMAT=None, + VERB=2, + cache=True, timeout=None, + error_msg='No data found for requested query', + ): """Get data in response to the query send to SVO FPS. This method is not generally intended for users, but it can be helpful if you want something very specific from the SVO FPS service. If you don't know what you're doing, try `get_filter_index`, `get_filter_list`, and `get_transmission_data` instead. + Description of search parameters can be found at + https://svo2.cab.inta-csic.es/theory/fps/index.php?mode=voservice + + Parameters ---------- - query : dict - Used to create a HTTP query string i.e. send to SVO FPS to get data. - In dictionary, specify keys as search parameters (str) and - values as required. Description of search parameters can be found at - https://svo2.cab.inta-csic.es/theory/fps/index.php?mode=voservice + WavelengthRef_min : float, optional + Min value for WavelengthRef parameter + WavelengthRef_max : float, optional + Max value for WavelengthRef parameter + WavelengthMean_min : float, optional + Min value for WavelengthMean parameter + WavelengthMean_max : float, optional + Max value for WavelengthMean parameter + WavelengthEff_min : float, optional + Min value for WavelengthEff parameter + WavelengthEff_max : float, optional + Max value for WavelengthEff parameter + WavelengthMin_min : float, optional + Min value for WavelengthMin parameter + WavelengthMin_max : float, optional + Max value for WavelengthMin parameter + WavelengthMax_min : float, optional + Min value for WavelengthMax parameter + WavelengthMax_max : float, optional + Max value for WavelengthMax parameter + WidthEff_min : float, optional + Min value for WidthEff parameter + WidthEff_max : float, optional + Max value for WidthEff parameter + FWHM_min : float, optional + Min value for FWHM parameter + FWHM_max : float, optional + Max value for FWHM parameter + Instrument : str, optional + Instrument for filters (default is None). Leave empty if there are no instruments for specified facility + Facility : str, optional + Facility for filters (default is None) + PhotSystem : str, optional + Photometric system for filters (default is None) + ID : str, optional + Filter ID (default is None) + PhotCalID : str, optional + Photometric calibration ID (default is None) + FORMAT : str, optional + Format of the output. Default includes all data, ``metadata`` includes only metadata. + VERB : 0, 1, or 2 + 0: The resulting VOTable won't include the transmission curve or PARAM descriptions. + 1: The resulting VOTable won't include the transmission curve but it will include PARAM descriptions. + 2: The resulting VOTable will include the transmission curve and PARAM descriptions. error_msg : str, optional Error message to be shown in case no table element found in the responded VOTable. Use this to make error message verbose in context @@ -59,14 +131,39 @@ def data_from_svo(self, query, *, cache=True, timeout=None, astropy.table.table.Table object Table containing data fetched from SVO (in response to query) """ - bad_params = [param for param in query if param not in QUERY_PARAMETERS] - if bad_params: - raise InvalidQueryError( - f"parameter{'s' if len(bad_params) > 1 else ''} " - f"{', '.join(bad_params)} {'are' if len(bad_params) > 1 else 'is'} " - f"invalid. For a description of valid query parameters see " - "https://svo2.cab.inta-csic.es/theory/fps/index.php?mode=voservice" - ) + + query = { + 'WavelengthRef_min': WavelengthRef_min, + 'WavelengthRef_max': WavelengthRef_max, + 'WavelengthMean_min': WavelengthMean_min, + 'WavelengthMean_max': WavelengthMean_max, + 'WavelengthEff_min': WavelengthEff_min, + 'WavelengthEff_max': WavelengthEff_max, + 'WavelengthMin_min': WavelengthMin_min, + 'WavelengthMin_max': WavelengthMin_max, + 'WavelengthMax_min': WavelengthMax_min, + 'WavelengthMax_max': WavelengthMax_max, + 'WidthEff_min': WidthEff_min, + 'WidthEff_max': WidthEff_max, + 'FWHM_min': FWHM_min, + 'FWHM_max': FWHM_max, + 'Instrument': Instrument, + 'Facility': Facility, + 'PhotSystem': PhotSystem, + 'ID': ID, + 'PhotCalID': PhotCalID, + 'FORMAT': FORMAT, + 'VERB': VERB + } + + # check validity of query parameters with limited allowed values + for key in ALLOWED_QUERY_PARAMETERS: + if key in query and query[key] not in ALLOWED_QUERY_PARAMETERS[key]: + raise InvalidQueryError( + f"Invalid value for parameter {key}. Allowed values are " + f"{ALLOWED_QUERY_PARAMETERS[key]}" + ) + response = self._request("GET", self.SVO_MAIN_URL, params=query, timeout=timeout or self.TIMEOUT, cache=cache) @@ -97,18 +194,21 @@ def get_filter_index(self, wavelength_eff_min, wavelength_eff_max, **kwargs): astropy.table.table.Table object Table containing data fetched from SVO (in response to query) """ - query = {'WavelengthEff_min': wavelength_eff_min.to_value(u.angstrom), - 'WavelengthEff_max': wavelength_eff_max.to_value(u.angstrom)} error_msg = 'No filter found for requested Wavelength Effective range' try: - return self.data_from_svo(query=query, error_msg=error_msg, **kwargs) + return self.data_from_svo( + WavelengthEff_min=wavelength_eff_min.to_value(u.angstrom), + WavelengthEff_max=wavelength_eff_max.to_value(u.angstrom), + error_msg=error_msg, + **kwargs + ) except requests.ReadTimeout: raise TimeoutError( "Query did not finish fast enough. A smaller wavelength range might " "succeed. Try increasing the timeout limit if a large range is needed." ) - def get_filter_metadata(self, filter_id, *, cache=True, timeout=None): + def get_filter_metadata(self, filter_id, *, cache=True, timeout=None, **kwargs): """Get metadata/parameters for the requested Filter ID from SVO Parameters @@ -122,6 +222,9 @@ def get_filter_metadata(self, filter_id, *, cache=True, timeout=None): See :ref:`caching documentation `. timeout : int Timeout in seconds. If not specified, defaults to ``conf.timeout``. + kwargs : dict + Appended to the ``query`` dictionary sent to SVO. See the API + documentation of `data_from_svo` for the valid parameter names. Returns ------- @@ -129,6 +232,16 @@ def get_filter_metadata(self, filter_id, *, cache=True, timeout=None): Dictionary of VOTable PARAM names and values. """ query = {'ID': filter_id, 'VERB': 0} + query.update(kwargs) + + bad_params = [param for param in query if param not in QUERY_PARAMETERS] + if bad_params: + raise InvalidQueryError( + f"parameter{'s' if len(bad_params) > 1 else ''} " + f"{', '.join(bad_params)} {'are' if len(bad_params) > 1 else 'is'} " + f"invalid. For a description of valid query parameters see the docstring for SvoFps.data_from_svo" + ) + response = self._request("GET", self.SVO_MAIN_URL, params=query, timeout=timeout or self.TIMEOUT, cache=cache) @@ -143,6 +256,57 @@ def get_filter_metadata(self, filter_id, *, cache=True, timeout=None): params[param.name] = param.value return params + def get_zeropoint(self, filter_id, *, mag_system='Vega', **kwargs): + """ + Get the zero point for a specififed filter in a specified system. + + This is a highly-specific downselection of the metadata returned by + `get_filter_metadata`; the full metadata includes the zero point with + ``Vega`` as the default system. + + Parameters + ---------- + filter_id : str + Filter ID in the format SVO specifies it: 'facilty/instrument.filter'. + This is returned by `get_filter_list` and `get_filter_index` as the + ``filterID`` column. + mag_system : 'Vega' (default), 'AB', or 'ST' + The magnitude system for which to return the zero point. + kwargs : dict + Appended to the ``query`` dictionary sent to SVO. See the API + documentation of `data_from_svo` for the valid parameter names. + + Examples + -------- + >>> from astroquery.svo_fps import SvoFps # doctest: +REMOTE_DATA + >>> SvoFps.get_zeropoint(filter_id='2MASS/2MASS.J', mag_system='AB') # doctest: +REMOTE_DATA + {'MagSys': 'AB', + 'ZeroPoint': , + 'ZeroPointUnit': 'Jy', + 'ZeroPointType': 'Pogson'} + >>> SvoFps.get_filter_metadata(filter_id='2MASS/2MASS.J', PhotCalID='2MASS/2MASS.J/AB') # doctest: +REMOTE_DATA + {'FilterProfileService': 'ivo://svo/fps', + 'filterID': '2MASS/2MASS.J', + ... + 'PhotCalID': '2MASS/2MASS.J/AB', + 'MagSys': 'AB', + 'ZeroPoint': , + 'ZeroPointUnit': 'Jy', + 'ZeroPointType': 'Pogson'} + + """ + if mag_system not in ['Vega', 'AB', 'ST']: + raise InvalidQueryError("Invalid magnitude system. Allowed values are 'Vega', 'AB', and 'ST'.") + + metadata = self.get_filter_metadata(filter_id=filter_id, + PhotCalID=f'{filter_id}/{mag_system}', **kwargs) + + zeropoint_keys = ['MagSys', 'ZeroPoint', 'ZeroPointUnit', 'ZeroPointType'] + + zp = {key: metadata[key] for key in zeropoint_keys if key in metadata} + + return zp + def get_transmission_data(self, filter_id, **kwargs): """Get transmission data for the requested Filter ID from SVO @@ -160,9 +324,8 @@ def get_transmission_data(self, filter_id, **kwargs): astropy.table.table.Table object Table containing data fetched from SVO (in response to query) """ - query = {'ID': filter_id} error_msg = 'No filter found for requested Filter ID' - return self.data_from_svo(query=query, error_msg=error_msg, **kwargs) + return self.data_from_svo(ID=filter_id, error_msg=error_msg, **kwargs) def get_filter_list(self, facility, *, instrument=None, **kwargs): """Get filters data for requested facilty and instrument from SVO @@ -182,10 +345,13 @@ def get_filter_list(self, facility, *, instrument=None, **kwargs): astropy.table.table.Table object Table containing data fetched from SVO (in response to query) """ - query = {'Facility': facility, - 'Instrument': instrument} error_msg = 'No filter found for requested Facilty (and Instrument)' - return self.data_from_svo(query=query, error_msg=error_msg, **kwargs) + return self.data_from_svo( + Facility=facility, + Instrument=instrument, + error_msg=error_msg, + **kwargs + ) SvoFps = SvoFpsClass() diff --git a/astroquery/svo_fps/tests/data/svo_fps_PhotCalID=2MASS.2MASS.H.Vega.xml b/astroquery/svo_fps/tests/data/svo_fps_PhotCalID=2MASS.2MASS.H.Vega.xml new file mode 100644 index 0000000000..cc7dab20af --- /dev/null +++ b/astroquery/svo_fps/tests/data/svo_fps_PhotCalID=2MASS.2MASS.H.Vega.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + Manually specified. See reference + + + + + + + + + + +
+
+
diff --git a/astroquery/svo_fps/tests/test_svo_fps.py b/astroquery/svo_fps/tests/test_svo_fps.py index d8c8c36047..39b62c413b 100644 --- a/astroquery/svo_fps/tests/test_svo_fps.py +++ b/astroquery/svo_fps/tests/test_svo_fps.py @@ -3,18 +3,20 @@ from astropy import units as u from requests import ReadTimeout -from astroquery.exceptions import InvalidQueryError, TimeoutError +from astroquery.exceptions import TimeoutError, InvalidQueryError from astroquery.utils.mocks import MockResponse from ..core import SvoFps DATA_FILES = {'filter_index': 'svo_fps_WavelengthEff_min=12000_WavelengthEff_max=12100.xml', 'transmission_data': 'svo_fps_ID=2MASS.2MASS.H.xml', - 'filter_list': 'svo_fps_Facility=Keck_Instrument=NIRC2.xml' + 'filter_list': 'svo_fps_Facility=Keck_Instrument=NIRC2.xml', + 'zeropoint': 'svo_fps_PhotCalID=2MASS.2MASS.H.Vega.xml', } TEST_LAMBDA = 12000 TEST_FILTER_ID = '2MASS/2MASS.H' TEST_FACILITY = 'Keck' TEST_INSTRUMENT = 'NIRC2' +TEST_MAG_SYSTEM = 'Vega' def data_path(filename): @@ -35,6 +37,10 @@ def get_mockreturn(method, url, params=None, timeout=10, cache=None, **kwargs): and (params['WavelengthEff_min'] == TEST_LAMBDA and params['WavelengthEff_max'] == TEST_LAMBDA+100)): filename = data_path(DATA_FILES['filter_index']) + elif ('PhotCalID' in params + and params.get('ID') == TEST_FILTER_ID + and params['PhotCalID'] == f'{TEST_FILTER_ID}/{TEST_MAG_SYSTEM}'): + filename = data_path(DATA_FILES['zeropoint']) elif 'ID' in params and params['ID'] == TEST_FILTER_ID: filename = data_path(DATA_FILES['filter_index']) elif 'Facility' in params and (params['Facility'] == TEST_FACILITY @@ -84,10 +90,29 @@ def test_get_filter_list(patch_get): assert 'filterID' in table.colnames +def test_get_zeropoint(patch_get): + zp = SvoFps.get_zeropoint(TEST_FILTER_ID, mag_system=TEST_MAG_SYSTEM) + assert 'ZeroPoint' in zp + assert 'MagSys' in zp + assert zp['MagSys'] == TEST_MAG_SYSTEM + assert 'ZeroPointType' in zp + assert zp['ZeroPointType'] == 'Pogson' + assert 'ZeroPointUnit' in zp + assert zp['ZeroPoint'].unit == u.Jy + + def test_invalid_query(patch_get): - msg = r"^parameter bad_param is invalid\. For a description of valid query " + msg = 'Invalid value for parameter VERB. Allowed values are {0, 1, 2}' + with pytest.raises(InvalidQueryError, match=msg): + SvoFps.data_from_svo(VERB=5) + # need this wonky regex b/c {'metadata', None} is a set and the order flips every time + msg = r"Invalid value for parameter FORMAT\. Allowed values are \{(?=.*'metadata')(?=.*None).*\}" with pytest.raises(InvalidQueryError, match=msg): - SvoFps.data_from_svo(query={"bad_param": 0, "FWHM": 20}) - msg = r"^parameters invalid_param, bad_param are invalid\. For a description of " + SvoFps.data_from_svo(FORMAT='silly_format') + + +def test_invalid_get_filter_metadata(patch_get): + msg = ('parameter flag_system is invalid. ' + 'For a description of valid query parameters see the docstring for SvoFps.data_from_svo') with pytest.raises(InvalidQueryError, match=msg): - SvoFps.data_from_svo(query={"invalid_param": 0, 'bad_param': -1}) + SvoFps.get_filter_metadata(TEST_FILTER_ID, flag_system='no such kwd') diff --git a/astroquery/svo_fps/tests/test_svo_fps_remote.py b/astroquery/svo_fps/tests/test_svo_fps_remote.py index e195318dc0..cab0bdfc13 100644 --- a/astroquery/svo_fps/tests/test_svo_fps_remote.py +++ b/astroquery/svo_fps/tests/test_svo_fps_remote.py @@ -40,6 +40,23 @@ def test_get_filter_list(self, test_facility, test_instrument): # Check if column for Filter ID (named 'filterID') exists in table assert 'filterID' in table.colnames + @pytest.mark.parametrize('test_filter_id, mag_system, expected_zp_jy', [ + ('2MASS/2MASS.J', 'Vega', 1594.0), + ('2MASS/2MASS.J', 'AB', 3631.0), + ]) + def test_get_zeropoint(self, test_filter_id, mag_system, expected_zp_jy): + zp = SvoFps.get_zeropoint(test_filter_id, mag_system=mag_system) + # Check all expected keys are present + assert 'ZeroPoint' in zp + assert 'MagSys' in zp + assert 'ZeroPointType' in zp + assert 'ZeroPointUnit' in zp + # Check the magnitude system matches what was requested + assert zp['MagSys'] == mag_system + # Check zero point has the right unit and an approximately correct value + assert zp['ZeroPoint'].unit == u.Jy + assert abs(zp['ZeroPoint'].value - expected_zp_jy) < 10.0 + def test_query_parameter_names(self): # Checks if `QUERY_PARAMETERS` is up to date. query = {"FORMAT": "metadata"}