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 circuit simulation
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.
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:
This translates to the following scattering matrix for our waveguide:
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:
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:
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.
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.
First, we described the netlist of the directional coupler. It has four ports as shown below:
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_nets function. For a more detailed guide about netlists, please see the
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++).
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.
_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.
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 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:
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)
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
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’:
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.
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.
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
For more information on view overruling within a hierarchy, we refer to the hierarchy guide.
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
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']
In this guide, we explained the basic concepts of a scatter matrix, and how these concepts are used in IPKISS/Caphe.
- IPKISS Reference
- Parametric Cells
- The Layout View
- Hierarchical PCells
- Caphe introduction
- Frequency-domain circuit simulation
- Compiling circuit models in Caphe
- Picazzo Library Reference
- IPKISS.eda user manual
- Indices and tables