diff --git a/src/elli/dispersions/base_dispersion.py b/src/elli/dispersions/base_dispersion.py index baa2a36f..1a6c434a 100644 --- a/src/elli/dispersions/base_dispersion.py +++ b/src/elli/dispersions/base_dispersion.py @@ -8,6 +8,7 @@ import numpy as np import numpy.typing as npt import pandas as pd +from lmfit import Parameter from numpy.lib.scimath import sqrt from .. import dispersions @@ -56,6 +57,8 @@ def _fill_params_dict(template: dict, *args, **kwargs) -> dict: for i, val in enumerate(args): key = list(template.keys())[i] + if isinstance(val, Parameter): + val = val.value params[key] = val pos_arguments.add(key) @@ -64,6 +67,8 @@ def _fill_params_dict(template: dict, *args, **kwargs) -> dict: raise InvalidParameters( f"Parameter {key} already set by positional argument" ) + if isinstance(value, Parameter): + value = value.value params[key] = value return params @@ -80,6 +85,17 @@ def __init__(self, *args, **kwargs): if self.single_params[param] is None: raise InvalidParameters(f"Please specify parameter {param}") + self.last_lbda = None + self.hash_single_params = None + self.hash_rep_params = None + + def _hash_params(self, params: Union[dict, List[dict]]) -> int: + """Creates an single_params_dict or the repeating_params_list.""" + if isinstance(params, list): + return hash(tuple([self._hash_params(dictionary) for dictionary in params])) + else: + return hash(tuple([item for _, item in params.items()])) + @abstractmethod def dielectric_function(self, lbda: npt.ArrayLike) -> npt.NDArray: """Calculates the dielectric function in a given wavelength window. @@ -114,6 +130,40 @@ def get_dielectric(self, lbda: Optional[npt.ArrayLike] = None) -> npt.NDArray: """Returns the dielectric constant for wavelength 'lbda' default unit (nm) in the convention ε1 + iε2.""" lbda = self.default_lbda_range if lbda is None else lbda + + from .table_epsilon import TableEpsilon + from .table_index import Table + from .pseudo_dielectric import PseudoDielectricFunction + + if not isinstance(self, (DispersionSum, IndexDispersionSum)): + if isinstance(self, (TableEpsilon, Table, PseudoDielectricFunction)): + if self.last_lbda is lbda: + return self.cached_diel + else: + self.last_lbda = lbda + self.cached_diel = np.asarray( + self.dielectric_function(lbda), dtype=np.complex128 + ) + return self.cached_diel + else: + new_single_hash = self._hash_params(self.single_params) + new_rep_hash = self._hash_params(self.rep_params) + + if ( + self.last_lbda is lbda + and self.hash_single_params == new_single_hash + and self.hash_rep_params == new_rep_hash + ): + return self.cached_diel + else: + self.last_lbda = lbda + self.hash_single_params = new_single_hash + self.hash_rep_params = new_rep_hash + self.cached_diel = np.asarray( + self.dielectric_function(lbda), dtype=np.complex128 + ) + return self.cached_diel + return np.asarray(self.dielectric_function(lbda), dtype=np.complex128) def get_refractive_index(self, lbda: Optional[npt.ArrayLike] = None) -> npt.NDArray: diff --git a/src/elli/fitting/decorator_psi_delta.py b/src/elli/fitting/decorator_psi_delta.py index 6ce2f57c..5f2bdde1 100644 --- a/src/elli/fitting/decorator_psi_delta.py +++ b/src/elli/fitting/decorator_psi_delta.py @@ -252,8 +252,9 @@ def fit_function( """ result = self.model(lbda, params) - resid_rhor = rhor - result.rho.real - resid_rhoi = rhoi - result.rho.imag + sim_rho = result.rho + resid_rhor = rhor - sim_rho.real + resid_rhoi = rhoi - sim_rho.imag return np.concatenate((resid_rhor, resid_rhoi)) @@ -268,11 +269,10 @@ def fit(self, method="leastsq"): Returns: Result: The fitting result """ - rho = calc_rho(self.exp_data) res = minimize( self.fit_function, self.params, - args=(rho.index.to_numpy(), rho.values.real, rho.values.imag), + args=(self.lbda, self.rhor, self.rhoi), method=method, ) @@ -390,7 +390,13 @@ def __init__( """ super().__init__() self.model = model + self.exp_data = exp_data + tmp_rho = calc_rho(self.exp_data) + self.lbda = tmp_rho.index.to_numpy() + self.rhor = tmp_rho.values.real + self.rhoi = tmp_rho.values.imag + self.params = params self.fitted_params = params.copy() self.angle = angle diff --git a/src/elli/solver.py b/src/elli/solver.py index e0e8d48a..4f3ada49 100644 --- a/src/elli/solver.py +++ b/src/elli/solver.py @@ -26,8 +26,11 @@ class Solver(ABC): def calculate(self) -> Result: pass - def __init__(self, experiment: "Experiment") -> None: - self.experiment = deepcopy(experiment) + def __init__(self, experiment: "Experiment", save_experiment: bool = False) -> None: + if save_experiment: + self.experiment = deepcopy(experiment) + else: + self.experiment = experiment self.structure = self.experiment.structure self.lbda = self.experiment.lbda self.theta_i = self.experiment.theta_i diff --git a/src/elli/solver4x4.py b/src/elli/solver4x4.py index 4f2610de..ee620ab6 100644 --- a/src/elli/solver4x4.py +++ b/src/elli/solver4x4.py @@ -322,9 +322,12 @@ def get_k_z( return sqrt(k_z2) def __init__( - self, experiment: "Experiment", propagator: Propagator = PropagatorExpm() + self, + experiment: "Experiment", + propagator: Propagator = PropagatorExpm(), + save_experiment: bool = False, ) -> None: - super().__init__(experiment) + super().__init__(experiment, save_experiment) self.propagator = propagator def calculate(self) -> Result: diff --git a/tests/benchmark_fitting.py b/tests/benchmark_fitting.py new file mode 100644 index 00000000..8d924131 --- /dev/null +++ b/tests/benchmark_fitting.py @@ -0,0 +1,114 @@ +"""Benchmark for using the formula dispersion""" + +import elli +import numpy as np +from elli.fitting import ParamsHist, fit +from fixtures import datadir +from pytest import fixture + + +def test_fitting_structure_creation(benchmark, datadir): + ANGLE = 70 + rii_db = elli.db.RII() + Si = rii_db.get_mat("Si", "Aspnes") + + psi_delta = ( + elli.read_nexus_psi_delta(datadir / "SiO2onSi.ellips.nxs") + .loc[ANGLE] + .loc[210:800] + ) + + params = ParamsHist() + params.add("SiO2_n0", value=1.6, min=-100, max=100, vary=True) + params.add("SiO2_n1", value=36, min=-40000, max=40000, vary=False) + params.add("SiO2_n2", value=0, min=-40000, max=40000, vary=False) + params.add("SiO2_k0", value=0, min=-100, max=100, vary=False) + params.add("SiO2_k1", value=0, min=-40000, max=40000, vary=False) + params.add("SiO2_k2", value=0, min=-40000, max=40000, vary=False) + params.add("SiO2_d", value=20, min=0, max=40000, vary=True) + + @fit(psi_delta, params) + def model(lbda, params): + SiO2 = elli.Cauchy( + params["SiO2_n0"], + params["SiO2_n1"], + params["SiO2_n2"], + params["SiO2_k0"], + params["SiO2_k1"], + params["SiO2_k2"], + ).get_mat() + + return elli.Structure( + elli.AIR, + [elli.Layer(SiO2, params["SiO2_d"])], + Si, + ).evaluate(lbda, ANGLE, solver=elli.Solver2x2) + + result = benchmark.pedantic( + model.fit, + args=(), + iterations=1, + rounds=10, + ) + assert result.chisqr < 0.02 + + +def test_fitting_structure_updates(benchmark, datadir): + ANGLE = 70 + rii_db = elli.db.RII() + Si = rii_db.get_mat("Si", "Aspnes") + + psi_delta = ( + elli.read_nexus_psi_delta(datadir / "SiO2onSi.ellips.nxs") + .loc[ANGLE] + .loc[210:800] + ) + + params = ParamsHist() + params.add("SiO2_n0", value=1.6, min=-100, max=100, vary=True) + params.add("SiO2_n1", value=36, min=-40000, max=40000, vary=False) + params.add("SiO2_n2", value=0, min=-40000, max=40000, vary=False) + params.add("SiO2_k0", value=0, min=-100, max=100, vary=False) + params.add("SiO2_k1", value=0, min=-40000, max=40000, vary=False) + params.add("SiO2_k2", value=0, min=-40000, max=40000, vary=False) + params.add("SiO2_d", value=20, min=0, max=40000, vary=True) + + SiO2 = elli.Cauchy( + params["SiO2_n0"], + params["SiO2_n1"], + params["SiO2_n2"], + params["SiO2_k0"], + params["SiO2_k1"], + params["SiO2_k2"], + ) + + SiO2_mat = SiO2.get_mat() + + layer = elli.Layer(SiO2_mat, params["SiO2_d"]) + + structure = elli.Structure( + elli.AIR, + [layer], + Si, + ) + + @fit(psi_delta, params) + def model(lbda, params): + SiO2.single_params["n0"] = params["SiO2_n0"].value + SiO2.single_params["n1"] = params["SiO2_n1"].value + SiO2.single_params["n2"] = params["SiO2_n2"].value + SiO2.single_params["k0"] = params["SiO2_k0"].value + SiO2.single_params["k1"] = params["SiO2_k1"].value + SiO2.single_params["k2"] = params["SiO2_k2"].value + + layer.set_thickness(params["SiO2_d"]) + + return structure.evaluate(lbda, ANGLE, solver=elli.Solver2x2) + + result = benchmark.pedantic( + model.fit, + args=(), + iterations=1, + rounds=10, + ) + assert result.chisqr < 0.02 diff --git a/tests/benchmark_fitting/SiO2onSi.ellips.nxs b/tests/benchmark_fitting/SiO2onSi.ellips.nxs new file mode 100644 index 00000000..a44b6be4 Binary files /dev/null and b/tests/benchmark_fitting/SiO2onSi.ellips.nxs differ