def add_port( self, name: Optional[Union[str, int, object]] = None, midpoint: Tuple[float, float] = ( 0.0, 0.0, ), width: float = 1.0, orientation: int = 45, port: Optional[Port] = None, layer: Tuple[int, int] = (1, 0), port_type: str = "optical", cross_section: Optional[CrossSection] = None, ) -> Port: """Can be called to copy an existing port like add_port(port = existing_port) or to create a new port add_port(myname, mymidpoint, mywidth, myorientation). Can also be called to copy an existing port with a new name add_port(port = existing_port, name = new_name)""" if port: if not isinstance(port, Port): raise ValueError(f"add_port() needs a Port, got {type(port)}") p = port.copy(new_uid=True) if name is not None: p.name = name p.parent = self elif isinstance(name, Port): p = name.copy(new_uid=True) p.parent = self name = p.name else: half_width = width / 2 half_width_correct = snap_to_grid(half_width, nm=1) if not np.isclose(half_width, half_width_correct): warnings.warn( f"port width = {width} will create off-grid points.\n" f"You can fix it by changing width to {2*half_width_correct}\n" f"port {name}, {midpoint} {orientation} deg", stacklevel=3, ) p = Port( name=name, midpoint=(snap_to_grid(midpoint[0]), snap_to_grid(midpoint[1])), width=snap_to_grid(width), orientation=orientation, parent=self, layer=layer, port_type=port_type, cross_section=cross_section, ) if name is not None: p.name = name if p.name in self.ports: raise ValueError( f"add_port() Port name {p.name} exists in {self.name}") self.ports[p.name] = p return p
def get_instance_name( component: Component, reference: ComponentReference, layer_label: Tuple[int, int] = LAYER.LABEL_INSTANCE, ) -> str: """Takes component names from instance XY location or label in layer_label Loop over references and find the reference under and associate reference with instance label map instance names to references Check if it has a instance name label and return the instance name from the label Args: component: with labels reference: reference that needs naming layer_label: ignores layer_label[1] """ x = snap_to_grid(reference.x) y = snap_to_grid(reference.y) labels = component.labels # default instance name follows componetName_x_y text = clean_name(f"{reference.parent.name}_{x}_{y}") # text = f"{reference.parent.name}_X{int(x)}_Y{int(y)}" # text = f"{reference.parent.name}_{reference.uid}" # try to get the instance name from a label for label in labels: xl = snap_to_grid(label.position[0]) yl = snap_to_grid(label.position[1]) if x == xl and y == yl and label.layer == layer_label[0]: # print(label.text, xl, yl, x, y) return label.text return text
def assert_on_grid(self, nm: int = 1) -> None: """Ensures ports edges are on grid to avoid snap_to_grid errors.""" half_width = self.width / 2 half_width_correct = snap_to_grid(half_width, nm=nm) component_name = self.parent.name if not np.isclose(half_width, half_width_correct): raise PortNotOnGridError( f"{component_name}, port = {self.name}, midpoint = {self.midpoint} width = {self.width} will create off-grid points", f"you can fix it by changing width to {2*half_width_correct}", ) if self.port_type.startswith("vertical"): return if self.orientation in [0, 180]: x = self.y + self.width / 2 if not np.isclose(snap_to_grid(x, nm=nm), x): raise PortNotOnGridError( f"{self.name} port in {component_name} has an off-grid point {x}", f"you can fix it by moving the point to {snap_to_grid(x, nm=nm)}", ) elif self.orientation in [90, 270]: x = self.x + self.width / 2 if not np.isclose(snap_to_grid(x, nm=nm), x): raise PortNotOnGridError( f"{self.name} port in {component_name} has an off-grid point {x}", f"you can fix it by moving the point to {snap_to_grid(x, nm=nm)}", ) else: raise PortOrientationError( f"{component_name} port {self.name} has invalid orientation" f" {self.orientation}")
def get_instance_name( component: Component, reference: ComponentReference, layer_label: Tuple[int, int] = LAYER.LABEL_INSTANCE, ) -> str: """Returns the instance name from the label. If no label returns to instanceName_x_y Args: component: with labels reference: reference that needs naming layer_label: ignores layer_label[1] """ x = snap_to_grid(reference.x) y = snap_to_grid(reference.y) labels = component.labels # default instance name follows componetName_x_y text = clean_name(f"{reference.parent.name}_{x}_{y}") # text = f"{reference.parent.name}_X{int(x)}_Y{int(y)}" # text = f"{reference.parent.name}_{reference.uid}" # try to get the instance name from a label for label in labels: xl = snap_to_grid(label.position[0]) yl = snap_to_grid(label.position[1]) if x == xl and y == yl and label.layer == layer_label[0]: # print(label.text, xl, yl, x, y) return label.text return text
def add_ports_from_markers_square( component: Component, pin_layer: Layer = (69, 0), port_layer: Optional[Layer] = None, orientation: Optional[int] = 90, min_pin_area_um2: float = 0, max_pin_area_um2: float = 150 * 150, pin_extra_width: float = 0.0, port_names: Optional[Tuple[str, ...]] = None, port_name_prefix: str = "o", ) -> Component: """add ports from markers center in port_layer squared Args: component: to read polygons from and to write ports to pin_layer: for port markers port_layer: for the new created port orientation: in degrees 90: north, 0: east, 180: west, 270: south min_pin_area_um2: ignores pins with area smaller than min_pin_area_um2 max_pin_area_um2: ignore pins for area above certain size pin_extra_width: 2*offset from pin to straight port_names: names of the ports (defaults to {i}) """ port_markers = read_port_markers(component, [pin_layer]) port_names = port_names or [ f"{port_name_prefix}{i+1}" for i in range(len(port_markers.polygons)) ] layer = port_layer or pin_layer for port_name, p in zip(port_names, port_markers.polygons): dy = snap_to_grid(p.ymax - p.ymin) dx = snap_to_grid(p.xmax - p.xmin) x = p.x y = p.y if dx == dy and max_pin_area_um2 > dx * dy > min_pin_area_um2: component.add_port( port_name, midpoint=(x, y), width=dx - pin_extra_width, orientation=orientation, layer=layer, ) return component
def get_ports_xsize(self, **kwargs) -> float: """Returns a the xdistance from east to west ports Args: kwargs: orientation, port_type, layer """ ports_cw = self.get_ports_list(clockwise=True, **kwargs) ports_ccw = self.get_ports_list(clockwise=False, **kwargs) return snap_to_grid(ports_ccw[0].x - ports_cw[0].x)
def clean_value(value: Any) -> str: """returns a readable string representation.""" if isinstance(value, int): value = str(value) elif isinstance(value, (float, np.float64)): if 1 > value > 1e-3: value = f"{int(value*1e3)}n" elif int(value) == value: value = str(int(value)) elif 1e-6 < value < 1e-3: value = f"{snap_to_grid(value*1e6)}u" elif 1e-9 < value < 1e-6: value = f"{snap_to_grid(value*1e9)}n" elif 1e-12 < value < 1e-9: value = f"{snap_to_grid(value*1e12)}p" else: # Any unit < 1pm will disappear value = str(snap_to_grid(value)).replace(".", "p") elif isinstance(value, Device): value = clean_name(value.name) elif isinstance(value, str): value = value.strip() elif (isinstance(value, dict) and len(value) > 0 and not isinstance(list(value.keys())[0], str)): value = [ f"{clean_value(key)}={clean_value(value[key])}" for key in sorted(value.keys()) ] value = "_".join(value) elif isinstance(value, dict): value = dict2name(**value) # value = [f"{k}={v!r}" for k, v in value.items()] elif isinstance(value, Port): value = f"{value.name}_{value.width}_{value.x}_{value.y}" elif isinstance(value, PathPhidl): value = f"path_{hash_points(value.points)}" elif (isinstance(value, object) and hasattr(value, "name") and isinstance(value.name, str)): value = clean_name(value.name) elif callable(value) and isinstance(value, functools.partial): sig = inspect.signature(value.func) args_as_kwargs = dict(zip(sig.parameters.keys(), value.args)) args_as_kwargs.update(**value.keywords) value = value.func.__name__ + dict2name(**args_as_kwargs) elif callable(value) and isinstance(value, toolz.functoolz.Compose): value = "_".join([clean_value(v) for v in value.funcs] + [clean_value(value.first)]) elif callable(value) and hasattr(value, "__name__"): value = value.__name__ elif hasattr(value, "get_name"): value = value.get_name() elif isinstance(value, Iterable): value = "_".join(clean_value(v) for v in value) return str(value)
def bend_circular(angle: int = 90, npoints: int = 720, with_cladding_box: bool = True, cross_section: CrossSectionOrFactory = strip, **kwargs) -> Component: """Returns a radial arc. Args: angle: angle of arc (degrees) npoints: number of points with_cladding_box: square in layers_cladding to remove DRC cross_section: kwargs: cross_section settings .. code:: o2 | / / / o1_____/ """ x = cross_section(**kwargs) if callable(cross_section) else cross_section radius = x.info["radius"] p = arc(radius=radius, angle=angle, npoints=npoints) c = Component() path = extrude(p, x) ref = c << path c.add_ports(ref.ports) c.info.length = snap_to_grid(p.length()) c.info.dy = float(abs(p.points[0][0] - p.points[-1][0])) c.info.radius = float(radius) if with_cladding_box and x.info["layers_cladding"]: layers_cladding = x.info["layers_cladding"] cladding_offset = x.info["cladding_offset"] top = cladding_offset if angle == 180 else 0 points = get_padding_points( component=c, default=0, bottom=cladding_offset, right=cladding_offset, top=top, ) for layer in layers_cladding or []: c.add_polygon(points, layer=layer) c.absorb(ref) return c
def bend_circular_heater( radius: float = 10, angle: int = 90, npoints: int = 720, heater_to_wg_distance: float = 1.2, heater_width: float = 0.5, layer_heater=TECH.layer.HEATER, cross_section: CrossSectionFactory = strip, ) -> Component: """Creates an arc of arclength ``theta`` starting at angle ``start_angle`` Args: radius angle: angle of arc (degrees) npoints: Number of points used per 360 degrees heater_to_wg_distance: heater_width layer_heater cross_section: """ x = cross_section() width = x.info["width"] cladding_offset = x.info["cladding_offset"] layers_cladding = x.info["layers_cladding"] or [] layer = x.info["layer"] x = gf.CrossSection() x.add(width=width, offset=0, layer=layer, ports=["in", "out"]) for layer_cladding in layers_cladding: x.add(width=width + 2 * cladding_offset, offset=0, layer=layer_cladding) offset = heater_to_wg_distance + width / 2 x.add( width=heater_width, offset=+offset, layer=layer_heater, ) x.add( width=heater_width, offset=-offset, layer=layer_heater, ) p = arc(radius=radius, angle=angle, npoints=npoints) c = extrude(p, x) c.length = snap_to_grid(p.length()) c.dx = abs(p.points[0][0] - p.points[-1][0]) c.dy = abs(p.points[0][0] - p.points[-1][0]) return c
def bend_circular(angle: int = 90, npoints: int = 720, with_cladding_box: bool = True, cross_section: CrossSectionFactory = strip, **kwargs) -> Component: """Returns a radial arc. Args: angle: angle of arc (degrees) npoints: number of points with_cladding_box: square in layers_cladding to remove DRC cross_section: **kwargs: cross_section settings .. plot:: :include-source: import gdsfactory as gf c = gf.components.bend_circular(radius=10, angle=90, npoints=720) c.plot() """ x = cross_section(**kwargs) radius = x.info["radius"] p = arc(radius=radius, angle=angle, npoints=npoints) c = extrude(p, x) c.info.length = snap_to_grid(p.length()) c.info.dy = float(abs(p.points[0][0] - p.points[-1][0])) c.info.radius_min = float(radius) if with_cladding_box and x.info["layers_cladding"]: layers_cladding = x.info["layers_cladding"] cladding_offset = x.info["cladding_offset"] top = cladding_offset if angle == 180 else 0 points = get_padding_points( component=c, default=0, bottom=cladding_offset, right=cladding_offset, top=top, ) for layer in layers_cladding or []: c.add_polygon(points, layer=layer) return c
def clean_value(value: Any) -> str: """returns more readable value (integer) if number is < 1: returns number units in nm (integer) units are in um by default. Therefore when we multiply by 1e3 we get nm. """ if isinstance(value, int): value = str(value) elif isinstance(value, (float, np.float64)): if 1 > value > 1e-3: value = f"{int(value*1e3)}n" elif float(int(value)) == value: value = str(int(value)) elif 1e-6 < value < 1e-3: value = f"{snap_to_grid(value*1e6)}u" elif 1e-9 < value < 1e-6: value = f"{snap_to_grid(value*1e9)}n" elif 1e-12 < value < 1e-9: value = f"{snap_to_grid(value*1e12)}p" else: value = str(snap_to_grid(value)).replace(".", "p") elif isinstance(value, list): value = "_".join(clean_value(v) for v in value) elif isinstance(value, tuple): value = "_".join(clean_value(v) for v in value) elif isinstance(value, dict): value = dict2name(**value) # value = [f"{k}={v!r}" for k, v in value.items()] elif isinstance(value, Device): value = clean_name(value.name) elif isinstance(value, Port): value = f"{value.name}_{value.width}_{value.x}_{value.y}" elif (isinstance(value, object) and hasattr(value, "name") and isinstance(value.name, str)): value = clean_name(value.name) elif callable(value) and hasattr(value, "__name__"): value = value.__name__ elif callable(value) and isinstance(value, functools.partial): value = value.func.__name__ + dict2name(**value.keywords) elif isinstance(value, str): value = value.strip() return str(value)
def to_dict_polygons(self) -> DictConfig: """Returns a dict representation of the flattened compoment.""" d = DictConfig({}) polygons = {} layer_to_polygons = self.get_polygons(by_spec=True) for layer, polygons_layer in layer_to_polygons.items(): for polygon in polygons_layer: layer_name = f"{layer[0]}_{layer[1]}" polygons[layer_name] = [ tuple(snap_to_grid(v)) for v in polygon ] ports = {port.name: port.settings for port in self.get_ports_list()} clean_dict(ports) clean_dict(polygons) d.info = self.info d.polygons = polygons d.ports = ports return OmegaConf.create(d)
def spiral_circular( length: float = 1e3, wg_width: float = 0.5, spacing: float = 3.0, min_bend_radius: float = 5.0, points: int = 1000, layer: Tuple[int, int] = gf.LAYER.WG, ) -> Component: """Returns a circular spiral. FIXME, has some issues Args: length: length in um wg_width: spacing: between straights min_bend_radius: points: layer: """ wg_datatype = layer[1] wg_layer = layer[0] def pol_to_rect(radii, angles_deg): angles_rad = np.radians(angles_deg) z = radii * np.exp(1.0j * angles_rad) return z.real, z.imag ps = [] # Estimate number of revolutions length_total = 0.0 i = 0 while length_total <= length: length_total += 3.0 * np.pi * (min_bend_radius * 2.0 + (i + 0.5) * spacing) i += 1 revolutions = i + 1 # Long spiral inner_revs = min_bend_radius * 4.0 / (spacing * 4.0) theta_1 = np.linspace(360.0 * inner_revs, 360.0 * (revolutions - 1) + 270, points) theta_2 = np.linspace(360.0 * inner_revs, 360.0 * revolutions, points) a = np.sqrt(spacing / 180.0) radii_1 = a**2 * theta_1 radii_2 = -(a**2) * theta_2 x_1, y_1 = pol_to_rect(radii_1, theta_1) x_1 = np.append(x_1, x_1[-1] + 0.03) y_1 = np.append(y_1, y_1[-1]) p = gds.PolyPath(np.c_[x_1, y_1], wg_width, layer=wg_layer, datatype=wg_datatype) ps.append(p) x_2, y_2 = pol_to_rect(radii_2, theta_2) x_2 = np.append(x_2, x_2[-1]) y_2 = np.append(y_2, y_2[-1] - 0.03) p = gds.PolyPath(np.c_[x_2, y_2], wg_width, layer=wg_layer, datatype=wg_datatype) ps.append(p) start_1 = (x_1[-1], y_1[-1]) start_2 = (x_2[-1], y_2[-1]) end_1 = (x_1[0], y_1[0]) end_2 = (x_2[0], y_2[0]) length_1 = np.sum(np.hypot(np.diff(x_1), np.diff(y_1))) length_2 = np.sum(np.hypot(np.diff(x_2), np.diff(y_2))) # Inner bend theta_inner = np.linspace(360.0 * inner_revs, 360.0 * inner_revs + 180.0, 50) radii_inner_1 = min_bend_radius radii_inner_2 = -min_bend_radius x_1, y_1 = pol_to_rect(radii_inner_1, theta_inner) x_1 -= end_1[0] / 2.0 y_1 -= end_1[1] / 2.0 x_1 = np.append(x_1, x_1[-1]) y_1 = np.append(y_1, y_1[-1] - 0.03) p = gds.PolyPath(np.c_[x_1, y_1], wg_width, layer=wg_layer, datatype=wg_datatype) ps.append(p) x_2, y_2 = pol_to_rect(radii_inner_2, theta_inner) x_2 -= end_2[0] / 2.0 y_2 -= end_2[1] / 2.0 x_2 = np.append(x_2, x_2[-1]) y_2 = np.append(y_2, y_2[-1] + 0.03) p = gds.PolyPath(np.c_[x_2, y_2], wg_width, layer=wg_layer, datatype=wg_datatype) ps.append(p) length_3 = np.sum(np.hypot(np.diff(x_2), np.diff(y_2))) # Output straight p, _, e = straight(wg_width, radii_1[-1], start_1, layer=wg_layer, datatype=wg_datatype) ps.append(p) # Outer bend theta_input = np.linspace(0.0, -90.0, 50) r_input = min_bend_radius * 2.0 x_input, y_input = pol_to_rect(r_input, theta_input) x_input += start_2[0] - r_input y_input += start_1[1] + r_input s = (x_input[-1], y_input[-1]) end_input = (x_input[0], y_input[0]) x_input = np.append(x_input[0], x_input) y_input = np.append(y_input[0] + 0.03, y_input) x_input = np.append(x_input, x_input[-1] - 0.03) y_input = np.append(y_input, y_input[-1]) p = gds.PolyPath(np.c_[x_input, y_input], wg_width, layer=wg_layer, datatype=wg_datatype) ps.append(p) length = start_2[1] - end_input[1] p, _, _ = straight( length, wg_width, (end_input[0] - wg_width / 2.0, end_input[1] + length / 2.0), layer=wg_layer, datatype=wg_datatype, ) ps.append(p) length = length_1 + length_2 + length_3 # Electrode ring # inner_radius = min_bend_radius * 2. # outer_radius = a**2 * theta_2[-1] # mid_radius = 0.5*(inner_radius + outer_radius) # thickness = outer_radius - mid_radius # r = gds.Round((0.,0.), outer_radius, inner_radius, layer=1, max_points=1000) ps = gds.fast_boolean(ps, None, "or") c = gf.Component() c.info.length = snap_to_grid(length) c.add_polygon(ps, layer=layer) c.add_port( name="o1", midpoint=(s[0], s[1]), orientation=180, layer=layer, width=wg_width, ) c.add_port( name="o2", midpoint=(e[0], e[1]), orientation=180, layer=layer, width=wg_width, ) return c
def straight_heater_doped_rib( length: float = 320.0, nsections: int = 3, cross_section: CrossSectionFactory = strip_rib_tip, cross_section_heater: CrossSectionFactory = rib_heater_doped, contact: Optional[ComponentFactory] = contact_slab_npp_m3, contact_metal: Optional[ComponentFactory] = contact_metal_function, contact_metal_size: Tuple[float, float] = (10.0, 10.0), contact_size: Tuple[float, float] = (10.0, 10.0), taper: Optional[ComponentOrFactory] = taper_cross_section, with_taper1: bool = True, with_taper2: bool = True, heater_width: float = 2.0, heater_gap: float = 0.8, contact_gap: float = 0.0, width: float = 0.5, with_top_contact: bool = True, with_bot_contact: bool = True, **kwargs ) -> Component: r"""Returns a doped thermal phase shifter. dimensions from https://doi.org/10.1364/OE.27.010456 Args: length: of the waveguide nsections: between contacts cross_section: for the input/output ports cross_section_heater: for the heater contact: function to connect the heated strip contact_metal: function to connect the metal area contact_metal_size: contact_size: taper: optional taper function heater_width: heater_gap: contact_gap: from edge of contact to waveguide width: waveguide width on the ridge kwargs: cross_section settings .. code:: length |<--------------------------------------------->| | length_section | | <---------------------------> | | length_contact | | <-------> taper| | _________ _________ | | | | | | | | | contact|____________________| | | | | size | heater width | | | | /|________|____________________|________|\ | | / | heater_gap | \ | |/ |______________________________________| \ | \ |_______________width__________________| / \ | | / \|_____________heater_gap______________ |/ | | | | | |____heater_width____| | | | | | |________| |________| taper cross_section_heater |<------width------>| ____________________ heater_gap slab_gap top_contact | |<---------->| bot_contact <--> ___ ______________________| |___________________________|___ | | | undoped Si | | | | |layer_heater| intrinsic region |layer_heater | | |___|____________|__________________________________________|______________|___| <------------> heater_width <------------------------------------------------------------------------------> slab_width """ c = Component() cross_section_heater = gf.partial( cross_section_heater, heater_width=heater_width, heater_gap=heater_gap, width=width, **kwargs ) if taper: taper = ( taper(cross_section1=cross_section, cross_section2=cross_section_heater) if callable(taper) else taper ) length -= taper.get_ports_xsize() * 2 wg = c << gf.c.straight( cross_section=cross_section_heater, length=snap_to_grid(length), ) if taper: if with_taper1: taper1 = c << taper taper1.connect("o2", wg.ports["o1"]) c.add_port("o1", port=taper1.ports["o1"]) else: c.add_port("o1", port=wg.ports["o1"]) if with_taper2: taper2 = c << taper taper2.mirror() taper2.connect("o2", wg.ports["o2"]) c.add_port("o2", port=taper2.ports["o1"]) else: c.add_port("o2", port=wg.ports["o2"]) else: c.add_port("o2", port=wg.ports["o2"]) c.add_port("o1", port=wg.ports["o1"]) if contact_metal: contact_section = contact_metal(size=contact_metal_size) contacts = [] length_contact = snap_to_grid(contact_size[1]) length_section = snap_to_grid((length - length_contact) / nsections) x0 = contact_size[0] / 2 for i in range(0, nsections + 1): xi = x0 + length_section * i if contact_metal and contact: contact_center = c.add_ref(contact_section) contact_center.x = xi contact_ref = c << contact_section contact_ref.x = xi contact_ref.y = ( +contact_metal_size[1] if i % 2 == 0 else -contact_metal_size[1] ) contacts.append(contact_ref) if contact: if with_top_contact: contact_top = c << contact(size=contact_size) contact_top.x = xi contact_top.ymin = +(heater_gap + width / 2 + contact_gap) if with_bot_contact: contact_bot = c << contact(size=contact_size) contact_bot.x = xi contact_bot.ymax = -(heater_gap + width / 2 + contact_gap) if contact_metal and contact: contact_length = length + contact_metal_size[0] contact_top = c << contact_metal( size=(contact_length, contact_metal_size[0]), ) contact_bot = c << contact_metal( size=(contact_length, contact_metal_size[0]), ) contact_bot.xmin = contacts[0].xmin contact_top.xmin = contacts[0].xmin contact_top.ymin = contacts[0].ymax contact_bot.ymax = contacts[1].ymin c.add_ports(contact_top.ports, prefix="top_") c.add_ports(contact_bot.ports, prefix="bot_") return c
def import_gds( gdspath: Union[str, Path], cellname: Optional[str] = None, flatten: bool = False, snap_to_grid_nm: Optional[int] = None, name: Optional[str] = None, decorator: Optional[Callable] = None, gdsdir: Optional[Union[str, Path]] = None, **kwargs, ) -> Component: """Returns a Componenent from a GDS file. Adapted from phidl/geometry.py if any cell names are found on the component CACHE we append a $ with a number to the name Args: gdspath: path of GDS file. cellname: cell of the name to import (None) imports top cell. flatten: if True returns flattened (no hierarchy) snap_to_grid_nm: snap to different nm grid (does not snap if False) name: Optional name. Over-rides the default imported name. decorator: function to apply over the imported gds. gdsdir: optional GDS directory. kwargs: settings for the imported component (polarization, wavelength ...). """ gdspath = Path(gdsdir) / Path(gdspath) if gdsdir else Path(gdspath) if not gdspath.exists(): raise FileNotFoundError(f"No file {gdspath!r} found") metadata_filepath = gdspath.with_suffix(".yml") gdsii_lib = gdspy.GdsLibrary() gdsii_lib.read_gds(str(gdspath)) top_level_cells = gdsii_lib.top_level() cellnames = [c.name for c in top_level_cells] if cellname is not None: if cellname not in gdsii_lib.cells: raise ValueError( f"cell {cellname} is not in file {gdspath} with cells {cellnames}" ) topcell = gdsii_lib.cells[cellname] elif cellname is None and len(top_level_cells) == 1: topcell = top_level_cells[0] elif cellname is None and len(top_level_cells) > 1: raise ValueError( f"import_gds() There are multiple top-level cells in {gdspath!r}, " f"you must specify `cellname` to select of one of them among {cellnames}" ) if name: if name in CACHE or name in CACHE_IMPORTED_CELLS: raise ValueError( f"name = {name!r} already on cache. " "Please, choose a different name or set name = None. ") else: topcell.name = name if flatten: component = Component(name=name or cellname or cellnames[0]) polygons = topcell.get_polygons(by_spec=True) for layer_in_gds, polys in polygons.items(): component.add_polygon(polys, layer=layer_in_gds) component = avoid_duplicated_cells(component) else: D_list = [] cell_to_device = {} for c in gdsii_lib.cells.values(): D = Component(name=c.name) D.polygons = c.polygons D.references = c.references D.name = c.name for label in c.labels: rotation = label.rotation if rotation is None: rotation = 0 label_ref = D.add_label( text=label.text, position=np.asfarray(label.position), magnification=label.magnification, rotation=rotation * 180 / np.pi, layer=(label.layer, label.texttype), ) label_ref.anchor = label.anchor D = avoid_duplicated_cells(D) D.unlock() cell_to_device.update({c: D}) D_list += [D] for D in D_list: # First convert each reference so it points to the right Device converted_references = [] for e in D.references: ref_device = cell_to_device[e.ref_cell] if isinstance(e, gdspy.CellReference): dr = DeviceReference( device=ref_device, origin=e.origin, rotation=e.rotation, magnification=e.magnification, x_reflection=e.x_reflection, ) dr.owner = D converted_references.append(dr) elif isinstance(e, gdspy.CellArray): dr = CellArray( device=ref_device, columns=e.columns, rows=e.rows, spacing=e.spacing, origin=e.origin, rotation=e.rotation, magnification=e.magnification, x_reflection=e.x_reflection, ) dr.owner = D converted_references.append(dr) D.references = converted_references # Next convert each Polygon # temp_polygons = list(D.polygons) # D.polygons = [] # for p in temp_polygons: # D.add_polygon(p) # Next convert each Polygon temp_polygons = list(D.polygons) D.polygons = [] for p in temp_polygons: if snap_to_grid_nm: points_on_grid = snap_to_grid(p.polygons[0], nm=snap_to_grid_nm) p = gdspy.Polygon(points_on_grid, layer=p.layers[0], datatype=p.datatypes[0]) D.add_polygon(p) component = cell_to_device[topcell] cast(Component, component) name = name or component.name component.name = name if metadata_filepath.exists(): logger.info(f"Read YAML metadata from {metadata_filepath}") metadata = OmegaConf.load(metadata_filepath) for port_name, port in metadata.ports.items(): if port_name not in component.ports: component.add_port( name=port_name, midpoint=port.midpoint, width=port.width, orientation=port.orientation, layer=port.layer, port_type=port.port_type, ) component.info = metadata.info component.info.update(**kwargs) component.name = name component.info.name = name if decorator: component_new = decorator(component) component = component_new or component if flatten: component.flatten() component.lock() return component
def straight_heater_doped_rib( length: float = 320.0, nsections: int = 3, cross_section: CrossSectionFactory = strip_rib_tip, cross_section_heater: CrossSectionFactory = rib_heater_doped, contact_contact: Optional[ComponentFactory] = contact_slab_npp, contact_metal: Optional[ComponentFactory] = contact_metal_function, contact_metal_size: Tuple[float, float] = (10.0, 10.0), contact_contact_size: Tuple[float, float] = (10.0, 10.0), contact_contact_yspacing: float = 2.0, port_orientation_top: int = 0, port_orientation_bot: int = 180, taper: Optional[ComponentFactory] = taper_cross_section, taper_length: float = 10.0, **kwargs, ) -> Component: r"""Returns a doped thermal phase shifter. dimensions from https://doi.org/10.1364/OE.27.010456 Args: length: of the waveguide nsections: between contacts cross_section_heater: for the heater contact_contact: function to connect the heated strip contact_metal: function to connect the metal area contact_metal_size: contact_contact_yspacing: spacing from waveguide to contact port_orientation_top: for top contact port_orientation_bot: for bottom contact kwargs: cross_section settings taper: optional taper taper_length: .. code:: length <--------------------------------------------> length_section <---------------------------> length_contact <-------> taper ______________________________________ /| |____________________| |\ __/ |viastack| | | \___ __ | | | | ___cross_section \ | size |____________________| | / \|________|____________________|________|/ taper cross_section_heater |<------width------>| ____________________ heater_gap slab_gap | |<---------->| <--> ___ ______________________| |___________________________|___ | | | undoped Si | | | | |layer_heater| intrinsic region |layer_heater | | |___|____________|__________________________________________|______________|___| <------------> heater_width <------------------------------------------------------------------------------> slab_width """ c = Component() if taper: taper = taper( cross_section1=cross_section, cross_section2=cross_section_heater, length=taper_length, **kwargs, ) length -= taper_length * 2 wg = c << gf.c.straight( cross_section=cross_section_heater, length=snap_to_grid(length), **kwargs, ) if taper: taper1 = c << taper taper2 = c << taper taper1.connect("o2", wg.ports["o1"]) taper2.connect("o2", wg.ports["o2"]) c.add_port("o1", port=taper1.ports["o1"]) c.add_port("o2", port=taper2.ports["o1"]) else: c.add_port("o1", port=wg.ports["o1"]) c.add_port("o2", port=wg.ports["o2"]) if contact_metal: contact_section = contact_metal(size=contact_metal_size) contacts = [] length_contact = snap_to_grid(contact_contact_size[1]) length_section = snap_to_grid((length - length_contact) / nsections) x0 = contact_contact_size[0] / 2 for i in range(0, nsections + 1): xi = x0 + length_section * i if contact_metal and contact_contact: contact_center = c.add_ref(contact_section) contact_center.x = xi contact = c << contact_section contact.x = xi contact.y = +contact_metal_size[ 1] if i % 2 == 0 else -contact_metal_size[1] contacts.append(contact) if contact_contact: contact_bot = c << contact_contact(size=contact_contact_size) contact_top = c << contact_contact(size=contact_contact_size) contact_top.x = xi contact_bot.x = xi contact_top.ymin = +contact_contact_yspacing contact_bot.ymax = -contact_contact_yspacing if contact_metal and contact_contact: contact_length = length + contact_metal_size[0] contact_top = c << contact_metal( size=(contact_length, contact_metal_size[0]), ) contact_bot = c << contact_metal( size=(contact_length, contact_metal_size[0]), ) contact_bot.xmin = contacts[0].xmin contact_top.xmin = contacts[0].xmin contact_top.ymin = contacts[0].ymax contact_bot.ymax = contacts[1].ymin c.add_port("e1", port=contact_top.get_ports_list( orientation=port_orientation_top)[0]) c.add_port("e2", port=contact_bot.get_ports_list( orientation=port_orientation_bot)[0]) return c
def import_gds( gdspath: Union[str, Path], cellname: Optional[str] = None, flatten: bool = False, snap_to_grid_nm: Optional[int] = None, decorator: Optional[Callable] = None, **kwargs, ) -> Component: """Returns a Componenent from a GDS file. Adapted from phidl/geometry.py Args: gdspath: path of GDS file cellname: cell of the name to import (None) imports top cell flatten: if True returns flattened (no hierarchy) snap_to_grid_nm: snap to different nm grid (does not snap if False) **kwargs """ gdspath = Path(gdspath) if not gdspath.exists(): raise FileNotFoundError(f"No file {gdspath} found") gdsii_lib = gdspy.GdsLibrary() gdsii_lib.read_gds(str(gdspath)) top_level_cells = gdsii_lib.top_level() cellnames = [c.name for c in top_level_cells] if cellname is not None: if cellname not in gdsii_lib.cells: raise ValueError( f"cell {cellname} is not in file {gdspath} with cells {cellnames}" ) topcell = gdsii_lib.cells[cellname] elif cellname is None and len(top_level_cells) == 1: topcell = top_level_cells[0] elif cellname is None and len(top_level_cells) > 1: raise ValueError( f"import_gds() There are multiple top-level cells in {gdspath}, " f"you must specify `cellname` to select of one of them among {cellnames}" ) if flatten: component = Component() polygons = topcell.get_polygons(by_spec=True) for layer_in_gds, polys in polygons.items(): component.add_polygon(polys, layer=layer_in_gds) else: D_list = [] c2dmap = {} for cell in gdsii_lib.cells.values(): D = Component(name=cell.name) D.polygons = cell.polygons D.references = cell.references D.name = cell.name for label in cell.labels: rotation = label.rotation if rotation is None: rotation = 0 label_ref = D.add_label( text=label.text, position=np.asfarray(label.position), magnification=label.magnification, rotation=rotation * 180 / np.pi, layer=(label.layer, label.texttype), ) label_ref.anchor = label.anchor c2dmap.update({cell: D}) D_list += [D] for D in D_list: # First convert each reference so it points to the right Device converted_references = [] for e in D.references: ref_device = c2dmap[e.ref_cell] if isinstance(e, gdspy.CellReference): dr = DeviceReference( device=ref_device, origin=e.origin, rotation=e.rotation, magnification=e.magnification, x_reflection=e.x_reflection, ) dr.owner = D converted_references.append(dr) elif isinstance(e, gdspy.CellArray): dr = CellArray( device=ref_device, columns=e.columns, rows=e.rows, spacing=e.spacing, origin=e.origin, rotation=e.rotation, magnification=e.magnification, x_reflection=e.x_reflection, ) dr.owner = D converted_references.append(dr) D.references = converted_references # Next convert each Polygon # temp_polygons = list(D.polygons) # D.polygons = [] # for p in temp_polygons: # D.add_polygon(p) # Next convert each Polygon temp_polygons = list(D.polygons) D.polygons = [] for p in temp_polygons: if snap_to_grid_nm: points_on_grid = snap_to_grid(p.polygons[0], nm=snap_to_grid_nm) p = gdspy.Polygon( points_on_grid, layer=p.layers[0], datatype=p.datatypes[0] ) D.add_polygon(p) component = c2dmap[topcell] cast(Component, component) for key, value in kwargs.items(): setattr(component, key, value) if decorator: decorator(component) component._autoname = False return component
def get_ports_ysize(self, **kwargs) -> float: """Returns a the ydistance from east to west ports""" ports_cw = self.get_ports_list(clockwise=True, **kwargs) ports_ccw = self.get_ports_list(clockwise=False, **kwargs) return snap_to_grid(ports_ccw[0].y - ports_cw[0].y)
def round_corners( points: Coordinates, straight: ComponentFactory = straight_function, bend: ComponentFactory = bend_euler, bend_s_factory: Optional[ComponentFactory] = bend_s, taper: Optional[ComponentFactory] = None, straight_fall_back_no_taper: Optional[ComponentFactory] = None, mirror_straight: bool = False, straight_ports: Optional[List[str]] = None, cross_section: CrossSectionFactory = strip, on_route_error: Callable = get_route_error, with_point_markers: bool = False, snap_to_grid_nm: Optional[int] = 1, **kwargs, ) -> Route: """Returns Route: - references list with rounded straight route from a list of manhattan points. - ports: Tuple of ports - length: route length (float) Args: points: manhattan route defined by waypoints bend90: the bend to use for 90Deg turns straight: the straight library to use to generate straight portions taper: taper for straight portions. If None, no tapering straight_fall_back_no_taper: in case there is no space for two tapers mirror_straight: mirror_straight waveguide straight_ports: port names for straights. If None finds them automatically. cross_section: on_route_error: function to run when route fails with_point_markers: add route points markers (easy for debugging) snap_to_grid_nm: nm to snap to grid kwargs: cross_section settings """ x = cross_section(**kwargs) points = (gf.snap.snap_to_grid(points, nm=snap_to_grid_nm) if snap_to_grid_nm else points) auto_widen = x.info.get("auto_widen", False) auto_widen_minimum_length = x.info.get("auto_widen_minimum_length", 200.0) taper_length = x.info.get("taper_length", 10.0) width = x.info.get("width", 2.0) width_wide = x.info.get("width_wide", None) references = [] bend90 = bend(cross_section=cross_section, ** kwargs) if callable(bend) else bend # bsx = bsy = _get_bend_size(bend90) taper = taper or taper_function( cross_section=cross_section, width1=width, width2=width_wide, length=taper_length, ) taper = taper(cross_section=cross_section, ** kwargs) if callable(taper) else taper # If there is a taper, make sure its length is known if taper and isinstance(taper, Component): if "length" not in taper.info: _taper_ports = list(taper.ports.values()) taper.info["length"] = _taper_ports[-1].x - _taper_ports[0].x straight_fall_back_no_taper = straight_fall_back_no_taper or straight # Remove any flat angle, otherwise the algorithm won't work points = remove_flat_angles(points) points = np.array(points) straight_sections = [] # (p0, angle, length) p0_straight = points[0] p1 = points[1] total_length = 0 # Keep track of the total path length if not hasattr(bend90.info, "length"): raise ValueError( f"bend {bend90} needs to have bend.info.length defined") bend_length = bend90.info.length dp = p1 - p0_straight bend_orientation = None if _is_vertical(p0_straight, p1): if dp[1] > 0: bend_orientation = 90 elif dp[1] < 0: bend_orientation = 270 elif _is_horizontal(p0_straight, p1): if dp[0] > 0: bend_orientation = 0 elif dp[0] < 0: bend_orientation = 180 if bend_orientation is None: return on_route_error(points=points, cross_section=x) layer = x.info["layer"] try: pname_west, pname_north = [ p.name for p in _get_bend_ports(bend=bend90, layer=layer) ] except ValueError as exc: raise ValueError( f"Did not find 2 ports on layer {layer}. Got {list(bend90.ports.values())}" ) from exc n_o_bends = points.shape[0] - 2 total_length += n_o_bends * bend_length previous_port_point = points[0] bend_points = [previous_port_point] # Add bend sections and record straight-section information for i in range(1, points.shape[0] - 1): bend_origin, rotation, x_reflection = _get_bend_reference_parameters( points[i - 1], points[i], points[i + 1], bend90, x.info["layer"]) bend_ref = gen_sref(bend90, rotation, x_reflection, pname_west, bend_origin) references.append(bend_ref) dx_points = points[i][0] - points[i - 1][0] dy_points = points[i][1] - points[i - 1][1] if abs(dx_points) < TOLERANCE: matching_ports = [ port for port in bend_ref.ports.values() if np.isclose(port.x, points[i][0]) ] if abs(dy_points) < TOLERANCE: matching_ports = [ port for port in bend_ref.ports.values() if np.isclose(port.y, points[i][1]) ] if matching_ports: next_port = matching_ports[0] other_port_name = set(bend_ref.ports.keys()) - {next_port.name} other_port = bend_ref.ports[list(other_port_name)[0]] bend_points.append(next_port.midpoint) bend_points.append(other_port.midpoint) previous_port_point = other_port.midpoint try: straight_sections += [( p0_straight, bend_orientation, get_straight_distance(p0_straight, bend_origin), )] except RouteError: on_route_error( points=(p0_straight, bend_origin), cross_section=x, references=references, ) p0_straight = bend_ref.ports[pname_north].midpoint bend_orientation = bend_ref.ports[pname_north].orientation bend_points.append(points[-1]) try: straight_sections += [( p0_straight, bend_orientation, get_straight_distance(p0_straight, points[-1]), )] except RouteError: on_route_error( points=(p0_straight, points[-1]), cross_section=x, references=references, ) # with_point_markers=True # print() # for i, point in enumerate(points): # print(i, point) # print() # for i, point in enumerate(bend_points): # print(i, point) # ensure bend connectivity for i, point in enumerate(points[:-1]): sx = np.sign(points[i + 1][0] - point[0]) sy = np.sign(points[i + 1][1] - point[1]) bsx = np.sign(bend_points[2 * i + 1][0] - bend_points[2 * i][0]) bsy = np.sign(bend_points[2 * i + 1][1] - bend_points[2 * i][1]) if bsx * sx == -1 or bsy * sy == -1: return on_route_error(points=points, cross_section=x, references=references) wg_refs = [] for straight_origin, angle, length in straight_sections: with_taper = False # wg_width = list(bend90.ports.values())[0].width length = snap_to_grid(length) total_length += length if auto_widen and length > auto_widen_minimum_length and width_wide: # Taper starts where straight would have started with_taper = True length = length - 2 * taper_length taper_origin = straight_origin pname_west, pname_east = [ p.name for p in _get_straight_ports(taper, layer=x.info["layer"]) ] taper_ref = taper.ref(position=taper_origin, port_id=pname_west, rotation=angle) references.append(taper_ref) wg_refs += [taper_ref] # Update start straight position straight_origin = taper_ref.ports[pname_east].midpoint # Straight waveguide kwargs_wide = kwargs.copy() kwargs_wide.update(width=width_wide) cross_section_wide = gf.partial(cross_section, **kwargs_wide) wg = straight(length=length, cross_section=cross_section_wide) else: wg = straight_fall_back_no_taper(length=length, cross_section=cross_section, **kwargs) if straight_ports is None: straight_ports = [ p.name for p in _get_straight_ports(wg, layer=x.info["layer"]) ] pname_west, pname_east = straight_ports wg_ref = wg.ref() wg_ref.move(wg.ports[pname_west], (0, 0)) if mirror_straight: wg_ref.reflect_v(list(wg_ref.ports.values())[0].name) wg_ref.rotate(angle) wg_ref.move(straight_origin) if length > 0: references.append(wg_ref) wg_refs += [wg_ref] port_index_out = 1 if with_taper: # Second taper: # Origin at end of straight waveguide, starting from east side of taper taper_origin = wg_ref.ports[pname_east] pname_west, pname_east = [ p.name for p in _get_straight_ports(taper, layer=x.info["layer"]) ] taper_ref = taper.ref( position=taper_origin, port_id=pname_east, rotation=angle + 180, v_mirror=True, ) # references += [ # gf.Label( # text=f"a{angle}", # position=taper_ref.center, # layer=2, # texttype=0, # ) # ] references.append(taper_ref) wg_refs += [taper_ref] port_index_out = 0 if with_point_markers: route = get_route_error(points, cross_section=x) references += route.references port_input = list(wg_refs[0].ports.values())[0] port_output = list(wg_refs[-1].ports.values())[port_index_out] length = snap_to_grid(float(total_length)) return Route(references=references, ports=(port_input, port_output), length=length)
def spiral_inner_io(N: int = 6, x_straight_inner_right: float = 150.0, x_straight_inner_left: float = 50.0, y_straight_inner_top: float = 50.0, y_straight_inner_bottom: float = 10.0, grating_spacing: float = 127.0, waveguide_spacing: float = 3.0, bend90_function: ComponentFactory = bend_euler, bend180_function: ComponentFactory = bend_euler180, straight: ComponentFactory = straight_function, length: Optional[float] = None, cross_section: CrossSectionFactory = strip, cross_section_bend: Optional[CrossSectionFactory] = None, **kwargs) -> Component: """Returns Spiral with ports inside the spiral loop. You can add grating couplers inside . Args: N: number of loops x_straight_inner_right: x_straight_inner_left: y_straight_inner_top: y_straight_inner_bottom: grating_spacing: defaults to 127 for fiber array waveguide_spacing: center to center spacing bend90_function bend180_function straight: straight function length: spiral target length (um), overrides x_straight_inner_left to match the length by a simple 1D interpolation cross_section: cross_section_bend: for the bends kwargs: cross_section settings """ dx = dy = waveguide_spacing x = cross_section(**kwargs) width = x.info.get("width") layer = x.info.get("layer") cross_section_bend = cross_section_bend or cross_section if length: x_straight_inner_left = get_straight_length( length=length, spiral_function=spiral_inner_io, N=N, x_straight_inner_right=x_straight_inner_right, x_straight_inner_left=x_straight_inner_left, y_straight_inner_top=y_straight_inner_top, y_straight_inner_bottom=y_straight_inner_bottom, grating_spacing=grating_spacing, waveguide_spacing=waveguide_spacing, ) _bend180 = gf.call_if_func(bend180_function, cross_section=cross_section_bend, **kwargs) _bend90 = gf.call_if_func(bend90_function, cross_section=cross_section_bend, **kwargs) rx, ry = get_bend_port_distances(_bend90) _, rx180 = get_bend_port_distances( _bend180) # rx180, second arg since we rotate component = Component() p1 = gf.Port( name="o1", midpoint=(0, y_straight_inner_top), orientation=270, width=width, layer=layer, cross_section=cross_section_bend, ) p2 = gf.Port( name="o2", midpoint=(grating_spacing, y_straight_inner_top), orientation=270, width=width, layer=layer, cross_section=cross_section_bend, ) component.add_port(name="o1", port=p1) component.add_port(name="o2", port=p2) # Create manhattan path going from west grating to westest port of bend 180 _pt = np.array(p1.position) pts_w = [_pt] for i in range(N): y1 = y_straight_inner_top + ry + (2 * i + 1) * dy x2 = grating_spacing + 2 * rx + x_straight_inner_right + (2 * i + 1) * dx y3 = -y_straight_inner_bottom - ry - (2 * i + 3) * dy x4 = -x_straight_inner_left - (2 * i + 1) * dx if i == N - 1: x4 = x4 - rx180 + dx _pt1 = np.array([_pt[0], y1]) _pt2 = np.array([x2, _pt1[1]]) _pt3 = np.array([_pt2[0], y3]) _pt4 = np.array([x4, _pt3[1]]) _pt5 = np.array([_pt4[0], 0]) _pt = _pt5 pts_w += [_pt1, _pt2, _pt3, _pt4, _pt5] route_west = round_corners(pts_w, bend=_bend90, straight=straight, cross_section=cross_section, **kwargs) component.add(route_west.references) # Add loop back bend180_ref = _bend180.ref(port_id="o2", position=route_west.ports[1], rotation=90) component.add(bend180_ref) # Create manhattan path going from east grating to eastest port of bend 180 _pt = np.array(p2.position) pts_e = [_pt] for i in range(N): y1 = y_straight_inner_top + ry + (2 * i) * dy x2 = grating_spacing + 2 * rx + x_straight_inner_right + 2 * i * dx y3 = -y_straight_inner_bottom - ry - (2 * i + 2) * dy x4 = -x_straight_inner_left - (2 * i) * dx _pt1 = np.array([_pt[0], y1]) _pt2 = np.array([x2, _pt1[1]]) _pt3 = np.array([_pt2[0], y3]) _pt4 = np.array([x4, _pt3[1]]) _pt5 = np.array([_pt4[0], 0]) _pt = _pt5 pts_e += [_pt1, _pt2, _pt3, _pt4, _pt5] route_east = round_corners(pts_e, bend=_bend90, straight=straight, cross_section=cross_section, **kwargs) component.add(route_east.references) length = route_east.length + route_west.length + _bend180.info.length component.info.length = snap_to_grid(length + 2 * y_straight_inner_top) return component
def get_netlist( component: Component, full_settings: bool = False, layer_label: Tuple[int, int] = LAYER.LABEL_INSTANCE, tolerance: int = 1, ) -> omegaconf.DictConfig: """From a component returns instances, connections and placements dict. It assumes that ports with same width, x, y are connected. Args: component: to Extract netlist full_settings: True returns all settings, false only the ones that have changed layer_label: label to read instanceNames from (if any) tolerance: tolerance in nm to consider two ports the same Returns: connections: Dict of Instance1Name,portName: Instace2Name,portName instances: Dict of instances and settings placements: Dict of instances and placements (x, y, rotation) port: Dict portName: CompoentName,port name: name of component """ placements = {} instances = {} connections = {} top_ports = {} for reference in component.references: c = reference.parent origin = reference.origin x = float(snap_to_grid(origin[0])) y = float(snap_to_grid(origin[1])) reference_name = get_instance_name(component, reference, layer_label=layer_label) settings = (c.info.get("full", {}) if full_settings else c.info.get( "changed", {})) instances[reference_name] = dict( component=getattr(c.info, "function_name", c.name), settings=settings, ) placements[reference_name] = dict( x=x, y=y, rotation=int(reference.rotation), mirror=reference.x_reflection, ) # store where ports are located name2port = {} # Initialize a dict of port locations to Instance1Name,PortNames port_locations = {} # TOP level ports ports = component.get_ports(depth=0) top_ports_list = set() for port in ports: src = port.name name2port[src] = port top_ports_list.add(src) # lower level ports for reference in component.references: for port in reference.ports.values(): reference_name = get_instance_name(component, reference, layer_label=layer_label) src = f"{reference_name},{port.name}" name2port[src] = port # build connectivity port_locations = Dict[Tuple(x,y,width), set of portNames] for name, port in name2port.items(): xyw = tuple( round(1000 * v) for v in snap_to_grid((port.x, port.y, port.width), nm=tolerance)) if xyw not in port_locations: port_locations[xyw] = set() port_locations[xyw].add(name) for xyw, names_set in port_locations.items(): if len(names_set) > 2: x, y, w = (v / 1000 for v in xyw) raise ValueError( f"more than 2 connections at {x, y} {list(names_set)}, width = {w} " ) if len(names_set) == 2: names_list = list(names_set) src = names_list[0] dst = names_list[1] if src in top_ports_list: top_ports[src] = dst elif dst in top_ports_list: top_ports[dst] = src else: src_dest = sorted([src, dst]) connections[src_dest[0]] = src_dest[1] connections_sorted = { k: connections[k] for k in sorted(list(connections.keys())) } placements_sorted = { k: placements[k] for k in sorted(list(placements.keys())) } instances_sorted = { k: instances[k] for k in sorted(list(instances.keys())) } return omegaconf.DictConfig( dict( connections=connections_sorted, instances=instances_sorted, placements=placements_sorted, ports=top_ports, name=component.name, ))
def coupler(gap: float = 0.236, length: float = 20.0, coupler_symmetric: ComponentFactory = coupler_symmetric_function, coupler_straight: ComponentFactory = coupler_straight_function, dy: float = 5.0, dx: float = 10.0, cross_section: CrossSectionFactory = strip, **kwargs) -> Component: r"""Symmetric coupler. Args: gap: between straights length: of coupling region coupler_symmetric coupler_straight dy: port to port vertical spacing dx: length of bend in x direction cross_section: factory kwargs: cross_section settings .. code:: dx dx |------| |------| o2 ________ ______o3 \ / | \ length / | ======================= gap | dy / \ | ________/ \_______ | o1 o4 coupler_straight coupler_symmetric """ length = snap_to_grid(length) assert_on_2nm_grid(gap) c = Component() sbend = coupler_symmetric(gap=gap, dy=dy, dx=dx, cross_section=cross_section, **kwargs) sr = c << sbend sl = c << sbend cs = c << coupler_straight( length=length, gap=gap, cross_section=cross_section, **kwargs) sl.connect("o2", destination=cs.ports["o1"]) sr.connect("o1", destination=cs.ports["o4"]) c.add_port("o1", port=sl.ports["o3"]) c.add_port("o2", port=sl.ports["o4"]) c.add_port("o3", port=sr.ports["o3"]) c.add_port("o4", port=sr.ports["o4"]) c.absorb(sl) c.absorb(sr) c.absorb(cs) c.info.length = sbend.info.length c.info.min_bend_radius = sbend.info.min_bend_radius c.auto_rename_ports() return c
def add_ports_from_markers_center( component: Component, pin_layer: Layer = (1, 10), port_layer: Optional[Layer] = None, inside: bool = False, tol: float = 0.1, pin_extra_width: float = 0.0, min_pin_area_um2: Optional[float] = None, max_pin_area_um2: float = 150.0 * 150.0, skip_square_ports: bool = True, xcenter: Optional[float] = None, ycenter: Optional[float] = None, port_name_prefix: str = "", port_type: str = "optical", ) -> Component: """Add ports from rectangular pin markers. markers at port center, so half of the marker goes inside and half ouside the port. guess port orientation from the component center Args: component: to read polygons from and to write ports to pin_layer: GDS layer for maker [int, int] port_layer: for the new created port inside: True-> markers inside. False-> markers at center tol: tolerance for comparing how rectangular is the pin pin_extra_width: 2*offset from pin to straight min_pin_area_um2: ignores pins with area smaller than min_pin_area_um2 max_pin_area_um2: ignore pins for area above certain size skip_square_ports: skips square ports (hard to guess orientation) xcenter: for guessing orientation of rectangular ports ycenter: for guessing orientation of rectangular ports port_name_prefix: o for optical ports (o1, o2, o3) For the default center case (inside=False) .. code:: _______________ | | | | ||| |||____ | pin_extra_width/2 > 0 ||| ||| ||| |||____ ||| ||| | __ | |_______________| __ For the inside case (inside=True) .. code:: _______________ | | | | | | | | | | | | | __ | | | |_______________| dx < dy: port is east or west x > xc: east x < xc: west dx > dy: port is north or south y > yc: north y < yc: south dx = dy x > xc: east x < xc: west """ xc = xcenter or component.x yc = ycenter or component.y xmax = component.xmax xmin = component.xmin ymax = component.ymax ymin = component.ymin port_markers = read_port_markers(component, layers=(pin_layer,)) layer = port_layer or pin_layer port_locations = [] ports = {} for i, p in enumerate(port_markers.polygons): port_name = f"{port_name_prefix}{i+1}" if port_name_prefix else i dy = p.ymax - p.ymin dx = p.xmax - p.xmin x = p.x y = p.y if min_pin_area_um2 and dx * dy <= min_pin_area_um2: continue if max_pin_area_um2 and dx * dy > max_pin_area_um2: continue # skip square ports as they have no clear orientation if skip_square_ports and snap_to_grid(dx) == snap_to_grid(dy): continue pxmax = p.xmax pxmin = p.xmin pymax = p.ymax pymin = p.ymin if dx < dy and x > xc: # east orientation = 0 width = dy if inside: x = p.xmax elif dx < dy and x < xc: # west orientation = 180 width = dy if inside: x = p.xmin elif dx > dy and y > yc: # north orientation = 90 width = dx if inside: y = p.ymax elif dx > dy and y < yc: # south orientation = 270 width = dx if inside: y = p.ymin # port markers have same width and height # check which edge (E, W, N, S) they are closer to elif pxmax > xmax - tol: # east orientation = 0 width = dy x = p.xmax elif pxmin < xmin + tol: # west orientation = 180 width = dy x = p.xmin elif pymax > ymax - tol: # north orientation = 90 width = dx y = p.ymax elif pymin < ymin + tol: # south orientation = 270 width = dx y = p.ymin x = snap_to_grid(x) y = snap_to_grid(y) width = np.round(width - pin_extra_width, 3) if (x, y) not in port_locations: port_locations.append((x, y)) ports[port_name] = Port( name=port_name, midpoint=(x, y), width=width, orientation=orientation, layer=layer, port_type=port_type, ) ports = sort_ports_clockwise(ports) for port_name, port in ports.items(): component.add_port(name=port_name, port=port) return component
def bend_euler(angle: int = 90, p: float = 0.5, with_arc_floorplan: bool = True, npoints: int = 720, direction: str = "ccw", with_cladding_box: bool = True, cross_section: CrossSectionFactory = strip, **kwargs) -> Component: """Returns an euler bend that adiabatically transitions from straight to curved. By default, `radius` corresponds to the minimum radius of curvature of the bend. However, if `with_arc_floorplan` is True, `radius` corresponds to the effective radius of curvature (making the curve a drop-in replacement for an arc). If p < 1.0, will create a "partial euler" curve as described in Vogelbacher et. al. https://dx.doi.org/10.1364/oe.27.031394 default p = 0.5 based on this paper https://www.osapublishing.org/oe/fulltext.cfm?uri=oe-25-8-9150&id=362937 Args: angle: total angle of the curve p: Proportion of the curve that is an Euler curve with_arc_floorplan: If False: `radius` is the minimum radius of curvature If True: The curve scales such that the endpoints match a bend_circular with parameters `radius` and `angle` npoints: Number of points used per 360 degrees direction: cw (clock-wise) or ccw (counter clock-wise) with_cladding_box: to avoid DRC acute angle errors in cladding cross_section: """ x = cross_section(**kwargs) radius = x.info["radius"] p = euler(radius=radius, angle=angle, p=p, use_eff=with_arc_floorplan, npoints=npoints) c = extrude(p, x) c.info.length = snap_to_grid(p.length()) c.info.dy = abs(float(p.points[0][0] - p.points[-1][0])) c.info.radius_min = float(p.info["Rmin"]) c.info.radius = radius if with_cladding_box and x.info["layers_cladding"]: layers_cladding = x.info["layers_cladding"] cladding_offset = x.info["cladding_offset"] top = cladding_offset if angle == 180 else 0 points = get_padding_points( component=c, default=0, bottom=cladding_offset, right=cladding_offset, top=top, ) for layer in layers_cladding or []: c.add_polygon(points, layer=layer) if direction == "cw": c.mirror(p1=[0, 0], p2=[1, 0]) return c