Getting started

Here we will briefly demonstrate loading in ellipsometry datasets, creating a model and optimising that model to the dataset. For a more detailed description of model creation, please see the getting started tutorial on refnx.

Fitting an ellipsometry dataset

We begin by importing all of the relevant packages.

[1]:
import sys
import os
from os.path import join as pjoin
import numpy as np
import matplotlib.pyplot as plt
import scipy

import refnx
from refnx.analysis import CurveFitter
from refnx.reflect import Slab

import refellips
from refellips.dataSE import DataSE, open_EP4file
from refellips.reflect_modelSE import ReflectModelSE
from refellips.objectiveSE import ObjectiveSE
from refellips.structureSE import RI, Cauchy, load_material

For reproducibility, it is important to note the versions of software that you’re using.

[2]:
print(
    f"refellips: {refellips.version.version}\n"
    f"refnx: {refnx.version.version}\n"
    f"scipy: {scipy.version.version}\n"
    f"numpy: {np.version.version}"
)
refellips: 0.0.3.dev0+92f1c50
refnx: 0.1.30
scipy: 1.8.0
numpy: 1.22.3

Loading a dataset

refellips has the capability of loading data directly from output files of both Accurion EP3 and EP4 ellipsometers, as well as Horiba ellipsometers using open_EP4file and open_HORIBAfile respectively.

Alternatively, other datasets can be imported using DataSE. The file must be formatted to contain four columns: wavelength, angle of incidence, psi and delta.

[3]:
pth = os.path.dirname(refellips.__file__)
dname = "testData1_11nm_PNIPAM_on_Si_EP4.dat"
file_path = pjoin(pth, "../", "demos", dname)

We will now use DataSE to import our dataset.

[4]:
data = DataSE(data=file_path)

Creating a model for our interface

As with refnx, ComponentSE objects are assembled into a StructureSE object which describes the interface. The simplest of these ComponentSE objects is a SlabSE, which is what we will use here.

We begin by loading in dispersion curves which describe the refractive index for each layer within our StructureSE (i.e., for each ComponentSE). refellips offers multiple ways to prescribe the refractive index of a layer. Here we will demonstrate the different ways to prescribe refractive indices. - RI(): Users can provide a refractive index (\(n\)) and extinction coefficient (\(k\)) for a given wavelength by RI([n, k]) or alternatively, for spectroscopic analysis users load in their own dispersion curve of \(n\),\(k\) as a function of wavelength by providing a path to the desired file as RI('my_materials\material.csv'). - Cauchy(): Creates a dispersion curve of a given material using the provided \(a\), \(b\) and \(c\) values. - load_material(): Searches the refellips materials database for the provided material.

Note, dispersion curves provided in the refellips materials database are downloaded as a .csv file from refractiveindex.info. When providing a dispersion curve, files must contain at least two columns, assumed to be wavelength (in microns) and refractive index. If three columns are provided the third is loaded as the extinction coefficient. Further details on modelling material optical properties are provided on the FAQ page.

[5]:
si = load_material("silicon")
sio2 = RI([1.4563, 0])
PNIPAM = Cauchy(1.47, 0.00495)
air = RI(pjoin(pth, "materials/air.csv"))

Now we have defined the refractive indices of our layers, we can create a Slab object for each interfacial layer.

[6]:
# this is a 20 Angstrom layer
silica_layer = sio2(20)

polymer_layer = PNIPAM(200)

Each Slab has an associated thickness (as defined above), roughness, and volume fraction of solvent. As this is a dry film we will leave vfsolv as 0.

[7]:
silica_layer.name = "Silica"
silica_layer.thick.setp(vary=True, bounds=(1, 30))
silica_layer.vfsolv.setp(vary=False, value=0)

polymer_layer.name = "PNIPAM"
polymer_layer.thick.setp(vary=True, bounds=(100, 500))
polymer_layer.vfsolv.setp(vary=False, value=0)

We now create the Structure by assembling the Component objects. Our structure is defined from fronting to backing, where the thickness of the fronting and backing are defined to be infinite (i.e., np.inf).

[8]:
structure = air() | polymer_layer | silica_layer | si()

Finally, we can create our model. A wavelength must be provided here, however, if your ellipsometry dataset contains a wavelength that will be automatically used. We have the option to define the delta_offset parameter here.

[9]:
model = ReflectModelSE(structure)

model.delta_offset.setp(value=0, vary=False, bounds=(-10, 10))

We can now have a quick preview of how our model compares to our dataset prior to fitting.

[10]:
fig, ax = plt.subplots()
axt = ax.twinx()

aois = np.linspace(50, 75, 100)

for dat in data.unique_wavelength_data():
    wavelength, aois, psi_d, delta_d = dat
    wavelength_aois = np.c_[np.ones_like(aois) * wavelength, aois]

    psi, delta = model(wavelength_aois)
    ax.plot(aois, psi, color="r")
    p = ax.scatter(data.aoi, data.psi, color="r")

    axt.plot(aois, delta, color="b")
    d = axt.scatter(data.aoi, data.delta, color="b")

ax.legend(handles=[p, d], labels=["Psi", "Delta"])
ax.set(ylabel="Psi", xlabel="AOI, °")
axt.set(ylabel="Delta")
plt.show()
_images/getting_started_24_0.png

Creating an objective

We will now create an objective. The Objective object is made by combining the model and the data, and is used to calculate statistics during the fitting process.

[11]:
objective = ObjectiveSE(model, data)

Fitting the data

The optimisation of our Objective is performed by refnx’s CurveFitter. Data can be fit using a local optimisation such as least_squares, or a more global optimisation technique such as differential_evolution. For more information on the available fitting methods, see *refnx*.

[12]:
fitter = CurveFitter(objective)
fitter.fit(method="least_squares");

Optimised model and data post fit

We can now view our optimised objective, including our fit parameters. Users can plot this using the above method, or alternatively use the objectiveSE.plot() function.

[13]:
fig, ax = objective.plot()
_images/getting_started_33_0.png
[14]:
for i, x in enumerate(objective.model.parameters):
    print(x)
________________________________________________________________________________
Parameters: 'instrument parameters'
<Parameter:'delta offset' , value=0 (fixed)  , bounds=[-10.0, 10.0]>
________________________________________________________________________________
Parameters: 'Structure - '
________________________________________________________________________________
Parameters:       ''
<Parameter:  ' - thick'   , value=0 (fixed)  , bounds=[-inf, inf]>
<Parameter:  ' - rough'   , value=0 (fixed)  , bounds=[-inf, inf]>
<Parameter:' - volfrac solvent', value=0 (fixed)  , bounds=[0.0, 1.0]>
________________________________________________________________________________
Parameters:    'PNIPAM'
<Parameter:  ' - thick'   , value=132.339  +/- 982 , bounds=[100.0, 500.0]>
<Parameter: ' - cauchy A' , value=1.47 (fixed)  , bounds=[-inf, inf]>
<Parameter: ' - cauchy B' , value=0.00495 (fixed)  , bounds=[-inf, inf]>
<Parameter: ' - cauchy C' , value=0 (fixed)  , bounds=[-inf, inf]>
<Parameter:  ' - rough'   , value=0 (fixed)  , bounds=[-inf, inf]>
<Parameter:' - volfrac solvent', value=0 (fixed)  , bounds=[0.0, 1.0]>
________________________________________________________________________________
Parameters:    'Silica'
<Parameter:  ' - thick'   , value=1 +/- 1.01e+03, bounds=[1.0, 30.0]>
<Parameter:  ' - rough'   , value=0 (fixed)  , bounds=[-inf, inf]>
<Parameter:' - volfrac solvent', value=0 (fixed)  , bounds=[0.0, 1.0]>
________________________________________________________________________________
Parameters:       ''
<Parameter:  ' - thick'   , value=0 (fixed)  , bounds=[-inf, inf]>
<Parameter:  ' - rough'   , value=0 (fixed)  , bounds=[-inf, inf]>
<Parameter:' - volfrac solvent', value=0 (fixed)  , bounds=[0.0, 1.0]>

We can also view the resultant refractive index profile of the interface as well.

[15]:
structure.reverse_structure = True
plt.plot(*structure.ri_profile())
plt.text(110, 3.7, f"{structure.wavelength} nm", fontsize=14)
[15]:
Text(110, 3.7, '658.0 nm')
_images/getting_started_36_1.png

Using the plotting tools

[16]:
sys.path.insert(1, "../tools")
from plottools import plot_ellipsdata, plot_structure
[18]:
fig, ax = plt.subplots(1, 2, figsize=(10, 4))

plot_ellipsdata(ax[0], data=data, model=model, xaxis="aoi")
plot_structure(ax[1], objective=objective)

fig.tight_layout()
_images/getting_started_39_0.png

Saving the objective

If you would like to save the Objective or model to a file, this is best done through serialisation to a Python pickle.

[19]:
import pickle

pickle.dump(objective, open("my_objective.pkl", "wb"))

You can then simply reload your objective.

[20]:
objective = pickle.load(open("my_objective.pkl", "rb"))

If you would like to save your objective as a .csv file, this can be done as below.

[21]:
with open("my_objective.csv", "wb") as fh:
    data = objective.data
    wav = data.wavelength
    aoi = data.aoi
    psi_d = data.psi
    delta_d = data.delta
    for dat in data.unique_wavelength_data():
        wavelength, aois, psi_d, delta_d = dat
        wavelength_aois = np.c_[np.ones_like(aois) * wavelength, aois]
        psi_m, delta_m = objective.model(wavelength_aois)
    save_arr = np.array([wav, aoi, psi_d, delta_d, psi_m, delta_m])

    np.savetxt(
        fh,
        save_arr.T,
        delimiter=",",
        header="Wavelength, AOI, Measured Psi, Measured Delta, Modelled Psi, Modelled Delta",
    )