2.2. Designing a unit cell with IPKISS

In the previous section, we briefly discussed the scripts that are required to design the WBG temperature sensor. In this section, we will focus on all the topics related to designing a general rectangular unit cell in the SiEPIC platform. In particular, you will learn to

  • import a new foundry PDK,
  • create the layout of the unit cell,
  • retrieve the S-matrices of the unit cell with CAMFR simulations for different temperatures and wavelengths,
  • do a 2D polynomial fitting to the S-matrices,
  • save the coefficients of the polynomial fitting to a .txt file,
  • load the polynomial fitting data and use it to generate the circuit model of the unit cell.

It is thus clear you will need to create custom building blocks, separate from the building blocks that are present in the foundry PDK. As such, this Luceda Academy tutorial follows the file structure as outlined in the Develop and distribute your component library tutorial. If you open the luceda-academy/libraries/pteam_library_siepic/ipkiss/pteam_library_siepic/components folder, you will see it contains the following folders:

  • grating_unit_cell: this folder contains the scripts for realising the unit cell layout, circuit model, etc. It also contains the doc folder containing some example files and the model_data folder containing all the simulation data.
  • grating_waveguide: this folder contains the scripts for realising the WBG layout and circuit model. As we will see later on, it will rely heavily on the methods and data stored in the grating_unit_cell folder.

2.2.1. Importing a foundry PDK

Before we dive into the actual design of the unit cell, we first need to import a new PDK. In the getting started sections and topical training tutorials, we used the generic SiFab PDK for the creation of the building blocks. Now we will implement our WBG-based temperature sensor on the SiEPIC platform and create our own building blocks, so instead of importing the SiFab PDK, we will import the IPKISS PDK for SiEPIC. This PDK contains not only the layer stack of SiEPIC’s SOI platform, but also several mature building blocks, among them WBGs as well. However, since we will introduce some new features to the WBGs not covered by the WBG building block in the PDK, we will design our own WBG component.

Importing a new PDK in IPKISS is very simple. Instead of importing SiFab, we simply import SiEPIC at the top of the Python file:

import siepic.all as pdk

To make this import statement work, we need to add the ipkiss folder of the IPKISS PDK for SiEPIC to the PYTHONPATH. In order to do that, you may right-click on siepic/ipkiss and select ‘Mark Directory as’ -> ‘Sources Root’ (see Fig. 2.10).


Fig. 2.10 Mark the ipkiss folder of the IPKISS PDK for SiEPIC as ‘Sources Root’

Now you can start using the IPKISS PDK for SiEPIC in your designs.

2.2.2. Define the layout of the unit cell (+ create a new building block)

As explained previously, the unit cell is the basic building block of the WBG and will determine the properties of the whole device. To do so, we first need to tell IPKISS what the unit cell looks like.

Just like for the layout of the MMI in the MMI PCell tutorial, we define a PCell for the unit cell that defines its full geometry. We create a Python file cell.py in the grating_unit_cell folder in which we will store the unit cell and its layout:

Listing 2.30 luceda-academy/libraries/pteam_library_siepic/ipkiss/pteam_library_siepic/components/grating_unit_cell/cell.py
class UnitCellRectangular(i3.PCell):
    """Unit cell of the WBG. It contains a wide center section with length L2 and width w2.
    It is surrounded by 2 waveguides with a width of w1 and combined length of L1.
    _name_prefix = "uc_rect"
    trace_template = i3.TraceTemplateProperty(
        doc="Trace template of the unit cell waveguide sections"
    length1 = i3.PositiveNumberProperty(default=0.175, doc="Length of the narrow waveguide section")
    length2 = i3.PositiveNumberProperty(default=0.175, doc="Length of the wide waveguide section")
    width = i3.PositiveNumberProperty(default=0.5, doc="Average width of the grating")
    deltawidth = i3.PositiveNumberProperty(default=0.52, doc="Corrugation width")

    uc_attributes = ['length1', 'length2', 'width', 'deltawidth']

    class Layout(i3.LayoutView):
        def _generate_elements(self, elems):
            core_layer = self.trace_template.core_layer
            cladding_layer = self.trace_template.cladding_layer
            # Si core
            elems += i3.Rectangle(
                center=(0.25 * self.length1, 0.0),
                box_size=(0.5 * self.length1, self.width-self.deltawidth)
            elems += i3.Rectangle(
                center=(0.5 * (self.length1 + self.length2), 0.0),
                box_size=(self.length2, self.width+self.deltawidth)
            elems += i3.Rectangle(
                center=(0.75 * self.length1 + self.length2, 0.0),
                box_size=(0.5 * self.length1, self.width-self.deltawidth)
            # Cladding
            elems += i3.Rectangle(
                center=(0.5 * (self.length1 + self.length2), 0.0),
                box_size=(0.0, 4*self.width)
            return elems

        def _generate_ports(self, ports):
            lambda_b = self.length1 + self.length2
            ports += i3.OpticalPort(
                position=(0.0, 0.0),
            ports += i3.OpticalPort(
                position=(lambda_b, 0.0),
            return ports

Since we focus on designing a rectangular unit cell, we name the class UnitCellRectangular. You can now see the following properties:

  • _name_prefix: prefix to the unit cell name. Since we have a rectangular unit cell, we set this property as uc_rect.
  • trace_template: this property describes, among others, the cross section of the waveguides. Although SiEPIC offers only one cross section where the silicon is fully etched (as compared to the generic SiFab that also offers shallowly etched waveguides), SiEPIC does offer some freedom in regards to the resolution in which the structures are defined in the mask during the foundry’s postprocessing. Put shortly, a higher resolution will result in a lower amount of quantization errors on the final mask but will also result in a more costly and time consuming fabrication. To alleviate the cost and time issues while still retaining sufficient resolution, shot pitch fracturing can be applied to the layout of WBGs. More information about shot pitch fracturing can be found here. To ensure shot pitch fracturing is applied to the unit cells (and thus WBGs), we set the trace template to pdk.WaveguideBraggGratingTemplate(), contrary to pdk.StripWaveguideTemplate() which is used for the routing waveguides and is drawn in a coarser resolution without shot pitch fracturing.
  • The lengths length1, length2, the average width width and the corrugation width deltawidth: these properties fully describe the layout of the unit cell as illustrated in Fig. 2.11.

We also give UnitCellRectangular also the property uc_attributes. As explained in the previous section, the coefficients describing the optical properties of the unit cells will be stored in .txt files. The name of the .txt file hence should reflect the layout of its corresponding unit cell. For a rectangular unit cell, we have 5 properties: the shape of the unit cell (in our case rectangular) and the properties describing its layout: length1, length2, width and deltawidth. The shape of the unit cell is already given by _name_prefix, so we need a list that contains the attributes describing the unit cell layout. That list is thus the property uc_attributes. Using both _name_prefix and uc_attributes, we will name the .txt file containing the polynomial fitting coefficients.


Fig. 2.11 Layout parameters of a general rectangular unit cell: \(l_1\) (length1), \(l_2\) (length2), \(w\), (width) and \(\Delta w\) (deltawidth).

After the unit cell properties, we have the Layout class in which we define the shape of the unit cell based on the given properties. You can see we define three rectangles corresponding to the shape of the rectangular unit cell. More information about how to define the layout of a PCell can be found in the MMI PCell tutorial. The same applies to the port information, which is done in the method _generate_ports.


Before you can import and use the unit cell PCell in an external script, you will need to add the folder luceda-academy/libraries/pteam_library_siepic/ipkiss to the PYTHONPATH. This is done by marking this directory as ‘Sources Root’, as done for the SiEPIC PDK in Fig. 2.10.

The unit cell can now be initiated with the Python file example_unit_cell.py found in the doc folder. At the top of the file, we import the unit cell building block and other relevant packages. We then define and visualise a unit cell with dimensions \(l_{1}=158\,\mathrm{nm}\), \(l_{2}=158\,\mathrm{nm}\), \(w=500\,\mathrm{nm}\) and \(\Delta w=50\,\mathrm{nm}\) using the following code:

Listing 2.31 example_unit_cell.py
uc = UnitCellRectangular(
uc_layout = uc.Layout()

and is plotted in Fig. 2.12:


Fig. 2.12 Unit cell layout

2.2.3. Calculating the S-parameters of the unit cell with CAMFR

Everything is now set for CAMFR to calculate the unit cell response, i.e. the S-matrices for different wavelengths and temperatures. Unlike the MMI in the Optimize the MMI using CAMFR tutorial, the simulation recipe here is defined outside of the SiEPIC PDK, i.e. in the Luceda Academy tutorial, as the temperature dependence of the materials is not covered by the SiEPIC PDK. Nevertheless, the simulation recipe still needs the information of the layer stacks as defined in the SiEPIC PDK. Indeed, CAMFR turns the 3D layer stack of the SiEPIC foundry into a 2D grid of effective indices, and will then apply its EME methods to simulate the circuit. The function that contains the CAMFR simulation recipe for the unit cell is called simulate_uc_by_camfr. We assume there is no polarization conversion, meaning TE-polarized light coupling to TM-polarized modes and vice versa. This will enable us to simulate the optical behaviour of the unit cell for TE and TM polarized light separately, which will save simulation time if our WBG only operates at one particular wavelength.

The simulation recipe simulate_uc_by_camfr takes the following parameters as input:

  • uc_object: the unit cell object, containing its attributes and Layout View, you wish to simulate,
  • wavelengths: a set of wavelengths to use for the simulation (ndarray, unless single value),
  • temperatures: a set of temperatures to use for the simulation (ndarray, unless single value),
  • discretization_res: discretization resolution (default is 20), with the factor 1/discretisation_res defining what is the minimal change between the widths and lengths of consecutive slabs of a longitudinally variant structure,
  • num_mod: number of modes to simulate (default is 25),
  • polarization: polarization (string) of the light (default is ‘TE’).

and will output the following parameters (in this order):

  • sim_result[0]: the wavelengths for which the simulation is performed,
  • sim_result[1]: the temperatures for which the simulation is performed,
  • sim_result[2]: S-matrix coefficients describing the optical response of the unit cell for the given polarization, wavelengths and temperatures.

For our unit cell that has two ports and is simulated at one particular polarization, there are four S-matrix coefficients:

  • \(T_{12}\): the transmission from the input port to the output port,
  • \(T_{21}\): the transmission from the output port to the input port,
  • \(R_{12}\): the reflection from the input port,
  • \(R_{21}\): the reflection from the output port.

These four S-matrix coefficients are thus calculated for a set of wavelengths and temperatures by the simulation recipe simulate_uc_by_camfr. Since we are dealing with a multidimensional array of data for each of the four S-matrix coefficients (a set of wavelengths and a set of temperatures), handling and saving the dataset can be challenging. To deal with these multidimensional datasets, simulate_uc_by_camfr outputs the simulation data for each S-matrix coefficient as a long array containing both the results from the wavelength and temperature sweeps. This is illustrated in Fig. 2.13 for \(T_{12}\), simulated for \(M_w\) wavelengths and \(M_t\) temperatures.


Fig. 2.13 Illustration of how to store 2D data (\(T_{12}\) as a function of wavelength \(\lambda\) and temperature \(T\)) as a 1D array

More generally, multidimensional data of the form \(M_1\times M_2\times...\times M_n\) can be stored into a 1D array of length \(M_1M_2...M_n\) and retrieved if the dimensions \(M_1\times M_2\times...\times M_n\) are known as well. So, for sim_result[2], the data is saved in a \(4\times M_wM_t\) matrix, with the first, second, third and fourth row corresponding to \(T_{12}\), \(T_{21}\), \(R_{12}\) and \(R_{21}\) respectively.

We now calculate, for TE-polarization, the unit cell S-matrix coefficients for wavelengths between 1400 nm to 1700 nm and at the temperatures \(T=273\,\mathrm{K}\) and \(T=300\,\mathrm{K}\). In example_unit_cell.py where we defined the unit cell PCell, we simply initiate simulate_uc_by_camfr with the following code:

Listing 2.32 example_unit_cell.py
print("Calculating the properties of the unit cell for different wavelengths and temperatures...")
sim_result = simulate_uc_by_camfr(
    wavelengths=np.arange(1.4, 1.7, 0.01),
    temperatures=np.array([273., 300.]),

with uc the unit cell PCell we initiated earlier, and where we take a sufficiently large number of modes of 50. The S-matrix coefficients can then be plotted using the function plot_smatrix_coefficients and are depicted in Fig. 2.14.


Fig. 2.14 Illustration of the S-matrix coefficients as a function of wavelength and for the temperatures 273 K and 300 K (blue: real part, red: imaginary part and green: absolute value)

2.2.4. Fit and save the S-matrix coefficients to a .txt file

Now that we have calculated the S-matrix coefficients, we need to fit them with polynomials and save them in a text file. This data will be used for the circuit model of the unit cell and the WBG later on. Since we will place the WBG in a circuit, using building blocks provided by the SiEPIC PDK, we need to ensure compatibility with the models from those building blocks. One key feature of the SiEPIC PDK components is the fact they take into account both the optical performance for both TE- and TM-polarization. So for our unit cell, we need to retrieve the component’s behavior at both TE- and TM-polarization as well.

To create and save the data necessary for the circuit models, we simply initiate the function create_uc_model_data() on the unit cell PCell uc in example_unit_cell.py:


which takes the following parameters:

  • uc_pcell: the unit cell PCell, containing its attributes and Layout View, you wish to simulate,
  • wavelengths: the set of wavelengths used for the model fitting (default is np.linspace(1.4, 1.7, 11)),
  • temperatures: the set of temperatures used for the model fitting (default is np.linspace(173., 473., 7)),
  • npoly_wavelengths: order of the polynomial fitting for the wavelength dependence (default is 7),
  • npoly_temperatures: order of the polynomial fitting for the temperature dependence (default is 2),
  • center_wavelength: center wavelength around which the wavelength fitting occurs (default is 1.55),
  • center_temperature: center temperature around which the temperature fitting occurs (default is 273),
  • plot_fitting: plot the fitting to the S-matrix coefficients (default is False).

Most of the default values of these parameters are applicable for this WBG temperature sensor, so we can simply pass on the unit cell object to create_uc_model_data().

Now let’s take a look at the code in create_uc_model_data in more detail:

Listing 2.33 luceda-academy/libraries/pteam_library_siepic/ipkiss/pteam_library_siepic/components/grating_unit_cell/fitting.py
def create_uc_model_data(
        wavelengths=np.linspace(1.4, 1.7, 11),
        temperatures=np.linspace(173., 473., 7),

    print('Calculate the performance of the unit cell for different wavelengths and temperatures for TE polarisation.')
    uc_for_model_te = simulate_uc_by_camfr(

    print('Calculate the performance of the unit cell for different wavelengths and temperatures for TM polarisation.')
    uc_for_model_tm = simulate_uc_by_camfr(

    print('Retrieving the fitting coefficients.')
    te_fit_coeff = fit_camfr_data(
    tm_fit_coeff = fit_camfr_data(

    print('Saving the fitting coefficients to a text file.')
    save_to_txt_files(uc_pcell, te_fit_coeff, tm_fit_coeff)

We can see that we perform two CAMFR simulations: one for TE-polarized light and one for TM-polarized light, for which the simulation data is outputted in the variables uc_for_model_te and uc_for_model_tm respectively. This data will then be fitted to a set of polynomials using the function fit_camfr_data. Since we are dealing with multidimensional data here, fit_camfr_data will first do a polynomial fitting of the data around the center wavelength \(\lambda_0\) at each of the temperatures considered in the simulation data. In other words, the polynomial fit of the S-parameters (along wavelength) has fitting coefficients that are dependent on the temperature. For npoly_wavelengths equal to \(P_w\), this can be mathematically described as:


All the fitting coefficients \(p_i(T)\) for \(i=0,...,m\) are thus in itself functions - now dependent on the temperature - that can be fitted with a polynomial along \(T\) around the center temperature \(T_0\). For npoly_temperatures equal to \(P_t\), each of the S-parameters will therefore be approximated by:


fit_camfr_data outputs the fitting coefficients \(p_{ij}\) in the te_fit_coeff and tm_fit_coeff matrices for TE- and TM-polarized light respectively. It should be noted that the \(p_{ij}\) coefficients are stored in te_fit_coeff and tm_fit_coeff as \(8\times P_wP_t\) matrices, which is illustrated in Fig. 2.15.


Fig. 2.15 Illustration of how the \(p_{ij}\) fitting coefficients are stored for the four S-parameters for a given polarization (te_fit_coeff for TE and tm_fit_coeff for TM)

We need eight rows for the four S-parameters because we fit the real and imaginary part of the four S-parameters separately as the fitting in Python can only deal with real numbers. Finally, both the te_fit_coeff and tm_fit_coeff matrices, the order of the polynomial fits and the center wavelength/temperature are saved to a .txt file using the function save_to_txt_files. The .txt files will be saved in the folder model_data. When looking at the name of the .txt file, you will see that it contains the shape of the unit cell configuration and its dimensions. The folder also contains a file with the suffix ‘whats_there’, which includes the unit cell configurations for which there is simulation data.

To check whether the fitting does not yield unphysical behavior, we can plot the fitted data either directly with create_uc_model_data() by setting plot_fitting=True in example_unit_cell.py:

create_uc_model_data(uc_pcell=uc, plot_fitting=True)

which is illustrated here for TE in Fig. 2.16:


Fig. 2.16 Fitted of the S-matrix coefficients for TE-polarization, as a function of wavelength and for the default temperatures used in create_uc_model_data (blue: real part and red: imaginary part)

or afterwards, by invoking the show_model() function, which has the following parameters:

  • uc_pcell: the unit cell PCell of which you want to visualise the model,
  • wavelengths: the set of wavelengths at which you want to verify the model (np.array),
  • temperatures: the set of temperatures at which you want to verify the model (np.array).

The results for the model of our unit cell are plotted in Fig. 2.17.


Fig. 2.17 Fitted S-matrix coefficients for TE- and TM-polarization, as a function of wavelength and for the temperatures 200 K, 300 K and 400 K (blue: real part and red: imaginary part)

2.2.5. Define the circuit model of the unit cell

Now that we know how to generate and save the model data, we need to actually implement the circuit model in the unit cell component. We first create the netlist from the layout and ensure each port contains two modes (TE and TM). To do so, we add the following code to the unit cell PCell:

class Netlist(pdk.NetlistFromLayout2Modes):

Implementing the circuit model itself however is done by including the CircuitModel class in the unit cell PCell:

Listing 2.34 luceda-academy/libraries/pteam_library_siepic/ipkiss/pteam_library_siepic/components/grating_unit_cell/cell.py
    class CircuitModel(i3.CircuitModelView):
        temperature = i3.PositiveNumberProperty(doc="Temperature of the device", default=293.)

        def _generate_model(self):
            fit_param = get_and_fit_sparameters(uc_config=self.cell, temperature=self.temperature)
            return UnitCellModel(

which only has temperature, that determines the temperature at which you want to retrieve the circuit model data, as an attribute. To retrieve the fitting data for the circuit model, we use the get_and_fit_sparameters() function, which has the following parameters:

  • uc_config: attributes of the unit cell - since we invoke get_and_fit_sparameters() within the unit cell PCell, this parameter is set at self.cell,
  • temperature: temperature at which you want to retrieve the circuit model data.

You can see we invoke it within the _generate_model method as it outputs the necessary information for the unit cell model in the variable fitparam:

  • fitparam[0][0]: the center wavelength for which the wavelength polynomial coefficients are retrieved.
  • fitparam[0][1]: the center temperature for which the temperature polynomial coefficients are retrieved. It is not used here, but can be useful for debugging.
  • fitparam[1]: the wavelength polynomial coefficients for the S-parameters at the given temperature.

get_and_fit_sparameters() will also be used for the WBG as we will see later on. The circuit model is then invoked with UnitCellModel() in _generate_model(self) that has two parameters, sdata_coeff and center_wavelength, which are derived from the circuit model attributes as explained above. The code for UnitCellModel() is given here:

Listing 2.35 luceda-academy/libraries/pteam_library_siepic/ipkiss/pteam_library_siepic/components/grating_unit_cell/model.py
class UnitCellModel(i3.CompactModel):

    parameters = [
    terms = [
        i3.OpticalTerm(name='in', n_modes=2),
        i3.OpticalTerm(name='out', n_modes=2),

    def calculate_smatrix(parameters, env, S):

        sdata_coeff = parameters.sdata_coeff
        sparam_fitted = []
        for index in range(0, 8):
                horner_polyval(sdata_coeff[2*index, :], env.wavelength-parameters.center_wavelength) +
                1j*horner_polyval(sdata_coeff[2*index+1, :], env.wavelength-parameters.center_wavelength)

        S['in:0', 'out:0'] = sparam_fitted[0]
        S['out:0', 'in:0'] = sparam_fitted[1]
        S['in:0', 'in:0'] = sparam_fitted[2]
        S['out:0', 'out:0'] = sparam_fitted[3]
        S['in:1', 'out:1'] = sparam_fitted[4]
        S['out:1', 'in:1'] = sparam_fitted[5]
        S['in:1', 'in:1'] = sparam_fitted[6]
        S['out:1', 'out:1'] = sparam_fitted[7]

As can be seen, the number of modes n_modes in i3.OpticalTerm is 2. This is because we take into account both the TE and TM behavior of the unit cell in our models. Furthermore, we see that polynomial fitting is done using Horner’s method. Finally, we see that the port names in the S-parameter library have a number such as S['in:0', 'out:0'] or S['out:1', 'out:1']. Again, this is due to the fact we have both TE and TM behavior taken into account in our model; 0 corresponds to the TE-modes while 1 indicates TM-polarized light.

Now that the unit cell model has been implemented in its PCell, we can initiate it and plot it as follows in example_unit_cell.py (taking a temperature of 300 K as an example):

Listing 2.36 example_unit_cell.py
wavelengths = np.linspace(1.5, 1.6, 2501)
uc_cm = uc.CircuitModel(temperature=300.)
S = uc_cm.get_smatrix(wavelengths=wavelengths)
transmission_te = np.abs(S["in:0", "out:0"])**2
reflection_te = np.abs(S["in:0", "in:0"])**2
transmission_tm = np.abs(S["in:1", "out:1"])**2
reflection_tm = np.abs(S["in:1", "in:1"])**2

plt.plot(wavelengths*1000, transmission_te, 'b-', linewidth=2.2, label=r'$|T_{12}|^2$ TE')
plt.plot(wavelengths*1000, reflection_te, 'r-', linewidth=2.2, label=r'$|R_{12}|^2$ TE')
plt.plot(wavelengths*1000, transmission_tm, 'b--', linewidth=2.2, label=r'$|T_{12}|^2$ TM')
plt.plot(wavelengths*1000, reflection_tm, 'r--', linewidth=2.2, label=r'$|R_{12}|^2$ TM')
plt.xlabel('Wavelength (nm)', fontsize=16)
plt.ylabel('Transmission and Reflection', fontsize=16)
plt.legend(fontsize=14, loc=6)
plt.tick_params(which='both', labelsize=14)

As an illustration, we plot the reflection from the input port and the transmission from the input to the output port for both TE- and TM-polarized light in Fig. 2.18.


Fig. 2.18 Absolute value of the S-matrix coefficients calculated at a temperature of 300 K over a wavelength range of 100 nm around 1550 nm with the generated unit cell model.

While this result might not mean much yet, we can now finally start optimizing the unit cell (and thus the WBG) configuration for temperature sensing and use its circuit model when characterizing the final circuit.