# 3. Parametric splitter tree¶

In the previous section, we have seen how to create a circuit, specifically a two-level splitter tree with three splitters. As an exercise, you will have tried to create a three-level splitter tree. Defining all the child cells, connections and place specifications manually every time can be tiresome and time-consuming. In this section, the goal is to create a parametric circuit, i.e. a circuit where the number of levels can easily be changed as parameter, without needing to redefine a new and increasingly complicated PCell every time. It also has other parameters that describe the geometry of the circuit, such as the distance between the MMIs. If defined correctly, changing these parameters will adapt the circuit simulation along with the layout.

## 3.1. Class SplitterTree¶

As before, the splitter tree is based on the CircuitCell class. The difference from the previous example is that here we are going to use for loops to create a circuit with a variable number of components and levels.

class SplitterTree(CircuitCell):

_name_prefix = "SPLITTER_TREE"
splitter = i3.ChildCellProperty(doc="Splitter used")
levels = i3.IntProperty(default=3, doc="Number of levels")
spacing_x = i3.PositiveNumberProperty(default=150.0, doc="Horizontal spacing between the levels")
spacing_y = i3.PositiveNumberProperty(default=50.0, doc="Vertical spacing between the MMIs in the last level")

def _default_splitter(self):
return pdk.MMI1x2Optimized()

def _default_child_cells(self):
child_cells = dict()
n_levels = self.levels
for lev in range(n_levels):
n_splitters = int(2 ** lev)  # Number of splitters per level
for sp in range(n_splitters):
child_cells["sp_{}_{}".format(lev, sp)] = self.splitter
return child_cells

def _default_connectors(self):
conn = []
n_levels = self.levels
for lev in range(1, n_levels):
n_splitters = int(2 ** lev)  # Number of splitters per level
for sp in range(n_splitters):
if sp % 2 == 0:
in_port = "sp_{}_{}:out1".format(lev - 1, int(sp / 2.0))
else:
in_port = "sp_{}_{}:out2".format(lev - 1, int(sp / 2.0))
out_port = "sp_{}_{}:in1".format(lev, sp)
return conn

def _default_place_specs(self):
place_specs = []
n_levels = self.levels
spacing_x = self.spacing_x
spacing_y = self.spacing_y
for lev in range(n_levels):
n_splitters = int(2 ** lev)  # Number of splitters per level
y_0 = - 0.5 * spacing_y * 2**(n_levels - 1)
for sp in range(n_splitters):
x_coord = lev * spacing_x
y_coord = y_0 + (sp + 0.5) * spacing_y * 2**(n_levels - lev - 1)
place_specs.append(
i3.Place("sp_{}_{}".format(lev, sp), (x_coord, y_coord))
)
return place_specs

def _default_external_port_names(self):
epn = {"sp_{}_{}:in1".format(0, 0): "in"}
cnt = 1
lev = self.levels - 1
n_splitters = int(2 ** lev)  # Number of splitters per level
for sp in range(n_splitters):
epn["sp_{}_{}:out1".format(lev, sp)] = "out{}".format(cnt)
cnt += 1
epn["sp_{}_{}:out2".format(lev, sp)] = "out{}".format(cnt)
cnt += 1
return epn


Let’s analyze this piece of code:

• Properties. These are the parameters that can be changed when instantiating the splitter tree. Each property has a default value which is used when no value is assigned by the user.
• Child cells. The method _default_child_cells is used to define the child cells that are needed in the splitter tree. The logic behind the naming of each child cell is the same as that used for the splitter tree with two levels shown in the previous section. However, here we perform the same operation using a for loop instead of manually listing each child cell.
• Connectors. The method _default_connectors defines the connections between the input and the output ports of the splitters. Also, here this list of tuples is defined iteratively.
• Place specs. The layout transformations of the splitters are defined in _default_place_specs. Once again, this is done iteratively to adapt to the number of levels given as a parameter.
• External port names. The method _default_external_port_names is used to expose the ports that we want to access once the circuit is completed. This is useful to expose only the ports that need to be routed on the upper hierarchical level as external ports, and to rename them.

## 3.2. Instantiating and simulating the parametric splitter¶

Now, we can instantiate the splitter tree and visualize the circuit simulation. You can open this file with PyCharm on your computer and play around with the number of levels to see how the layout and circuit simulation change automatically.

    # 1. Instantiate the splitter
splitter = pdk.MMI1x2Optimized()
splitter_cm = splitter.CircuitModel()

# 2. Instantiate the splitter tree with the desired splitter and number of levels
my_circuit = SplitterTree(levels=4, splitter=splitter)
my_circuit_lv = my_circuit.Layout()
my_circuit_cm = my_circuit.CircuitModel()
my_circuit_lv.visualize(annotate=True)
my_circuit_lv.write_gdsii("splitter_tree.gds")

# 3. Simulate the splitter and the circuit
wavelengths = np.linspace(1.5, 1.6, 501)
S_splitter = splitter_cm.get_smatrix(wavelengths=wavelengths)  # S-matrix of the splitter
S_total = my_circuit_cm.get_smatrix(wavelengths=wavelengths)  # S-matrix of the totalev circuit
Ss = [S_splitter, S_total]
names = ["Single splitter", "Splitter tree with {} levels".format(my_circuit.levels)]
in_names = ["in1", "in"]
nports = [2, 2 ** (my_circuit.levels)]

# 4. Plot the transmission
for S, name, in_name, nport in zip(Ss, names, in_names, nports):
fig = plt.figure()
for p in range(nport):
plt.plot(wavelengths, 10 * np.log10(np.abs(S["out{}".format(p + 1), in_name]) ** 2), '-', linewidth=2.2,
label="out{}".format(p + 1))
plt.plot(wavelengths, 10 * np.log10(np.abs(S[in_name, in_name]) ** 2), '-', linewidth=2.2,
label="in")
plt.ylim(-70, 0)
plt.xlabel('Wavelength [um]', fontsize=16)
plt.ylabel('Transmission [dB]', fontsize=16)
plt.title("{} - Transmission".format(name), fontsize=16)
plt.legend(fontsize=14, loc='lower center', ncol=6, columnspacing=0.5, handletextpad=0.2)
plt.tight_layout()
plt.show()   ## 3.3. Adding fiber grating couplers¶

Another advantage of the parametric splitter tree is that we can automate the placement and connection of the fiber grating couplers to the circuit. To learn how to do this, we advise you to follow the following tutorial: Optical phased array (OPA).