Source code for exorad.models.optics.opticalElement

import numpy as np
from astropy import units as u
from scipy.interpolate import interp1d

from exorad.log import Logger
from exorad.models.utils import get_wl_col_name


[docs] class OpticalElement(Logger): """ Handler for an optical element. This class represents an optical element within an optical path, managing its transmission, emissivity, temperature, and other relevant properties. Parameters ---------- description : dict Dictionary containing the optical element's description from the payload configuration file. wl : `astropy.units.Quantity` or array_like Wavelength grid where the optical properties of the element are evaluated. Should have units of length (e.g., microns). Attributes ---------- name : str Name of the optical element. description : dict Original description dictionary of the optical element. wl : `astropy.units.Quantity` or array_like Wavelength grid. transmission : `numpy.ndarray` Transmission of the optical element evaluated over the wavelength grid. emissivity : `numpy.ndarray` Emissivity of the optical element evaluated over the wavelength grid. temperature : `astropy.units.Quantity` or None Temperature of the optical element in Kelvin. `None` if not specified. type : str Type of the optical element (e.g., "detector box", "optics box", "surface"). position : str Position of the optical element in the optical path (e.g., "detector", "optics box", "path"). angle : `astropy.units.Quantity` or None Solid angle associated with the optical element in steradians. `None` if not specified. Methods ------- _get_angle() Retrieves and processes the solid angle of the optical element. _get_position() Determines the position of the optical element in the optical path. _get_temperature() Retrieves and processes the temperature of the optical element. _get_transmission() Calculates the transmission of the optical element over the wavelength grid. _get_emissivity() Calculates the emissivity of the optical element over the wavelength grid. """ def __init__(self, description, wl): """ Initialize the OpticalElement instance. Parameters ---------- description : dict Dictionary containing the optical element's description. wl : `astropy.units.Quantity` or array_like Wavelength grid with appropriate units. """ super().__init__() self.name = description["value"] self.debug(f"Initializing OpticalElement: {self.name}") self.description = description self.wl = wl self.type = self.description["type"]["value"] self.transmission = self._get_transmission() self.emissivity = self._get_emissivity() self.temperature = self._get_temperature() self.position = self._get_position() self.angle = self._get_angle()
[docs] def _get_angle(self): """ Retrieve and process the solid angle of the optical element. Returns ------- angle : `astropy.units.Quantity` or None Solid angle in steradians if specified; otherwise, `None`. """ if "solid_angle" in self.description: angle_value = self.description["solid_angle"]["value"] if hasattr(angle_value, "unit"): return angle_value.to(u.sr) else: self.debug("Angle assumed to be in steradians (sr)") return angle_value * u.sr else: # If not specified, return None to use the default value from the instrument return None
[docs] def _get_position(self): """ Determine the position of the optical element in the optical path. Returns ------- position : str Position identifier ("detector", "optics box", or "path"). """ if self.type == "detector box": return "detector" elif self.type == "optics box": return "optics box" else: return "path"
[docs] def _get_temperature(self): """ Retrieve and process the temperature of the optical element. Returns ------- temperature : `astropy.units.Quantity` or None Temperature in Kelvin if specified; otherwise, `None`. """ if "temperature" in self.description: temp_value = self.description["temperature"]["value"] if hasattr(temp_value, "unit"): return temp_value.to(u.K) else: self.debug("Temperature assumed to be in Kelvin (K)") return temp_value * u.K else: return None
[docs] def _get_transmission(self): """ Calculate the transmission of the optical element over the wavelength grid. The method handles various scenarios: - If a transmission data file is provided, it interpolates the transmission based on the wavelength grid. - If both transmission and reflectivity are provided without a specified "use" key, it raises an error. - Applies wavelength boundaries (`wl_min` and `wl_max`) if specified. Returns ------- transmission : `numpy.ndarray` Transmission values corresponding to the wavelength grid. Raises ------ KeyError If the required transmission or reflectivity column is not found in the transmission data file. """ if "data" in self.description: self.debug("Transmission data file found") # Determine which column to use for transmission if "use" in self.description: colname = self.description["use"]["value"] else: try: colname = "Transmission" _ = self.description["data"][colname] except KeyError: try: colname = "Reflectivity" _ = self.description["data"][colname] except KeyError: error_msg = f"{colname} column name not found in transmission data file" self.error(error_msg) raise KeyError(error_msg) # Get the wavelength column name from the data wl_colname = get_wl_col_name( self.description["data"], description=self.description ) # Create an interpolation function for transmission tr_func = interp1d( self.description["data"][wl_colname], self.description["data"][colname], assume_sorted=False, fill_value=0.0, bounds_error=False, ) # Evaluate the transmission over the wavelength grid transmission = tr_func(self.wl) elif "transmission" in self.description and "reflectivity" in self.description: if "use" in self.description: key = self.description["use"]["value"] transmission = np.ones(self.wl.size) * self.description[key]["value"] else: error_msg = ( "Both transmission and reflectivity are included but 'use' is not indicated" ) self.error(error_msg) raise KeyError(error_msg) elif "transmission" in self.description or "reflectivity" in self.description: try: transmission = np.ones(self.wl.size) * self.description["transmission"]["value"] except KeyError: transmission = np.ones(self.wl.size) * self.description["reflectivity"]["value"] self.debug( f"Reflectivity keyword found for transmission for {self.name}" ) # Apply wavelength lower bound if specified if "wl_min" in self.description: wl_min = self.description["wl_min"]["value"].to(self.wl.unit) self.debug(f"Transmission lower boundary at {wl_min}") idx = np.where(self.wl < wl_min) transmission[idx] = 0.0 # Apply wavelength upper bound if specified if "wl_max" in self.description: wl_max = self.description["wl_max"]["value"].to(self.wl.unit) self.debug(f"Transmission upper boundary at {wl_max}") idx = np.where(self.wl > wl_max) transmission[idx] = 0.0 else: # Default transmission is 1 (no attenuation) transmission = np.ones(self.wl.size) self.debug(f"Transmission: {transmission}") return transmission
[docs] def _get_emissivity(self): """ Calculate the emissivity of the optical element over the wavelength grid. The method handles various scenarios: - If an emissivity data file is provided, it interpolates the emissivity based on the wavelength grid. - If emissivity is specified directly, it applies the constant value. - For "detector box" type, emissivity is set to 1. - Otherwise, emissivity is set to 0. Returns ------- emissivity : `numpy.ndarray` Emissivity values corresponding to the wavelength grid. Raises ------ KeyError If the emissivity column is not found in the emissivity data file. """ if "data" in self.description: self.debug("Emissivity data file found") # Get the wavelength column name from the data wl_colname = get_wl_col_name( self.description["data"], description=self.description ) # Determine the emissivity column name em_colname = None for em_key in ["Emissivity", "emissivity"]: if em_key in self.description["data"].keys(): em_colname = em_key break if em_colname is None: error_msg = "Emissivity column not found in transmission data file" self.error(error_msg) raise KeyError(error_msg) # Create an interpolation function for emissivity em_func = interp1d( self.description["data"][wl_colname], self.description["data"][em_colname], assume_sorted=False, fill_value=0.0, bounds_error=False, ) # Evaluate the emissivity over the wavelength grid emissivity = em_func(self.wl) elif "emissivity" in self.description: # Apply a constant emissivity value emissivity = np.ones(self.wl.size) * self.description["emissivity"]["value"] elif self.type == "detector box": # Default emissivity for detector box is 1 emissivity = np.ones(self.wl.size) else: # Default emissivity is 0 (non-emissive) emissivity = np.zeros(self.wl.size) self.debug(f"Emissivity: {emissivity}") return emissivity