From e41cf5e4e43755c5f72cc55f3e80e429f65f1114 Mon Sep 17 00:00:00 2001 From: domna Date: Fri, 27 Jan 2023 17:41:55 +0100 Subject: [PATCH 1/5] Adds unsummable dispersion --- src/elli/dispersions/base_dispersion.py | 38 +++++++++++++++++++ src/elli/dispersions/cauchy.py | 9 ++++- src/elli/dispersions/cauchy_custom.py | 9 ++++- .../dispersions/constant_refractive_index.py | 8 +++- tests/test_unsummable_dispersion.py | 18 +++++++++ 5 files changed, 76 insertions(+), 6 deletions(-) create mode 100644 tests/test_unsummable_dispersion.py diff --git a/src/elli/dispersions/base_dispersion.py b/src/elli/dispersions/base_dispersion.py index 99090435..bc9c8191 100644 --- a/src/elli/dispersions/base_dispersion.py +++ b/src/elli/dispersions/base_dispersion.py @@ -101,8 +101,18 @@ def add(self, *args, **kwargs) -> "Dispersion": return self + def __radd__(self, other: Union[int, float, "Dispersion"]) -> "Dispersion": + """Add up the dielectric function of multiple models""" + return self.__add__(other) + def __add__(self, other: Union[int, float, "Dispersion"]) -> "Dispersion": """Add up the dielectric function of multiple models""" + if isinstance(other, UnsummableDispersion): + other.__add__(self) + + if isinstance(other, DispersionSum): + return other.__add__(self) + if isinstance(other, (int, float)): return DispersionSum(self, dispersions.EpsilonInf(eps=other)) @@ -195,6 +205,19 @@ def _dict_to_str(dic): ) +class UnsummableDispersion(Dispersion): + """This denotes a dispersion which is not summable""" + + @property + @abstractmethod + def summation_error_message(self): + """The message being displayed when someone tries + to perform an addition with this dispersion.""" + + def __add__(self, _: Union[int, float, "Dispersion"]) -> "Dispersion": + raise ValueError(self.summation_error_message) + + class DispersionFactory: """A factory class for dispersion objects""" @@ -227,6 +250,21 @@ def __init__(self, *disps: Dispersion) -> None: super().__init__() self.dispersions = disps + def __add__(self, other: Union[int, float, "Dispersion"]) -> "Dispersion": + if isinstance(other, UnsummableDispersion): + other.__add__(self) + + if isinstance(other, DispersionSum): + self.dispersions += other.dispersions + return self + + if isinstance(other, (int, float)): + self.dispersions.append(dispersions.EpsilonInf(eps=other)) + return self + + self.dispersions.append(other) + return self + def dielectric_function(self, lbda: npt.ArrayLike) -> npt.NDArray: dielectric_function = sum( disp.dielectric_function(lbda) for disp in self.dispersions diff --git a/src/elli/dispersions/cauchy.py b/src/elli/dispersions/cauchy.py index 854b7368..0438cc7c 100644 --- a/src/elli/dispersions/cauchy.py +++ b/src/elli/dispersions/cauchy.py @@ -2,10 +2,10 @@ """Cauchy dispersion.""" import numpy.typing as npt -from .base_dispersion import Dispersion +from .base_dispersion import UnsummableDispersion -class Cauchy(Dispersion): +class Cauchy(UnsummableDispersion): r"""Cauchy dispersion. Single parameters: @@ -27,6 +27,11 @@ class Cauchy(Dispersion): + 10^7 \boldsymbol{k_2}/\lambda^4) """ + summation_error_message = ( + "The cauchy dispersion cannot be added to other dispersions. " + "Try the Poles or Lorentz model instead." + ) + single_params_template = {"n0": 1.5, "n1": 0, "n2": 0, "k0": 0, "k1": 0, "k2": 0} rep_params_template = {} diff --git a/src/elli/dispersions/cauchy_custom.py b/src/elli/dispersions/cauchy_custom.py index 0dd70708..4dc68672 100644 --- a/src/elli/dispersions/cauchy_custom.py +++ b/src/elli/dispersions/cauchy_custom.py @@ -2,10 +2,10 @@ """Cauchy dispersion with custom exponents.""" import numpy.typing as npt -from .base_dispersion import Dispersion +from .base_dispersion import UnsummableDispersion -class CauchyCustomExponent(Dispersion): +class CauchyCustomExponent(UnsummableDispersion): r"""Cauchy dispersion with custom exponents. Single parameters: @@ -21,6 +21,11 @@ class CauchyCustomExponent(Dispersion): \boldsymbol{n_0} + \sum_j \boldsymbol{f}_j \cdot \lambda^{\boldsymbol{e}_j} """ + summation_error_message = ( + "The cauchy dispersion cannot be added to other dispersions. " + "Try the Poles or Lorentz model instead." + ) + single_params_template = {"n0": 1.5} rep_params_template = {"f": 0, "e": 1} diff --git a/src/elli/dispersions/constant_refractive_index.py b/src/elli/dispersions/constant_refractive_index.py index 9ffb8ae9..ea1b93f5 100644 --- a/src/elli/dispersions/constant_refractive_index.py +++ b/src/elli/dispersions/constant_refractive_index.py @@ -2,10 +2,10 @@ """Constant refractive index.""" import numpy.typing as npt -from .base_dispersion import Dispersion +from .base_dispersion import UnsummableDispersion -class ConstantRefractiveIndex(Dispersion): +class ConstantRefractiveIndex(UnsummableDispersion): r"""Constant refractive index. Single parameters: @@ -18,6 +18,10 @@ class ConstantRefractiveIndex(Dispersion): .. math:: \varepsilon(\lambda) = \boldsymbol{n}^2 """ + summation_error_message = ( + "The constant refractive index cannot be added to other dispersions. " + "Try EpsilonInf instead." + ) single_params_template = {"n": 1} rep_params_template = {} diff --git a/tests/test_unsummable_dispersion.py b/tests/test_unsummable_dispersion.py new file mode 100644 index 00000000..997da2b9 --- /dev/null +++ b/tests/test_unsummable_dispersion.py @@ -0,0 +1,18 @@ +"""Test kramers kronig relations""" +import pytest +from elli import Cauchy + + +def test_fail_on_adding_cauchy(): + """Test whether the kkr reproduces the analytical expression of Tauc-Lorentz""" + cauchy_err_str = ( + "The cauchy dispersion cannot be added to other dispersions. " + "Try the Poles or Lorentz model instead." + ) + with pytest.raises(ValueError) as sum_err: + _ = Cauchy() + Cauchy() + + assert cauchy_err_str in str(sum_err.value) + + with pytest.raises(ValueError): + _ = 1 + Cauchy() From b183fb56a0bc4620dbc526b270da33cc1045389f Mon Sep 17 00:00:00 2001 From: domna Date: Sat, 28 Jan 2023 16:18:25 +0100 Subject: [PATCH 2/5] Replaces UnsummableDispersion with IndexDispersion --- src/elli/dispersions/base_dispersion.py | 77 +++++++++++++------ src/elli/dispersions/cauchy.py | 14 +--- src/elli/dispersions/cauchy_custom.py | 15 +--- .../dispersions/constant_refractive_index.py | 13 +--- tests/test_dispersion_adding.py | 65 ++++++++++++++++ tests/test_unsummable_dispersion.py | 18 ----- 6 files changed, 130 insertions(+), 72 deletions(-) create mode 100644 tests/test_dispersion_adding.py delete mode 100644 tests/test_unsummable_dispersion.py diff --git a/src/elli/dispersions/base_dispersion.py b/src/elli/dispersions/base_dispersion.py index bc9c8191..07c59ac6 100644 --- a/src/elli/dispersions/base_dispersion.py +++ b/src/elli/dispersions/base_dispersion.py @@ -1,7 +1,7 @@ # Encoding: utf-8 """Abstract base class and utility classes for pyElli dispersion""" from abc import ABC, abstractmethod -from typing import Union +from typing import List, Union import numpy as np import numpy.typing as npt @@ -101,23 +101,32 @@ def add(self, *args, **kwargs) -> "Dispersion": return self - def __radd__(self, other: Union[int, float, "Dispersion"]) -> "Dispersion": + def _check_valid_operand(self, other: Union[int, float, "Dispersion"]): + if not isinstance(other, (int, float, Dispersion)): + raise TypeError( + f"unsupported operand type(s) for +: '{type(self)}' and '{type(other)}'" + ) + + def _is_non_diel_dispersion(self, other: Union[int, float, "Dispersion"]) -> bool: + return isinstance(other, IndexDispersion) + + def __radd__(self, other: Union[int, float, "Dispersion"]) -> "DispersionSum": """Add up the dielectric function of multiple models""" return self.__add__(other) - def __add__(self, other: Union[int, float, "Dispersion"]) -> "Dispersion": + def __add__(self, other: Union[int, float, "Dispersion"]) -> "DispersionSum": """Add up the dielectric function of multiple models""" - if isinstance(other, UnsummableDispersion): - other.__add__(self) + self._check_valid_operand(other) - if isinstance(other, DispersionSum): + if self._is_non_diel_dispersion(other): return other.__add__(self) - if isinstance(other, (int, float)): - return DispersionSum(self, dispersions.EpsilonInf(eps=other)) + if isinstance(other, DispersionSum): + other.dispersions.append(self) + return other - if not isinstance(other, Dispersion): - raise TypeError(f"Invalid type {type(other)} added to dispersion") + if isinstance(other, (int, float)): + return DispersionSum(self, dispersions.EpsilonInf(other)) return DispersionSum(self, other) @@ -205,17 +214,34 @@ def _dict_to_str(dic): ) -class UnsummableDispersion(Dispersion): - """This denotes a dispersion which is not summable""" +class IndexDispersion(Dispersion): + """A dispersion based on a refractive index formulation.""" - @property @abstractmethod - def summation_error_message(self): - """The message being displayed when someone tries - to perform an addition with this dispersion.""" + def refractive_index(self, lbda: npt.ArrayLike) -> npt.NDArray: + """Calculates the refractive index in a given wavelength window. + + Args: + lbda (npt.ArrayLike): The wavelength window with unit nm. - def __add__(self, _: Union[int, float, "Dispersion"]) -> "Dispersion": - raise ValueError(self.summation_error_message) + Returns: + npt.NDArray: The refractive index for each wavelength point. + """ + + def __add__(self, other: Union[int, float, "Dispersion"]) -> "DispersionSum": + self._check_valid_operand(other) + + if isinstance(other, IndexDispersion): + raise NotImplementedError( + "Adding of index based dispersions is not supported yet" + ) + + raise TypeError( + "Cannot add refractive index and dielectric function based dispersions." + ) + + def dielectric_function(self, lbda: npt.ArrayLike) -> npt.NDArray: + return self.refractive_index(lbda) ** 2 class DispersionFactory: @@ -243,16 +269,19 @@ def get_dispersion(identifier: str, *args, **kwargs) -> Dispersion: class DispersionSum(Dispersion): """Represents a sum of two dispersions""" - single_params_template = {} - rep_params_template = {} + single_params_template: dict = {} + rep_params_template: dict = {} + dispersions: List[Dispersion] def __init__(self, *disps: Dispersion) -> None: super().__init__() - self.dispersions = disps + self.dispersions = list(disps) - def __add__(self, other: Union[int, float, "Dispersion"]) -> "Dispersion": - if isinstance(other, UnsummableDispersion): - other.__add__(self) + def __add__(self, other: Union[int, float, "Dispersion"]) -> "DispersionSum": + self._check_valid_operand(other) + + if self._is_non_diel_dispersion(other): + return other.__add__(self) if isinstance(other, DispersionSum): self.dispersions += other.dispersions diff --git a/src/elli/dispersions/cauchy.py b/src/elli/dispersions/cauchy.py index 0438cc7c..0bea46c6 100644 --- a/src/elli/dispersions/cauchy.py +++ b/src/elli/dispersions/cauchy.py @@ -2,10 +2,10 @@ """Cauchy dispersion.""" import numpy.typing as npt -from .base_dispersion import UnsummableDispersion +from .base_dispersion import IndexDispersion -class Cauchy(UnsummableDispersion): +class Cauchy(IndexDispersion): r"""Cauchy dispersion. Single parameters: @@ -27,16 +27,11 @@ class Cauchy(UnsummableDispersion): + 10^7 \boldsymbol{k_2}/\lambda^4) """ - summation_error_message = ( - "The cauchy dispersion cannot be added to other dispersions. " - "Try the Poles or Lorentz model instead." - ) - single_params_template = {"n0": 1.5, "n1": 0, "n2": 0, "k0": 0, "k1": 0, "k2": 0} rep_params_template = {} - def dielectric_function(self, lbda: npt.ArrayLike) -> npt.NDArray: - refr_index = ( + def refractive_index(self, lbda: npt.ArrayLike) -> npt.NDArray: + return ( self.single_params.get("n0") + 1e2 * self.single_params.get("n1") / lbda**2 + 1e7 * self.single_params.get("n2") / lbda**4 @@ -47,4 +42,3 @@ def dielectric_function(self, lbda: npt.ArrayLike) -> npt.NDArray: + 1e7 * self.single_params.get("k2") / lbda**4 ) ) - return refr_index**2 diff --git a/src/elli/dispersions/cauchy_custom.py b/src/elli/dispersions/cauchy_custom.py index 4dc68672..0acaafcf 100644 --- a/src/elli/dispersions/cauchy_custom.py +++ b/src/elli/dispersions/cauchy_custom.py @@ -2,10 +2,10 @@ """Cauchy dispersion with custom exponents.""" import numpy.typing as npt -from .base_dispersion import UnsummableDispersion +from .base_dispersion import IndexDispersion -class CauchyCustomExponent(UnsummableDispersion): +class CauchyCustomExponent(IndexDispersion): r"""Cauchy dispersion with custom exponents. Single parameters: @@ -21,17 +21,10 @@ class CauchyCustomExponent(UnsummableDispersion): \boldsymbol{n_0} + \sum_j \boldsymbol{f}_j \cdot \lambda^{\boldsymbol{e}_j} """ - summation_error_message = ( - "The cauchy dispersion cannot be added to other dispersions. " - "Try the Poles or Lorentz model instead." - ) - single_params_template = {"n0": 1.5} rep_params_template = {"f": 0, "e": 1} - def dielectric_function(self, lbda: npt.ArrayLike) -> npt.NDArray: - refr_index = self.single_params.get("n0") + sum( + def refractive_index(self, lbda: npt.ArrayLike) -> npt.NDArray: + return self.single_params.get("n0") + sum( c.get("f") * lbda ** c.get("e") for c in self.rep_params ) - - return refr_index**2 diff --git a/src/elli/dispersions/constant_refractive_index.py b/src/elli/dispersions/constant_refractive_index.py index ea1b93f5..0c3ac787 100644 --- a/src/elli/dispersions/constant_refractive_index.py +++ b/src/elli/dispersions/constant_refractive_index.py @@ -2,10 +2,10 @@ """Constant refractive index.""" import numpy.typing as npt -from .base_dispersion import UnsummableDispersion +from .base_dispersion import IndexDispersion -class ConstantRefractiveIndex(UnsummableDispersion): +class ConstantRefractiveIndex(IndexDispersion): r"""Constant refractive index. Single parameters: @@ -18,13 +18,8 @@ class ConstantRefractiveIndex(UnsummableDispersion): .. math:: \varepsilon(\lambda) = \boldsymbol{n}^2 """ - summation_error_message = ( - "The constant refractive index cannot be added to other dispersions. " - "Try EpsilonInf instead." - ) - single_params_template = {"n": 1} rep_params_template = {} - def dielectric_function(self, _: npt.ArrayLike) -> npt.NDArray: - return self.single_params.get("n") ** 2 + def refractive_index(self, _: npt.ArrayLike) -> npt.NDArray: + return self.single_params.get("n") diff --git a/tests/test_dispersion_adding.py b/tests/test_dispersion_adding.py new file mode 100644 index 00000000..2b012585 --- /dev/null +++ b/tests/test_dispersion_adding.py @@ -0,0 +1,65 @@ +"""Test adding of dispersions""" +import pytest +from numpy.testing import assert_array_almost_equal +from elli import Cauchy, Sellmeier +from elli.dispersions.base_dispersion import DispersionSum + + +def test_fail_on_adding_index_dispersion(): + """Test whether adding for an index based model fails""" + cauchy_err_str = "Adding of index based dispersions is not supported yet" + with pytest.raises(NotImplementedError) as sum_err: + _ = Cauchy() + Cauchy() + + assert cauchy_err_str in str(sum_err.value) + + +def test_fail_on_adding_index_and_diel_dispersion(): + """Test whether the adding fails for an index based and dielectric dispersion""" + + for disp in [1, Sellmeier()]: + with pytest.raises(TypeError) as sum_err: + _ = disp + Cauchy() + + assert ( + "Cannot add refractive index and dielectric function based dispersions." + in str(sum_err.value) + ) + + +def test_adding_of_diel_dispersions(): + """Test if dielectric dispersions are added correctly""" + + dispersion_sum = Sellmeier() + Sellmeier() + + assert isinstance(dispersion_sum, DispersionSum) + assert len(dispersion_sum.dispersions) == 2 + + for disp in dispersion_sum.dispersions: + assert isinstance(disp, Sellmeier) + + assert_array_almost_equal( + dispersion_sum.get_dielectric_df().values, + 2 * Sellmeier().get_dielectric_df().values, + ) + + +def test_flat_dispersion_sum_on_multiple_add(): + """Test whether the DispersionSum stays flat on multiple adds""" + + dispersion_sum = Sellmeier() + Sellmeier() + Sellmeier() + + assert isinstance(dispersion_sum, DispersionSum) + assert len(dispersion_sum.dispersions) == 3 + + for disp in dispersion_sum.dispersions: + assert isinstance(disp, Sellmeier) + + assert_array_almost_equal( + dispersion_sum.get_dielectric_df().values, + 3 * Sellmeier().get_dielectric_df().values, + ) + + +def test_adding_of_tabular_dispersions(): + """Tests correct adding of tabular dispersions""" diff --git a/tests/test_unsummable_dispersion.py b/tests/test_unsummable_dispersion.py deleted file mode 100644 index 997da2b9..00000000 --- a/tests/test_unsummable_dispersion.py +++ /dev/null @@ -1,18 +0,0 @@ -"""Test kramers kronig relations""" -import pytest -from elli import Cauchy - - -def test_fail_on_adding_cauchy(): - """Test whether the kkr reproduces the analytical expression of Tauc-Lorentz""" - cauchy_err_str = ( - "The cauchy dispersion cannot be added to other dispersions. " - "Try the Poles or Lorentz model instead." - ) - with pytest.raises(ValueError) as sum_err: - _ = Cauchy() + Cauchy() - - assert cauchy_err_str in str(sum_err.value) - - with pytest.raises(ValueError): - _ = 1 + Cauchy() From a791e8bf4059febeabb55936ff634671b0dd6376 Mon Sep 17 00:00:00 2001 From: domna Date: Sat, 28 Jan 2023 16:23:40 +0100 Subject: [PATCH 3/5] Lets IndexTable inherit from IndexDispersion --- src/elli/dispersions/table_index.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/elli/dispersions/table_index.py b/src/elli/dispersions/table_index.py index e1bc9436..e6281c79 100644 --- a/src/elli/dispersions/table_index.py +++ b/src/elli/dispersions/table_index.py @@ -4,10 +4,10 @@ import numpy.typing as npt import scipy.interpolate -from .base_dispersion import Dispersion, InvalidParameters +from .base_dispersion import IndexDispersion, InvalidParameters -class Table(Dispersion): +class Table(IndexDispersion): """Dispersion specified by a table of wavelengths (nm) and refractive index values. Please not that this model will produce errors for wavelengths outside the provided wavelength range. @@ -40,9 +40,9 @@ def __init__(self, *args, **kwargs) -> None: self.interpolation = scipy.interpolate.interp1d( self.single_params.get("lbda"), - self.single_params.get("n") ** 2, + self.single_params.get("n"), kind="cubic", ) - def dielectric_function(self, lbda: npt.ArrayLike) -> npt.NDArray: + def refractive_index(self, lbda: npt.ArrayLike) -> npt.NDArray: return self.interpolation(lbda) From ec479e32e4c8fe794ddf1589b1b6e088c92a9a90 Mon Sep 17 00:00:00 2001 From: domna Date: Sat, 28 Jan 2023 16:28:36 +0100 Subject: [PATCH 4/5] Raise error on adding of tabular dispersions --- src/elli/dispersions/table_epsilon.py | 4 ++++ tests/test_dispersion_adding.py | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/src/elli/dispersions/table_epsilon.py b/src/elli/dispersions/table_epsilon.py index 5f8ea62f..57afa6da 100644 --- a/src/elli/dispersions/table_epsilon.py +++ b/src/elli/dispersions/table_epsilon.py @@ -1,5 +1,6 @@ # Encoding: utf-8 """Dispersion specified by a table of wavelengths (nm) and dielectric function values.""" +from typing import Union import numpy as np import numpy.typing as npt import scipy.interpolate @@ -49,5 +50,8 @@ def __init__(self, *args, **kwargs) -> None: kind="cubic", ) + def __add__(self, _: Union[int, float, "Dispersion"]) -> "DispersionSum": + raise NotImplementedError("Adding of tabular dispersions is not yet supported") + def dielectric_function(self, lbda: npt.ArrayLike) -> npt.NDArray: return self.interpolation(lbda) diff --git a/tests/test_dispersion_adding.py b/tests/test_dispersion_adding.py index 2b012585..172f7346 100644 --- a/tests/test_dispersion_adding.py +++ b/tests/test_dispersion_adding.py @@ -3,6 +3,7 @@ from numpy.testing import assert_array_almost_equal from elli import Cauchy, Sellmeier from elli.dispersions.base_dispersion import DispersionSum +from elli.dispersions.table_epsilon import TableEpsilon def test_fail_on_adding_index_dispersion(): @@ -63,3 +64,10 @@ def test_flat_dispersion_sum_on_multiple_add(): def test_adding_of_tabular_dispersions(): """Tests correct adding of tabular dispersions""" + + with pytest.raises(NotImplementedError) as not_impl_err: + _ = TableEpsilon() + 1 + + assert ( + str(not_impl_err.value) == "Adding of tabular dispersions is not yet supported" + ) From 79c031dbfe80ad5c7e486806ea7276a0b28a84a4 Mon Sep 17 00:00:00 2001 From: domna Date: Mon, 30 Jan 2023 09:09:04 +0100 Subject: [PATCH 5/5] Offload add to table dispersion --- src/elli/dispersions/base_dispersion.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/elli/dispersions/base_dispersion.py b/src/elli/dispersions/base_dispersion.py index 07c59ac6..de4c6a69 100644 --- a/src/elli/dispersions/base_dispersion.py +++ b/src/elli/dispersions/base_dispersion.py @@ -107,8 +107,8 @@ def _check_valid_operand(self, other: Union[int, float, "Dispersion"]): f"unsupported operand type(s) for +: '{type(self)}' and '{type(other)}'" ) - def _is_non_diel_dispersion(self, other: Union[int, float, "Dispersion"]) -> bool: - return isinstance(other, IndexDispersion) + def _is_non_std_dispersion(self, other: Union[int, float, "Dispersion"]) -> bool: + return isinstance(other, (IndexDispersion, dispersions.Table)) def __radd__(self, other: Union[int, float, "Dispersion"]) -> "DispersionSum": """Add up the dielectric function of multiple models""" @@ -118,7 +118,7 @@ def __add__(self, other: Union[int, float, "Dispersion"]) -> "DispersionSum": """Add up the dielectric function of multiple models""" self._check_valid_operand(other) - if self._is_non_diel_dispersion(other): + if self._is_non_std_dispersion(other): return other.__add__(self) if isinstance(other, DispersionSum): @@ -280,7 +280,7 @@ def __init__(self, *disps: Dispersion) -> None: def __add__(self, other: Union[int, float, "Dispersion"]) -> "DispersionSum": self._check_valid_operand(other) - if self._is_non_diel_dispersion(other): + if self._is_non_std_dispersion(other): return other.__add__(self) if isinstance(other, DispersionSum):