167 lines
8.0 KiB
Python
167 lines
8.0 KiB
Python
"""Optical splitting tree composite layouts."""
|
|
|
|
from typing import Any
|
|
import nazca as nd
|
|
import numpy as np
|
|
import nazca.interconnects as IC
|
|
|
|
from ..geometry import *
|
|
from ..routing import *
|
|
|
|
from ..primitives.pic import *
|
|
|
|
## Class for splitting tree
|
|
class SplittingTree():
|
|
"""Binary splitter tree built from repeated Y-branch cells.
|
|
|
|
Parameters
|
|
----------
|
|
ybranch : Any, optional
|
|
Y-branch object with a ``cell`` attribute and ``a1``, ``b1``, and
|
|
``b2`` pins. If omitted, a simple box-based Y-branch is generated.
|
|
output_number : int, optional
|
|
Number of output channels. Values are coerced to the nearest lower
|
|
power of two when needed.
|
|
bend_radius : int, optional
|
|
Bend radius used to route between splitter levels.
|
|
output_pitch : Any, optional
|
|
Output channel pitch in microns. If omitted, the Y-branch output
|
|
pitch is used.
|
|
show_pins : bool, optional
|
|
Show Nazca pin stubs in the generated layout.
|
|
|
|
Attributes
|
|
----------
|
|
cell : nazca.Cell
|
|
Generated splitting tree layout cell.
|
|
"""
|
|
|
|
def __init__(self, ybranch: Any=None, output_number: int=16, bend_radius: int=10, output_pitch: Any=None, show_pins: bool=False) -> None:
|
|
if ybranch == None:
|
|
self._generate_default_ybranch_()
|
|
else:
|
|
self.yb_cell = ybranch.cell
|
|
|
|
|
|
# self.yb_length = ybranch.length
|
|
## revised in 2026.06.07 by Qin Yue
|
|
# legacy: self.yb_length = np.abs(self.yb_cell.pin['a1'].x - self.yb_cell.pin['b1'].x)
|
|
self.yb_length = np.abs(self.yb_cell.pin['opt_a1'].x - self.yb_cell.pin['opt_b1'].x)
|
|
# self.yb_width = ybranch.width
|
|
## revised in 2026.06.07 by Qin Yue
|
|
# legacy: self.yb_width = np.abs(self.yb_cell.pin['b1'].y - self.yb_cell.pin['b2'].y)
|
|
self.yb_width = np.abs(self.yb_cell.pin['opt_b1'].y - self.yb_cell.pin['opt_b2'].y)
|
|
|
|
self.w_wg = ybranch.w_wg
|
|
self.output_number = output_number
|
|
self.bend_radius = bend_radius
|
|
if output_pitch == None:
|
|
self.output_pitch = self.yb_width
|
|
else:
|
|
self.output_pitch = output_pitch
|
|
self.show_pins = show_pins
|
|
|
|
self.cell = self.generate_gds()
|
|
|
|
def generate_gds(self):
|
|
'''
|
|
Generate the gds of splitting tree.
|
|
'''
|
|
## initialize the parameters of splitting tree
|
|
self.level_number = np.log2(self.output_number)
|
|
if self.level_number != int(np.log2(self.output_number)):
|
|
print("WARNNING:: Please check the output number of your generated splitting tree, which is not 2^N. ")
|
|
self.level_number = int(self.level_number)
|
|
self.output_number = np.power(2, self.level_number)
|
|
## Generate the splitting tree
|
|
with nd.Cell(name="SplittingTree_N"+str(self.output_number), instantiate=False) as C:
|
|
stripe_class = Route(xs="strip", width=self.w_wg, radius=self.bend_radius)
|
|
cell_dic = {}
|
|
cell_dic['00'] = self.yb_cell.put('a0', 0, 0)
|
|
x_cur = 0
|
|
for level_index in range(1, int(self.level_number)):
|
|
# Calculate the x location for current level
|
|
y_pitch = self.output_pitch * np.power(2, self.level_number-level_index)
|
|
if self.bend_radius > (y_pitch/2-self.yb_width/2)/2:
|
|
x_cur = x_cur + self.yb_length + 2*np.sqrt(np.power(self.bend_radius,2)-np.power(self.bend_radius-(y_pitch/2)/2, 2)) + 2
|
|
else:
|
|
x_cur = x_cur + self.yb_length + self.bend_radius*2 + 2
|
|
for yb_index in range(np.power(2, level_index)):
|
|
y_cur = y_pitch * ((np.power(2, level_index)-1)/2 - yb_index)
|
|
# Put the Y-branch
|
|
## revised in 2026.06.07 by Qin Yue
|
|
# legacy: cell_dic[str(level_index)+str(yb_index)] = self.yb_cell.put('a1', x_cur, y_cur)
|
|
cell_dic[str(level_index)+str(yb_index)] = self.yb_cell.put('opt_a1', x_cur, y_cur)
|
|
# Do the routing
|
|
stripe_class.sbend_p2p(
|
|
## revised in 2026.06.07 by Qin Yue
|
|
# legacy: pin1=cell_dic[str(level_index-1)+str(yb_index//2)].pin['b'+str(yb_index%2+1)],
|
|
pin1=cell_dic[str(level_index-1)+str(yb_index//2)].pin['opt_b'+str(yb_index%2+1)],
|
|
## revised in 2026.06.07 by Qin Yue
|
|
# legacy: pin2=cell_dic[str(level_index)+str(yb_index)].pin['a1'],
|
|
pin2=cell_dic[str(level_index)+str(yb_index)].pin['opt_a1'],
|
|
Lstart=1,
|
|
arrow=False
|
|
).put()
|
|
## Put pins
|
|
nd.Pin(name="a0").put(0, 0, 180)
|
|
## revised in 2026.06.07 by Qin Yue
|
|
# legacy: nd.Pin(name="a1",width=self.w_wg).put(0, 0, 180)
|
|
nd.Pin(name="opt_a1",width=self.w_wg,type="optical:").put(0, 0, 180)
|
|
level_index = int(self.level_number-1)
|
|
for yb_index in range(np.power(2, level_index)):
|
|
nd.Pin(
|
|
## revised in 2026.06.07 by Qin Yue
|
|
# legacy: name="b"+str(2*yb_index+1),
|
|
name="opt_b"+str(2*yb_index+1),
|
|
width=self.w_wg,
|
|
type="optical:"
|
|
## revised in 2026.06.07 by Qin Yue
|
|
# legacy: ).put(cell_dic[str(level_index)+str(yb_index)].pin['b1'])
|
|
).put(cell_dic[str(level_index)+str(yb_index)].pin['opt_b1'])
|
|
nd.Pin(
|
|
## revised in 2026.06.07 by Qin Yue
|
|
# legacy: name="b"+str(2*yb_index+2),
|
|
name="opt_b"+str(2*yb_index+2),
|
|
width=self.w_wg,
|
|
type="optical:"
|
|
## revised in 2026.06.07 by Qin Yue
|
|
# legacy: ).put(cell_dic[str(level_index)+str(yb_index)].pin['b2'])
|
|
).put(cell_dic[str(level_index)+str(yb_index)].pin['opt_b2'])
|
|
nd.Pin(name="b0").put(
|
|
## revised in 2026.06.07 by Qin Yue
|
|
# legacy: cell_dic[str(level_index)+str(yb_index)].pin['b2'].x,
|
|
cell_dic[str(level_index)+str(yb_index)].pin['opt_b2'].x,
|
|
0,
|
|
0
|
|
)
|
|
if self.show_pins:
|
|
nd.put_stub()
|
|
return C
|
|
|
|
def _generate_default_ybranch_(self, length=28, width=2, w_wg=0.45):
|
|
'''
|
|
Generate a ybranch built by box for quick showing.
|
|
'''
|
|
self.yb_length = length
|
|
self.yb_width = width
|
|
self.w_wg = w_wg
|
|
with nd.Cell(name="Ybranch", instantiate=False) as C:
|
|
## Put block to show the size of the device and the location of input&output waveguide
|
|
nd.strt(length=self.yb_length, width=self.yb_width+2, layer=(1001, 1)).put(0, 0)
|
|
nd.strt(length=0.2, width=self.w_wg, layer=(1001, 2)).put(0, 0)
|
|
nd.strt(length=0.2, width=self.w_wg, layer=(1001, 2)).put(self.yb_length, self.yb_width/2, 180)
|
|
nd.strt(length=0.2, width=self.w_wg, layer=(1001, 2)).put(self.yb_length, -self.yb_width/2, 180)
|
|
## Put pins
|
|
nd.Pin(name="a0", width=self.w_wg).put(0, 0, 180)
|
|
## revised in 2026.06.07 by Qin Yue
|
|
# legacy: nd.Pin(name="a1", width=self.w_wg).put(0, 0, 180)
|
|
nd.Pin(name="opt_a1", width=self.w_wg,type="optical:").put(0, 0, 180)
|
|
## revised in 2026.06.07 by Qin Yue
|
|
# legacy: nd.Pin(name="b1", width=self.w_wg).put(self.yb_length, self.yb_width/2, 0)
|
|
nd.Pin(name="opt_b1", width=self.w_wg,type="optical:").put(self.yb_length, self.yb_width/2, 0)
|
|
## revised in 2026.06.07 by Qin Yue
|
|
# legacy: nd.Pin(name="b2", width=self.w_wg).put(self.yb_length, -self.yb_width/2, 0)
|
|
nd.Pin(name="opt_b2", width=self.w_wg,type="optical:").put(self.yb_length, -self.yb_width/2, 0)
|
|
self.yb_cell = C
|