Advanced topics in circuit modeling

Model building guidelines

Below we summarize some guidelines you can use to write efficient models.

  • Use simple parameter types in the calculation methods (calculate_smatrix, calculate_signals and calculate_dydt)

    The following are considered simple parameter types: integers, floats, complex numbers, and numpy arrays. In most cases, circuit models can be described as a function of simple parameters, this allows the model to be recompiled by the simulator into a more efficient form. In case you need more flexibility, or need to debug, the model will need to run in Python mode (see enabling Python mode).

  • Preload stored data.

    Instead of loading the data during the calculation methods, make sure to load them upfront. Compare the following code:

    import numpy as np
    
    class MyModel(i3.CompactModel):
        parameters = ['length']
        terms = [
           i3.OpticalTerm(name='in'),
           i3.OpticalTerm(name='out')
        ]
    
        def calculate_smatrix(parameters, env, S):
            n_eff_data = np.loadtxt('n_eff_tabulated_data.csv')
            idx = np.argmin(np.abs(n_eff_data[:, 0] - env.wavelength))
            n_eff = n_eff_data[idx, 1]
            S['in', 'out'] = S['out', 'in'] = np.exp(1j * 2 * np.pi * parameters.length / env.wavelength * n_eff)
    

    Instead, use:

    import numpy as np
    
    class MyModel(i3.CompactModel):
        parameters = ['length', 'n_eff_data']
        terms = [
           i3.OpticalTerm(name='in'),
           i3.OpticalTerm(name='out')
        ]
    
        def calculate_smatrix(parameters, env, S):
            idx = np.argmin(np.abs(parameters.n_eff_data[:, 0] - env.wavelength))
            n_eff = parameters.n_eff_data[idx, 1]
            S['in', 'out'] = S['out', 'in'] = np.exp(1j * 2 * np.pi * parameters.length / env.wavelength * n_eff)
    

    Now n_eff_data can be passed onto the model. The important difference is that now the data is only loaded once. n_eff_data is a 2D data array, so it can be a property of the model. The CircuitModelView can then organize the passing of the variables, as such:

    import numpy as np
    
    class CircuitModel(i3.CircuitModelView):
        filename = i3.StringProperty(default='n_eff_tabulated_data.csv')
        length = i3.NonNegativeNumberProperty(default=10)
        n_eff_data = i3.NumpyArrayProperty(doc="Effective index, 2D array of (wavelengths, n_eff)")
    
        def _default_n_eff_data(self):
            return np.loadtxt(self.filename)
    
        def _generate_model(self):
            return NeffModel(length=self.length, n_eff_data=self.n_eff_data)
    

    To improve the interpolation we can implement a simple linear interpolation scheme:

    idx = np.argmin(np.abs(n_eff_data[:, 0] - env.wavelength))
    if n_eff_data[idx, 0] > env.wavelength:
        idx = idx - 1
    frac = (env.wavelength - n_eff_data[idx, 0]) / (n_eff_data[idx + 1, 0] - n_eff_data[idx, 0])
    n_eff = frac * n_eff_data[idx, 1] + (1 - frac) * n_eff_data[idx + 1, 1]
    

    Download the full example here.

Debugging Models

By default, models are compiled to make them very efficient at runtime. Especially in timedomain simulations this can result in a significant performance boost. Though there are some restrictions in the way models can be written.

Sometimes compilation gets in the way because you can’t debug your code just as you would with normal python code. To circumvent this you can run your simulations in ‘debug’ mode, your code will run slower but you can use your debugger to debug your code (see for example how to debug in PyCharm).

To enable debug mode just pass the debug=True keyword argument when invoking the simulation:


  wavelengths = np.linspace(1.54, 1.56, 1001)
  wg = MyWaveguide()
  wg_cm = wg.CircuitModel()
  S = wg_cm.get_smatrix(wavelengths, debug=True)
  

Python Models

In some cases, the restrictions imposed by the compiler limit you so much that you can’t implement the model you desire. In those cases you can fall back to pure python models.

to enable python mode for a model:

class MyPythonModel(i3.CompactModel):
      _model_type = 'python'
  
      terms = [
        i3.OpticalTerm(name='in'),
        i3.OpticalTerm(name='out'),
      ]
  
      def calculate_smatrix(parameters, env, S):
          # strings are not supported (yet)
          S['in', 'out'] = len("abcde") * 0.1
  

Writing a netlist manually

In most use cases when you write a netlist, it’s sufficient to start from ConnectComponents or any of its inherited classes (see placement and routing).

Sometimes you want more control over the netlist, or you want to start from an empty PCell to build your components. For these use cases, we explain how to build your netlist manually. It consists of two steps:

  1. Write the netlist in the NetlistView.
  2. Inside the CircuitModelView, the model should be a i3.HierarchicalModel. To reuse the netlist defined in the NetlistView, you can use the class method from_netlistview(netlist).

Here’s the MZI from the tutorial rewritten in this syntax:

class MyMZI(i3.PCell):
    arm1 = i3.ChildCellProperty(locked=True)
    arm2 = i3.ChildCellProperty(locked=True)
    splitter = i3.ChildCellProperty(locked=True)
    combiner = i3.ChildCellProperty(locked=True)

    def _default_arm1(self):
        return MyWaveguide(name=self.name + '_arm1')

    def _default_arm2(self):
        return MyWaveguide(name=self.name + '_arm2')

    def _default_splitter(self):
        return MyDC(name=self.name + '_splitter')

    def _default_combiner(self):
        return MyDC(name=self.name + '_combiner')

    class Netlist(i3.NetlistView):

        def _generate_netlist(self, nl):
            nl += i3.OpticalTerm(name='in1')
            nl += i3.OpticalTerm(name='in2')
            nl += i3.OpticalTerm(name='out1')
            nl += i3.OpticalTerm(name='out2')
            nl += i3.Instance(name='arm2', reference=self.arm2)
            nl += i3.Instance(name='arm1', reference=self.arm1)
            nl += i3.Instance(name='splitter', reference=self.splitter)
            nl += i3.Instance(name='combiner', reference=self.combiner)
            nl.link('splitter:in1', 'in1')
            nl.link('splitter:in2', 'in2')
            nl.link('combiner:out1', 'out1')
            nl.link('combiner:out2', 'out2')
            nl.link('splitter:out1', 'arm1:in')
            nl.link('splitter:out2', 'arm2:in')
            nl.link('combiner:in1', 'arm1:out')
            nl.link('combiner:in2', 'arm2:out')

            return nl

    class CircuitModel(i3.CircuitModelView):
        delay_difference = i3.PositiveNumberProperty(default=100.)

        def _default_arm1(self):
            arm1_model = self.cell.arm1.get_default_view(i3.CircuitModelView)
            arm1_model.set(length=100, use_py=True)
            return arm1_model

        def _default_arm2(self):
            arm2_model = self.cell.arm2.get_default_view(i3.CircuitModelView)
            arm2_model.set(length=100 + self.delay_difference)
            return arm2_model

        def _generate_model(self):
            return i3.HierarchicalModel.from_netlistview(self.netlist_view)

Download the full example here.