Skip to content
Snippets Groups Projects
Commit b0f4c076 authored by Markus Kuehbach's avatar Markus Kuehbach
Browse files

Merge branch 'update-north-markus' into 'develop'

Fixes relevant for consolidated nomad release to make all examples work and deploy a proper nomad oasis version in september 2023

See merge request !154
parents 9dbf3ead c5e35eca
No related branches found
No related tags found
1 merge request!154Fixes relevant for consolidated nomad release to make all examples work and deploy a proper nomad oasis version in september 2023
Pipeline #182204 failed
%% Cell type:markdown id: tags:
# Ellipsometry workflow example
In this notebook, an ellipsometry data set of 2 nm SiO2 on Si is analyzed using the analysis tool [pyElli](https://pyelli.readthedocs.io/en/latest/)
%% Cell type:markdown id: tags:
## 1. Create NeXus file from measurement data
The metadata of the experiment are listed in a YAML file (**eln-data.yaml**, which is automatically created when saving the metadata entered into the electronic lab notebook (ELN) within NOMAD) according to the application definition [**NXellipsometry**](https://manual.nexusformat.org/classes/contributed_definitions/NXellipsometry.html). The name of the data file (here **test-data.dat**) needs to be specified in the ELN and, hence, is defined as an entry 'filename' in the YAML file. Using the **ellips** reader and the application definition in NXDL format, a NeXus file (**SiO2onSi.ellips.nxs**) is created. Both the data and metadata files must be stored in this repository.
Note: When creating or modifying the YAML file without using the ELN, make sure that all required fields are provided; recommended and optional fields may be provided if known and meaningful.
%% Cell type:code id: tags:
``` python
from pynxtools.dataconverter.convert import convert
```
%% Cell type:code id: tags:
``` python
convert(input_file=["eln_data.yaml"],
reader='ellips',
nxdl='NXellipsometry',
output='SiO2onSi.ellips.nxs')
```
%% Cell type:markdown id: tags:
## 2. Inspect the NeXus file with h5web
%% Cell type:code id: tags:
``` python
from jupyterlab_h5web import H5Web
```
%% Cell type:code id: tags:
``` python
H5Web('SiO2onSi.ellips.nxs')
```
%% Cell type:markdown id: tags:
This is the end of the general template. Continue to fill the notebook based on **your own** post-processing of the *.nxs file.
%% Cell type:markdown id: tags:
## 3. Analyze $\Psi$ and $\Delta$ values using a transfer-matrix solver
Import the analysis tool pyElli:
%% Cell type:code id: tags:
``` python
import elli
from elli.fitting import ParamsHist, fit
from elli.importer.nexus import read_nexus_psi_delta
from elli.dispersions import TableSpectraRay
```
%% Cell type:markdown id: tags:
### 3.1. Load data from NeXus file
We load the data from the generated NeXus file and select the angle of incidence we want to analyze. You may set ANGLE to any angle of incidence present in the example file (in this example, the angles of incidence are 50, 60 and 70 degrees.).
We load the data from the generated NeXus file and select the angle of incidence we want to analyze. You may set ANGLE to any angle of incidence present in the example file (in this example, the angles of incidence are 50, 60 and 70 degree.).
Additionally, we are cutting the wavelength axis to lie inbetween 210 nm and 800 nm. This is because we are going to use literature values for Si, which are only defined in this wavelength range.
%% Cell type:code id: tags:
``` python
ANGLE = 70
psi_delta = read_nexus_psi_delta('SiO2onSi.ellips.nxs').loc[ANGLE].loc[210:800]
```
%% Cell type:markdown id: tags:
### 3.2. Set model parameters
As an example, we analyze an oxide layer of SiO2 on a Si substrate. Prior to defining our model, we have to set the parameters we want to use. We are going to use a [Cauchy model](https://pyelli.readthedocs.io/en/latest/dispersions.html#cauchy) for SiO2 and load the Si values from [literature values](https://refractiveindex.info/?shelf=main&book=Si&page=Aspnes). The parameter names can be chosen freely, but you have to use the exact same names in the model definition in section 3.3.
The package uses **lmfit** as fitting tool and you may refer to its [documentation](https://lmfit.github.io/lmfit-py/parameters.html#lmfit.parameter.Parameters.add) for details on parameter definition. To choose which parameters should be fitted, set `vary` equal to `True` if the parameter should be fitted or `False` if it should be fixed during the fit.
%% Cell type:code id: tags:
``` python
params = ParamsHist()
params.add("SiO2_n0", value=1.452, min=-100, max=100, vary=False)
params.add("SiO2_n1", value=36.0, 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)
```
%% Cell type:markdown id: tags:
### 3.3. Build the model and show interactive plot to set the parameters
For simple parameter estimation, the fit decorator (`@fit`) in conjuction with the model definition is used.
The fitting decorator takes a pandas dataframe containing the $\Psi$ and $\Delta$ measurement data (**psi_delta**) and the model parameters (**params**) as an input. It then passes the wavelength from the measurement dataframe (**lbda**) and the parameters to the actual model function.
Inside the model function, the optical model is built, i.e. the Si literature values are loaded and the fitting parameters are filled into the Cauchy dispersion. For details on how to insert data into the [Cauchy model](https://pyelli.readthedocs.io/en/latest/dispersions.html#cauchy) or other optical dispersion models, you may refer to the [documentation of pyElli](https://pyelli.readthedocs.io/en/latest/dispersions.html). Please keep in mind that the parameters you use here are defined in the parameter object **params** (see cell above in section 3.2).
From the dispersion model, an isotropic material is generated (could also be an anisotropic material, refer to the [docs](https://pyelli.readthedocs.io/en/latest/materials.html#isotropic-and-non-isotropic-materials) for an overview). This is done by calling the `elli.IsotropicMaterial(...)` function with a dispersion model as a parameter or simply calling `.get_mat()` on a dispersion model. These two approaches are equivalent.
From this material, the layer is built, which only conists of the SiO2 layer in this example. The final structure consists of an incoming half-space, the layers and an outgoing half space. Specifically, typically the light is coming from air, travels through the oxide layer and finally gets absorbed by the bulk material. In our example the latter is Si, i.e. we call `elli.Structure(elli.AIR, Layer, Si)`.
To provide simulated data, we have to evaluate the structure by calling the `evaluate(...)` function, which takes the experimental wavelength array **lbda**, the angle of incidence (**ANGLE**) under which the experiment was performed and the solver used to solve the transfer-matrix problem. Here, we use a simple 2x2 matrix approach, which splits the interaction in parts of s- and p-polarization and, therefore, cannot account for anisotropy. There exist 4x4 matrix solvers based on the formulation by [D. W. Berremann](https://opg.optica.org/josa/abstract.cfm?uri=josa-62-4-502) as well. You may refer to the [documentation](https://pyelli.readthedocs.io/en/latest/solvers.html) or a [mueller matrix example](https://pyelli.readthedocs.io/en/latest/auto_examples/plot_SiO2_Si_MM.html) on how to use these solvers.
Executing the cell below compares the simulated $\Psi$ and $\Delta$ values at the current parameter values with their measured counterparts. Additionally, input fields for each model parameter are shown. You may change the parameters and the calcualted curves will change accordingly. Note that if the box of a parameter is checked the parameter will be treated as a fit parameter and the setting is taken over to `params`. For clarification, the modeled data are shown with *_calc* suffix in the legend.
%% Cell type:code id: tags:
``` python
@fit(psi_delta, params, ANGLE)
def model(lbda, params):
# Load the literature values for Si
Si = elli.IsotropicMaterial(TableSpectraRay("./").load_dispersion_table("Si_Aspnes.mat"))
# Generate the cauchy model from the current lmfit parameters
SiO2 = elli.Cauchy(
params["SiO2_n0"],
params["SiO2_n1"],
params["SiO2_n2"],
params["SiO2_k0"],
params["SiO2_k1"],
params["SiO2_k2"],
).get_mat()
# get_mat() generates an IsotropicMaterial from the dispersion relation
# Construct the layers you expect in your sample.
# Here, it only consists of one layer SiO2 in between air and a Si substrate.
# We build the structure coming from air, through the SiO2 layer,
# represented as an array, and having Si as bulk material.
structure = elli.Structure(elli.AIR, # Input medium
[elli.Layer(SiO2, params["SiO2_d"])], # Overlayer structure
Si) # Output medium / Substrate
# The model should return the evaluation of the structure at the experimental wavelengths lbda,
# the experimental angle of incidence ANGLE and should define a solver to calculate the transfer matrix.
return structure.evaluate(lbda, ANGLE, solver=elli.Solver2x2)
```
%% Cell type:markdown id: tags:
Check which parameters are currently set as fit parameters, i.e. `vary = True`:
%% Cell type:code id: tags:
``` python
params
```
%% Cell type:markdown id: tags:
### 3.4 Fit and plot fit result
The fit of the data can be executed by calling the `fit()` function on the model function, which automatically gets attached by the `@fit` decorator in the cell above.
The following cell basically executes the fit with the fit parameters defined above and plots a comparison between the measurement and fitted data.
%% Cell type:code id: tags:
``` python
fit_stats = model.fit()
model.plot()
```
%% Cell type:markdown id: tags:
### 3.5 Extracting the optical properties from the fit
Since we want to extract the dispersion relation of a layer in our measured stack, we can use our fitted parameters.
The fit parameters are contained in the fits output `params` attribute, i.e. `fit_stats.params` for our example.
We can use it to call the dispersion relation we used in our model (here it is a Cauchy dispersion relation) and fill in our fitted value.
By calling `get_dielectric_df()` we can get the dielectric function of the layer material, which is plotted here for SiO2 as an example. `get_dielectric_df` uses a default wavelength range which can also be changed by inputting a wavelength array as a parameter, refer to its [documentation](https://pyelli.readthedocs.io/en/latest/dispersions.html#elli.dispersions.base_dispersion.Dispersion.get_dielectric_df) for further details.
%% Cell type:code id: tags:
``` python
fitted_model = elli.Cauchy(
fit_stats.params["SiO2_n0"],
fit_stats.params["SiO2_n1"],
fit_stats.params["SiO2_n2"],
fit_stats.params["SiO2_k0"],
fit_stats.params["SiO2_k1"],
fit_stats.params["SiO2_k2"],
)
fitted_model.get_dielectric_df().plot(backend='plotly')
```
%% Cell type:markdown id: tags:
We can also call `get_refractive_index_df()` to get the refractive index of the material as dataframe.
%% Cell type:code id: tags:
``` python
fitted_model.get_refractive_index_df().plot(backend='plotly')
```
%% Cell type:markdown id: tags:
If you want to write your data to a file you simply call pandas' `to_csv(...)` function to write a CSV file, i.e. for the dielectric function this writes as
%% Cell type:code id: tags:
``` python
fitted_model.get_dielectric_df().to_csv('SiO2_diel_func.csv')
```
%% Cell type:markdown id: tags:
You may also access a single value of your optical model, for example
%% Cell type:code id: tags:
``` python
fit_stats.params['SiO2_n0'].value
```
%% Cell type:markdown id: tags:
Or simply print the fitted values in a list together with their fitting errors:
%% Cell type:code id: tags:
``` python
fit_stats.params
```
%% Cell type:markdown id: tags:
### 3.6 Show fit statistics
Now, we may also print out the fit statictics from the model fit in the cell above. The fit statistics are simple [lmfit fit statistics](https://lmfit.github.io/lmfit-py/fitting.html#), too. Typically, one uses chi square values as a figure of merit for the fit quality. It is stored in the `chisqr` attribute of the `fit_stats` variable we defined in 3.4.
%% Cell type:code id: tags:
``` python
fit_stats.chisqr
```
%% Cell type:markdown id: tags:
We can print the full fit statistics, too.
%% Cell type:code id: tags:
``` python
fit_stats
```
%% Cell type:code id: tags:
``` python
```
......
......@@ -45,7 +45,7 @@
"ellipsometer_type": "dual compensator",
"rotating_element_type": "compensator (source side)",
"calibration_status": "no calibration",
"angle_of_incidence/@unit": "degrees",
"angle_of_incidence/@unit": "degree",
"Beam_path": {
"light_source": {
"source_type": "arc lamp"
......@@ -86,7 +86,7 @@
"data_identifier": 1,
"data_type": "Psi/Delta",
"spectrum_type": "wavelength",
"spectrum_unit": "Angstroms"
"spectrum_unit": "angstrom"
}
}
}
\ No newline at end of file
}
......@@ -5,7 +5,7 @@ Data:
data_software/version: '3.882'
data_type: Psi/Delta
spectrum_type: wavelength
spectrum_unit: Angstroms
spectrum_unit: angstrom
Instrument:
Beam_path:
Detector:
......@@ -26,7 +26,7 @@ Instrument:
environment_conditions:
medium: air
stage_type: manual stage
angle_of_incidence/@unit: degrees
angle_of_incidence/@unit: degree
calibration_status: no calibration
company: J. A. Woollam Co.
ellipsometer_type: dual compensator
......@@ -69,4 +69,4 @@ filename: test-data.dat
plot_name: Psi and Delta
sep: \t
skip: 3
start_time: '2022-01-27T03:35:00+00:00'
\ No newline at end of file
start_time: '2022-01-27T03:35:00+00:00'
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment