Source code for dphox.prefab.active

from collections import defaultdict
from dataclasses import dataclass, field
from typing import Tuple, List, Union

import numpy as np

from ..device import Device, Via
from ..foundry import CommonLayer
from .passive import DC, RibDevice, TSplitter
from ..pattern import Box, MEMSFlexure, Pattern, Port
from ..parametric import straight
from ..transform import GDSTransform
from ..utils import fix_dataclass_init_docs


[docs]@fix_dataclass_init_docs @dataclass class ThermalPS(Device): """Thermal phase shifter (e.g. TiN phase shifter). Attributes: waveguide: Waveguide under the phase shifter ps_w: Phase shifter width via: Via to connect heater to the top metal layer ridge: Waveguide layer ps_layer: Phase shifter layer (e.g. TiN) """ waveguide: Pattern ps_w: float via: Via ridge: str = CommonLayer.RIDGE_SI heater: str = CommonLayer.HEATER name: str = "thermal_ps" def __post_init__(self): ps = self.waveguide.curve.coalesce().path(self.ps_w) left_via = self.via.copy.align(self.waveguide.port['a0'].xy) right_via = self.via.copy.align(self.waveguide.port['b0'].xy) super(ThermalPS, self).__init__( self.name, [(self.waveguide, self.ridge), (ps, self.heater)] + left_via.pattern_to_layer + right_via.pattern_to_layer ) self.port = self.waveguide.port self.ps = ps self.port['gnd'] = Port(self.waveguide.port['a0'].x, 0, -180) self.port['pos'] = Port(self.waveguide.port['b0'].x, 0) self.wg_path = self.layer_to_polys[self.ridge]
[docs]@fix_dataclass_init_docs @dataclass class PullOutNemsActuator(Device): """Pull out NEMS actuator moves material `away` from the core waveguide to change the phase. Attributes: pos_pad: Electrode box flexure: Connector box dope_expand_tuple: Dope expand tuple. This first applies expand, then grow operation on pos pad. For the crab-leg flexure, this applies a grow operation that is the sum of the two tuple elements ridge: Waveguide layer (usually silicon) actuator_dope: Actuator dope setting (set lower than pos_pad dope if possible to improve spring mechanics) pos_pad_dope: Electrode dope setting """ pos_pad: Box pad_sep: float flexure: MEMSFlexure connector: Box via: Via dope_expand_tuple: Tuple[float, float] = (0, 0) ridge: str = CommonLayer.RIDGE_SI actuator_dope: str = CommonLayer.P_SI pos_pad_dope: str = CommonLayer.PPP_SI name: str = "pull_out_actuator" def __post_init__(self): dope_total_offset = self.dope_expand_tuple[0] + self.dope_expand_tuple[1] pos_pad = self.pos_pad.copy.vstack(self.flexure, bottom=True).translate(dy=self.pad_sep) connectors = [ (self.connector.copy.vstack(self.flexure).halign(self.flexure.box, left=True), self.ridge), (self.connector.copy.vstack(self.flexure).halign(self.flexure.box, left=False), self.ridge) ] dopes = [ (pos_pad.copy.expand(self.dope_expand_tuple[0]).buffer(self.dope_expand_tuple[1]), self.pos_pad_dope), (self.flexure.copy.buffer(dope_total_offset), self.actuator_dope), ] via = self.via.copy.align(pos_pad.center) super(PullOutNemsActuator, self).__init__( self.name, dopes + connectors + [(pos_pad, self.ridge), (self.flexure, self.ridge)] + via.pattern_to_layer ) self.translate(dy=-self.bounds[1])
[docs]@fix_dataclass_init_docs @dataclass class PullInNemsActuator(Device): """Pull in NEMS actuator moves material `toward` the core waveguide to change the phase. Actually, this class is really not an actuator, but it provides the electrode that produces the actuation. However, in terms of constructing the phase shifter object, it serves the same purpose as the pull-out variety in terms of functionality. Attributes: pos_pad: Pad for the positive terminal for the NEMS actuation connector: Connector box for connecting the device dope_expand_tuple: Dope expand tuple (first applies expand, then grow operation on ground pad) ridge: Waveguide layer (usually silicon) dope: Electrode dope setting """ pos_pad: Box connector: Box via: Via dope_expand_tuple: Tuple[float, float] = (0, 0) ridge: str = CommonLayer.RIDGE_SI dopes: str = CommonLayer.PPP_SI name: str = "pull_in_actuator" def __post_init__(self): via = self.via.align(self.pos_pad.center) connectors = [ (self.connector.copy.halign(self.pos_pad, left=True).valign(self.pos_pad, bottom=False, opposite=True), self.ridge), (self.connector.copy.halign(self.pos_pad, left=False).valign(self.pos_pad, bottom=False, opposite=True), self.ridge) ] dopes = [ (self.pos_pad.copy.expand(self.dope_expand_tuple[0]).buffer(self.dope_expand_tuple[1]), self.dopes) ] super(PullInNemsActuator, self).__init__( self.name, connectors + dopes + [(self.pos_pad, self.ridge)] + via.pattern_to_layer )
[docs]@fix_dataclass_init_docs @dataclass class GndAnchorWaveguide(Device): """Ground anchor waveguide device useful for connecting a waveguide to the ground plane. Attributes: rib_waveguide: Transition waveguide for connecting to ground pads gnd_pad: Ground pad for ultimately connecting the waveguide to the ground plane gnd_connector: Ground connector for connecting the anchor waveguide to the ground pad :code:`gnd_pad`. offset_into_rib: Offset of the ground connector into the rib. dope_expand_tuple: Dope expand tuple (first applies expand, then grow operation on ground pad) ridge: Waveguide layer (usually silicon) gnd_pad_dope: Electrode dope setting """ rib_waveguide: RibDevice gnd_pad: Box gnd_connector: Box via: Via offset_into_rib: float dope_expand_tuple: Tuple[float, float] = (0, 0) ridge: str = CommonLayer.RIDGE_SI gnd_pad_dope: str = CommonLayer.PPP_SI name: str = "gnd_anchor_waveguide" def __post_init__(self): gnd_connectors = [ self.gnd_connector.copy.vstack(self.rib_waveguide.slab_waveguide).translate(dy=self.offset_into_rib), self.gnd_connector.copy.vstack(self.rib_waveguide.slab_waveguide, bottom=True).translate(dy=-self.offset_into_rib), ] gnd_pads = [ self.gnd_pad.copy.vstack(gnd_connectors[0]), self.gnd_pad.copy.vstack(gnd_connectors[1], bottom=True) ] vias = self.via.copy.align(gnd_pads[0]).pattern_to_layer + self.via.copy.align(gnd_pads[1]).pattern_to_layer dopes = [ (gnd_pads[0].expand(self.dope_expand_tuple[0]).buffer(self.dope_expand_tuple[1]), self.gnd_pad_dope), (gnd_pads[1].expand(self.dope_expand_tuple[0]).buffer(self.dope_expand_tuple[1]), self.gnd_pad_dope), (gnd_connectors[0].expand(self.dope_expand_tuple[0]).buffer(self.dope_expand_tuple[1]), self.gnd_pad_dope), (gnd_connectors[1].expand(self.dope_expand_tuple[0]).buffer(self.dope_expand_tuple[1]), self.gnd_pad_dope), ] pattern_to_layer = [(p, self.ridge) for p in gnd_connectors + gnd_pads] super(GndAnchorWaveguide, self).__init__( self.name, pattern_to_layer + dopes + vias + self.rib_waveguide.pattern_to_layer ) self.port = { 'e0': gnd_pads[0].port['e'].copy, 'e1': gnd_pads[1].port['e'].copy, 'w0': gnd_pads[0].port['w'].copy, 'w1': gnd_pads[1].port['w'].copy, 'n0': gnd_pads[0].port['n'].copy, 'n1': gnd_pads[1].port['n'].copy, 's0': gnd_pads[0].port['s'].copy, 's1': gnd_pads[1].port['s'].copy, 'a0': self.rib_waveguide.port['a0'].copy, 'b0': self.rib_waveguide.port['b0'].copy }
[docs]@fix_dataclass_init_docs @dataclass class Clearout(Device): """Clearout device which is generally useful for sacrificial etches in MEMS devices. Attributes: clearout_etch: THe clearout etch box, which selectively removes silicon dioxide. clearout_etch_stop_grow: The etch stop offset factor beyond the size of the clearout etch box clearout_layer: The clearout layer clearout_etch_stop_layer: The clearout etch stop layer (this can vary more than the clearout layer). """ clearout_etch: Box clearout_etch_stop_grow: float clearout_layer: str = CommonLayer.CLEAROUT clearout_etch_stop_layer: str = CommonLayer.ALUMINA name: str = "clearout" def __post_init__(self): super(Clearout, self).__init__("clearout", [(self.clearout_etch, self.clearout_layer), (self.clearout_etch.buffer(self.clearout_etch_stop_grow), self.clearout_etch_stop_layer)])
[docs]@fix_dataclass_init_docs @dataclass class LateralNemsPS(Device): """Lateral NEMS phase shifter, which is actuated by the specified :code:`actuator` (pull-in or -out). Attributes: phase_shifter_waveguide: Phase shifter waveguide (including the nanofins) gnd_anchor_waveguide: Ground anchor waveguide connecting the waveguide to ground. clearout: Clearout of the oxide cladding material for the MEMS actuation. actuator: Actuator (pull-out or pull-in) for controlling the phase shift. ridge: Ridge silicon waveguide. trace_w: Trace width for the metal routing. clearout_pos_sep: Clearout trace separation for the pos terminal avoiding metal accidental etching (HF nasty). clearout_gnd_sep: Clearout trace separation for the gnd terminal avoiding metal accidental etching (HF nasty). pos_metal_layer: Positive electrode metal layer. gnd_metal_layer: Ground electrode metal layer. """ waveguide_w: float phase_shifter_waveguide: Pattern gnd_anchor_waveguide: GndAnchorWaveguide clearout: Clearout actuator: Union[PullInNemsActuator, PullOutNemsActuator] trace_w: float clearout_pos_sep: float clearout_gnd_sep: float ridge: str = CommonLayer.RIDGE_SI pos_metal_layer: str = CommonLayer.METAL_1 gnd_metal_layer: str = CommonLayer.METAL_2 name: str = "lateral_nems_ps" def __post_init__(self): psw = self.phase_shifter_waveguide.copy top_actuator = self.actuator.copy.to(Port(psw.center[0], psw.bounds[3], 0)) bottom_actuator = self.actuator.copy.to(Port(psw.center[0], psw.bounds[1], -180)) clearout = self.clearout.copy.align(psw) left_gnd_waveguide = self.gnd_anchor_waveguide.copy.to(psw.port['a0']) right_gnd_waveguide = self.gnd_anchor_waveguide.copy.to(psw.port['b0']) pos_metal = Box((self.clearout.size[0] + self.clearout_pos_sep, top_actuator.bounds[3] - bottom_actuator.bounds[1])).hollow(self.trace_w).align(clearout.center) gnd_metal = Box((right_gnd_waveguide.port['e0'].x - left_gnd_waveguide.port['e0'].x, self.gnd_anchor_waveguide.size[1] + self.clearout_gnd_sep)).cup(self.trace_w).halign( left_gnd_waveguide.port['e0'].x).valign(left_gnd_waveguide.port['e1'].y) super(LateralNemsPS, self).__init__(self.name, [(psw, self.ridge), (pos_metal, self.pos_metal_layer), (gnd_metal, self.gnd_metal_layer), top_actuator, bottom_actuator, clearout, left_gnd_waveguide, right_gnd_waveguide]) self.merge_patterns() self.port = { 'a0': left_gnd_waveguide.port['b0'], 'b0': right_gnd_waveguide.port['b0'] } self.port['a0'].w = self.port['b0'].w = self.waveguide_w self.wg_path = self.phase_shifter_waveguide
PathComponent = Union[Pattern, Device, "MultilayerPath", float]
[docs]@fix_dataclass_init_docs @dataclass class MultilayerPath(Device): """Multilayer path for appending a linear sequence of elements end-to-end Attributes: waveguide_w: Waveguide width. sequence: Sequence of :code:`Device`, :code:`Pattern` or :code:`float`s. The :code:`float` corresponds to straight waveguides of such lengths and waveguide width :code:`waveguide_w`). path_layer: Pattern layer. name: Name of the device. """ waveguide_w: float sequence: List[PathComponent] path_layer: str name: str = 'multilayer_path' start_port: Port = Port() def __post_init__(self): waveguided_patterns = [] if not self.sequence or np.isscalar(self.sequence[0]): port = self.start_port else: start = self.sequence[0].port['a0'] port = Port(*start.xy, start.a - 180) child_to_device = {} child_to_ports = defaultdict(list) for p in self.sequence: if p is not None: d = p.copy if isinstance(p, (Device, Pattern)) else straight(p).path(self.waveguide_w) if isinstance(d, Device) and d.child_to_device: child_to_device[d.name] = d child_to_ports[d.name].append(port.copy) port = d.dummy_port_pattern.to(port, 'a0').port['b0'].copy else: waveguided_patterns.append(d.to(port, 'a0')) port = d.port['b0'].copy pattern_to_layer = sum(([(p, self.path_layer)] if isinstance(p, Pattern) else [p] for p in waveguided_patterns), []) super(MultilayerPath, self).__init__(self.name, pattern_to_layer) for child in child_to_device: self.place(child_to_device[child], child_to_ports[child], 'a0') self.port['a0'] = waveguided_patterns[0].port['a0'].copy if waveguided_patterns else Port(*port.xy, 180) self.port['b0'] = port self.x_length = self.port['b0'].x - self.port['a0'].x
[docs] def extend(self, path: Union[float, Pattern]): """Extend this multilayer path using a path in the :code:`path_layer`. Args: path: The path to add (either a float for straight segment or a path pattern) Returns: The updated multilayer path. """ self.sequence.append(path) path = straight(path).path(self.waveguide_w) if np.isscalar(path) else path segment = path.to(self.port['b0']) self.add(segment, self.path_layer) self.port['b0'] = segment.port['b0'] return self
[docs]@fix_dataclass_init_docs @dataclass class MZI(Device): """An MZI with multilayer devices in the arms (e.g., phase shifters and/or grating taps). Note: This class assumes that the arms begin and end along the horizontal (:math:`x`-axis). Attributes: coupler: Directional coupler or MMI for MZI ridge: Waveguide layer string top_internal: Top center arm (waveguide matching bottom arm length if None) bottom_internal: Bottom center arm (waveguide matching top arm length if None) top_external: Top input (waveguide matching bottom arm length if None) bottom_external: Bottom input (waveguide matching top arm length if None) name: Name of the MZI """ coupler: DC ridge: str = CommonLayer.RIDGE_SI top_internal: List[PathComponent] = field(default_factory=list) bottom_internal: List[PathComponent] = field(default_factory=list) top_external: List[PathComponent] = field(default_factory=list) bottom_external: List[PathComponent] = field(default_factory=list) name: str = "mzi" def __post_init__(self): dc_device = self.coupler.device(self.ridge) w = self.coupler.waveguide_w self.top_input = MultilayerPath(w, self.top_external, self.ridge, name=f'{self.name}_top_input') self.bottom_input = MultilayerPath(w, self.bottom_external, self.ridge, name=f'{self.name}_bottom_input') self.top_arm = MultilayerPath(w, self.top_internal, self.ridge, name=f'{self.name}_top_arm') self.bottom_arm = MultilayerPath(w, self.bottom_internal, self.ridge, name=f'{self.name}_bottom_arm') arm_length_diff = self.top_arm.x_length - self.bottom_arm.x_length if self.top_arm.x_length > self.bottom_arm.x_length: self.bottom_arm.extend(arm_length_diff) else: self.top_arm.extend(arm_length_diff) super(MZI, self).__init__(self.name) self.place(self.bottom_input, Port(), 'a0') self.place(self.top_input, Port(0, self.coupler.interport_distance), 'a0') dc_port = dc_device.dummy_port_pattern.to(self.bottom_input.port['b0'], 'a0').port self.place(dc_device, self.top_input.port['b0']) self.place(self.bottom_arm, dc_port['b0'], 'a0') self.place(self.top_arm, dc_port['b1'], 'a0') bottom_arm_port = self.bottom_arm.dummy_port_pattern.to(dc_port['b0'], 'a0').port['b0'] self.place(dc_device, bottom_arm_port, 'a0') self.init_coupler = dc_device self.final_coupler = dc_device.copy.to(bottom_arm_port, 'a0') self.port = { 'a0': Port(0, 0, 180, w=w), 'a1': Port(0, self.coupler.interport_distance, 180, w=w), 'b0': self.final_coupler.port['b0'].copy, 'b1': self.final_coupler.port['b1'].copy } self.interport_distance = self.coupler.interport_distance self.waveguide_w = self.coupler.waveguide_w self.input_length = self.top_input.x_length self.arm_length = self.top_arm.x_length self.full_length = self.port['b0'].x - self.port['a0'].x
[docs] def path(self, flip: bool = False): first = self.init_coupler.lower_path.copy.reflect() if flip else self.init_coupler.lower_path second = self.final_coupler.lower_path.copy.reflect() if flip else self.final_coupler.lower_path return MultilayerPath( waveguide_w=self.coupler.waveguide_w, sequence=[self.bottom_input.copy, first.copy, self.bottom_arm.copy, second.copy], path_layer=self.ridge )
[docs]@fix_dataclass_init_docs @dataclass class LocalMesh(Device): """A locally interacting rectangular mesh, or triangular mesh if specified. Note: triangular meshes can self-configure, but rectangular meshes cannot. Attributes: mzi: The :code:`MZI` object, which acts as the unit cell for the mesh. n: The number of inputs and outputs for the mesh. triangular: Triangular mesh, otherwise rectangular mesh name: Name of the device """ mzi: MZI n: int triangular: bool = True name: str = 'mesh' def __post_init__(self): mzi = self.mzi n = self.n triangular = self.triangular num_straight = (n - 1) - np.hstack([np.arange(1, n), np.arange(n - 2, 0, -1)]) - 1 if triangular \ else np.tile((0, 1), n // 2)[:n] n_layers = 2 * n - 3 if triangular else n self.upper_path = mzi.path(flip=False) self.lower_path = mzi.path(flip=True) self.mzi_out = mzi.bottom_input.copy self.upper_path.name = 'upper_mzi_path' self.lower_path.name = 'lower_mzi_path' self.mzi_out.name = 'mzi_out' self.upper_transforms = [] self.lower_transforms = [] self.out_transforms = [] self.flip_array = np.zeros((n, n_layers)) for idx in range(n): # waveguides for layer in range(n_layers): flip = idx == n - 1 or (idx - layer % 2 < n and idx > num_straight[layer]) and (idx + layer) % 2 if flip: self.lower_transforms.append((layer * mzi.full_length, idx * mzi.interport_distance)) self.flip_array[idx, layer] = 1 else: self.upper_transforms.append((layer * mzi.full_length, idx * mzi.interport_distance)) self.out_transforms.append((n_layers * mzi.full_length, idx * mzi.interport_distance)) super(LocalMesh, self).__init__(self.name) self.place(self.upper_path, np.array(self.upper_transforms)) self.place(self.lower_path, np.array(self.lower_transforms)) self.place(self.mzi_out, np.array(self.out_transforms)) self.port = { **{f'a{i}': Port(0, i * mzi.interport_distance, 180, mzi.waveguide_w) for i in range(n)}, **{f'b{i}': Port(n_layers * mzi.full_length + mzi.input_length, i * mzi.interport_distance, 0, mzi.waveguide_w) for i in range(n)} } self.interport_distance = mzi.interport_distance self.waveguide_w = self.mzi.waveguide_w self.n_layers = n_layers
[docs] def demo_polys(self, ps_w_factor: float = 4) -> Tuple[np.ndarray, np.ndarray]: """Demo polygons, useful for plotting stuff, using only the polygons in the silicon layer. Note: This method is generally useless unless used for demo purposes and will be deleted once a cleaner solution is found. Args: ps_w_factor: phase shifter width factor Returns: A numpy array consisting of lists of polygons (note: NOT numbers) """ geoms = [] lower_polys = self.lower_path.full_layer_to_polys[self.mzi.ridge] upper_polys = self.upper_path.full_layer_to_polys[self.mzi.ridge] out_polys = self.mzi_out.full_layer_to_polys[self.mzi.ridge] transformed_lower_polys = GDSTransform.parse(np.array(self.lower_transforms))[0].transform_geoms(lower_polys) transformed_upper_polys = GDSTransform.parse(np.array(self.upper_transforms))[0].transform_geoms(upper_polys) transformed_out_polys = GDSTransform.parse(np.array(self.out_transforms))[0].transform_geoms(out_polys) idx_upper = idx_lower = idx_out = 0 for idx in range(self.n): path = [] for layer in range(self.n_layers): if self.flip_array[idx, layer] == 1: path.extend([p[idx_lower] for p in transformed_lower_polys]) idx_lower += 1 else: path.extend([p[idx_upper] for p in transformed_upper_polys]) idx_upper += 1 path.extend([p[idx_out] for p in transformed_out_polys]) idx_out += 1 geoms.append(path) # TODO: this is a temporary hack to slice the path for the simplest mesh sizes = [0, 2] + [4] * (2 * self.n_layers - 1) + [3] slices = np.cumsum(sizes, dtype=int) path_array = np.array( [[geoms[i][slices[s]:slices[s + 1]] for s in range(len(slices) - 1)] for i in range(self.n)], dtype=object) ps_array = np.array([Pattern(geoms[i][s * 4]).scale(1, ps_w_factor).points.T for i in range(self.n) for s in range(len(slices) - 1)]) return path_array, ps_array
[docs] def demo_3d_arrays(self, ps_w_factor: float = 4, height=0.22, sep=0.22): from trimesh.creation import extrude_polygon import trimesh path_array, ps_array = self.demo_polys(ps_w_factor) ps_array = ps_array.reshape((*path_array.shape, 4, 2)) def _shapely_to_mesh_from_step(_geom, translation): _meshes = [extrude_polygon(_poly, height=height) for _poly in _geom.geoms] _mesh = trimesh.util.concatenate(_meshes) if _meshes else trimesh.Trimesh() return _mesh.apply_translation((0, 0, translation)) path_3d_array = [] ps_3d_array = [] for i in range(6): path_row = [] ps_row = [] for j in range(19): path_row.append(_shapely_to_mesh_from_step(Pattern(path_array[i, j]).shapely, 0)) ps_row.append(_shapely_to_mesh_from_step(Pattern(ps_array[i, j].T).shapely, sep + height)) path_3d_array.append(path_row) ps_3d_array.append(ps_row) return path_3d_array, ps_3d_array
[docs]@fix_dataclass_init_docs @dataclass class HTree(Device): """An H-tree for optical phased array and transceiver applications. Args: splitter: Splitter for the h tree (t splitter) depth: Depth of the H-tree pitch: Pitch of the final H-tree devices root_dx: Optional user-calculated value to adjust waveguide length for the root of the h-tree leaf_dx: Optional user-calculated value to adjust waveguide length for the leaves of the h-tree This could be based on distances calculated in final device to maintain pitch. wg_layer: Waveguide layer for the H-tree photonic routing and splitting top_level: Whether this layer should be considered the top level of the tree (useful for labelling ports) Returns: The H-tree with devices attached if provided. """ splitter: Device depth: int pitch: float root_dx: float = 0 leaf_dx: float = 0 name: str = "htree" wg_layer: str = CommonLayer.RIDGE_SI top_level: bool = True def __post_init__(self): if self.top_level: self.splitter = Device.from_pattern(self.splitter, "tsplitter", self.wg_layer) \ if isinstance(self.splitter, TSplitter) else self.splitter self.name = f"htree_{self.depth}" if self.name is None else self.name super(HTree, self).__init__(self.name) waveguide_w = self.splitter.port['a0'].w silver_ratio = 1 / np.sqrt(2) if self.depth % 2 else 1 silver_length = self.pitch * 2 ** (self.depth / 2 - 1) * silver_ratio segment_length = silver_length - self.splitter.size[0] - self.splitter.size[1] / 2 + waveguide_w / 2 + self.root_dx if self.depth == 0: segment_length += self.splitter.size[0] + self.leaf_dx - waveguide_w / 2 segment = straight(segment_length).path(waveguide_w) self.add(segment, self.wg_layer) self.port['a0'] = segment.port['a0'] if self.depth > 0: splitter_port = self.place(self.splitter, segment.port['b0'], from_port='a0', return_ports=True) top_htree = HTree(self.splitter, self.depth - 1, self.pitch, 0, self.leaf_dx, f"{self.name}_{self.depth - 1}", wg_layer=self.wg_layer, top_level=False) bottom_htree = HTree(self.splitter, self.depth - 1, self.pitch, 0, self.leaf_dx, f"{self.name}_{self.depth - 1}", wg_layer=self.wg_layer, top_level=False) self.place(top_htree, splitter_port['b0'], from_port='a0') self.place(bottom_htree, splitter_port['b1'], from_port='a0') if self.top_level: b = self.bounds[-2:] - np.array((0, waveguide_w / 2)) self.port.update({f'b_{i}_{j}': Port(b[0] - i * self.pitch * 2, b[1] - j * self.pitch, w=waveguide_w) for i in range(int(2 ** np.ceil(self.depth / 2 - 1))) for j in range(int(2 ** np.floor(self.depth / 2)))}) dxy = (self.pitch + 2 * self.leaf_dx) * np.array((1 - self.depth % 2, self.depth % 2)) self.port.update( {f't_{i}_{j}': Port(b[0] - i * self.pitch * 2 - dxy[0], b[1] - j * self.pitch - dxy[1], 180, waveguide_w) for i in range(int(2 ** np.ceil(self.depth / 2 - 1))) for j in range(int(2 ** np.floor(self.depth / 2)))})
[docs] def fill_ports(self, device: Device): self.place(device, [port for pname, port in self.port.items() if 'b' in pname]) self.place(device, [port for pname, port in self.port.items() if 't' in pname], flip_y=True)