From f951f5c6363e1754fd9a1e46ddce7876481519e6 Mon Sep 17 00:00:00 2001 From: LukasK13 Date: Sat, 12 Sep 2020 11:51:49 +0200 Subject: [PATCH] Factories unified --- docs/source/developer/extending.rst | 5 +- esbo_etc/classes/optical_component/Filter.py | 88 +++++++++++-------- .../OpticalComponentFactory.py | 18 +--- esbo_etc/classes/sensor/SensorFactory.py | 35 ++------ esbo_etc/classes/target/TargetFactory.py | 16 ++-- tests/optical_component/test_Filter.py | 8 +- tests/test_RadiantFactory.py | 3 +- 7 files changed, 76 insertions(+), 97 deletions(-) diff --git a/docs/source/developer/extending.rst b/docs/source/developer/extending.rst index 46ba3d4..ed86c5b 100644 --- a/docs/source/developer/extending.rst +++ b/docs/source/developer/extending.rst @@ -34,7 +34,7 @@ The method ``propagate()`` is used to model the propagation of incoming radiatio Therefore, this method receives as parameter the incoming radiation as ``SpectralQty``-object and must return the manipulated radiation as ``SpectralQty``-object. The optional second task consists of modifying the factory method ``create()`` of the class ``OpticalComponentFactory`` in order to properly initialize the new optical component from the configuration. -This is only necessary if the new optical component needs additional parameters besides the attributes of the corresponding configuration tag of if the new component provides multiple constructors (see class ``Filter``). +This is only necessary if the new optical component needs additional parameters besides the attributes of the corresponding configuration tag. Adding Detector Components -------------------------- @@ -46,5 +46,4 @@ Thereby, the new class must implement the three methods ``calcSNR()``, ``calcExp All three methods obtain the incoming background and signal radiation as well as the obstruction factor as parameters apart from some specific parameters and must return the corresponding calculated value. Additionally, all three methods must be able to calculate multiple SNRs, exposure times or sensitivities at once if an array of specific parameters is provided. -Besides the implementation of the detector, the factory method ``create()`` of the class ``SensorFactory`` must be modified. -In detail, a new if-branch must be added which assembles all necessary parameters for the constructor of the new component and calls the constructor. +Besides the implementation of the detector, the factory method ``create()`` of the class ``SensorFactory`` can be modified. diff --git a/esbo_etc/classes/optical_component/Filter.py b/esbo_etc/classes/optical_component/Filter.py index 68ab9fd..7ff3ba0 100644 --- a/esbo_etc/classes/optical_component/Filter.py +++ b/esbo_etc/classes/optical_component/Filter.py @@ -4,7 +4,7 @@ from ..IRadiant import IRadiant from ...lib.logger import logger from ..Entry import Entry from astropy import units as u -from typing import Union, Callable +from typing import Union import numpy as np @@ -21,10 +21,7 @@ class Filter(AHotOpticalComponent): L=dict(cwl=3600 * u.nm, bw=1200 * u.nm), M=dict(cwl=4800 * u.nm, bw=800 * u.nm), N=dict(cwl=10200 * u.nm, bw=2500 * u.nm)) - @u.quantity_input(temp=[u.Kelvin, u.Celsius], obstructor_temp=[u.Kelvin, u.Celsius]) - def __init__(self, parent: IRadiant, transmittance: Union[SpectralQty, Callable[[u.Quantity], u.Quantity]], - emissivity: Union[str, float] = 1, temp: u.Quantity = 0 * u.K, obstruction: float = 0, - obstructor_temp: u.Quantity = 0 * u.K, obstructor_emissivity: float = 1): + def __init__(self, **kwargs): """ Instantiate a new filter model @@ -32,8 +29,15 @@ class Filter(AHotOpticalComponent): ---------- parent : IRadiant The parent element of the optical component from which the electromagnetic radiation is received. - transmittance : Union[SpectralQty, Callable] - The spectral transmittance coefficients of the filter. + transmittance : str + Path to the file containing the spectral transmittance-coefficients of the filter element. + The format of the file will be guessed by `astropy.io.ascii.read()`. + band : str + The spectral band of the filter. Can be one of [U, B, V, R, I, J, H, K]. + start : length-quantity + Start wavelength of the pass-band + end : length-quantity + End wavelength of the pass-band emissivity : Union[str, float] The spectral emissivity coefficient for the optical surface. temp: Quantity in Kelvin / Celsius @@ -48,14 +52,22 @@ class Filter(AHotOpticalComponent): obstructor_emissivity : float Emissivity of the obstructing component. """ - super().__init__(parent, emissivity, temp, obstruction, obstructor_temp, obstructor_emissivity) - self._transmittance = transmittance + args = dict() + if "band" in kwargs: + args = self._fromBand(**kwargs) + elif "transmittance" in kwargs: + args = self._fromFile(**kwargs) + elif "start" in kwargs and "end" in kwargs: + args = self._fromRange(**kwargs) + else: + logger.error("Wrong parameters for filter.") + self._transmittance = args.pop("transmittance") + super().__init__(**args) - @classmethod # @u.quantity_input(temp=[u.Kelvin, u.Celsius], obstructor_temp=[u.Kelvin, u.Celsius]) - def fromBand(cls, parent: IRadiant, band: str, emissivity: Union[str, float] = 1, temp: u.Quantity = 0 * u.K, - obstruction: float = 0, obstructor_temp: u.Quantity = 0 * u.K, - obstructor_emissivity: float = 1) -> "Filter": + def _fromBand(self, parent: IRadiant, band: str, emissivity: Union[str, float] = 1, temp: u.Quantity = 0 * u.K, + obstruction: float = 0, obstructor_temp: u.Quantity = 0 * u.K, + obstructor_emissivity: float = 1) -> dict: """ Instantiate a new filter model from a spectral band. The filter will be modelled as bandpass filter of infinite order and therefore similar to a hat-function. @@ -82,20 +94,19 @@ class Filter(AHotOpticalComponent): Returns ------- - filter : Filter - The instantiated filter object. + args : dict + The arguments for the class instantiation. """ - if band not in cls._band.keys(): - logger.error("Band has to be one of '[" + ", ".join(list(cls._band.keys())) + "]'") - return cls.fromRange(parent, cls._band[band]["cwl"] - cls._band[band]["bw"] / 2, - cls._band[band]["cwl"] + cls._band[band]["bw"] / 2, emissivity, temp, obstruction, - obstructor_temp, obstructor_emissivity) + if band not in self._band.keys(): + logger.error("Band has to be one of '[" + ", ".join(list(self._band.keys())) + "]'") + return self._fromRange(parent, self._band[band]["cwl"] - self._band[band]["bw"] / 2, + self._band[band]["cwl"] + self._band[band]["bw"] / 2, emissivity, temp, obstruction, + obstructor_temp, obstructor_emissivity) - @classmethod # @u.quantity_input(temp=[u.Kelvin, u.Celsius], obstructor_temp=[u.Kelvin, u.Celsius]) - def fromFile(cls, parent: IRadiant, transmittance: str, emissivity: Union[str, float] = 1, - temp: u.Quantity = 0 * u.K, obstruction: float = 0, obstructor_temp: u.Quantity = 0 * u.K, - obstructor_emissivity: float = 1) -> "Filter": + def _fromFile(self, parent: IRadiant, transmittance: str, emissivity: Union[str, float] = 1, + temp: u.Quantity = 0 * u.K, obstruction: float = 0, obstructor_temp: u.Quantity = 0 * u.K, + obstructor_emissivity: float = 1) -> dict: """ Instantiate a new filter model from a file containing the spectral transmittance coefficients. @@ -122,17 +133,17 @@ class Filter(AHotOpticalComponent): Returns ------- - filter : Filter - The instantiated filter object. + args : dict + The arguments for the class instantiation. """ - return cls(parent, SpectralQty.fromFile(transmittance, u.nm, u.dimensionless_unscaled), emissivity, temp, - obstruction, obstructor_temp, obstructor_emissivity) + return {"parent": parent, "transmittance": SpectralQty.fromFile(transmittance, u.nm, u.dimensionless_unscaled), + "emissivity": emissivity, "temp": temp, "obstruction": obstruction, "obstructor_temp": obstructor_temp, + "obstructor_emissivity": obstructor_emissivity} - @classmethod # @u.quantity_input(start="length", end="length", temp=[u.Kelvin, u.Celsius], obstructor_temp=[u.Kelvin, u.Celsius]) - def fromRange(cls, parent: IRadiant, start: u.Quantity, end: u.Quantity, emissivity: Union[str, float] = 1, - temp: u.Quantity = 0 * u.K, obstruction: float = 0, obstructor_temp: u.Quantity = 0 * u.K, - obstructor_emissivity: float = 1) -> "Filter": + def _fromRange(self, parent: IRadiant, start: u.Quantity, end: u.Quantity, emissivity: Union[str, float] = 1, + temp: u.Quantity = 0 * u.K, obstruction: float = 0, obstructor_temp: u.Quantity = 0 * u.K, + obstructor_emissivity: float = 1) -> dict: """ Instantiate a new filter model from a spectral range. The filter will be modelled as bandpass filter of infinite order and therefore similar to a hat-function. @@ -161,11 +172,12 @@ class Filter(AHotOpticalComponent): Returns ------- - filter : Filter - The instantiated filter object. + args : dict + The arguments for the class instantiation. """ - return cls(parent, cls.__filter_factory(start, end), emissivity, temp, - obstruction, obstructor_temp, obstructor_emissivity) + return {"parent": parent, "transmittance": self.__filter_factory(start, end), + "emissivity": emissivity, "temp": temp, "obstruction": obstruction, "obstructor_temp": obstructor_temp, + "obstructor_emissivity": obstructor_emissivity} def _propagate(self, sqty: SpectralQty) -> SpectralQty: """ @@ -201,8 +213,8 @@ class Filter(AHotOpticalComponent): lambda : Callable[[u.Quantity], u.Quantity] The filter function """ - return lambda wl: np.logical_and(np.greater_equal(wl, start), np.greater_equal(end, wl)).astype(int) *\ - u.dimensionless_unscaled + return lambda wl: np.logical_and(np.greater_equal(wl, start), np.greater_equal(end, wl)).astype( + int) * u.dimensionless_unscaled @staticmethod def check_config(conf: Entry) -> Union[None, str]: diff --git a/esbo_etc/classes/optical_component/OpticalComponentFactory.py b/esbo_etc/classes/optical_component/OpticalComponentFactory.py index caac9e0..812c351 100644 --- a/esbo_etc/classes/optical_component/OpticalComponentFactory.py +++ b/esbo_etc/classes/optical_component/OpticalComponentFactory.py @@ -37,26 +37,16 @@ class OpticalComponentFactory(ARadiantFactory): obj : AOpticalComponent The created optical component """ - opts = self.collectOptions(options) if parent is not None: - # New component is of type Optical Component + opts = self.collectOptions(options) opts["parent"] = parent - class_ = getattr(oc, options.type) - if options.type in ["Atmosphere", "StrayLight", "CosmicBackground", "Mirror", "Lens", "BeamSplitter"]: + if hasattr(oc, options.type): + class_ = getattr(oc, options.type) return class_(**opts) - elif options.type == "Filter": - if hasattr(options, "band"): - return oc.Filter.fromBand(**opts) - elif hasattr(options, "transmittance"): - return oc.Filter.fromFile(**opts) - elif hasattr(options, "start") and hasattr(options, "end"): - return oc.Filter.fromRange(**opts) - else: - logger.error("Wrong parameters for filter.") else: logger.error("Unknown optical component type: '" + options.type + "'") else: - logger.error("No parent given for optical component.") + logger.error("Parent object is required for optical component.") def fromConfigBatch(self, conf: Entry, parent: IRadiant) -> AOpticalComponent: """ diff --git a/esbo_etc/classes/sensor/SensorFactory.py b/esbo_etc/classes/sensor/SensorFactory.py index 52d7145..1c76e62 100644 --- a/esbo_etc/classes/sensor/SensorFactory.py +++ b/esbo_etc/classes/sensor/SensorFactory.py @@ -2,8 +2,7 @@ from ..AFactory import AFactory from ..IRadiant import IRadiant from ..Entry import Entry from .ASensor import ASensor -from .Imager import Imager -from .Heterodyne import Heterodyne +from ...classes import sensor as sensor from ...lib.logger import logger @@ -38,29 +37,13 @@ class SensorFactory(AFactory): obj : ASensor The created sensor object """ - opts = self.collectOptions(options) - - if options.type == "Imager": + if parent is not None: + opts = self.collectOptions(options) args = dict(parent=parent, **opts, common_conf=self._common_conf) - if hasattr(options, "center_offset"): - # noinspection PyCallingNonCallable - args["center_offset"] = options.center_offset() - if hasattr(options, "photometric_aperture"): - if hasattr(options.photometric_aperture, "shape") and isinstance( - options.photometric_aperture.shape, Entry): - args["shape"] = options.photometric_aperture.shape() - if hasattr(options.photometric_aperture, "contained_energy") and isinstance( - options.photometric_aperture.contained_energy, Entry): - args["contained_energy"] = options.photometric_aperture.contained_energy() - if hasattr(options.photometric_aperture, "aperture_size") and isinstance( - options.photometric_aperture.aperture_size, Entry): - args["aperture_size"] = options.photometric_aperture.aperture_size() - return Imager(**args) - elif options.type == "Heterodyne": - args = dict(parent=parent, **opts, common_conf=self._common_conf) - if hasattr(options, "n_on"): - # noinspection PyCallingNonCallable - args["n_on"] = options.n_on() - return Heterodyne(**args) + if hasattr(sensor, options.type): + class_ = getattr(sensor, options.type) + return class_(**args) + else: + logger.error("Unknown sensor type: '" + options.type + "'") else: - logger.error("Wrong sensor type: " + options.type) + logger.error("Parent object is required for sensor.") diff --git a/esbo_etc/classes/target/TargetFactory.py b/esbo_etc/classes/target/TargetFactory.py index cc9ce60..2d3ba5d 100644 --- a/esbo_etc/classes/target/TargetFactory.py +++ b/esbo_etc/classes/target/TargetFactory.py @@ -1,4 +1,3 @@ -import astropy.units as u from ..ARadiantFactory import ARadiantFactory from ..Entry import Entry from ..IRadiant import IRadiant @@ -38,18 +37,13 @@ class TargetFactory(ARadiantFactory): obj : ATarget The created target object """ - opts = self.collectOptions(options) + if parent is None: - # New component is of type target + opts = self.collectOptions(options) opts["wl_bins"] = self._common_conf.wl_bins.val - if options.type == "BlackBodyTarget": - # Black Body Target - if "mag" in opts and type(opts["mag"]) == str: - opts["mag"] = float(opts["mag"]) * u.mag - return tg.BlackBodyTarget(**opts) - elif options.type == "FileTarget": - # File Target - return getattr(tg, options.type)(**opts) + if hasattr(tg, options.type): + class_ = getattr(tg, options.type) + return class_(**opts) else: logger.error("Unknown target type: '" + options.type + "'") else: diff --git a/tests/optical_component/test_Filter.py b/tests/optical_component/test_Filter.py index 26121f3..18fcf12 100644 --- a/tests/optical_component/test_Filter.py +++ b/tests/optical_component/test_Filter.py @@ -8,14 +8,14 @@ class TestFilter(TestCase): def test_fromBand(self): wl = np.array([400, 500, 501, 545, 589, 590, 600]) << u.nm target = BlackBodyTarget(wl, temp=5778 * u.K, mag=10 * u.mag, band="U") - filt = Filter.fromBand(target, "V") + filt = Filter(parent=target, band="V") self.assertEqual(filt.calcSignal()[0], SpectralQty(wl, np.array([0.0, 0.0, 0.0, 5.52730709e-15, 5.29671115e-15, - 5.29030718e-15, 0.0]) << u.W / - (u.m ** 2 * u.nm))) + 5.29030718e-15, 0.0]) << u.W / + (u.m ** 2 * u.nm))) def test_fromFile(self): target = FileTarget("tests/data/target/target_demo_1.csv", np.arange(200, 210, 1) << u.nm) - filt = Filter.fromFile(target, "tests/data/filter/filter_transmittance.csv") + filt = Filter(parent=target, transmittance="tests/data/filter/filter_transmittance.csv") self.assertEqual(filt.calcSignal()[0], SpectralQty(np.arange(200, 210, 1) << u.nm, np.array([1.10e-15, 1.20e-15, 1.30e-15, 1.40e-15, 1.35e-15, 1.44e-15, 1.53e-15, 1.44e-15, diff --git a/tests/test_RadiantFactory.py b/tests/test_RadiantFactory.py index 2634fdb..d8a2bad 100644 --- a/tests/test_RadiantFactory.py +++ b/tests/test_RadiantFactory.py @@ -25,7 +25,8 @@ class TestRadiantFactory(TestCase): "tests/data/mirror/emissivity.csv", 70 * u.K) parent_2 = oc.Mirror(parent_2, "tests/data/mirror/reflectance.csv", "tests/data/mirror/emissivity.csv", 70 * u.K) - parent_2 = oc.Filter.fromRange(parent_2, 400 * u.nm, 480 * u.nm, "tests/data/filter/emissivity.csv", 70 * u.K) + parent_2 = oc.Filter(parent=parent_2, start=400 * u.nm, end=480 * u.nm, + emissivity="tests/data/filter/emissivity.csv", temp=70 * u.K) parent_2 = oc.Lens(parent_2, "tests/data/lens/transmittance.csv", "tests/data/lens/emissivity.csv", 70 * u.K) self.assertEqual(parent.calcSignal()[0], parent_2.calcSignal()[0])