Routing in batch form
PhotoCAD has a built-in layout and routing tool in batch form that can be used for modular layout design with similar components, and is often used to quickly generate components or modules in a test chip in batch.
In a test chip, the devices or modules to be verified usually have similar connection relationships, and the devices or modules are placed in a certain pattern to better match the probes for testing.
Components Scan
This example implements a script to simultaneously call different devices in each line to be placed between the same input and output ports. The script can unify the planning of port-to-component connections and simultaneously implement the layout design for equally spaced component placement and waveguide connections.
Full script
import math
from functools import partial
from typing import Any, Callable, Iterable, List, Optional, Protocol, Sequence, Tuple, cast
from fnpcell import all as fp
from gpdk.components.sbend.sbend import SBend
from gpdk.components.straight.straight import Straight
from gpdk.routing.auto_transitioned.auto_transitioned import AutoTransitioned
from gpdk.routing.extended.extended import Extended
from gpdk.technology import get_technology
from gpdk.util import all as util
class DeviceAdapter(Protocol):
def __call__(self, device: fp.IDevice) -> fp.IDevice:
...
class FiberCouplerFactory(Protocol):
def __call__(self, at: fp.IRay, device: fp.IDevice) -> Tuple[fp.IDevice, str]:
...
class ConstFiberCouplerFactory(FiberCouplerFactory):
def __init__(self, coupler: fp.IDevice, port: Optional[str]):
self.coupler = coupler
self.port = port
def __call__(self, at: fp.IRay, device: fp.IDevice) -> Tuple[fp.IDevice, str]:
coupler = self.coupler
port = self.port or "op_0"
return (coupler, port)
class Block:
def __init__(
self,
content: fp.ICellRef,
*,
offset: Tuple[float, float] = (0, 0),
repeat: int = 1,
bend_factory: Optional[fp.IBendWaveguideFactory] = None,
bend_factories: Optional[Callable[[fp.IWaveguideType], fp.IBendWaveguideFactory]] = None,
) -> None:
self.content = content
self.offset = offset
self.repeat = repeat
self.bend_factory = bend_factory
self.bend_factories = bend_factories
class Alignment(Block):
def __init__(
self,
*,
offset: Tuple[float, float] = (0, 0),
waveguide_type: fp.IWaveguideType,
) -> None:
super().__init__(
content=fp.Device(
name="Alignment",
content=[],
ports=[
fp.Port(name="op_0", position=(0, 0), orientation=0, waveguide_type=waveguide_type),
fp.Port(name="op_1", position=(0, 0), orientation=math.pi, waveguide_type=waveguide_type),
],
),
offset=offset,
)
class Title(Block):
def __init__(
self,
content: str,
*,
gap: float = 20,
font_size: float = 5,
layer: fp.ILayer,
) -> None:
super().__init__(
content=fp.Device(
name="Title",
content=[
fp.el.Label(
content,
font_size=font_size,
layer=layer,
),
],
ports=[],
),
)
self.gap = gap
class Blank(Block):
def __init__(
self,
*,
left: int = 1,
right: int = 1,
) -> None:
super().__init__(
content=fp.Device(name="Blank", content=[], ports=[]),
)
self.left = left
self.right = right
def _get_ports_center_y(ports: Iterable[fp.IPort]):
ys = tuple(p.position[1] for p in ports)
return (min(ys) + max(ys)) / 2
def _get_block_content(block: Block, left_y: float, right_y: float, spacing: float, device_adapter: DeviceAdapter):
SHORT_STRAIGHT = 1
ox, oy = block.offset
device = block.content
left_ports = util.ports.get_left_ports(device, reverse=True)
right_ports = util.ports.get_right_ports(device, reverse=True)
center_y = _get_ports_center_y(left_ports + right_ports)
left_y2 = left_y + (len(left_ports) - 1) * spacing
right_y2 = right_y + (len(right_ports) - 1) * spacing
y = (min(left_y, right_y) + max(left_y2, right_y2)) / 2 - center_y
if block.repeat > 1:
prev = device
joints: List[Tuple[fp.IOwnedTerminal, fp.IOwnedTerminal]] = []
for _ in range(1, block.repeat):
curr = prev.h_mirrored() # device.h_mirrored() if i % 2 else device.translated(0, 0)
right_ports = util.ports.get_right_ports(prev, reverse=True)
left_ports = util.ports.get_left_ports(curr, reverse=True)
for a, b in zip(right_ports, left_ports):
s = Straight(length=SHORT_STRAIGHT, waveguide_type=a.waveguide_type)
joints.append(a <= s["op_0"])
joints.append(s["op_1"] <= b)
prev = curr
left_ports = util.ports.get_left_ports(device, reverse=True)
right_ports = list(util.ports.get_right_ports(prev, reverse=False))
ports = [port.with_name(f"op_{i}") for i, port in enumerate(left_ports + right_ports)]
distance = fp.distance_between(left_ports[0].position, right_ports[0].position)
block_content = fp.Connected(joints=joints, ports=ports)
tx, ty = -distance / 2 + ox, y + oy
else:
block_content = device
tx, ty = 0 + ox, y + oy
return device_adapter(device=block_content).translated(tx, ty)
class CompScan(fp.PCell):
"""
Attributes:
max_lines: Optional, max lines, raise error if exceeded
blocks: blocks of devices
width: defaults to 2000, total width between grating couplers
spacing: defaults to 127, spacing between lines
bend_degrees: defaults to 45, central angle of generated bend
bend_factory: Optional, will be used to generate all bends if provided
bend_factories: Optional, providing `IBendWaveguideFactory` for each waveguide type
waveguide_type: Optional, type of generated waveguide
connection_type: Optional, type of generated connection straight
device_connection_length: defaults to 20, minimum distance between device and sbend
min_io_connection_length: defaults to 20, minimum distance between grating coupler and sbend
Examples:
```python
TECH = get_technology()
# ...
device = CompScan(spacing=255, width=2000, blocks=blocks)
fp.plot(device)
```

"""
fiber_coupler_factory: FiberCouplerFactory = fp.Param()
fiber_coupler_adapter: Optional[fp.IDevice] = fp.DeviceParam(required=False)
fiber_coupler_adapter_port: Optional[str] = fp.TextParam(required=False)
fiber_coupler_v_mirrored: Sequence[bool] = fp.Param(default=(False, False))
max_lines: Optional[int] = fp.PositiveIntParam(required=False)
blocks: Sequence[Block] = fp.ListParam(element_type=Block, immutable=True)
width: float = fp.PositiveFloatParam(default=2000)
spacing: float = fp.PositiveFloatParam(default=127)
bend_degrees: float = fp.DegreeParam(default=45)
bend_factory: Optional[fp.IBendWaveguideFactory] = fp.Param(required=False)
bend_factories: Optional[Callable[[fp.IWaveguideType], fp.IBendWaveguideFactory]] = fp.Param(required=False)
waveguide_type: Optional[fp.IWaveguideType] = fp.WaveguideTypeParam(required=False)
connection_type: Optional[fp.IWaveguideType] = fp.WaveguideTypeParam(required=False)
device_connection_length: float = fp.PositiveFloatParam(default=20)
min_io_connection_length: float = fp.PositiveFloatParam(default=20)
def _default_fiber_coupler_factory(self):
if self.fiber_coupler_adapter is not None:
return ConstFiberCouplerFactory(self.fiber_coupler_adapter, self.fiber_coupler_adapter_port or "op_0")
return None
def __post_pcell_init__(self):
assert len(self.fiber_coupler_v_mirrored) == 2, "`fiber_coupler_v_mirrored` must have its length equals to 2"
def build(self) -> Tuple[fp.InstanceSet, fp.ElementSet, fp.PortSet]:
insts, elems, ports = super().build()
TECH = get_technology()
fiber_coupler_factory = self.fiber_coupler_factory
left_v_mirrored, right_v_mirrored = self.fiber_coupler_v_mirrored
max_lines = self.max_lines
blocks = self.blocks
width = self.width
spacing = self.spacing
bend_degrees = self.bend_degrees
default_bend_factory = self.bend_factory
default_bend_factories = self.bend_factories
waveguide_type = self.waveguide_type
connection_type = self.connection_type
device_connection_length = self.device_connection_length
min_io_connection_length = self.min_io_connection_length
SHORT_STRAIGHT = 0.1
content: List[fp.ICellRef] = []
left_x = -width / 2
right_x = width / 2
left_y: float = 0
right_y: float = 0
links: List[
Tuple[
Tuple[fp.IOwnedPort, fp.IOwnedPort], str, Optional[fp.IBendWaveguideFactory], Optional[Callable[[fp.IWaveguideType], fp.IBendWaveguideFactory]]
]
] = []
total_lines = 0
if connection_type is None:
connection_type = waveguide_type
for block in blocks:
assert isinstance(block, Block)
y = max(left_y, right_y)
if isinstance(block, Title):
label: Any = block.content.cell.content[0]
distance, _ = label.size
count = int(width / (distance + block.gap))
labels: List[fp.IElement] = []
for i in range(count):
labels.append(label.translated(-width / 2 + i * (distance + block.gap), y))
content.append(fp.Device(name="Title", content=labels, ports=[]))
left_y = y + spacing
right_y = y + spacing
continue
if isinstance(block, Blank):
left_y += block.left * spacing
right_y += block.right * spacing
continue
block_bend_factory = block.bend_factory
block_bend_factories = block.bend_factories
bend_factory = block_bend_factory or default_bend_factory
bend_factories = block_bend_factories or default_bend_factories
device_adapter = cast(DeviceAdapter, partial(Extended, waveguide_type=waveguide_type, lengths={"*": device_connection_length}))
instance = _get_block_content(block, left_y, right_y, spacing, device_adapter)
content.append(instance)
left_ports = util.ports.get_left_ports(instance, reverse=True)
right_ports = util.ports.get_right_ports(instance, reverse=True)
for left_port in left_ports:
left_gc_at = fp.Waypoint(left_x, left_y, 180)
left_gc, left_gc_port = fiber_coupler_factory(at=left_gc_at, device=instance)
if left_v_mirrored:
left_gc = left_gc.v_mirrored()
left_gc_instance = left_gc if waveguide_type is None else AutoTransitioned(device=left_gc, waveguide_types={"*": waveguide_type})
left_gc_transition_length = fp.distance_between(left_gc[left_gc_port].position, left_gc_instance[left_gc_port].position)
left_gc_instance = fp.place(left_gc_instance, left_gc_port, at=left_gc_at.advanced(-left_gc_transition_length))
content.append(left_gc_instance)
left_y += spacing
turning_angle = fp.normalize_angle(math.pi - left_port.orientation)
if fp.is_nonzero(turning_angle):
left_port = util.links.bend(
TECH,
content,
start=left_port,
radians=turning_angle,
bend_factory=bend_factory or bend_factories and bend_factories(left_port.waveguide_type),
)
left_port = util.links.straight(TECH, content, start=left_port, length=SHORT_STRAIGHT)
links.append((left_port <= cast(fp.IOwnedPort, left_gc_instance[left_gc_port]), "left", bend_factory, bend_factories))
for right_port in right_ports:
right_gc_at = fp.Waypoint(right_x, right_y, 0)
right_gc, right_gc_port = fiber_coupler_factory(at=right_gc_at, device=instance)
if right_v_mirrored:
right_gc = right_gc.v_mirrored()
right_gc_instance = right_gc if waveguide_type is None else AutoTransitioned(device=right_gc, waveguide_types={"*": waveguide_type})
right_gc_transition_length = fp.distance_between(right_gc[right_gc_port].position, right_gc_instance[right_gc_port].position)
right_gc_instance = fp.place(right_gc_instance, right_gc_port, at=right_gc_at.advanced(-right_gc_transition_length))
content.append(right_gc_instance)
right_y += spacing
turning_angle = fp.normalize_angle(0 - right_port.orientation)
if fp.is_nonzero(turning_angle):
right_port = util.links.bend(
TECH,
content,
start=right_port,
radians=turning_angle,
bend_factory=bend_factory or bend_factories and bend_factories(right_port.waveguide_type),
)
right_port = util.links.straight(TECH, content, start=right_port, length=SHORT_STRAIGHT)
links.append((right_port <= cast(fp.IOwnedPort, right_gc_instance[right_gc_port]), "right", bend_factory, bend_factories))
total_lines += max(len(left_ports), len(right_ports))
if max_lines is not None:
assert total_lines <= max_lines, f"exceed max lines: {max_lines}, got: {total_lines}"
for (dev, gc), p, bend_factory, bend_factories in links:
if p == "left":
x0, y0 = gc.position
x1, y1 = dev.position
else:
x0, y0 = dev.position
x1, y1 = gc.position
length = x1 - x0
height = y1 - y0
end_type = waveguide_type
if fp.is_nonzero(height):
sbend_type = waveguide_type or dev.waveguide_type
sbend = SBend(
height=height,
bend_degrees=bend_degrees,
max_distance=length - min_io_connection_length,
waveguide_type=sbend_type,
bend_factory=bend_factory or (bend_factories and bend_factories(sbend_type)) or sbend_type.bend_factory,
)
sbend_distance = abs(sbend["op_1"].position[0] - sbend["op_0"].position[0])
sbend = fp.place(sbend, "op_1" if p == "left" else "op_0", at=dev.position)
content.append(sbend)
length -= sbend_distance
end_type = sbend_type
util.links.straight(TECH, content, start=gc, length=length, link_type=connection_type, end_type=end_type)
insts += content
return insts, elems, ports
class CompScanBuilder:
blocks: List[Block]
def __init__(
self,
*,
name: Optional[str] = None,
fiber_coupler_factory: Optional[FiberCouplerFactory] = None,
fiber_coupler_adapter: Optional[fp.IDevice] = None,
fiber_coupler_v_mirrored: Sequence[bool] = (False, False),
max_lines: Optional[int] = None,
width: float = 2000,
spacing: float = 127,
waveguide_type: Optional[fp.IWaveguideType] = None,
bend_degrees: Optional[float] = None,
connection_type: Optional[fp.IWaveguideType] = None,
device_connection_length: float = 20,
min_io_connection_length: float = 20,
bend_factory: Optional[fp.IBendWaveguideFactory] = None,
bend_factories: Optional[Callable[[fp.IWaveguideType], fp.IBendWaveguideFactory]] = None,
) -> None:
self.name = name
self.fiber_coupler_factory = fiber_coupler_factory
self.fiber_coupler_adapter = fiber_coupler_adapter
self.fiber_coupler_v_mirrored = fiber_coupler_v_mirrored
self.max_lines = max_lines
self.width = width
self.spacing = spacing
self.waveguide_type = waveguide_type
self.bend_degrees = bend_degrees
self.connection_type = connection_type
self.device_connection_length = device_connection_length
self.min_io_connection_length = min_io_connection_length
self.bend_factory = bend_factory
self.bend_factories = bend_factories
self.blocks = []
def build(self, transform: fp.Affine2D = fp.Affine2D.identity()):
params = dict(
name=self.name or "",
fiber_coupler_factory=self.fiber_coupler_factory,
fiber_coupler_adapter=self.fiber_coupler_adapter,
fiber_coupler_v_mirrored=self.fiber_coupler_v_mirrored,
max_lines=self.max_lines,
blocks=self.blocks,
width=self.width,
spacing=self.spacing,
waveguide_type=self.waveguide_type,
connection_type=self.connection_type,
device_connection_length=self.device_connection_length,
min_io_connection_length=self.min_io_connection_length,
bend_factory=self.bend_factory,
bend_factories=self.bend_factories,
transform=transform,
)
for key, value in list(params.items()):
if value is None:
del params[key]
return CompScan(**params)
def add_block(
self,
content: fp.IDevice,
*,
offset: Tuple[float, float] = (0, 0),
repeat: int = 1,
bend_factory: Optional[fp.IBendWaveguideFactory] = None,
bend_factories: Optional[Callable[[fp.IWaveguideType], fp.IBendWaveguideFactory]] = None,
):
self.blocks.append(Block(content, offset=offset, repeat=repeat, bend_factory=bend_factory, bend_factories=bend_factories))
def add_alignment(self, *, offset: Tuple[float, float] = (0, 0), waveguide_type: Optional[fp.IWaveguideType] = None):
waveguide_type = waveguide_type or self.waveguide_type
assert waveguide_type is not None, "waveguide_type must be supplied"
self.blocks.append(Alignment(offset=offset, waveguide_type=waveguide_type))
def add_title(self, content: str, *, gap: float = 20, font_size: float = 5, layer: fp.ILayer):
self.blocks.append(Title(content, gap=gap, font_size=font_size, layer=layer))
def add_blank(self, left: int = 1, right: int = 1):
self.blocks.append(Blank(left=left, right=right))
if __name__ == "__main__":
from gpdk.util.path import local_output_file
gds_file = local_output_file(__file__).with_suffix(".gds")
library = fp.Library()
TECH = get_technology()
# =============================================================
from gpdk.components.fixed_terminator_te_1550.fixed_terminator_te_1550 import Fixed_Terminator_TE_1550
from gpdk.components.ring_filter.ring_filter import RingFilter
from gpdk.components.ring_resonator.ring_resonator import RingResonator
from gpdk.routing.extended.extended import Extended
from gpdk.technology.waveguide_factory import EulerBendFactory
from gpdk.components.grating_coupler.grating_coupler import GratingCoupler
def gc_factory(at: fp.IRay, device: fp.IDevice):
gc = GratingCoupler() # type: ignore
return gc, "op_0"
def bend_factories(waveguide_type: fp.IWaveguideType):
if waveguide_type == TECH.WG.FWG.C.WIRE:
return EulerBendFactory(radius_min=35, l_max=35, waveguide_type=waveguide_type)
elif waveguide_type == TECH.WG.SWG.C.EXPANDED:
return EulerBendFactory(radius_min=55, l_max=35, waveguide_type=waveguide_type)
elif waveguide_type == TECH.WG.SWG.C.WIRE:
return EulerBendFactory(radius_min=45, l_max=35, waveguide_type=waveguide_type)
return waveguide_type.bend_factory
def get_ring_resonator_with_terminator(ring_radius: float):
terminator = Fixed_Terminator_TE_1550(waveguide_type=TECH.WG.FWG.C.WIRE)
ring_resonator = RingResonator(ring_radius=ring_radius, ring_type=TECH.WG.FWG.C.WIRE)
return Extended(
device=fp.Connected(
joints=[ring_resonator["op_2"] <= terminator["op_0"]], ports=[ring_resonator["op_0"], ring_resonator["op_1"], ring_resonator["op_3"]]
),
lengths={"*": 20},
)
blocks = [
Alignment(
waveguide_type=TECH.WG.FWG.C.WIRE,
),
Title(
"TEST TITLE",
layer=TECH.LAYER.LABEL_DRW,
),
Block(get_ring_resonator_with_terminator(25)),
# Blank(left=0, right=1),
Block(
get_ring_resonator_with_terminator(50),
repeat=3,
),
Block(
get_ring_resonator_with_terminator(75),
repeat=3,
),
Block(get_ring_resonator_with_terminator(90), bend_factories=bend_factories),
# Blank(left=0, right=1),
Block(
RingFilter(
ring_radius=25,
waveguide_type=TECH.WG.FWG.C.WIRE,
).rotated(degrees=30)
),
Block(
RingResonator(ring_radius=90, ring_type=TECH.WG.FWG.C.WIRE),
repeat=3,
),
]
def term_factory(at: fp.IRay, device: fp.IDevice):
from gpdk.components.fixed_terminator_te_1550.fixed_terminator_te_1550 import Fixed_Terminator_TE_1550
instance = Fixed_Terminator_TE_1550().h_mirrored() # type: ignore
return instance, "op_0"
library += CompScan(name="comp_scan", spacing=255, width=2000, blocks=blocks, fiber_coupler_factory=term_factory)
library += CompScan(name="comp_scan", spacing=255, width=2000, blocks=blocks, fiber_coupler_adapter=Fixed_Terminator_TE_1550())
library += CompScan(name="comp_scan", spacing=255, width=2000, blocks=blocks, bend_factories=bend_factories, fiber_coupler_factory=gc_factory)
library += CompScan(
name="comp_scan",
spacing=255,
width=2000,
blocks=blocks,
bend_factories=bend_factories,
waveguide_type=TECH.WG.SWG.C.EXPANDED,
bend_factory=TECH.WG.SWG.C.WIRE.bend_factory,
connection_type=TECH.WG.MWG.C.WIRE,
fiber_coupler_factory=gc_factory,
)
library += CompScan(name="comp_scam", spacing=255, width=2000, blocks=blocks, bend_factories=bend_factories,
fiber_coupler_factory=gc_factory)
# =============================================================
fp.export_gds(library, file=gds_file)
# fp.plot(library)
Section Script Definition
Importing python libraries and functional modules of PhotoCAD
import math
from functools import partial
from typing import Any, Callable, Iterable, List, Optional, Protocol, Sequence, Tuple, cast
from fnpcell import all as fp
from gpdk.components.sbend.sbend import SBend
from gpdk.components.straight.straight import Straight
from gpdk.routing.auto_transitioned.auto_transitioned import AutoTransitioned
from gpdk.routing.extended.extended import Extended
from gpdk.technology import get_technology
from gpdk.util import all as util
Define device adaptation, fiber coupling, constant fiber coupler and several other classes
class DeviceAdapter(Protocol):
def __call__(self, device: fp.IDevice) -> fp.IDevice:
...
class FiberCouplerFactory(Protocol):
def __call__(self, at: fp.IRay, device: fp.IDevice) -> Tuple[fp.IDevice, str]:
...
class ConstFiberCouplerFactory(FiberCouplerFactory):
def __init__(self, coupler: fp.IDevice, port: Optional[str]):
self.coupler = coupler
self.port = port
def __call__(self, at: fp.IRay, device: fp.IDevice) -> Tuple[fp.IDevice, str]:
coupler = self.coupler
port = self.port or "op_0"
return (coupler, port)
Define the batch class Block
class Block:
def __init__(
self,
content: fp.ICellRef,
*,
offset: Tuple[float, float] = (0, 0),
repeat: int = 1,
bend_factory: Optional[fp.IBendWaveguideFactory] = None,
bend_factories: Optional[Callable[[fp.IWaveguideType], fp.IBendWaveguideFactory]] = None,
) -> None:
self.content = content
self.offset = offset
self.repeat = repeat
self.bend_factory = bend_factory
self.bend_factories = bend_factories
Define Alignment
class Alignment(Block):
def __init__(
self,
*,
offset: Tuple[float, float] = (0, 0),
waveguide_type: fp.IWaveguideType,
) -> None:
super().__init__(
content=fp.Device(
name="Alignment",
content=[],
ports=[
fp.Port(name="op_0", position=(0, 0), orientation=0, waveguide_type=waveguide_type),
fp.Port(name="op_1", position=(0, 0), orientation=math.pi, waveguide_type=waveguide_type),
],
),
offset=offset,
Define Title
class Title(Block):
def __init__(
self,
content: str,
*,
gap: float = 20,
font_size: float = 5,
layer: fp.ILayer,
) -> None:
super().__init__(
content=fp.Device(
name="Title",
content=[
fp.el.Label(
content,
font_size=font_size,
layer=layer,
),
],
ports=[],
),
)
self.gap = gap
Define Blank
class Blank(Block):
def __init__(
self,
*,
left: int = 1,
right: int = 1,
) -> None:
super().__init__(
content=fp.Device(name="Blank", content=[], ports=[]),
)
self.left = left
self.right = right
Define method to get the port center
def _get_ports_center_y(ports: Iterable[fp.IPort]):
ys = tuple(p.position[1] for p in ports)
return (min(ys) + max(ys)) / 2
Define methods for obtaining module content
def _get_block_content(block: Block, left_y: float, right_y: float, spacing: float, device_adapter: DeviceAdapter):
SHORT_STRAIGHT = 1
ox, oy = block.offset
device = block.content
left_ports = util.ports.get_left_ports(device, reverse=True)
right_ports = util.ports.get_right_ports(device, reverse=True)
center_y = _get_ports_center_y(left_ports + right_ports)
left_y2 = left_y + (len(left_ports) - 1) * spacing
right_y2 = right_y + (len(right_ports) - 1) * spacing
y = (min(left_y, right_y) + max(left_y2, right_y2)) / 2 - center_y
if block.repeat > 1:
prev = device
joints: List[Tuple[fp.IOwnedTerminal, fp.IOwnedTerminal]] = []
for _ in range(1, block.repeat):
curr = prev.h_mirrored() # device.h_mirrored() if i % 2 else device.translated(0, 0)
right_ports = util.ports.get_right_ports(prev, reverse=True)
left_ports = util.ports.get_left_ports(curr, reverse=True)
for a, b in zip(right_ports, left_ports):
s = Straight(length=SHORT_STRAIGHT, waveguide_type=a.waveguide_type)
joints.append(a <= s["op_0"])
joints.append(s["op_1"] <= b)
prev = curr
left_ports = util.ports.get_left_ports(device, reverse=True)
right_ports = list(util.ports.get_right_ports(prev, reverse=False))
ports = [port.with_name(f"op_{i}") for i, port in enumerate(left_ports + right_ports)]
distance = fp.distance_between(left_ports[0].position, right_ports[0].position)
block_content = fp.Connected(joints=joints, ports=ports)
tx, ty = -distance / 2 + ox, y + oy
else:
block_content = device
tx, ty = 0 + ox, y + oy
return device_adapter(device=block_content).translated(tx, ty)
Define CompScan
class CompScan(fp.PCell):
"""
Attributes:
max_lines: Optional, max lines, raise error if exceeded
blocks: blocks of devices
width: defaults to 2000, total width between grating couplers
spacing: defaults to 127, spacing between lines
bend_degrees: defaults to 45, central angle of generated bend
bend_factory: Optional, will be used to generate all bends if provided
bend_factories: Optional, providing `IBendWaveguideFactory` for each waveguide type
waveguide_type: Optional, type of generated waveguide
connection_type: Optional, type of generated connection straight
device_connection_length: defaults to 20, minimum distance between device and sbend
min_io_connection_length: defaults to 20, minimum distance between grating coupler and sbend
Examples:
```python
TECH = get_technology()
# ...
device = CompScan(spacing=255, width=2000, blocks=blocks)
fp.plot(device)
```

"""
fiber_coupler_factory: FiberCouplerFactory = fp.Param().as_field()
fiber_coupler_adapter: Optional[fp.IDevice] = fp.DeviceParam(required=False)
fiber_coupler_adapter_port: Optional[str] = fp.TextParam(required=False)
fiber_coupler_v_mirrored: Sequence[bool] = fp.Param(default=(False, False))
max_lines: Optional[int] = fp.PositiveIntParam(required=False)
blocks: Sequence[Block] = fp.ListParam(element_type=Block, immutable=True)
width: float = fp.PositiveFloatParam(default=2000)
spacing: float = fp.PositiveFloatParam(default=127)
bend_degrees: float = fp.DegreeParam(default=45)
bend_factory: Optional[fp.IBendWaveguideFactory] = fp.Param(required=False)
bend_factories: Optional[Callable[[fp.IWaveguideType], fp.IBendWaveguideFactory]] = fp.Param(required=False)
waveguide_type: Optional[fp.IWaveguideType] = fp.WaveguideTypeParam(required=False)
connection_type: Optional[fp.IWaveguideType] = fp.WaveguideTypeParam(required=False)
device_connection_length: float = fp.PositiveFloatParam(default=20)
min_io_connection_length: float = fp.PositiveFloatParam(default=20)
def _default_fiber_coupler_factory(self):
if self.fiber_coupler_adapter is not None:
return ConstFiberCouplerFactory(self.fiber_coupler_adapter, self.fiber_coupler_adapter_port or "op_0")
return None
def __post_pcell_init__(self):
assert len(self.fiber_coupler_v_mirrored) == 2, "`fiber_coupler_v_mirrored` must have its length equals to 2"
def build(self) -> Tuple[fp.InstanceSet, fp.ElementSet, fp.PortSet]:
insts, elems, ports = super().build()
TECH = get_technology()
fiber_coupler_factory = self.fiber_coupler_factory
left_v_mirrored, right_v_mirrored = self.fiber_coupler_v_mirrored
max_lines = self.max_lines
blocks = self.blocks
width = self.width
spacing = self.spacing
bend_degrees = self.bend_degrees
default_bend_factory = self.bend_factory
default_bend_factories = self.bend_factories
waveguide_type = self.waveguide_type
connection_type = self.connection_type
device_connection_length = self.device_connection_length
min_io_connection_length = self.min_io_connection_length
SHORT_STRAIGHT = 0.1
content: List[fp.ICellRef] = []
left_x = -width / 2
right_x = width / 2
left_y: float = 0
right_y: float = 0
links: List[
Tuple[
Tuple[fp.IOwnedPort, fp.IOwnedPort], str, Optional[fp.IBendWaveguideFactory], Optional[Callable[[fp.IWaveguideType], fp.IBendWaveguideFactory]]
]
] = []
total_lines = 0
if connection_type is None:
connection_type = waveguide_type
for block in blocks:
assert isinstance(block, Block)
y = max(left_y, right_y)
if isinstance(block, Title):
label: Any = block.content.cell.content[0]
distance, _ = label.size
count = int(width / (distance + block.gap))
labels: List[fp.IElement] = []
for i in range(count):
labels.append(label.translated(-width / 2 + i * (distance + block.gap), y))
content.append(fp.Device(name="Title", content=labels, ports=[]))
left_y = y + spacing
right_y = y + spacing
continue
if isinstance(block, Blank):
left_y += block.left * spacing
right_y += block.right * spacing
continue
block_bend_factory = block.bend_factory
block_bend_factories = block.bend_factories
bend_factory = block_bend_factory or default_bend_factory
bend_factories = block_bend_factories or default_bend_factories
device_adapter = cast(DeviceAdapter, partial(Extended, waveguide_type=waveguide_type, lengths={"*": device_connection_length}))
instance = _get_block_content(block, left_y, right_y, spacing, device_adapter)
content.append(instance)
left_ports = util.ports.get_left_ports(instance, reverse=True)
right_ports = util.ports.get_right_ports(instance, reverse=True)
for left_port in left_ports:
left_gc_at = fp.Waypoint(left_x, left_y, 180)
left_gc, left_gc_port = fiber_coupler_factory(at=left_gc_at, device=instance)
if left_v_mirrored:
left_gc = left_gc.v_mirrored()
left_gc_instance = left_gc if waveguide_type is None else AutoTransitioned(device=left_gc, waveguide_types={"*": waveguide_type})
left_gc_transition_length = fp.distance_between(left_gc[left_gc_port].position, left_gc_instance[left_gc_port].position)
left_gc_instance = fp.place(left_gc_instance, left_gc_port, at=left_gc_at.advanced(-left_gc_transition_length))
content.append(left_gc_instance)
left_y += spacing
turning_angle = fp.normalize_angle(math.pi - left_port.orientation)
if fp.is_nonzero(turning_angle):
left_port = util.links.bend(
TECH,
content,
start=left_port,
radians=turning_angle,
bend_factory=bend_factory or bend_factories and bend_factories(left_port.waveguide_type),
)
left_port = util.links.straight(TECH, content, start=left_port, length=SHORT_STRAIGHT)
links.append((left_port <= cast(fp.IOwnedPort, left_gc_instance[left_gc_port]), "left", bend_factory, bend_factories))
for right_port in right_ports:
right_gc_at = fp.Waypoint(right_x, right_y, 0)
right_gc, right_gc_port = fiber_coupler_factory(at=right_gc_at, device=instance)
if right_v_mirrored:
right_gc = right_gc.v_mirrored()
right_gc_instance = right_gc if waveguide_type is None else AutoTransitioned(device=right_gc, waveguide_types={"*": waveguide_type})
right_gc_transition_length = fp.distance_between(right_gc[right_gc_port].position, right_gc_instance[right_gc_port].position)
right_gc_instance = fp.place(right_gc_instance, right_gc_port, at=right_gc_at.advanced(-right_gc_transition_length))
content.append(right_gc_instance)
right_y += spacing
turning_angle = fp.normalize_angle(0 - right_port.orientation)
if fp.is_nonzero(turning_angle):
right_port = util.links.bend(
TECH,
content,
start=right_port,
radians=turning_angle,
bend_factory=bend_factory or bend_factories and bend_factories(right_port.waveguide_type),
)
right_port = util.links.straight(TECH, content, start=right_port, length=SHORT_STRAIGHT)
links.append((right_port <= cast(fp.IOwnedPort, right_gc_instance[right_gc_port]), "right", bend_factory, bend_factories))
total_lines += max(len(left_ports), len(right_ports))
if max_lines is not None:
assert total_lines <= max_lines, f"exceed max lines: {max_lines}, got: {total_lines}"
for (dev, gc), p, bend_factory, bend_factories in links:
if p == "left":
x0, y0 = gc.position
x1, y1 = dev.position
else:
x0, y0 = dev.position
x1, y1 = gc.position
length = x1 - x0
height = y1 - y0
end_type = waveguide_type
if fp.is_nonzero(height):
sbend_type = waveguide_type or dev.waveguide_type
sbend = SBend(
height=height,
bend_degrees=bend_degrees,
max_distance=length - min_io_connection_length,
waveguide_type=sbend_type,
bend_factory=bend_factory or (bend_factories and bend_factories(sbend_type)) or sbend_type.bend_factory,
)
sbend_distance = abs(sbend["op_1"].position[0] - sbend["op_0"].position[0])
sbend = fp.place(sbend, "op_1" if p == "left" else "op_0", at=dev.position)
content.append(sbend)
length -= sbend_distance
end_type = sbend_type
util.links.straight(TECH, content, start=gc, length=length, link_type=connection_type, end_type=end_type)
insts += content
return insts, elems, ports
Define CompScanBuilder
class CompScanBuilder:
blocks: List[Block]
def __init__(
self,
*,
name: Optional[str] = None,
fiber_coupler_factory: Optional[FiberCouplerFactory] = None,
fiber_coupler_adapter: Optional[fp.IDevice] = None,
fiber_coupler_v_mirrored: Sequence[bool] = (False, False),
max_lines: Optional[int] = None,
width: float = 2000,
spacing: float = 127,
waveguide_type: Optional[fp.IWaveguideType] = None,
bend_degrees: Optional[float] = None,
connection_type: Optional[fp.IWaveguideType] = None,
device_connection_length: float = 20,
min_io_connection_length: float = 20,
bend_factory: Optional[fp.IBendWaveguideFactory] = None,
bend_factories: Optional[Callable[[fp.IWaveguideType], fp.IBendWaveguideFactory]] = None,
) -> None:
self.name = name
self.fiber_coupler_factory = fiber_coupler_factory
self.fiber_coupler_adapter = fiber_coupler_adapter
self.fiber_coupler_v_mirrored = fiber_coupler_v_mirrored
self.max_lines = max_lines
self.width = width
self.spacing = spacing
self.waveguide_type = waveguide_type
self.bend_degrees = bend_degrees
self.connection_type = connection_type
self.device_connection_length = device_connection_length
self.min_io_connection_length = min_io_connection_length
self.bend_factory = bend_factory
self.bend_factories = bend_factories
self.blocks = []
def build(self, transform: fp.Affine2D = fp.Affine2D.identity()):
params = dict(
name=self.name or "",
fiber_coupler_factory=self.fiber_coupler_factory,
fiber_coupler_adapter=self.fiber_coupler_adapter,
fiber_coupler_v_mirrored=self.fiber_coupler_v_mirrored,
max_lines=self.max_lines,
blocks=self.blocks,
width=self.width,
spacing=self.spacing,
waveguide_type=self.waveguide_type,
connection_type=self.connection_type,
device_connection_length=self.device_connection_length,
min_io_connection_length=self.min_io_connection_length,
bend_factory=self.bend_factory,
bend_factories=self.bend_factories,
transform=transform,
)
for key, value in list(params.items()):
if value is None:
del params[key]
return CompScan(**params)
def add_block(
self,
content: fp.IDevice,
*,
offset: Tuple[float, float] = (0, 0),
repeat: int = 1,
bend_factory: Optional[fp.IBendWaveguideFactory] = None,
bend_factories: Optional[Callable[[fp.IWaveguideType], fp.IBendWaveguideFactory]] = None,
):
self.blocks.append(Block(content, offset=offset, repeat=repeat, bend_factory=bend_factory, bend_factories=bend_factories))
def add_alignment(self, *, offset: Tuple[float, float] = (0, 0), waveguide_type: Optional[fp.IWaveguideType] = None):
waveguide_type = waveguide_type or self.waveguide_type
assert waveguide_type is not None, "waveguide_type must be supplied"
self.blocks.append(Alignment(offset=offset, waveguide_type=waveguide_type))
def add_title(self, content: str, *, gap: float = 20, font_size: float = 5, layer: fp.ILayer):
self.blocks.append(Title(content, gap=gap, font_size=font_size, layer=layer))
def add_blank(self, left: int = 1, right: int = 1):
self.blocks.append(Blank(left=left, right=right))
Create the component and export the layout
if __name__ == "__main__":
from pathlib import Path
gds_file = Path(__file__).parent / "local" / Path(__file__).with_suffix(".gds").name
library = fp.Library()
TECH = get_technology()
# =============================================================
from gpdk.components.fixed_terminator_te_1550.fixed_terminator_te_1550 import Fixed_Terminator_TE_1550
from gpdk.components.ring_filter.ring_filter import RingFilter
from gpdk.components.ring_resonator.ring_resonator import RingResonator
from gpdk.routing.extended.extended import Extended
from gpdk.technology.waveguide_factory import EulerBendFactory
from gpdk.components.grating_coupler.grating_coupler import GratingCoupler
def gc_factory(at: fp.IRay, device: fp.IDevice):
gc = GratingCoupler() # type: ignore
return gc, "op_0"
def bend_factories(waveguide_type: fp.IWaveguideType):
if waveguide_type == TECH.WG.FWG.C.WIRE:
return EulerBendFactory(radius_min=35, l_max=35, waveguide_type=waveguide_type)
elif waveguide_type == TECH.WG.SWG.C.EXPANDED:
return EulerBendFactory(radius_min=55, l_max=35, waveguide_type=waveguide_type)
elif waveguide_type == TECH.WG.SWG.C.WIRE:
return EulerBendFactory(radius_min=45, l_max=35, waveguide_type=waveguide_type)
return waveguide_type.bend_factory
def get_ring_resonator_with_terminator(ring_radius: float):
terminator = Fixed_Terminator_TE_1550(waveguide_type=TECH.WG.FWG.C.WIRE)
ring_resonator = RingResonator(ring_radius=ring_radius, ring_type=TECH.WG.FWG.C.WIRE)
return Extended(
device=fp.Connected(
joints=[ring_resonator["op_2"] <= terminator["op_0"]], ports=[ring_resonator["op_0"], ring_resonator["op_1"], ring_resonator["op_3"]]
),
lengths={"*": 20},
)
blocks = [
Alignment(
waveguide_type=TECH.WG.FWG.C.WIRE,
),
Title(
"TEST TITLE",
layer=TECH.LAYER.LABEL_DRW,
),
Block(get_ring_resonator_with_terminator(25)),
Blank(left=0, right=1),
Block(
get_ring_resonator_with_terminator(50),
repeat=3,
),
Block(
get_ring_resonator_with_terminator(75),
repeat=3,
),
Block(get_ring_resonator_with_terminator(90), bend_factories=bend_factories),
Blank(left=0, right=1),
Block(
RingFilter(
ring_radius=25,
waveguide_type=TECH.WG.FWG.C.WIRE,
)
),
Block(
RingResonator(ring_radius=90, ring_type=TECH.WG.FWG.C.WIRE),
repeat=3,
),
]
def term_factory(at: fp.IRay, device: fp.IDevice):
from gpdk.components.fixed_terminator_te_1550.fixed_terminator_te_1550 import Fixed_Terminator_TE_1550
instance = Fixed_Terminator_TE_1550().h_mirrored() # type: ignore
return instance, "op_0"
library += CompScan(name="comp_scan", spacing=255, width=2000, blocks=blocks, fiber_coupler_factory=term_factory)
library += CompScan(name="comp_scan", spacing=255, width=2000, blocks=blocks, fiber_coupler_adapter=Fixed_Terminator_TE_1550())
library += CompScan(name="comp_scan", spacing=255, width=2000, blocks=blocks, bend_factories=bend_factories, fiber_coupler_factory=gc_factory)
library += CompScan(
name="comp_scan",
spacing=255,
width=2000,
blocks=blocks,
bend_factories=bend_factories,
waveguide_type=TECH.WG.SWG.C.EXPANDED,
bend_factory=TECH.WG.SWG.C.WIRE.bend_factory,
connection_type=TECH.WG.MWG.C.WIRE,
fiber_coupler_factory=gc_factory,
)
# =============================================================
fp.export_gds(library, file=gds_file)
# fp.plot(library)
Script Description
The first function get_ring_resonator_with_terminator defines the ring resonator cavity to be placed in the middle.
Then 10 modules are called through blocks, in the order of script definition:
waveguide connection
text label
1 ring resonator cavity (radius 25)
right GC (blank in the right)
3 ring resonator cavity (radius 50)
3 ring resonator cavity (radius 75)
1 ring resonator cavity (radius 90)
right GC (blank in the right)
1 ring filter (radius 25)
3 ring resonator cavity (radius 90)
The 10 modules will be placed in the layout from the bottom up.
Browse the script will find that in addition to the CompScan class also defines the CompScanBuilder class.
CompScan defines the steps and parameters of graphics generation in detail , the code is intuitive and readable; CompScanBuilder defines the part of the graphics generation can be summarized and extracted, thus the code is more concise.
GDS Layout
Open the generated comp_scan.gds file to see that there are several lines of GratingCoupler placed in the layout with equal spacing from bottom to top.
Line 2, Title, is a text label and there is no port for connection, so there is no GratingCoupler and waveguide for connection on the left and right sides.
Lines 4, 8, 13 and 14 are defined according to the script, and there is no GratingCoupler and waveguide on the right side.
The middle part has the called modules from bottom to top, and is connected to the left and right GratingCoupler by straight waveguides and bend.