Skip to content

Commit 9cd1fdf

Browse files
committed
Automate KLayout DRC and clean HW02 layout
1 parent 334a506 commit 9cd1fdf

3 files changed

Lines changed: 227 additions & 147 deletions

File tree

marimo_course/assignments/solutions/hw02_mzi_layout_solution.py

Lines changed: 48 additions & 147 deletions
Original file line numberDiff line numberDiff line change
@@ -223,16 +223,19 @@ def _(
223223
splitter = import_gds(splitter_path, cellname=splitter_cell, rename_duplicated_cells=True)
224224
add_ports_from_markers_center(splitter, pin_layer=(1, 10), port_layer=(1, 0))
225225
auto_rename_ports_orientation(splitter)
226+
splitter.remove_layers(layers=[(1, 10), (68, 0)], recursive=True)
226227
splitter.name = f"{splitter_cell}_splitter"
227228

228229
combiner = import_gds(combiner_path, cellname=combiner_cell, rename_duplicated_cells=True)
229230
add_ports_from_markers_center(combiner, pin_layer=(1, 10), port_layer=(1, 0))
230231
auto_rename_ports_orientation(combiner)
232+
combiner.remove_layers(layers=[(1, 10), (68, 0)], recursive=True)
231233
combiner.name = f"{combiner_cell}_combiner"
232234

233235
gc = import_gds(gc_path, cellname=gc_cell, rename_duplicated_cells=True)
234236
add_ports_from_markers_center(gc, pin_layer=(1, 10), port_layer=(1, 0))
235237
auto_rename_ports_orientation(gc)
238+
gc.remove_layers(layers=[(68, 0)], recursive=True)
236239
gc.name = f"{gc_cell}_gc"
237240

238241
mzi = gf.components.mzi(
@@ -257,166 +260,66 @@ def _(
257260
# 0 -------- 2
258261

259262
c = gf.Component()
263+
260264
def _ports_list(component):
261265
ports = component.ports
262266
return list(ports.values()) if hasattr(ports, "values") else list(ports)
263267

264268
def _pick_gc_port(component):
265269
ports = _ports_list(component)
266-
optical = [p for p in ports if getattr(p, "port_type", None) in (None, "optical")]
270+
optical = [
271+
p for p in ports if getattr(p, "port_type", None) in (None, "optical")
272+
]
267273
ports = optical or ports
268274
for port in ports:
269275
if port.orientation is not None and abs((port.orientation - 180) % 360) < 1e-3:
270276
return port
271277
return ports[0]
272278

273-
def _port_with_orientation(port, orientation):
274-
if port.orientation is not None:
275-
return port
276-
return gf.Port(
277-
name=port.name,
278-
center=port.center,
279-
width=port.width,
280-
orientation=orientation,
281-
layer=port.layer,
282-
port_type=port.port_type,
283-
)
284-
285-
mzi_ref = c << mzi
286-
mzi_ports = _ports_list(mzi_ref)
287-
if hasattr(mzi_ref.ports, "get") and all(
288-
name in mzi_ref.ports for name in ("o1", "o2", "o3")
289-
):
290-
mzi_input = mzi_ref.ports["o1"]
291-
mzi_out_through = mzi_ref.ports["o2"]
292-
mzi_out_cross = mzi_ref.ports["o3"]
279+
mzi_ports = _ports_list(mzi)
280+
if hasattr(mzi.ports, "get") and all(name in mzi.ports for name in ("o1", "o2", "o3")):
281+
mzi_input = mzi.ports["o1"]
282+
mzi_out_through = mzi.ports["o2"]
283+
mzi_out_cross = mzi.ports["o3"]
293284
else:
294285
optical_ports = [
295-
p
296-
for p in mzi_ports
297-
if getattr(p, "port_type", None) in (None, "optical")
286+
p for p in mzi_ports if getattr(p, "port_type", None) in (None, "optical")
298287
]
299288
mzi_ports = optical_ports or mzi_ports
300289
mzi_input = min(mzi_ports, key=lambda p: p.center[0])
301290
mzi_outputs = [p for p in mzi_ports if p is not mzi_input]
302-
mzi_outputs = [p for p in mzi_outputs if p.orientation in (0, None)]
303-
if len(mzi_outputs) < 2:
304-
max_x = max(p.center[0] for p in mzi_ports)
305-
x_tol = 1.0
306-
mzi_outputs = [
307-
p for p in mzi_ports if abs(p.center[0] - max_x) <= x_tol
308-
]
309-
mzi_outputs = [p for p in mzi_outputs if p is not mzi_input]
310291
mzi_outputs = sorted(mzi_outputs, key=lambda p: p.center[1])
311292
if len(mzi_outputs) >= 2:
312293
mzi_out_cross, mzi_out_through = mzi_outputs[0], mzi_outputs[-1]
313294
else:
314295
mzi_out_through = mzi_outputs[0]
315296
mzi_out_cross = mzi_outputs[0]
316297

317-
mzi_input = _port_with_orientation(mzi_input, 180)
318-
mzi_out_through = _port_with_orientation(mzi_out_through, 0)
319-
mzi_out_cross = _port_with_orientation(mzi_out_cross, 0)
320-
321298
gc_port = _pick_gc_port(gc)
322-
gc_pitch = 127.0
323-
gc_offset_x = 120.0
324-
325-
def place_gc_at(x: float, y: float):
326-
gc_ref = c << gc
327-
port_name = gc_port.name
328-
gc_ref_port = gc_ref.ports[port_name]
329-
if gc_ref_port.orientation is not None:
330-
rotation = (180 - gc_ref_port.orientation) % 360
331-
if rotation:
332-
gc_ref.rotate(rotation, center=gc_ref_port.center)
333-
gc_ref_port = gc_ref.ports[port_name]
334-
gc_ref.move((x - gc_ref_port.center[0], y - gc_ref_port.center[1]))
335-
gc_ref_port = gc_ref.ports[port_name]
336-
gc_ref_port = _port_with_orientation(gc_ref_port, 180)
337-
return gc_ref, gc_ref_port
338-
339-
mzi_bbox = (
340-
mzi_ref.bbox()
341-
if callable(getattr(mzi_ref, "bbox", None))
342-
else mzi_ref.bbox
343-
)
344-
if hasattr(mzi_bbox, "left"):
345-
mzi_xmin, mzi_ymin, mzi_xmax, mzi_ymax = (
346-
mzi_bbox.left,
347-
mzi_bbox.bottom,
348-
mzi_bbox.right,
349-
mzi_bbox.top,
350-
)
351-
else:
352-
(mzi_xmin, mzi_ymin), (mzi_xmax, mzi_ymax) = mzi_bbox[0], mzi_bbox[1]
353-
gc_column_x = mzi_xmax + gc_offset_x
354-
gc_center_y = mzi_input.center[1]
355-
356-
# Place input GC on top, outputs on middle/bottom to reduce crossings.
357-
gc_in, gc_in_port = place_gc_at(
358-
gc_column_x,
359-
gc_center_y + gc_pitch,
360-
)
361-
gc_out_through, gc_out_through_port = place_gc_at(
362-
gc_column_x,
363-
gc_center_y,
364-
)
365-
gc_out_cross, gc_out_cross_port = place_gc_at(
366-
gc_column_x,
367-
gc_center_y - gc_pitch,
368-
)
369-
370-
def route_on_track(start_port, end_port, track_y):
371-
start_x, start_y = start_port.center
372-
end_x, end_y = end_port.center
373-
x1 = start_x + 20.0
374-
x2 = end_x - 20.0
375-
if x2 <= x1:
376-
x_mid = (start_x + end_x) / 2
377-
waypoints = [(x_mid, start_y), (x_mid, track_y), (x_mid, end_y)]
378-
else:
379-
waypoints = [(x1, start_y), (x1, track_y), (x2, track_y), (x2, end_y)]
380-
return gf.routing.route_single(
381-
c,
382-
start_port,
383-
end_port,
384-
cross_section=xs,
385-
waypoints=waypoints,
386-
start_straight_length=0.0,
387-
end_straight_length=0.0,
388-
)
389-
390-
# Route on explicit tracks to avoid overlaps and mirror the example's intent.
391-
route_top_y = mzi_ymax + 40.0
392-
route_mid_y = gc_center_y
393-
route_bot_y = mzi_ymin - 40.0
394-
395-
input_stub_x = mzi_xmin - 40.0
396-
input_waypoints = [
397-
(gc_in_port.center[0] - 20.0, gc_in_port.center[1]),
398-
(gc_in_port.center[0] - 20.0, route_top_y),
399-
(input_stub_x, route_top_y),
400-
(input_stub_x, mzi_input.center[1]),
401-
(mzi_input.center[0] - 5.0, mzi_input.center[1]),
402-
]
403-
gf.routing.route_single(
404-
c,
405-
gc_in_port,
406-
mzi_input,
407-
cross_section=xs,
408-
waypoints=input_waypoints,
299+
gc_port_name = gc_port.name
300+
if gc.ports[gc_port_name].orientation is not None:
301+
rotation = (180 - gc.ports[gc_port_name].orientation) % 360
302+
if rotation:
303+
gc = gf.functions.rotate(gc, rotation)
304+
305+
port_names = [mzi_input.name, mzi_out_through.name, mzi_out_cross.name]
306+
if any(name is None for name in port_names):
307+
port_names = None
308+
309+
c = gf.routing.add_fiber_array(
310+
component=mzi,
311+
grating_coupler=gc,
312+
gc_port_name=gc_port_name,
313+
gc_port_name_fiber=gc_port_name,
314+
port_names=port_names,
315+
pitch=127.0,
316+
radius=20.0,
317+
with_loopback=False,
409318
start_straight_length=0.0,
410319
end_straight_length=0.0,
320+
force_manhattan=True,
411321
)
412322

413-
route_on_track(mzi_out_through, gc_out_through_port, route_mid_y)
414-
route_on_track(mzi_out_cross, gc_out_cross_port, route_bot_y)
415-
416-
c.add_port("o_in", port=gc_in_port)
417-
c.add_port("o_through", port=gc_out_through_port)
418-
c.add_port("o_cross", port=gc_out_cross_port)
419-
420323
import matplotlib.pyplot as plt_layout
421324
fig_layout = c.plot()
422325
plt_layout.show()
@@ -463,22 +366,7 @@ def _(c, mo):
463366
mo.md("No layout available yet.")
464367
)
465368

466-
pin_layer = (1, 10)
467-
pin_w = 2.0
468-
pin_h = 1.0
469-
ports = c.ports
470-
port_list = list(ports.values()) if hasattr(ports, "values") else list(ports)
471-
for port in port_list:
472-
cx, cy = port.center
473-
c.add_polygon(
474-
[
475-
(cx - pin_w / 2, cy - pin_h / 2),
476-
(cx + pin_w / 2, cy - pin_h / 2),
477-
(cx + pin_w / 2, cy + pin_h / 2),
478-
(cx - pin_w / 2, cy + pin_h / 2),
479-
],
480-
layer=pin_layer,
481-
)
369+
# PinRec markers are provided by the grating coupler cells.
482370

483371
layout_bbox = c.bbox() if callable(getattr(c, "bbox", None)) else c.bbox
484372
if hasattr(layout_bbox, "left"):
@@ -623,11 +511,24 @@ def _(c, export_gds, mo):
623511
mo.md("No layout available yet.")
624512
)
625513
import pathlib
514+
import subprocess
626515

627516
out = pathlib.Path(str(export_gds)).expanduser()
628517
out.parent.mkdir(parents=True, exist_ok=True)
629518
written = c.write_gds(out)
630-
mo.md(f"Wrote: `{written}`")
519+
script = pathlib.Path(__file__).resolve().parents[2] / "scripts" / "run_klayout_drc.sh"
520+
report = out.with_suffix(".lyrdb")
521+
if script.exists():
522+
try:
523+
subprocess.run(
524+
[str(script), str(out), str(report)],
525+
check=True,
526+
)
527+
mo.md(f"Wrote: `{written}`\n\nDRC report: `{report}`")
528+
except subprocess.CalledProcessError as exc:
529+
mo.md(f"Wrote: `{written}`\n\nDRC failed: `{exc}`")
530+
else:
531+
mo.md(f"Wrote: `{written}`\n\nDRC script not found: `{script}`")
631532
return
632533

633534

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
2+
# Basic DRC deck for SiEPIC_EBeam_PDK
3+
# Lukas Chrostowski, 2016
4+
# Mustafa Hammood, 2020 update
5+
# Lukas Chrostowski, 2023 update
6+
7+
# Read about DRC scripts in the User Manual under "Design Rule Check (DRC)"
8+
# http://klayout.de/doc/manual/drc_basic.html
9+
10+
# Added SiN based on publication: https://doi.org/10.1117/12.2650447
11+
12+
source($input)
13+
14+
report("SiEPIC-EBeam-PDK DRC", $output)
15+
16+
# Layers:
17+
LayerSi=input(1,0)
18+
#LayerSip6nm=input(31,0)
19+
#LayerSi_rib=input(2,0)
20+
LayerSiN = input(4,0)
21+
DevRec=input(68,0)
22+
PinRec=input(1,10)
23+
LayerFP=input(99)
24+
LayerM1=input(11,0)
25+
LayerM2=input(12,0)
26+
LayerMLOpen=input(13,0)
27+
LayerDeepTrench=input(201,0)
28+
#LayerVC=input(40,0)
29+
#LayerN=input(20,0)
30+
#LayerNpp=input(24,0)
31+
32+
#################
33+
# non-physical checks
34+
#################
35+
36+
# Check device overlaps (functional check)
37+
overlaps = DevRec.merged(2)
38+
output(overlaps, "Devices","Devices cannot be overlapping")
39+
40+
# make sure the devices are within the floor plan layer region;
41+
LayerSi.outside(LayerFP).output("Boundary","devices are out of boundary")
42+
LayerSiN.outside(LayerFP).output("Boundary","devices are out of boundary")
43+
44+
#################
45+
# physical checks
46+
#################
47+
48+
tol = 1e-3 # users typically shoot for exactly the min features
49+
# for curves, this leads to lots of false errors.
50+
51+
# minimum feature size of Si is 70 nm and minimum exclusion between Si is 70 nm
52+
LayerSi.width(0.07-tol, angle_limit(80)).output("Si_width","Si minimum feature size violation; min 60 nm")
53+
#LayerSip6nm.width(0.07-tol, angle_limit(80)).output("Sip6nm_width","Si minimum feature size violation; min 60 nm")
54+
LayerSi.space(0.07-tol, angle_limit(80)).output("Si_space","Si minimum space violation; min 60 nm")
55+
#LayerSip6nm.space(0.07-tol, angle_limit(80)).output("Sip6nm_space","Si minimum space violation; min 60 nm")
56+
57+
# minimum feature size of SiN is 120 nm and minimum exclusion between Si is 120 nm
58+
LayerSiN.width(0.12-tol, angle_limit(80)).output("SiN_width","SiN minimum feature size violation; min 120 nm")
59+
LayerSiN.space(0.12-tol, angle_limit(80)).output("SiN_space","SiN minimum space violation; min 120 nm")
60+
61+
# minimum feature size of Si rib is 100 nm and minimum exclusion between Si is 60 nm
62+
#LayerSi_rib.width(0.1-tol, angle_limit(80)).output("Si_rib_width","Si_rib minimum feature size violation; min 100 nm")
63+
64+
# Metal rules based on the metalization process provided by Applied Nanotools, based on contact optical lithography.
65+
# Metal heater, M1
66+
LayerM1.width(3.0-tol, angle_limit(70)).output("M1_width","M1 minimum feature size violation; min 3 µm")
67+
LayerM1.space(3.0-tol).output("M1_space","M1 minimum space violation; min 3 µm")
68+
69+
# Metal routing/contact, M2
70+
LayerM2.width(5.0-tol, angle_limit(70)).output("M2_width","M2 minimum feature size violation; min 5 µm")
71+
LayerM2.space(8.0-tol).output("M2_space","M2 minimum space violation; min 8 µm")
72+
73+
# Metal pad air opening, MLOpen
74+
LayerMLOpen.width(10.0-tol).output("MLOpen_width","MLOpen minimum feature size violation; min 10 µm")
75+
LayerMLOpen.space(10.0-tol).output("MLOpen_space","MLOpen minimum space violation; min 10 µm")
76+
77+
# Doping rules based on the experimental process
78+
# Oxide contact vias
79+
#LayerVC.width(5.0-tol, angle_limit(70)).output("VC_width","VC minimum feature size violation; min 5 µm")
80+
#LayerVC.space(5.0-tol).output("VC_space","VC minimum space violation; min 5 µm")
81+
82+
# NPP Doping
83+
#LayerNpp.width(2.0-tol, angle_limit(70)).output("Npp_width","Npp Doping minimum feature size violation; min 2 µm")
84+
#LayerNpp.space(2.0-tol).output("Npp_space","Npp minimum space violation; min 2 µm")
85+
86+
# N Doping
87+
#LayerN.width(2.0-tol, angle_limit(70)).output("N_width","N Doping minimum feature size violation; min 2 µm")
88+
#LayerN.space(2.0-tol).output("N_space","N minimum space violation; min 2 µm")
89+
90+
# minimum separation between NPP and Si
91+
#LayerNpp.separation(LayerSi, 2.0-tol).output("Npp_Si_separation","Npp-Si minimum separation violation; min 2 µm")
92+
93+
# minimum separation between VC and Si
94+
#LayerVC.separation(LayerSi, 3.0-tol).output("VC_Si_separation","VC-Si minimum separation violation; min 3 µm")
95+
96+
# minimum overlap rules:
97+
LayerM2.overlap(LayerM1,3.0-tol).output("M2_M1_overlap","M2 minimum overlap with M1 violation; min 3 µm")
98+
#LayerM2.overlap(LayerVC, 5.0-tol).output("M2_VC_overlap","Metal2-VC minimum overlap violation; min 5 µm")
99+
#LayerN.overlap(LayerNpp, 3.0-tol).output("N_Npp_overlap","N-Npp minimum overlap violation; min 3 µm")
100+
#LayerVC.overlap(LayerNpp, 5.0-tol).output("VC_Npp_overlap","VC-Npp minimum overlap violation; min 5 µm")
101+
102+
# minimum inclusion rules:
103+
#LayerM2.enclosing(LayerVC, 1.0-tol).output("M2_VC_enclosure","M2-VC minimum enclosure violation; min 1 µm")
104+
#LayerSi_rib.enclosing(LayerVC, 1.0-tol).output("Si_rib_VC_enclosure","Si_rib-VC minimum enclosure violation; min 1 µm")
105+
106+
107+
# Waveguide checks:
108+
# disconnects, mismatched
109+
110+
PinRec.not_inside(LayerSi).and(PinRec.not_inside(LayerSiN))
111+
.output( "SiEPIC-1a" , "Warning: Possible waveguide mismatch or waveguide disconnect: PinRec must enclose only one waveguide material." )
112+
113+
LayerDeepTrench.separation(LayerM2, 20.0-tol).output("DT_Metal_separation","DT-Metal minimum separation violation; min 20 µm")
114+

0 commit comments

Comments
 (0)