from typing import Iterable
import numpy as np
from .base_fitter import BaseFitter, reload_scans
from .results import FitResult, FitResultSeries
from ..models import DefaultTrainedModel
from ..xrrloader import SpecLoader
[docs]class SpecFitter(BaseFitter):
"""Load reflectivity scans from a SPEC file and fit them using a trained neural network model.
Before use:
- A neural network model has to be set via the ``set_trained_model()`` method.
- Import parameters have to be defined via the ``set_import_params()`` method.
- Parameters for footprint correction have to be defined via ``set_footprint_params()`` method.
- The input SPEC file has to be specified via the ``set_spec_file()`` method.
"""
@property
def spec_file(self):
return self._file_name
[docs] @reload_scans
def fit(self, scan_number: int, trim_front: int = None, trim_back: int = None, theta_offset: float = 0.0,
dq: float = 0.0, factor: float = 1.0, plot=False, polish=True, fraction_bounds: tuple = (0.5, 0.5, 0.1),
optimize_q=True, n_q_samples: int = 1000, optimize_scaling=False, n_scale_samples: int = 300,
reload=True) -> FitResult:
"""Extract scan from SPEC file and predict thin film parameters.
Args:
scan_number: SPEC scan number of the scan that is to be fitted.
trim_front: How many intensity points are cropped from the beginning.
trim_back: How many intensity points are cropped from the end.
theta_offset: Angular correction that is added before transformation to q space.
dq: Q-shift that is applied before interpolation of the data to the trained q values. Can sometimes
improve the results if the total reflection edge is not perfectly aligned.
factor: Multiplicative factor that is applied to the data after interpolation. Can sometimes
improve the results if the total reflection edge is not perfectly aligned.
plot: If set to ``True``, the intensity prediction is shown in a plot.
polish: If ``True``, the predictions will be refined with a simple least log mean squares minimization via
``scipy.optimize.minimize``. This can often improve the "fit" of the model curve to the data at the
expense of higher prediction times.
fraction_bounds: The relative fitting bounds if the LMS for thickness, roughness and SLD, respectively.
E.g. if the predicted thickness was 150 A, then a value of 0.5 would mean the fit bounds are
``(75, 225)``.
optimize_q: If ``True``, the q interpolation will be resampled with small q shifts in a range of about
+-0.003 1/A and the neural network prediction with the smallest MSE will be selected. If
``polish=True``, this step will happen before the LMS fit.
n_q_samples: Number of q shift samples that will be generated. More samples can lead to a better result,
but will increase the prediction time.
optimize_scaling: If ``True``, the interpolated input curve is randomly rescaled by a factor between 0.9
and 1.1 and the neural network prediction with the smallest MSE will be selected. If ``polish=True``,
this step will happen before the LMS fit. If ``optimize_q=True``, this will step will happen after
the q shift optimization.
n_scale_samples: Number of curve scaling samples that will be generated. More samples can lead to a better
result, but will increase the prediction time.
reload: Decide whether or not to reload all scans in the directory before extracting the data for the fit
(default ``True``). Depending on the number of scans, this can take some time.
Returns:
:class:`FitResult`: An object that contains the fit results as well as useful methods to plot and save
the results.
"""
try:
scan = self._spec_loader.load_scan(scan_number=scan_number, trim_front=trim_front, trim_back=trim_back)
except KeyError:
print(f'scan {scan_number} could not be found in {self._spec_file}')
return
scan.scattering_angle += theta_offset
fit_output = self._curve_fitter.fit_curve(corrected_curve=scan.corrected_intensity, q_values=scan.q, dq=dq,
factor=factor, polish=polish, fraction_bounds=fraction_bounds,
optimize_q=optimize_q, n_q_samples=n_q_samples,
optimize_scaling=optimize_scaling, n_scale_samples=n_scale_samples)
predicted_refl = fit_output['predicted_reflectivity'][0]
predicted_parameters = fit_output['predicted_parameters']
if fit_output['best_q_shift'] is None:
best_q_shift = None
else:
best_q_shift = fit_output['best_q_shift'][0]
fit_result = FitResult(scan_number=scan_number,
timestamp=scan.timestamp,
corrected_reflectivity=scan.corrected_intensity,
q_values_input=scan.q,
predicted_reflectivity=predicted_refl,
q_values_prediction=self._trained_model.q_values - dq,
predicted_parameters=predicted_parameters,
best_q_shift=best_q_shift,
sample=self._trained_model.sample)
if plot:
parameters = [self.trained_model.sample.layers[-1].name + param for param in ('_thickness', '_roughness',
'_sld')]
fit_result.plot_prediction(parameters)
fit_result.plot_sld_profile()
return fit_result
[docs] @reload_scans
def fit_range(self, scan_range: Iterable, trim_front: int = None, trim_back: int = None, theta_offset: float = 0.0,
dq: float = 0.0, factor: float = 1.0, plot=False, polish=True, fraction_bounds=(0.5, 0.5, 0.1),
optimize_q=True, n_q_samples=1000, reload=True) -> FitResultSeries:
"""Iterate fit method over a range of scans."""
scans = self._spec_loader.load_scans(scan_numbers=scan_range, trim_front=trim_front, trim_back=trim_back)
fit_results = []
for scan in scans:
scan.scattering_angle += theta_offset
fit_output = self._curve_fitter.fit_curve(corrected_curve=scan.corrected_intensity, q_values=scan.q, dq=dq,
factor=factor, polish=polish, fraction_bounds=fraction_bounds,
optimize_q=optimize_q, n_q_samples=n_q_samples)
predicted_refl = fit_output['predicted_reflectivity'][0]
predicted_parameters = fit_output['predicted_parameters']
if fit_output['best_q_shift'] is None:
best_q_shift = None
else:
best_q_shift = fit_output['best_q_shift'][0]
fit_result = FitResult(scan_number=scan.scan_number,
timestamp=scan.timestamp,
corrected_reflectivity=scan.corrected_intensity,
q_values_input=scan.q,
predicted_reflectivity=predicted_refl,
q_values_prediction=self._trained_model.q_values - dq,
predicted_parameters=predicted_parameters,
best_q_shift=best_q_shift,
sample=self._trained_model.sample)
if fit_result is not None:
fit_results.append(fit_result)
fit_result_series = FitResultSeries(fit_results)
if plot:
fit_result_series.plot_sld_profiles()
parameters = [self.trained_model.sample.layers[-1].name + param for param in ('_thickness', '_roughness',
'_sld')]
fit_result_series.plot_predicted_parameter_range(parameters)
return fit_result_series
[docs] @reload_scans
def show_scans(self, min_scan: int = None, max_scan: int = None, reload=True):
"""Show information about all scans from ``min_scan`` to ``max_scan``."""
parser = self._spec_loader.parser
if max_scan is None:
max_scan = np.max(np.asarray(list(parser.scan_info.keys()), dtype=int))
else:
max_scan = np.min((max_scan, np.max(np.asarray(list(parser.scan_info.keys()), dtype=int))))
if min_scan is None:
min_scan = np.min(np.asarray(list(parser.scan_info.keys()), dtype=int))
else:
min_scan = np.max((min_scan, np.min(np.asarray(list(parser.scan_info.keys()), dtype=int))))
for i in range(min_scan, max_scan + 1):
try:
out = f'scan #{i}\n' \
f'\tcommand: {parser.scan_info[i]["spec_command"]}\n' \
f'\ttime: {parser.scan_info[i]["time"]}'
print(out)
except KeyError:
print(f'scan #{i}\n\tnot found')
[docs] def set_file(self, spec_file_path: str):
"""Define the full path of the SPEC file from which the scans are read."""
self._spec_loader = SpecLoader(spec_file_path, **self._import_params, **self._footprint_params)
self._file_name = spec_file_path
[docs] def set_import_params(self, angle_columns: list, intensity_column: str, attenuator_column: str = None,
division_column: str = None):
"""Set the parameters necessary to correctly import the scans from the SPEC file.
Args:
angle_columns: List of SPEC counters that are summed up to form the full scattering angle (2theta).
intensity_column: SPEC counter from which the intensity is extracted from.
attenuator_column: SPEC counter of the applied attenuator used to correct possible kinks in the data.
division_column: Optional SPEC counter that is used to divide the intensity counter by.
"""
params = {
'angle_columns': angle_columns,
'intensity_column': intensity_column,
'attenuator_column': attenuator_column,
'division_column': division_column
}
self._import_params.update(params)
def _reload_loader(self):
self._loader = SpecLoader(self._file_name, **self._import_params, **self._footprint_params)
[docs]class DefaultSpecFitter(SpecFitter):
""":class:`SpecFitter` that is initialized with a pre-trained model for reflectivity on single-layer systems on
Si/SiOx."""
def __init__(self):
super().__init__()
self.set_trained_model(DefaultTrainedModel())