Frequency-domain circuit simulation

This guide serves as a basic introduction into frequency-domain photonic simulations. If you are new to circuit simulations, we advise you to first read the introductory chapter on circuit simulation.

First, we shortly describe the black box model of an optical component, and how these are combined to form a photonic circuit. Then we explain how this is implemented into Caphe, and how frequency-domain simulations are performed.

Frequency-domain models

In Caphe, frequency models are based on the so-called scattering matrix. The principle of the scattering matrix is best illustrated using a simple example of a photonic waveguide.

Waveguides

Fig. 82 Scattering matrix representation of a photonic waveguide.

In the figure above, we illustrate a photonic waveguide. Although its frequency behavior can be simulated using physical solvers, the behavior of a waveguide can essentially be caught in a much simpler model if we assume that the light enters and exits the waveguide through 2 terms (term 1 and term 2) and just one mode. At each term the mode-wave can enter and exit the waveguide. This means that per term-mode pair we have one input wave and one ouput wave. The key of building a scatter matrix is to define the amplitude-phase relationship between the ouput waves and input waves.

For our waveguide we have the following relations:

\[\begin{split}B_{out} = A_{in} \exp(j \frac{2 \pi}{\lambda} n_{eff} L_{wg} )\\ A_{out} = B_{in} \exp(j \frac{2 \pi}{\lambda} n_{eff} L_{wg} )\\\end{split}\]

This translates to the following scattering matrix for our waveguide:

\[\begin{split}\begin{array}{cccc} \underbrace{\left(\begin{array}{c} A_{out}\\ B_{out} \end{array}\right)} & = & \underbrace{\left(\begin{array}{cc} 0 & \exp(j\frac{2\pi}{\lambda}n_{eff}L_{wg})\\ \exp(j\frac{2\pi}{\lambda}n_{eff}L_{wg}) & 0 \end{array}\right)} & \underbrace{\left(\begin{array}{c} A_{in}\\ B_{in} \end{array}\right)}_{\mbox{}}\\ \mbox{Out waves} & & \mbox{S-matrix} & \mbox{Inwaves} \end{array}\end{split}\]

More generally, the behavior of any linear, component can be described using its scattering matrix (S-matrix). Just as for our waveguide, the scattering matrix defines the amplitude and phase relationship between input wave in each term and each mode, and the output wave in each term and each mode. It is therefore an (n x m, n x m) square matrix as illustrated below:

Scatter matrix

Fig. 83 An optical structure as a black box with several terms.

For more details about the properties of the scatter matrix, we refer to scatter matrix properties.

A photonic circuit consists out of several of these structures, which are then linked together. We return to the illustration from the circuit introduction guide, to show how such a circuit could look like:

Mach-Zehnder Interferometer (MZI)

Fig. 84 A Mach-Zehnder Interferometer (taken from the circuit introduction chapter).

Each of the building blocks in this circuit (i.e., the splitter, arms, and combiner) contains a scatter matrix (which can be equation-based, or extracted from a physical simulation or an actual measurement).

With this information, one can then perform a simulation in Caphe in order to retrieve the optical spectrum (in this case, from the MZI filter). Although shown for a very simple example, this concept works for any kind of circuit with any kind of topology. This means the components can have an arbitrary number of ports, the circuit can contain feedback loops, and the components may have reflections at several of it’s ports. Caphe can efficiently handle all these kind of topologies.

Implementation in Caphe

As explained in the circuit simulation guide, Caphe needs to know two things: the component connectivity (through the netlist), and the component behavior (through a compact model). Both views are described below. We use the directional coupler (one of the building blocks from the MZI shown above) as an example to explain these concepts.

Netlist view

First, we described the netlist of the directional coupler. It has four ports as shown below:

Directional coupler with four ports (in1, in2, out1, out2)

Fig. 85 A directional coupler with four ports.

The corresponding netlist only has to define the terms. This is done as follows (see also /samples/ipkiss3/circuit):

from ipkiss import all as ia

class DirectionalCoupler(i3.PCell):
    length = i3.PositiveNumberProperty(default=2.0, doc="Length of the directional coupler")

    class Netlist(i3.NetlistView):
        def _generate_terms(self, terms):
            terms += i3.OpticalTerm(name="in1")
            terms += i3.OpticalTerm(name="in2")
            terms += i3.OpticalTerm(name="out1")
            terms += i3.OpticalTerm(name="out2")
            return terms

Although this netlist only contains the terms, a netlist of a more complex circuit would also contain instances and nets, which are defined using a _generate_terms and _generate_nets function. For a more detailed guide about netlists, please see the netlist guide.

Frequency domain model

As explained above, the behavior of a linear, passive component can be described using its scatter matrix. Currently, there are two ways to define the S matrix on a PCell:

  • Using the CapheSModelView: currently the preferred way to specify the scatter matrix model. It achieves high simulation speeds through on-the-fly compilation.
  • Using the SMatrix view: this has a clean syntax but will currently give rise to a simulation speed slow-down (due to the inability to currently compile this part to C++).

The CapheSModelView

In this section we describe the CapheSModelView. The actual scatter elements are defined through the _calculate_S(self, environment, term1, term2, mode1, mode2) function. The result depends on the parameters given by the function, i.e. the environment (which contains the wavelength parameter), the terms, and modes, of the component.

class DirectionalCoupler(i3.PCell):

  class CapheModel(i3.CapheSModelView):
      delta_n_eff = i3.PositiveNumberProperty(default=0.01, doc="Difference of effective index between the two supermodes")

      def _calculate_S(self, environment, term1, term2, mode1, mode2):
          # calculate your S parameters here
          # This function returns a complex-valued number
          # based on the environment, terms, and modes.

The function _calculate_S is called for each data point in your sweep (in most cases, this is a set of wavelength points), and for each (term1, term2, mode1, mode2) combination. This means that the function is called many times, and hence it’s important to write this function efficiently. Long calculations in this routine can slow down the simulation considerably. Because this is such an important function for the circuit simulation, it can be compiled to C++ code. For more information about this, please check the guide about Caphe code optimization.

Note

If you want to optimize code, make sure you are using valid properties such as int, float, complex numbers and numpy arrays. This is also explained in Caphe code optimization.

The circuit solver

The PCell can be embedded in a simulation object, called CapheCircuitSolver. This solver automatically creates a testbench, by adding probes to each port in the top-level PCell, and creates a solver object that is used to perform different types of simulations (i.e., calulating the spectra from/to each port in the overall PCell or performing a time-domain simulation). This is illustrated, again for the case of a directional coupler:

Directional coupler in a testbench with four probes (s1, s2, s3, s4).

Fig. 86 Directional coupler (DUT = Device Under Test) in a testbench with four probes (s1, s2, s3, s4).

Below we provide a code snippet to demonstrate how to instantiate the coupler and the CapheModel:

from ipkiss import all as ia
import numpy as np
from pylab import plt

# see previous section for the definition of our DirectionalCoupler.

dc1 = dircoup.DirectionalCoupler(length=100.0)
caphemodel = dc1.CapheModel(delta_n_eff=0.2)

Note

The netlist has no properties, and a default netlist will be created automatically when it is needed. Hence, we do not explicitly instantiate it.

A frequency sweep can be performed by first creating a simulation engine, i3.CapheFrequencyEngine. From this engine, we can create an SMatrixSimulation, and pass it the CapheModel and the wavelengths of interest. Then, you can run a simulation and retrieve the results from the simulation monitors (the default monitor has the name s_matrix):

 wavelengths = np.arange(1.50, 1.60, 0.001)

# First create an engine object
 engine = i3.CapheFrequencyEngine()

 # 6. Using the engine, create a simulation object which contains the caphemodel and wavelengths of interest
 my_simulation = engine.SMatrixSimulation(model=caphemodel,
                                          wavelengths=wavelengths)

 # 7. Run the simulation and retrieve the results
 my_simulation.run()
 trans = my_simulation.monitors['s_matrix']

 plt.figure(figsize=(6, 4))
 plt.title("Directional coupler freq. sweep")
 plt.xlabel("Wavelength ($\mu m$)")
 plt.ylabel("Power transmission")
 plt.plot(wavelengths, np.abs(trans['in1', 'out1'])**2, label='Bar-coupling')
 plt.plot(wavelengths, np.abs(trans['in1', 'out2'])**2, label='Cross-coupling')
 plt.ylim([0, 1])
 plt.legend()
 plt.show()

This will plot the transmission from the input port ‘in1’ to the output port ‘out1’:

Directional coupler frequency sweep

Fig. 87 Directional coupler in a testbench with four probes (s1, s2, s3, s4).

Hierarchical circuits

Main use: Use of CapheModelFromNetlist

Hierarchical PCells are cells that contain several child cells (which also contain views). In order to be able to simulate a circuit that contains several PCells Caphe needs to know three things:

  • What are the instantiated child cells? –> This is defined in the Netlist.
  • How are the instantiated child cells interconnected? –> This is defined in the Netlist.
  • Which Caphemodel to use in each of the instantiated cells? –> This is defined in the CapheModelView of the child cells.

The instances of childcells and their interconnection is defined in the Netlist View of the parent PCell as illustrated in the code excerpt below where two child cells are connected together.

class Parent(i3.PCell):

    child1 = i3.ChildCellProperty()
    child2 = i3.ChildCellProperty()


    class Netlist(i3.NetlistView):

        def _generate_terms(self, terms):
            terms += i3.OpticalTerm(name="In")
            terms += i3.OpticalTerm(name="Out")
            return terms

        def _generate_instances(self, insts):
            insts += i3.Instance(reference=self.child1, name='child1')
            insts += i3.Instance(reference=self.child2, name='child2')
            return insts

        def _generate_nets(self, nets):
            nets += i3.OpticalLink(term1=self.terms['In'], term2=self.instances['child1'].terms['In'], name="link_in")
            nets += i3.OpticalLink(term1=self.terms['Out'], term2=self.instances['child2'].terms['Out'], name="link_out")
            nets += i3.OpticalLink(term1=self.instances['child1'].terms['In'], term2=self.instances['child2'].terms['Out'], name="link_out")
            return nets

Inside this Netlist view, the following additional methods were used:

  • _generate_instances(self, insts): used to define the instances of the child cells that are placed in the network, and
  • _generate_nets(self, nets): used to define the interconnection betweeen the childcells and the terms of the parent cell.

Again, a more complete description of netlists can be found in the netlist guide.

In order to simulate these, each of the child cells should contain a caphe model. The parent PCell can then tell Caphe that the model of the parent cell is built from the interconnected instances as defined in the netlist of the cell. This is done by using the CapheModelFromNetlist class that extracts the required information from the netlist.

Caution

When using CapheModelFromNetlist, make sure that each Child PCell that is instantiated in the Netlist view, has a CapheModel view.

class Parent(i3.PCell):

    child1 = i3.ChildCellProperty()
    child2 = i3.ChildCellProperty()


    class Netlist(i3.NetlistView):

        def _generate_terms(self, terms):
            terms += i3.OpticalTerm(name="In")
            terms += i3.OpticalTerm(name="Out")
            return terms

        def _generate_instances(self, insts):
            insts += i3.Instance(reference=self.child1, name='child1')
            insts += i3.Instance(reference=self.child2, name='child2')
            return insts

        def _generate_nets(self, nets):
            nets += i3.OpticalLink(term1=self.terms['In'], term2=self.instances['child1'].terms['In'], name="link_in")
            nets += i3.OpticalLink(term1=self.terms['Out'], term2=self.instances['child2'].terms['Out'], name="link_out")
            nets += i3.OpticalLink(term1=self.instances['child1'].terms['In'], term2=self.instances['child2'].terms['Out'], name="link_out")
            return nets


   class CapheModel(i3.CapheModelFromNetlist):
       pass

The CapheModel of the Parent now instructs to the solver that the simulation uses the CapheModel views from the instances, as defined in the netlist.

Advanced use: Overuling the default caphemodelview of a child

In most cases the use of CapheModelFromNetlist is all you need to perform simulation of hierarchical circuits. However in some cases, you might want more control on the caphemodels used for each of the instances defined in the Netlist view. This is possible by overruling the default caphe model of the child cells.

class Parent(i3.PCell):

    child1 = i3.ChildCellProperty()
    child2 = i3.ChildCellProperty()

    class Netlist(i3.NetlistView):

        def _generate_terms(self, terms):
          # ...
            return terms

        def _generate_instances(self, insts):
            insts += i3.Instance(reference=self.child1, name='child1')
            insts += i3.Instance(reference=self.child2, name='child2')
            return insts

        def _generate_nets(self, nets):
          #...

   class CapheModel(i3.CapheModelFromNetlist):

       # returns the caphemodelview of child1
       def _default_child1(self):
          return self.cell.child1.views['caphemodelview2']

By using the _default_child1 method, we overruled the caphemodel of child1 that is going to be used in the hierarchical caphe simulation (now, caphemodelview2 is used instead of the default CapheModel View of child1). For more information on view overruling within a hierarchy, we refer to the hierarchy guide.

Advanced use: Overuling the Netlist

It may be possible that your component has several netlist views. By default CapheModelFromNetlist uses the default Netlist View of the parent cell but you can overrule that by overruling the property netlist_view that is defined by CapheModelFromNetlist as shown in the code excerpt below.

class Parent(i3.PCell):

    # ...

    # First Netlist View
    class Netlist(i3.NetlistView):

      #...
    # Second Netlist View
    class Netlist2(i3.NetlistView):

      #...

   class CapheModel(i3.CapheModelFromNetlist):

      # Setting the netlist_view used by CapheModelFromNetlist
      def _default_netlist_view(self):
         return self.cell.views['netlist2']

Final remarks

In this guide, we explained the basic concepts of a scatter matrix, and how these concepts are used in IPKISS/Caphe.

  • If you want to start a more hands-on approach with Caphe simulations, please try our tutorials on Caphe: simple-caphe
  • If yoy want to read more on netlists: netlist
  • If you want to read more on hiearchy: hierarchy
  • If you want to read more on default views: pcell