Source code for scopesim.optics.surface

from pathlib import Path
from dataclasses import dataclass
from typing import Any

import numpy as np

from astropy import units as u
from astropy.io import ascii as ioascii
from astropy.table import Table

from synphot import SpectralElement
from synphot.models import Empirical1D

from ..effects import ter_curves_utils as ter_utils
from .surface_utils import (make_emission_from_emissivity,
                            make_emission_from_array, extract_type_from_unit)
from ..utils import (get_meta_quantity, quantify, from_currsys,
                     convert_table_comments_to_dict, find_file, get_logger)

logger = get_logger(__name__)


[docs]@dataclass class PoorMansSurface: """Solely used by SurfaceList.""" # FIXME: Use correct types instead of Any emission: Any throughput: Any meta: Any
[docs]class SpectralSurface: """ Initialised by a file containing one or more of the following columns. transmission, emissivity, reflection. The column wavelength must be given. Alternatively kwargs for the above mentioned quantities can be passed as arrays. If they are not Quantities, then a unit should also be passed with the <array_name>_unit syntax (i.e. emission_unit or wavelength_unit) If temperature is not given as a Quantity, it defaults to degrees Celsius. """ def __init__(self, filename=None, cmds=None, **kwargs): filename = find_file(filename) self.meta = {"filename": filename, "temperature": -270 * u.deg_C, # deg C "emission_unit": "", "wavelength_unit": u.um} self.cmds = cmds self.table = Table() if filename is not None and Path(filename).exists(): self.table = ioascii.read(filename) tbl_meta = convert_table_comments_to_dict(self.table) if isinstance(tbl_meta, dict): self.meta.update(tbl_meta) self.meta.update(kwargs) @property def area(self): if "area" in self.meta: the_area = self.from_meta("area", u.m ** 2) elif "outer" in self.meta: outer_diameter = self.from_meta("outer", u.m) the_area = np.pi * (0.5 * outer_diameter) ** 2 if "inner" in self.meta: inner_diameter = self.from_meta("inner", u.m) the_area -= np.pi * (0.5 * inner_diameter) ** 2 else: the_area = None return the_area @property def mirror_angle(self): if "angle" in self.meta: mirr_angle = self.from_meta("angle", u.deg) else: mirr_angle = 0 * u.deg return mirr_angle @property def wavelength(self): return self._get_array("wavelength") @property def throughput(self): return self._get_ter_property(self.meta.get("action", "transmission")) @property def transmission(self): return self._get_ter_property("transmission") @property def emissivity(self): return self._get_ter_property("emissivity") @property def reflection(self): return self._get_ter_property("reflection") @property def emission(self): """ Look for an emission array in self.meta. If it doesn't find this, it defaults to creating a blackbody and multiplies this by the emissivity. Assumption is that ``self.meta["temperature"]`` is in ``deg_C``, unless it is a ``u.Quantity`` with temperature unit attached. Return units are in ``PHOTLAM arcsec^-2``, even though ``arcsec^-2`` is not given. """ flux = self._get_array("emission") if flux is None and "temperature" not in self.meta: return None if flux is not None: wave = self._get_array("wavelength") flux = make_emission_from_array(flux, wave, meta=self.meta) elif "temperature" in self.meta: emiss = self.emissivity # SpectralElement [0..1] temp = from_currsys(self.meta["temperature"], self.cmds) if not isinstance(temp, u.Quantity): temp = quantify(temp, u.deg_C) temp = temp.to(u.Kelvin, equivalencies=u.temperature()) flux = make_emission_from_emissivity(temp, emiss) has_solid_angle = extract_type_from_unit(flux.meta["solid_angle"], "solid angle")[1] != u.Unit("") if flux is not None and has_solid_angle: conversion_factor = flux.meta["solid_angle"].to(u.arcsec ** -2) flux = flux * conversion_factor flux.meta["solid_angle"] = u.arcsec ** -2 flux.meta["history"].append( f"Converted to arcsec-2: {conversion_factor}") if flux is not None and "rescale_emission" in self.meta: dic = from_currsys(self.meta["rescale_emission"], self.cmds) amplitude = dic["value"] * u.Unit(dic["unit"]) filter_name = dic["filter_name"] if "filename_format" in dic: filter_name = dic["filename_format"].format(filter_name) flux = ter_utils.scale_spectrum(flux, filter_name, amplitude) return flux
[docs] def from_meta(self, key, default_unit=None): """ Convert a specific value in the meta dict to a ``Quantity``. Parameters ---------- key : str Which key to pull from self.meta default_unit : str, Unit In case self.meta doesn't contain a unit for the desired key Returns ------- meta_quantity : Quantity """ if default_unit is None: default_unit = "" meta_quantity = get_meta_quantity(self.meta, key, u.Unit(default_unit)) return meta_quantity
def _get_ter_property(self, ter_property, fmt="synphot"): """ Look for arrays for transmission, emissivity, or reflection. Parameters ---------- ter_property : str ``transmission``, ``emissivity``, ``reflection`` fmt : str Returns ------- response_curve : ``synphot.SpectralElement`` """ compliment_names = ["transmission", "emissivity", "reflection"] ii = np.where([ter_property == name for name in compliment_names])[0][0] compliment_names.pop(ii) wave = self._get_array("wavelength") value_arr = self._get_array(ter_property) if value_arr is None: value_arr = self._compliment_array(*compliment_names) if value_arr is not None and wave is not None and fmt == "synphot": response_curve = SpectralElement(Empirical1D, points=wave, lookup_table=value_arr) elif fmt == "array": response_curve = value_arr else: response_curve = None logger.warning("Both wavelength and %s must be set", ter_property) return response_curve def _compliment_array(self, colname_a, colname_b): """ Return an complimentary array using: ``a + b + c = 1``. E.g. ``Emissivity = 1 - (Transmission + Reflection)`` Parameters ---------- colname_a : str Name of the first TER property to look for colname_b Name of the second TER property to look for Returns ------- actual : ``synphot.SpectralElement`` Complimentary spectrum to those given """ compliment_a = self._get_array(colname_a) compliment_b = self._get_array(colname_b) if compliment_a is not None and compliment_b is not None: actual = 1 * compliment_a.unit - (compliment_a + compliment_b) elif compliment_a is not None and compliment_b is None: actual = 1 * compliment_a.unit - compliment_a elif compliment_b is not None and compliment_a is None: actual = 1 * compliment_b.unit - compliment_b elif compliment_b is None and compliment_a is None: actual = None return actual def _get_array(self, colname): """ Look for an array in either the self.meta or self.table attributes. Order of search goes: 1. self.meta, 2. self.table Parameters ---------- colname : str Array column (or key) name Returns ------- val_out : array-like Quantity """ if colname in self.meta: val = self.meta[colname] elif colname in self.table.colnames: val = self.table[colname].data else: logger.debug("%s not found in either '.meta' or '.table': [%s]", colname, self.meta.get("name", self.meta["filename"])) return None col_units = colname + "_unit" if isinstance(val, u.Quantity): units = val.unit elif col_units in self.meta: units = u.Unit(self.meta[col_units]) else: units = u.Unit("") if isinstance(val, u.Quantity): val_out = val.to(units) elif isinstance(val, (list, tuple, np.ndarray)): val_out = val * units elif val is None: val_out = None else: raise ValueError(f"{colname} must be of type: Quantity, array, " f"list, tuple, but is {type(val)}") return val_out def __repr__(self): msg = (f"{self.__class__.__name__}({self.meta['filename']}, " f"**{self.meta!r})") return msg def __str__(self): meta = self.meta name = meta["name"] if "name" in meta else meta["filename"] cols = "".join([col[0].upper() for col in self.table.colnames]) msg = f"SpectralSurface [{cols}] \"{name}\"" return msg