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