diff --git a/docs/source/configuration/optical_components.rst b/docs/source/configuration/optical_components.rst index 10fa6e3..c113c24 100644 --- a/docs/source/configuration/optical_components.rst +++ b/docs/source/configuration/optical_components.rst @@ -12,6 +12,8 @@ Attributes: Atmosphere ---------- This component models the behaviour of an atmosphere which has a spectral transmittance and a spectral emission. +It is possible to read the transmittance of the atmosphere from a CSV file, from an output file of ATRAN or call the webversion of ATRAN to compute the transmission profile. +The atmospheric emission can bei either read from a CSV file or computed as a grey body radiator of a given temperature and emissivity = 1 - transmission. .. code-block:: xml @@ -21,18 +23,52 @@ This component models the behaviour of an atmosphere which has a spectral transm +.. code-block:: xml + + Attributes: * | **transmittance:** str | The path to the file containing the spectral transmittance coefficients. For details on the required file structure see also :ref:`reading_csv`. * | **atran:** str - | Path to a file containing the output of ATRAN. In this case, the parameter emission is not available. Instead the parameter temp is used for the atmospheric emission. + | Path to a file containing the output of ATRAN. + * | **altitude:** float + | The observatory altitude for the call to ATRAN. + * | **altitude_unit:** str, *optional* = "ft" + | The unit of the observatory altitude for the call to ATRAN. + * | **wl_min:** float + | The minimum wavelength for the call to ATRAN. + * | **wl_min_unit:** str, *optional* = "um" + | The unit of the minimum wavelength for the call to ATRAN. + * | **wl_max:** float + | The maximum wavelength for the call to ATRAN. + * | **wl_max_unit:** str, *optional* = "um" + | The unit of the maximum wavelength for the call to ATRAN. + * | **latitude:** float, *optional* + | The observatory latitude for the call to ATRAN. + * | **latitude_unit:** str, *optional* = "degree" + | The unit of the observatory latitude for the call to ATRAN. + * | **water_vapor:** float, *optional* + | The water vapor overburden for the call to ATRAN. + * | **water_vapor_unit:** str, *optional* = "um" + | The unit of the water vapor overburden for the call to ATRAN. + * | **n_layers:** float, *optional* + | The number of atmospheric layers for the call to ATRAN. + * | **zenith_angle:** float, *optional* + | The zenith angle for the call to ATRAN. + * | **zenith_angle_unit:** str, *optional* = "degree" + | The unit of the zenith angle for the call to ATRAN (0 is towards the zenith). + * | **resolution:** float, *optional* + | The resolution for smoothing for the call to ATRAN (0 for no smoothing). * | **emission:** str, *optional* - | The path to the file containing the spectral radiance of the emission. For details on the required file structure see also :ref:`reading_csv`. This option is only available, if the parameter transmittance is given. + | The path to the file containing the spectral radiance of the emission. For details on the required file structure see also :ref:`reading_csv`. * | **temp:** float, *optional* - | The atmospheric temperature used for black body emission (only available for an ATRAN input). + | The atmospheric temperature used for grey body emission. * | **temp_unit:** str, *optional* = "K" - | Unit of the atmospheric temperature used for black body emission using the complement of the ATRAN tranmittance. + | Unit of the atmospheric temperature used for black body emission using the complement of the transmittance. StrayLight ---------- diff --git a/esbo_etc/classes/optical_component/Atmosphere.py b/esbo_etc/classes/optical_component/Atmosphere.py index 4796097..59b80b3 100644 --- a/esbo_etc/classes/optical_component/Atmosphere.py +++ b/esbo_etc/classes/optical_component/Atmosphere.py @@ -7,6 +7,9 @@ import astropy.units as u from astropy.io import ascii from astropy.modeling.models import BlackBody from typing import Union +import re +import requests as req +import numpy as np class Atmosphere(AOpticalComponent): @@ -14,6 +17,9 @@ class Atmosphere(AOpticalComponent): A class to model the atmosphere including the atmosphere's spectral transmittance and emission. """ + # defining the ATRAN-endpoint + ATRAN = "https://atran.arc.nasa.gov" + def __init__(self, **kwargs): """ Initialize a new atmosphere model @@ -27,6 +33,22 @@ class Atmosphere(AOpticalComponent): The format of the file will be guessed by `astropy.io.ascii.read()`. atran : str Path to the ATRAN output file containing the spectral transmittance-coefficients of the atmosphere. + altitude : u.Quantity + The observatory altitude in feet. + wl_min : u.Quantity + The minimal wavelength to consider in micrometer. + wl_max : u.Quantity + The maximal wavelength to consider in micrometer. + latitude : u.Quantity + The observatory's latitude in degrees. + water_vapor : u.Quantity + The water vapor overburden in microns (0 if unknown). + n_layers : int + The number of considered atmopsheric layers. + zenith_angle : u.Quantity + The zenith angle of the observation in degrees (0 is towards the zenith). + resolution : int + The resolution for smoothing (0 for no smoothing). emission : str Path to the file containing the spectral radiance of the atmosphere. The format of the file will be guessed by `astropy.io.ascii.read()`. @@ -36,14 +58,28 @@ class Atmosphere(AOpticalComponent): args = dict() if "atran" in kwargs: - args = self._fromATRAN(**kwargs) + args = self._fromATRAN(**{x: kwargs[x] for x in kwargs.keys() if x not in ["emission", "temp"]}) + elif "altitude" in kwargs: + logger.info("Requesting ATRAN transmission profile.") + data = self.__call_ATRAN(**{x: kwargs[x] for x in kwargs.keys() if x not in ["parent", "temp"]}) + args = self._fromATRAN(parent=kwargs["parent"], atran=data) elif "transmittance" in kwargs: - args = self._fromFiles(**kwargs) + args = self._fromFiles(**{x: kwargs[x] for x in kwargs.keys() if x not in ["emission", "temp"]}) else: logger.error("Wrong parameters for class Atmosphere.") + + if "temp" in kwargs: + # Create black body + bb = self.__gb_factory(kwargs["temp"]) + # Calculate emission + args["emission"] = SpectralQty(args["transmittance"].wl, bb(args["transmittance"].wl)) * ( + -1 * args["transmittance"] + 1) + elif "emission" in kwargs: + args["emission"] = SpectralQty.fromFile(kwargs["emission"], wl_unit_default=u.nm, + qty_unit_default=u.W / (u.m ** 2 * u.nm * u.sr)) super().__init__(parent=args["parent"], transreflectivity=args["transmittance"], noise=args["emission"]) - def _fromFiles(self, parent: IRadiant, transmittance: str, emission: str = None): + def _fromFiles(self, parent: IRadiant, transmittance: str): """ Initialize a new atmosphere model from two files @@ -54,9 +90,6 @@ class Atmosphere(AOpticalComponent): transmittance : str Path to the file containing the spectral transmittance-coefficients of the atmosphere. The format of the file will be guessed by `astropy.io.ascii.read()`. - emission : str - Path to the file containing the spectral radiance of the atmosphere. - The format of the file will be guessed by `astropy.io.ascii.read()`. Returns ------- @@ -66,14 +99,9 @@ class Atmosphere(AOpticalComponent): # Read the transmittance transmittance = SpectralQty.fromFile(transmittance, wl_unit_default=u.nm, qty_unit_default=u.dimensionless_unscaled) - if emission is None: - emission = 0 - else: - emission = SpectralQty.fromFile(emission, wl_unit_default=u.nm, - qty_unit_default=u.W / (u.m ** 2 * u.nm * u.sr)) - return {"parent": parent, "transmittance": transmittance, "emission": emission} + return {"parent": parent, "transmittance": transmittance} - def _fromATRAN(self, parent: IRadiant, atran: str, temp: u.Quantity = None): + def _fromATRAN(self, parent: IRadiant, atran: str): """ Initialize a new atmosphere model from an ATRAN output file @@ -83,8 +111,6 @@ class Atmosphere(AOpticalComponent): The parent element of the atmosphere from which the electromagnetic radiation is received. atran : str Path to the ATRAN output file containing the spectral transmittance-coefficients of the atmosphere. - temp : u.Quantity - The atmospheric temperature for the atmosphere's black body radiation. Returns ------- @@ -92,54 +118,109 @@ class Atmosphere(AOpticalComponent): The arguments for the class instantiation. """ # Read the file - data = ascii.read(atran, format=None) - # Set units - data["col2"].unit = u.um - data["col3"].unit = u.dimensionless_unscaled + data = self.__parse_ATRAN(atran) # Create spectral quantity transmittance = SpectralQty(data["col2"].quantity, data["col3"].quantity) + return {"parent": parent, "transmittance": transmittance} - if temp is not None: - # Create black body - bb = self.__gb_factory(temp) - # Calculate emission - emission = SpectralQty(transmittance.wl, bb(transmittance.wl)) * transmittance - else: - emission = 0 - return {"parent": parent, "transmittance": transmittance, "emission": emission} - - @staticmethod - def check_config(conf: Entry) -> Union[None, str]: + @u.quantity_input(altitude="length", latitude="angle", water_vapor="length", zenith_angle="angle", wl_min="length", + wl_max="length") + def __call_ATRAN(self, altitude: u.Quantity, wl_min: u.Quantity, wl_max: u.Quantity, + latitude: u.Quantity = 39 * u.degree, water_vapor: u.Quantity = 0 * u.um, n_layers: int = 2, + zenith_angle: u.Quantity = 0 * u.degree, resolution: int = 0): """ - Check the configuration for this class + Call the online version of ATRAN provided by SOFIA Parameters ---------- - conf : Entry - The configuration entry to be checked. + altitude : u.Quantity + The observatory altitude in feet. + wl_min : u.Quantity + The minimal wavelength to consider in micrometer. + wl_max : u.Quantity + The maximal wavelength to consider in micrometer. + latitude : u.Quantity + The observatory's latitude in degrees. + water_vapor : u.Quantity + The water vapor overburden in microns (0 if unknown). + n_layers : int + The number of considered atmopsheric layers. + zenith_angle : u.Quantity + The zenith angle of the observation in degrees (0 is towards the zenith). + resolution : int + The resolution for smoothing (0 for no smoothing). Returns ------- - mes : Union[None, str] - The error message of the check. This will be None if the check was successful. + data : str + The ATRAN computation results """ - if hasattr(conf, "transmittance"): - mes = conf.check_file("transmittance") - if mes is not None: - return mes - if hasattr(conf, "emission"): - mes = conf.check_file("emission") - if mes is not None: - return mes - else: - mes = conf.check_file("atran") - if mes is not None: - return mes - if hasattr(conf, "temp"): - mes = conf.check_quantity("temp", u.K) - if mes is not None: - return mes + # Select closest latitude from ATRAN options + latitude_ = min(np.array([9, 30, 39, 43, 59]) * u.degree, key=lambda x: abs(x - latitude.to(u.degree))) + # Select closest number of layers from ATRAN options + n_layers_ = min([2, 3, 4, 5], key=lambda x: abs(x - n_layers)) + # Assemble the data payload + data = {'Altitude': altitude.to(u.imperial.ft).value, + 'Obslat': '%d deg' % latitude_.value, + 'WVapor': water_vapor.to(u.um).value, + 'NLayers': n_layers_, + 'ZenithAngle': zenith_angle.to(u.degree).value, + 'WaveMin': wl_min.to(u.um).value, + 'WaveMax': wl_max.to(u.um).value, + 'Resolution': resolution} + # Send data to ATRAN via POST request + res = req.post(url=self.ATRAN + "/cgi-bin/atran/atran.cgi", data=data) + # Check if request was successful + if not res.ok: + logger.error("Error: Request returned status code " + str(res.status_code)) + # Extract the content of the reply + content = res.text + # Check if any ATRAN error occured + match = re.search('

ERROR!!

(.*)
', content) + if match: + logger.error("Error: " + match.group(1)) + + # Extract link to ATRAN result file + match = re.search('href="(/atran_calc/atran.(?:plt|smo).\\d*.dat)"', content) + # Check if link was found + if not match: + logger.error("Error: Link to data file not found.") + + # Request the ATRAN result via GET request + res = req.get(self.ATRAN + match.group(1)) + # Check if request was successful + if not res.ok: + logger.error("Error: Request returned status code " + str(res.status_code)) + + # Extract the content of the reply + data = res.text + # Check if result is empty + if data == "": + logger.error("Error: Request returned empty response.") + return data + + @staticmethod + def __parse_ATRAN(table: str): + """ + Parse an ATRAN result file and convert it to an astropy table + + Parameters + ---------- + table : str + Path to the file or content of the file. + + Returns + ------- + data : astropy.Table + The parsed table object. + """ + # Read the file + data = ascii.read(table, format=None) + # Set units + data["col2"].unit = u.um + data["col3"].unit = u.dimensionless_unscaled + return data @staticmethod @u.quantity_input(temp=[u.Kelvin, u.Celsius]) @@ -161,3 +242,65 @@ class Atmosphere(AOpticalComponent): """ bb = BlackBody(temperature=temp, scale=em * u.W / (u.m ** 2 * u.nm * u.sr)) return lambda wl: bb(wl) + + @staticmethod + def check_config(conf: Entry) -> Union[None, str]: + """ + Check the configuration for this class + + Parameters + ---------- + conf : Entry + The configuration entry to be checked. + + Returns + ------- + mes : Union[None, str] + The error message of the check. This will be None if the check was successful. + """ + if hasattr(conf, "transmittance"): + mes = conf.check_file("transmittance") + if mes is not None: + return mes + elif hasattr(conf, "atran"): + mes = conf.check_file("atran") + if mes is not None: + return mes + else: + mes = conf.check_quantity("altitude", u.imperial.ft) + if mes is not None: + return mes + mes = conf.check_quantity("wl_min", u.um) + if mes is not None: + return mes + mes = conf.check_quantity("wl_max", u.um) + if mes is not None: + return mes + if hasattr(conf, "latitude"): + mes = conf.check_quantity("latitude", u.degree) + if mes is not None: + return mes + if hasattr(conf, "water_vapor"): + mes = conf.check_quantity("water_vapor", u.um) + if mes is not None: + return mes + if hasattr(conf, "n_layers"): + mes = conf.check_float("n_layers") + if mes is not None: + return mes + if hasattr(conf, "zenith_angle"): + mes = conf.check_quantity("zenith_angle", u.degree) + if mes is not None: + return mes + if hasattr(conf, "resolution"): + mes = conf.check_float("resolution") + if mes is not None: + return mes + if hasattr(conf, "emission"): + mes = conf.check_file("emission") + if mes is not None: + return mes + elif hasattr(conf, "temp"): + mes = conf.check_quantity("temp", u.K) + if mes is not None: + return mes diff --git a/requirements.txt b/requirements.txt index c3117b9..d67becd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,4 +7,5 @@ halo~=0.0.29 pyfiglet~=0.8.post1 Sphinx~=3.1.2 -sphinx-rtd-theme~=0.5.0 \ No newline at end of file +sphinx-rtd-theme~=0.5.0 +requests~=2.24.0 \ No newline at end of file