Move ATRAN implementation to separate class

This commit is contained in:
Lukas Klass 2020-10-24 16:50:17 +02:00
parent 32a0885342
commit e1a78f053b
9 changed files with 10327 additions and 241 deletions

View File

@ -12,13 +12,34 @@ 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 transmittance is read from a CSV file.
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
<optical_component type="Atmosphere" transmittance="PathToTransmittanceFile" emission="PathToEmissionFile"/>
.. code-block:: xml
<optical_component type="Atmosphere" transmittance="PathToTransmittanceFile" temp="200" temp_unit="K"/>
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`.
* | **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`.
* | **temp:** float, *optional*
| 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 transmittance.
ATRAN
----------
This component uses the atmospheric transmission calculator ATRAN to model the behaviour of the atmosphere
It is possible to read the transmittance of the atmosphere from an output file of ATRAN or call the webversion of ATRAN to compute the transmission profile.
The atmospheric emission is computed as a grey body radiator of a given temperature and emissivity = 1 - transmission.
.. code-block:: xml
<optical_component type="Atmosphere" transmittance="PathToATRANFile" temp="200" temp_unit="K"/>
@ -32,8 +53,6 @@ The atmospheric emission can bei either read from a CSV file or computed as a gr
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.
* | **altitude:** float
| The observatory altitude for the call to ATRAN.
@ -63,13 +82,12 @@ Attributes:
| 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`.
* | **temp:** float, *optional*
| 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 transmittance.
StrayLight
----------
This component allows to model generic noise sources like stray light or zodiacal light from a file containing the spectral radiance of the emission.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

@ -134,6 +134,7 @@ components with a thermal grey body emission form a subclass of the optical comp
components are available
* An **atmosphere** component modelling the atmospheric transmittance and emission, both read from files.
* An **ATRAN** component to model the atmosphere using the ATRAN atmopsheric transmission calculator.
* A **stray light** component for modelling generic background source like zodiacal light or earth stray light.
* A **cosmic background** component to model thermal black body background radiation like the 2.7 K cosmic background radiation.
* Hot optical components with thermal emission of a given temperature and emissivity:

View File

@ -0,0 +1,246 @@
from .Atmosphere import Atmosphere
from ..IRadiant import IRadiant
from ..SpectralQty import SpectralQty
from ..Entry import Entry
from ...lib.logger import logger
from ...lib.cache import cache
import astropy.units as u
from astropy.io import ascii
from astropy.modeling.models import BlackBody
from astropy.table import QTable
from typing import Union
import re
import requests as req
import numpy as np
class ATRAN(Atmosphere):
"""
A class to model the atmosphere including the atmosphere's spectral transmittance and emission.
"""
# defining the ATRAN-endpoint
ATRAN = "https://atran.arc.nasa.gov"
@u.quantity_input(altitude="length", latitude="angle", water_vapor="length", zenith_angle="angle", wl_min="length",
wl_max="length", temp=[u.Kelvin, u.Celsius])
def __init__(self, parent: IRadiant, transmittance: str = None, altitude: u.Quantity = None,
wl_min: u.Quantity = None, wl_max: u.Quantity = None, 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, temp: u.Quantity = 0 * u.K):
"""
Initialize a new atmosphere model from ATRAN output
Parameters
----------
parent : IRadiant
The parent element of the atmosphere from which the electromagnetic radiation is received.
transmittance : 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).
temp : u.Quantity
The atmospheric temperature for the atmosphere's black body radiation.
"""
if transmittance is not None:
data = self.__parse_ATRAN(transmittance)
else:
logger.info("Requesting ATRAN transmission profile.")
data = self.__call_ATRAN(altitude, wl_min, wl_max, latitude, water_vapor, n_layers, zenith_angle,
resolution)
transmittance = SpectralQty(data["col2"].quantity, data["col3"].quantity)
super().__init__(parent, transmittance, temp=temp)
@u.quantity_input(altitude="length", latitude="angle", water_vapor="length", zenith_angle="angle", wl_min="length",
wl_max="length")
@cache
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):
"""
Call the online version of ATRAN provided by SOFIA
Parameters
----------
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
-------
data : QTable
The ATRAN computation results
"""
# 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('<CENTER><H2>ERROR!!</H2></CENTER><CENTER>(.*)</CENTER>', 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 self.__parse_ATRAN(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])
def __gb_factory(temp: u.Quantity, em: Union[int, float] = 1):
"""
Factory for a grey body lambda-function.
Parameters
----------
temp : Quantity in Kelvin / Celsius
The temperature fo the grey body.
em : Union[int, float]
Emissivity of the the grey body
Returns
-------
bb : Callable
The lambda function for the grey body.
"""
bb = BlackBody(temperature=temp, scale=em * u.W / (u.m ** 2 * u.nm * u.sr))
return lambda wl: bb(wl)
def __repr__(self):
return "ATRAN Object"
@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
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, "temp"):
mes = conf.check_quantity("temp", u.K)
if mes is not None:
return mes

View File

@ -2,16 +2,9 @@ from .AOpticalComponent import AOpticalComponent
from ..IRadiant import IRadiant
from ..SpectralQty import SpectralQty
from ..Entry import Entry
from ...lib.logger import logger
from ...lib.cache import cache
import astropy.units as u
from astropy.io import ascii
from astropy.modeling.models import BlackBody
from astropy.table import QTable
from typing import Union
import re
import requests as req
import numpy as np
class Atmosphere(AOpticalComponent):
@ -19,10 +12,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):
@u.quantity_input(temp=[u.Kelvin, u.Celsius])
def __init__(self, parent: IRadiant, transmittance: Union[str, float, SpectralQty], emission: str = None,
temp: u.Quantity = None):
"""
Initialize a new atmosphere model
@ -30,27 +22,9 @@ class Atmosphere(AOpticalComponent):
----------
parent : IRadiant
The parent element of the atmosphere from which the electromagnetic radiation is received.
transmittance : str
transmittance : str, float, SpectralQty
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()`.
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()`.
@ -58,173 +32,26 @@ class Atmosphere(AOpticalComponent):
The atmospheric temperature for the atmosphere's black body radiation.
"""
args = dict()
if "atran" in kwargs:
data = self.__parse_ATRAN(kwargs["atran"])
args = self._fromATRAN(parent=kwargs["parent"], atran=data)
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(**{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))
else:
args["emission"] = 0
super().__init__(parent=args["parent"], transreflectivity=args["transmittance"], noise=args["emission"])
def _fromFiles(self, parent: IRadiant, transmittance: str):
"""
Initialize a new atmosphere model from two files
Parameters
----------
parent : IRadiant
The parent element of the atmosphere from which the electromagnetic radiation is received.
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()`.
Returns
-------
args : dict
The arguments for the class instantiation.
"""
# Read the transmittance
transmittance = SpectralQty.fromFile(transmittance, wl_unit_default=u.nm,
qty_unit_default=u.dimensionless_unscaled)
return {"parent": parent, "transmittance": transmittance}
if isinstance(transmittance, str):
transmittance_sqty = SpectralQty.fromFile(transmittance, wl_unit_default=u.nm,
qty_unit_default=u.dimensionless_unscaled)
else:
transmittance_sqty = transmittance
if emission:
# Read the emission
emission_sqty = SpectralQty.fromFile(emission, wl_unit_default=u.nm,
qty_unit_default=u.W / (u.m ** 2 * u.nm * u.sr))
elif temp is not None:
# Create black body
bb = self.__gb_factory(temp)
# Calculate emission
emission_sqty = SpectralQty(transmittance_sqty.wl, bb(transmittance_sqty.wl)) * (
-1 * transmittance_sqty + 1)
else:
emission_sqty = 0
def _fromATRAN(self, parent: IRadiant, atran: QTable):
"""
Initialize a new atmosphere model from an ATRAN output file
Parameters
----------
parent : IRadiant
The parent element of the atmosphere from which the electromagnetic radiation is received.
atran : QTable
QTable containing the atmospheric transmission coefficients.
Returns
-------
args : dict
The arguments for the class instantiation.
"""
# Create spectral quantity
transmittance = SpectralQty(atran["col2"].quantity, atran["col3"].quantity)
return {"parent": parent, "transmittance": transmittance}
@u.quantity_input(altitude="length", latitude="angle", water_vapor="length", zenith_angle="angle", wl_min="length",
wl_max="length")
@cache
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):
"""
Call the online version of ATRAN provided by SOFIA
Parameters
----------
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
-------
data : QTable
The ATRAN computation results
"""
# 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('<CENTER><H2>ERROR!!</H2></CENTER><CENTER>(.*)</CENTER>', 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 self.__parse_ATRAN(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
super().__init__(parent=parent, transreflectivity=transmittance_sqty, noise=emission_sqty)
@staticmethod
@u.quantity_input(temp=[u.Kelvin, u.Celsius])
@ -247,9 +74,6 @@ class Atmosphere(AOpticalComponent):
bb = BlackBody(temperature=temp, scale=em * u.W / (u.m ** 2 * u.nm * u.sr))
return lambda wl: bb(wl)
def __repr__(self):
return "Atmosphere Object"
@staticmethod
def check_config(conf: Entry) -> Union[None, str]:
"""
@ -265,44 +89,9 @@ class Atmosphere(AOpticalComponent):
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
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:

View File

@ -1,5 +1,6 @@
from .AOpticalComponent import *
from .Atmosphere import *
from .ATRAN import *
from .StrayLight import *
from .AHotOpticalComponent import *
from .Filter import *

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,30 @@
from unittest import TestCase
from esbo_etc import ATRAN, FileTarget, SpectralQty
import numpy as np
import astropy.units as u
class TestATRAN(TestCase):
def setUp(self):
self.target = FileTarget("tests/data/target/target_demo_3.csv", np.arange(16, 16.1, 0.01) << u.um)
self.atmosphere = ATRAN(parent=self.target, transmittance="tests/data/atmosphere/atran.dat", temp=240 * u.K)
self.atmosphere2 = ATRAN(parent=self.target, altitude=15 * u.km, wl_min=16 * u.um, wl_max=16.1 * u.um,
temp=240 * u.K)
def test_calcSignal(self):
self.assertEqual(self.atmosphere.calcSignal()[0],
SpectralQty(np.arange(16, 16.1, 0.01) << u.um,
np.array([6.555e-16, 1.03e-15, 4.311e-16, 6.13e-16, 1.016e-15, 1.077e-15,
4.733e-16, 8.143e-16, 1.12e-15, 1.128e-15, 5.569e-16]) << u.W / (
u.m ** 2 * u.nm)))
self.assertEqual(self.atmosphere2.calcSignal()[0],
SpectralQty(np.arange(16, 16.1, 0.01) << u.um,
np.array([8.469e-16, 1.068e-15, 5.145e-16, 8.574e-16, 1.087e-15, 1.113e-15,
5.08e-16, 9.303e-16, 1.15e-15, 1.107e-15, 0]) << u.W / (
u.m ** 2 * u.nm)))
def test_calcBackground(self):
self.assertTrue(np.allclose(self.atmosphere.calcBackground().qty,
np.array([1.109e-3, 1.97e-4, 1.686e-3, 1.253e-3, 2.989e-4, 1.741e-4, 1.618e-3,
8.301e-4, 1.377e-4, 1.431e-4, 1.46e-3]) << u.W / (
u.m ** 2 * u.nm * u.sr), atol=1e-6))