A Ring Resonator based Filter based on the Vernier principle

Result

In this example, we construct a PCell which designs a Vernier principle based filter which can act as a sensing element (e.g. biosensing). Starting from functional properties, it designs its own subcomponents. This example is based on [ClaesOE2010].

The filter consists of two ring resonators with slightly different circumference, so that a Vernier effect is obtained. When the refractive index of one of the rings (the ‘sensing’ ring) is changed, a large shift of the transmission spectrum is obtained. This shift is much larger than what can be obtained with a single ring resonator, and hence the sensitivity is considerably enhanced. This can be obtained by opening a window in the back-end oxide on top of the sensing ring, whereas the other ring resonator maintains a top cladding and can therefore not be reached by the fluid or gas which modifies the refractive index of the waveguide’s cladding. For details of the principle and operation, see [ClaesOE2010]

Vernier filter at work

Vernier filter with and without refractive index difference on the sensing ring.

Vernier filter layout

Layout of the vernier filter with two ring resonators.

Illustrates

  1. how to define a PCell with CircuitModel and Layout views
  2. how to run Caphe simulations
  3. how to embed design algorithms into a complex PCell

Defining the Vernier Filter PCell

We start by defining a PCell with functional parameters: the target center wavelength, the desire free spectral range of the sensing ring and the desired vernier enhancement factor.

We also specify the two child cells (the sensing and reference ring) and the trace templates to use for them.

class VernierFilterSensor(PlaceAndAutoRoute):

    center_wavelength = i3.PositiveNumberProperty(default=1.55, doc="Center wavelength of the vernier comb.")
    target_vernier_factor = i3.PositiveNumberProperty(default=50.0, doc="Target vernier enhancement factor of the vernier filter. It will be larger than the provided value.")
    fsr_sensing_ring = i3.PositiveNumberProperty(default=0.001,doc="Free spectral range of the sensing ring resonator. Must be a fraction of the measurement bandwidth.")

    trace_template_sensing_ring = i3.TraceTemplateProperty(doc="Trace template used for the sensing ring. We use a copy of the trace_template of the PlaceAndRoute", locked=True)
    trace_template_reference_ring = i3.TraceTemplateProperty(doc="Trace template used for the reference ring. We use a copy of the trace_template of the PlaceAndRoute", locked=True)

    sensing_ring = i3.ChildCellProperty(doc="Sensing ring resonator of the vernier sensor", locked=True)
    reference_ring = i3.ChildCellProperty(doc="Reference ring resonator of the vernier sensor", locked=True)

As you can see, we reuse the logic which is already offered by PlaceAndAutoRoute, so that we can just describe the child instances and the links in between them to have the layout and netlist automatically generated:

class VernierFilterSensor(PlaceAndAutoRoute):

    ...

    def _default_child_cells(self):
        return {"sensing_ring": self.sensing_ring,
                "reference_ring": self.reference_ring}

    def _default_links(self):
        return [("sensing_ring:out2",
                 "reference_ring:in1")]

Embedding the design algorithm in the PCell

In this example, we embed the design algorithm and associated parameters into the CircuitModel view of the VernierFilterSensor PCell. The functional design parameters were already defined above on the PCell. On the CircuitModel view, we define a number of physical properties which influence the design and the behaviour of the cell:

class VernierFilterSensor(PlaceAndAutoRoute):

...

    class Layout(PlaceAndAutoRoute.Layout):

        ...

    class CircuitModel(PlaceAndAutoRoute.CircuitModel):
        coupler_coupling = i3.FractionProperty(default=0.5, doc="Coupling from the coupler to the rings. Ideally this is matched to the loss in the rings.")

        n_eff_sensing_ring = i3.PositiveNumberProperty(doc="Neff used for the measurement of the of the sensing ring. By default this is extracted from the caphemodel of the ring template.")
        n_eff_reference_ring = i3.PositiveNumberProperty(doc="Neff used for the measurement of the of the reference ring. By default this is extracted from the caphemodel of the ring template.")

        n_g_rings = i3.PositiveNumberProperty(doc="group index of the waveguides of both rings")

        ring_lengths = i3.DefinitionProperty(restriction=i3.RestrictList(i3.RESTRICT_NONNEGATIVE), doc="circumference of the ring resonators. calculated from functional parameters")

The group index (n_g_rings) is used to calculate the ring resonator circumferences (ring_lengths) so that the desired fsr is obtained. The effective indices of the trace templates are used to adjust the ring resonator circumferences so that the target center wavelength is matched as closely as possible.

This calculation is put in a separate method, which is used twice to calculate the default value for ring_lengths:

class VernierFilterSensor(PlaceAndAutoRoute):

...

    class Layout(PlaceAndAutoRoute.Layout):

        ...

    class CircuitModel(PlaceAndAutoRoute.CircuitModel):

        ...

        def _calc_ring_length(self, n_eff, min_fsr, delta_order=0):

            # Gets the length of a ring so that there would be a resonance at lambda_res at a mimimum free sprectral range.
            # This is done using heurisitics.
            #
            # Parameters
            # ----------
            #
            # n_eff : float
            #    Effective index of the ring waveguide
            #
            # min_fsr : float
            #   Minimum free spectral range of the filter.
            #
            # lambda_res : float
            #   Resonance wavelength
            #
            # delta_order : float
            #   Adds delta_order to the calculated order by the filter.
            #   If delta_order is 1 the filter would have the same resonance frequency but will be a superior order.
            #   If delta_order is 1/2 the resonance wavelength would be approximately shifted by half the min_fsr.

            # Calculate the ring length that fullfills the free spectral range criterium
            ring_length_fsr = self.center_wavelength ** 2.0 / (self.n_g_rings * min_fsr)
            # Calculate the order of the ring from the length matching the FSR
            order_ring = ring_length_fsr / self.center_wavelength * n_eff
            # Calculate the first length that fullfills resonance and the fsr criterium
            ring_length_fsr_and_resonance = (np.floor(order_ring) + 0.5 + delta_order) * self.center_wavelength / n_eff
            return ring_length_fsr_and_resonance

        def _default_ring_lengths(self):
            # calculate the lengths of the rings based on the functional specs given.
            l_sensing = self._calc_ring_length(n_eff=self.trace_template_reference_ring.n_eff,
                                               min_fsr=self.fsr_sensing_ring,
                                               delta_order=0)

            fsr_ref = self.fsr_sensing_ring * self.target_vernier_factor / (self.target_vernier_factor + 1)

            l_reference = self._calc_ring_length(n_eff=self.trace_template_reference_ring.n_eff,
                                                 min_fsr=fsr_ref,
                                                 delta_order=0)
            return [l_sensing, l_reference]

It is important to note that the effective index of the reference ring is used for calculating the lengths of both of the ring resonators. A change in effective index of the trace template of the sensing ring will not trigger a redesign, but just influence the simulation.

The full class can be seen in vernier_filter_pcell.py

Running the design, simulation and layout

We start the main program by creating a VernierFilterSensor object and setting up the simulation environment and parameters:

from technologies import silicon_photonics

import numpy as np
import pylab as plt
from vernier_filter_pcell import VernierFilterSensor

vernier_filter = VernierFilterSensor()

cm1 = vernier_filter.CircuitModel(n_eff_sensing_ring=2.4,
                                  n_eff_reference_ring=2.4,
                                  n_g_rings=4.5)


vernier_lay = vernier_filter.Layout()
vernier_lay.visualize(annotate=True)

wavelengths = np.arange(1.53, 1.57, 2e-6)
delta_neff = 2e-4       # effective index difference on sensing ring
../_images/sphx_glr_plot_vernier_filter_001.png

As a first step, we create the baseline design for this filter, in which the effective index of the two ring resonators is the same:

S = cm1.get_smatrix(wavelengths=wavelengths)
T1 = np.abs(S['sensing_ring_in1', 'reference_ring_out2'])
# This gives the following transmission spectrum:
plt.plot(wavelengths, T1, 'b', label='Transmission')
plt.xlabel("Wavelengths ($\mu$ m)")
plt.ylabel("Transmission (power)")
plt.legend()
../_images/sphx_glr_plot_vernier_filter_002.png

The next step is to simulate with a change on the sensing ring:

  • simulate with a difference in n_eff between reference and sensing ring
vernier_filter = VernierFilterSensor()
cm2 = vernier_filter.CircuitModel(n_eff_sensing_ring=2.4 + delta_neff,
                                  n_eff_reference_ring=2.4,
                                  n_g_rings=4.5)  # We do use the vernier effect by having a change on refractive index one ring.
S = cm2.get_smatrix(wavelengths=wavelengths)
T2 = np.abs(S['sensing_ring_in1', 'reference_ring_out2'])

In order to compare with the influence on single ring resonators, we obtain the transmission spectra for single ring resonators by using the vernier filter’s sensing ring. In this way we don’t need to redesign the single ring to exactly match the one in the Vernier configuration.

First we design and simulate without change of effective index:

vernier_filter = VernierFilterSensor()
cm1 = vernier_filter.CircuitModel(n_eff_sensing_ring=2.4,
                                  n_eff_reference_ring=2.4,
                                  n_g_rings=4.5)
S = cm1.get_smatrix(wavelengths=wavelengths)
T3 = np.abs(S['sensing_ring_in1', 'sensing_ring_out1'])

Then we extract the ring lengths from this design and create a new model, overriding the ring_lengths with the saved values. In this way, we are sure that our second simulation uses exactly the same ring resonator.

ring_lengths_cm1 = cm1.ring_lengths
vernier_filter = VernierFilterSensor()
cm2 = vernier_filter.CircuitModel(n_eff_sensing_ring=2.4 + delta_neff,
                                  n_eff_reference_ring=2.4,
                                  n_g_rings=4.5,
                                  ring_lengths=ring_lengths_cm1)
S = cm2.get_smatrix(wavelengths=wavelengths)
T4 = np.abs(S['sensing_ring_in1', 'sensing_ring_out1'])

Finally, we can compare the obtained spectra (T1, T2 for the vernier filter and T3, T4 for the single ring resonators) on the graphs below:

sphinx_gallery_thumbnail_number = 3

plt.figure()
plt.plot(wavelengths, T1, label='Transmission T1')
plt.plot(wavelengths, T2, label='Transmission T2')
plt.xlabel("Wavelengths ($\mu$ m)")
plt.ylabel("Transmission (power)")
plt.legend()
plt.show()

plt.figure()
plt.plot(wavelengths, T3, label='Transmission T3')
plt.plot(wavelengths, T4, label='Transmission T4')
plt.xlabel("Wavelengths ($\mu$ m)")
plt.ylabel("Transmission (power)")
plt.legend()
plt.show()
  • ../_images/sphx_glr_plot_vernier_filter_003.png
  • ../_images/sphx_glr_plot_vernier_filter_004.png

For the layout, we define a LayoutFromModel which extracts its ring lengths from the caphe model, and use that to export to a GDSII file. Since we re-use the logic inside PlaceAndAutoRoute, we only need to define the two child cells (sensing ring and reference ring)

[ClaesOE2010](1, 2)
  1. Claes, W. Bogaerts, P. Bienstman, “Experimental characterization of a silicon photonic biosensor consisting of two cascaded ring resonators based on the Vernier-effect and introduction of a curve fitting method for an improved detection limit”, Optics Express 18(22), pp. 22747-22761 (2010).