laylight 0.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- laylight/__init__.py +19 -0
- laylight/candy.py +39 -0
- laylight/main.py +6 -0
- laylight/placable.py +176 -0
- laylight/render.py +194 -0
- laylight/render_all.py +239 -0
- laylight/structure/__init__.py +19 -0
- laylight/structure/abstract.py +57 -0
- laylight/structure/connection.py +261 -0
- laylight/structure/coupler.py +637 -0
- laylight/structure/crossing.py +80 -0
- laylight/structure/curve.py +242 -0
- laylight/structure/cutback.py +121 -0
- laylight/structure/grating.py +656 -0
- laylight/structure/metal.py +232 -0
- laylight/structure/mmi.py +389 -0
- laylight/structure/mzi.py +134 -0
- laylight/structure/mzi_rect.py +310 -0
- laylight/structure/packaging.py +144 -0
- laylight/structure/prober.py +146 -0
- laylight/structure/racetrack.py +169 -0
- laylight/structure/ring.py +215 -0
- laylight/structure/spiral.py +196 -0
- laylight/structure/taper.py +89 -0
- laylight/structure/transition.py +63 -0
- laylight/structure/wg.py +199 -0
- laylight/technology.py +21 -0
- laylight-0.0.0.dist-info/METADATA +14 -0
- laylight-0.0.0.dist-info/RECORD +30 -0
- laylight-0.0.0.dist-info/WHEEL +4 -0
laylight/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from . import structure
|
|
2
|
+
from .render import (
|
|
3
|
+
LayoutRenderError,
|
|
4
|
+
RenderArtifacts,
|
|
5
|
+
execute_layout_code,
|
|
6
|
+
export_layout_png,
|
|
7
|
+
new_layout,
|
|
8
|
+
render_code_to_files,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"LayoutRenderError",
|
|
13
|
+
"RenderArtifacts",
|
|
14
|
+
"execute_layout_code",
|
|
15
|
+
"export_layout_png",
|
|
16
|
+
"new_layout",
|
|
17
|
+
"render_code_to_files",
|
|
18
|
+
"structure",
|
|
19
|
+
]
|
laylight/candy.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import klayout.db as kdb
|
|
2
|
+
|
|
3
|
+
from .technology import Technology
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Trans:
|
|
7
|
+
def __init__(self):
|
|
8
|
+
self.t = kdb.DCplxTrans()
|
|
9
|
+
|
|
10
|
+
def move(self, dx: float, dy: float):
|
|
11
|
+
self.t = self.t * kdb.DCplxTrans(1, 0, False, dx, dy)
|
|
12
|
+
return self
|
|
13
|
+
|
|
14
|
+
def rotate(self, degree: float):
|
|
15
|
+
self.t = self.t * kdb.DCplxTrans(1, degree, False, 0, 0)
|
|
16
|
+
return self
|
|
17
|
+
|
|
18
|
+
def trans(self):
|
|
19
|
+
return self.t
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def highlight(
|
|
23
|
+
points: list[kdb.DPoint],
|
|
24
|
+
cell: kdb.Cell,
|
|
25
|
+
size=1,
|
|
26
|
+
layer=Technology.LAYER_PORT,
|
|
27
|
+
text: str | None = None,
|
|
28
|
+
add_index: bool = False,
|
|
29
|
+
):
|
|
30
|
+
for out in points:
|
|
31
|
+
cell.shapes(layer).insert(
|
|
32
|
+
kdb.DBox(out.x - size, out.y + size, out.x + size, out.y - size)
|
|
33
|
+
)
|
|
34
|
+
if text is not None:
|
|
35
|
+
if add_index:
|
|
36
|
+
tt = f"{text}_{points.index(out)}"
|
|
37
|
+
else:
|
|
38
|
+
tt = text
|
|
39
|
+
cell.shapes(layer).insert(kdb.DText(tt, kdb.DTrans(out.x, out.y)))
|
laylight/main.py
ADDED
laylight/placable.py
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
import klayout.db as kdb
|
|
4
|
+
|
|
5
|
+
from .structure.abstract import AbstractDevice, AdhocDevice
|
|
6
|
+
|
|
7
|
+
type PlaceAt = Placer.OutPort | Placer.Position | Placer.AbsPos | Placer.Trans
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Placer:
|
|
11
|
+
_x = 0
|
|
12
|
+
_y = 0
|
|
13
|
+
_r = 0
|
|
14
|
+
_mirror = False
|
|
15
|
+
_sp: kdb.DCplxTrans = kdb.DCplxTrans()
|
|
16
|
+
_dev: AbstractDevice | None = None
|
|
17
|
+
parent: "Placer | None" = None
|
|
18
|
+
_placed = False
|
|
19
|
+
_as_output = False
|
|
20
|
+
_as_input = False
|
|
21
|
+
_anchor: str | None = None
|
|
22
|
+
_dummy = False
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class OutPort:
|
|
26
|
+
idx: int = 0
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class Position:
|
|
30
|
+
x: float = 0
|
|
31
|
+
y: float = 0
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class AbsPos:
|
|
35
|
+
x: float = 0
|
|
36
|
+
y: float = 0
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class Trans:
|
|
40
|
+
t: kdb.DCplxTrans
|
|
41
|
+
|
|
42
|
+
def __init__(self, dev: AbstractDevice | None = None):
|
|
43
|
+
self._dev = dev
|
|
44
|
+
self._anchor = None
|
|
45
|
+
|
|
46
|
+
def x(self, x: float):
|
|
47
|
+
self._x = x
|
|
48
|
+
return self
|
|
49
|
+
|
|
50
|
+
def y(self, y: float):
|
|
51
|
+
self._y = y
|
|
52
|
+
return self
|
|
53
|
+
|
|
54
|
+
def move(self, dx: float, dy: float):
|
|
55
|
+
self._x += dx
|
|
56
|
+
self._y += dy
|
|
57
|
+
return self
|
|
58
|
+
|
|
59
|
+
def rotate(self, r: float):
|
|
60
|
+
self._r = r
|
|
61
|
+
return self
|
|
62
|
+
|
|
63
|
+
def mirror(self):
|
|
64
|
+
self._mirror = True
|
|
65
|
+
return self
|
|
66
|
+
|
|
67
|
+
def _translate(self):
|
|
68
|
+
base = kdb.DCplxTrans(1, self._r, self._mirror, self._x, self._y)
|
|
69
|
+
if self._sp is not None:
|
|
70
|
+
return self._sp * base
|
|
71
|
+
return base
|
|
72
|
+
|
|
73
|
+
def then(
|
|
74
|
+
self,
|
|
75
|
+
dev: AbstractDevice,
|
|
76
|
+
at: PlaceAt = OutPort(0),
|
|
77
|
+
as_output: bool = False,
|
|
78
|
+
as_input=False,
|
|
79
|
+
anchor: str | None = None,
|
|
80
|
+
dummy=False,
|
|
81
|
+
):
|
|
82
|
+
np = Placer(dev)
|
|
83
|
+
np._as_output = as_output
|
|
84
|
+
np._as_input = as_input
|
|
85
|
+
np._anchor = anchor
|
|
86
|
+
np._dummy = dummy
|
|
87
|
+
|
|
88
|
+
match at:
|
|
89
|
+
case Placer.OutPort(idx):
|
|
90
|
+
if self._dev is None:
|
|
91
|
+
outputs = [kdb.DCplxTrans()]
|
|
92
|
+
else:
|
|
93
|
+
outputs = self._dev.output()
|
|
94
|
+
child_inputs = dev.input()
|
|
95
|
+
child_input = child_inputs[0] if child_inputs else kdb.DCplxTrans()
|
|
96
|
+
np._sp = self._translate() * outputs[idx] * child_input.inverted()
|
|
97
|
+
case Placer.AbsPos(x, y):
|
|
98
|
+
np._sp = kdb.DCplxTrans(1, 0, False, x, y)
|
|
99
|
+
case Placer.Trans(t):
|
|
100
|
+
np._sp = self._translate() * t
|
|
101
|
+
case Placer.Position(x, y):
|
|
102
|
+
np._sp = self._translate() * kdb.DCplxTrans(1, 0, False, x, y)
|
|
103
|
+
|
|
104
|
+
np.parent = self
|
|
105
|
+
return np
|
|
106
|
+
|
|
107
|
+
def _chain(self) -> list["Placer"]:
|
|
108
|
+
chain = []
|
|
109
|
+
current = self
|
|
110
|
+
while current is not None:
|
|
111
|
+
chain.append(current)
|
|
112
|
+
current = current.parent
|
|
113
|
+
chain.reverse()
|
|
114
|
+
return chain
|
|
115
|
+
|
|
116
|
+
def into(self, top: kdb.Cell) -> list[kdb.DCplxTrans]:
|
|
117
|
+
"""
|
|
118
|
+
Place the device into the top cell, and return the output DCplxTrans list.
|
|
119
|
+
"""
|
|
120
|
+
if self.parent is not None:
|
|
121
|
+
self.parent.into(top)
|
|
122
|
+
if self._placed:
|
|
123
|
+
return []
|
|
124
|
+
if self._dev is None:
|
|
125
|
+
return []
|
|
126
|
+
|
|
127
|
+
cell = self._dev.cell()
|
|
128
|
+
if not self._dummy:
|
|
129
|
+
top.insert(kdb.DCellInstArray(cell.cell_index(), self._translate()))
|
|
130
|
+
self._placed = True
|
|
131
|
+
|
|
132
|
+
outputs = self._dev.output()
|
|
133
|
+
return [self._translate() * out for out in outputs]
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def ports_input(self) -> dict[int, list[kdb.DCplxTrans]]:
|
|
137
|
+
result: dict[int, list[kdb.DCplxTrans]] = {}
|
|
138
|
+
for depth, placer in enumerate(self._chain()):
|
|
139
|
+
if placer._dev is not None and placer._as_input:
|
|
140
|
+
inputs = placer._dev.input()
|
|
141
|
+
result[depth] = [placer._translate() * inp for inp in inputs]
|
|
142
|
+
|
|
143
|
+
return result
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def ports_output(self) -> dict[int, list[kdb.DCplxTrans]]:
|
|
147
|
+
result: dict[int, list[kdb.DCplxTrans]] = {}
|
|
148
|
+
for depth, placer in enumerate(self._chain()):
|
|
149
|
+
if placer._dev is not None and placer._as_output:
|
|
150
|
+
outputs = placer._dev.output()
|
|
151
|
+
result[depth] = [placer._translate() * out for out in outputs]
|
|
152
|
+
|
|
153
|
+
return result
|
|
154
|
+
|
|
155
|
+
def anchors(self) -> dict[str, "Placer"]:
|
|
156
|
+
result: dict[str, Placer] = {}
|
|
157
|
+
for placer in self._chain():
|
|
158
|
+
if placer._dev is None or placer._anchor is None:
|
|
159
|
+
continue
|
|
160
|
+
if placer._anchor in result:
|
|
161
|
+
raise ValueError(f"Duplicate anchor key: {placer._anchor}")
|
|
162
|
+
|
|
163
|
+
result[placer._anchor] = placer
|
|
164
|
+
|
|
165
|
+
return result
|
|
166
|
+
|
|
167
|
+
def as_dev(self, layout: kdb.Layout, name: str):
|
|
168
|
+
cell = layout.create_cell(name)
|
|
169
|
+
dev = AdhocDevice(cell, layout=layout)
|
|
170
|
+
final_outputs = self.into(dev.cell())
|
|
171
|
+
endpoints = [t for ts in self.ports_output.values() for t in ts]
|
|
172
|
+
dev.set_output(endpoints if endpoints else final_outputs)
|
|
173
|
+
inputs = [t for ts in self.ports_input.values() for t in ts]
|
|
174
|
+
dev.set_input(inputs)
|
|
175
|
+
|
|
176
|
+
return dev
|
laylight/render.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import tempfile
|
|
6
|
+
import traceback
|
|
7
|
+
import zipfile
|
|
8
|
+
|
|
9
|
+
import klayout.db as kdb
|
|
10
|
+
|
|
11
|
+
from .technology import Technology
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class LayoutRenderError(RuntimeError):
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class RenderArtifacts:
|
|
20
|
+
gds_path: Path
|
|
21
|
+
png_path: Path
|
|
22
|
+
zip_path: Path
|
|
23
|
+
output_dir: Path
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def new_layout(top_name: str = "TOP") -> tuple[kdb.Layout, kdb.Cell, Technology]:
|
|
27
|
+
layout = kdb.Layout()
|
|
28
|
+
layout.dbu = 0.001
|
|
29
|
+
tech = Technology()
|
|
30
|
+
top = layout.create_cell(top_name)
|
|
31
|
+
return layout, top, tech
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _build_exec_namespace(top_name: str) -> dict[str, object]:
|
|
35
|
+
layout, top, tech = new_layout(top_name)
|
|
36
|
+
return {
|
|
37
|
+
"__builtins__": __builtins__,
|
|
38
|
+
"__name__": "__laylight_render__",
|
|
39
|
+
"kdb": kdb,
|
|
40
|
+
"Technology": Technology,
|
|
41
|
+
"new_layout": new_layout,
|
|
42
|
+
"layout": layout,
|
|
43
|
+
"top": top,
|
|
44
|
+
"tech": tech,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _layout_from_result(result: object) -> kdb.Layout | None:
|
|
49
|
+
if isinstance(result, kdb.Layout):
|
|
50
|
+
return result
|
|
51
|
+
if isinstance(result, kdb.Cell):
|
|
52
|
+
return result.layout()
|
|
53
|
+
if isinstance(result, tuple):
|
|
54
|
+
for item in result:
|
|
55
|
+
layout = _layout_from_result(item)
|
|
56
|
+
if layout is not None:
|
|
57
|
+
return layout
|
|
58
|
+
|
|
59
|
+
layout = getattr(result, "layout", None)
|
|
60
|
+
if isinstance(layout, kdb.Layout):
|
|
61
|
+
return layout
|
|
62
|
+
|
|
63
|
+
cell = getattr(result, "cell", None)
|
|
64
|
+
if callable(cell):
|
|
65
|
+
candidate = cell()
|
|
66
|
+
if isinstance(candidate, kdb.Cell):
|
|
67
|
+
return candidate.layout()
|
|
68
|
+
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def execute_layout_code(code: str, *, top_name: str = "TOP") -> kdb.Layout:
|
|
73
|
+
namespace = _build_exec_namespace(top_name)
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
exec(compile(code, "<laylight-render>", "exec"), namespace, namespace)
|
|
77
|
+
except Exception as exc:
|
|
78
|
+
raise LayoutRenderError(
|
|
79
|
+
"Failed to execute laylight code.\n" + traceback.format_exc()
|
|
80
|
+
) from exc
|
|
81
|
+
|
|
82
|
+
build = namespace.get("build")
|
|
83
|
+
if callable(build):
|
|
84
|
+
try:
|
|
85
|
+
result = build()
|
|
86
|
+
except Exception as exc:
|
|
87
|
+
raise LayoutRenderError(
|
|
88
|
+
"Failed to execute build().\n" + traceback.format_exc()
|
|
89
|
+
) from exc
|
|
90
|
+
|
|
91
|
+
layout = _layout_from_result(result)
|
|
92
|
+
if layout is not None:
|
|
93
|
+
return layout
|
|
94
|
+
|
|
95
|
+
for key in ("layout", "top", "device"):
|
|
96
|
+
layout = _layout_from_result(namespace.get(key))
|
|
97
|
+
if layout is not None:
|
|
98
|
+
return layout
|
|
99
|
+
|
|
100
|
+
raise LayoutRenderError(
|
|
101
|
+
"Could not find a KLayout layout. Define a `layout` variable or a `build()` "
|
|
102
|
+
"function that returns a `kdb.Layout`, `kdb.Cell`, or a device carrying one."
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _top_cell(layout: kdb.Layout) -> kdb.Cell:
|
|
107
|
+
top_cell = layout.top_cell()
|
|
108
|
+
if top_cell is not None:
|
|
109
|
+
return top_cell
|
|
110
|
+
|
|
111
|
+
top_cells = layout.top_cells()
|
|
112
|
+
if top_cells:
|
|
113
|
+
return top_cells[0]
|
|
114
|
+
|
|
115
|
+
raise LayoutRenderError("The rendered layout does not contain any top cell.")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _png_dimensions(cell: kdb.Cell, max_pixels: int) -> tuple[int, int]:
|
|
119
|
+
box = cell.dbbox()
|
|
120
|
+
if box.empty():
|
|
121
|
+
return max_pixels, max_pixels
|
|
122
|
+
|
|
123
|
+
width = max(box.width(), 1.0)
|
|
124
|
+
height = max(box.height(), 1.0)
|
|
125
|
+
if width >= height:
|
|
126
|
+
return max_pixels, max(1, round(max_pixels * (height / width)))
|
|
127
|
+
return max(1, round(max_pixels * (width / height))), max_pixels
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def export_layout_png(
|
|
131
|
+
layout: kdb.Layout,
|
|
132
|
+
output_path: str | Path,
|
|
133
|
+
*,
|
|
134
|
+
max_pixels: int = 2048,
|
|
135
|
+
) -> Path:
|
|
136
|
+
try:
|
|
137
|
+
from klayout import lay as klay
|
|
138
|
+
except ImportError as exc:
|
|
139
|
+
raise LayoutRenderError(
|
|
140
|
+
"PNG export requires the full KLayout Python package with `klayout.lay`."
|
|
141
|
+
) from exc
|
|
142
|
+
|
|
143
|
+
output = Path(output_path)
|
|
144
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
145
|
+
|
|
146
|
+
cell = _top_cell(layout)
|
|
147
|
+
width, height = _png_dimensions(cell, max_pixels)
|
|
148
|
+
|
|
149
|
+
with tempfile.TemporaryDirectory(prefix="laylight-png-") as tmp_dir:
|
|
150
|
+
tmp_gds = Path(tmp_dir) / "preview.gds"
|
|
151
|
+
layout.write(str(tmp_gds))
|
|
152
|
+
|
|
153
|
+
view = klay.LayoutView()
|
|
154
|
+
view.load_layout(str(tmp_gds), False)
|
|
155
|
+
view.set_config("background-color", "#FFFFFF")
|
|
156
|
+
view.set_config("grid-visible", "false")
|
|
157
|
+
view.max_hier()
|
|
158
|
+
view.add_missing_layers()
|
|
159
|
+
view.active_cellview().cell = view.cellview(0).layout().top_cell()
|
|
160
|
+
view.zoom_fit()
|
|
161
|
+
view.save_image(str(output), width, height)
|
|
162
|
+
|
|
163
|
+
return output
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def render_code_to_files(
|
|
167
|
+
code: str,
|
|
168
|
+
output_dir: str | Path,
|
|
169
|
+
*,
|
|
170
|
+
file_stem: str = "layout",
|
|
171
|
+
top_name: str = "TOP",
|
|
172
|
+
png_max_pixels: int = 2048,
|
|
173
|
+
) -> RenderArtifacts:
|
|
174
|
+
output = Path(output_dir)
|
|
175
|
+
output.mkdir(parents=True, exist_ok=True)
|
|
176
|
+
|
|
177
|
+
layout = execute_layout_code(code, top_name=top_name)
|
|
178
|
+
gds_path = output / f"{file_stem}.gds"
|
|
179
|
+
png_path = output / f"{file_stem}.png"
|
|
180
|
+
zip_path = output / f"{file_stem}.zip"
|
|
181
|
+
|
|
182
|
+
layout.write(str(gds_path))
|
|
183
|
+
export_layout_png(layout, png_path, max_pixels=png_max_pixels)
|
|
184
|
+
|
|
185
|
+
with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as bundle:
|
|
186
|
+
bundle.write(gds_path, arcname=gds_path.name)
|
|
187
|
+
bundle.write(png_path, arcname=png_path.name)
|
|
188
|
+
|
|
189
|
+
return RenderArtifacts(
|
|
190
|
+
gds_path=gds_path,
|
|
191
|
+
png_path=png_path,
|
|
192
|
+
zip_path=zip_path,
|
|
193
|
+
output_dir=output,
|
|
194
|
+
)
|
laylight/render_all.py
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
import importlib
|
|
6
|
+
import inspect
|
|
7
|
+
import math
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import pkgutil
|
|
10
|
+
|
|
11
|
+
import klayout.db as kdb
|
|
12
|
+
|
|
13
|
+
from .render import export_layout_png, new_layout
|
|
14
|
+
from .structure import __name__ as structure_package_name
|
|
15
|
+
from .structure.abstract import AbstractDevice
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class RenderedDeviceImage:
|
|
20
|
+
class_name: str
|
|
21
|
+
png_path: Path
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class SkippedDevice:
|
|
26
|
+
qualified_name: str
|
|
27
|
+
reason: str
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class RenderAllResult:
|
|
32
|
+
output_dir: Path
|
|
33
|
+
rendered: list[RenderedDeviceImage]
|
|
34
|
+
skipped: list[SkippedDevice]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _port_points(transforms: list[kdb.DCplxTrans]) -> list[kdb.DPoint]:
|
|
38
|
+
return [kdb.DPoint(t.disp.x, t.disp.y) for t in transforms]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _text_region(
|
|
42
|
+
layout: kdb.Layout,
|
|
43
|
+
text: str,
|
|
44
|
+
*,
|
|
45
|
+
mag: float,
|
|
46
|
+
position: kdb.DPoint,
|
|
47
|
+
) -> kdb.Region:
|
|
48
|
+
generator = kdb.TextGenerator.default_generator()
|
|
49
|
+
region = generator.text(text, layout.dbu, mag, False, 0, 0, 0)
|
|
50
|
+
region.transform(kdb.ICplxTrans(1, 0, False, position.x / layout.dbu, position.y / layout.dbu))
|
|
51
|
+
return region
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _annotate_ports(top: kdb.Cell, device: AbstractDevice) -> None:
|
|
55
|
+
layout = top.layout()
|
|
56
|
+
box_size = max(1.5, math.sqrt(max(top.dbbox().area(), 1.0)) * 0.03)
|
|
57
|
+
text_mag = max(0.6, box_size * 0.18)
|
|
58
|
+
text_offset = kdb.DVector(box_size * 1.2, box_size * 1.2)
|
|
59
|
+
|
|
60
|
+
for prefix, transforms, layer_info in (
|
|
61
|
+
("IN", device.input(), device.tech.LAYER_PORT_IN),
|
|
62
|
+
("OUT", device.output(), device.tech.LAYER_PORT_OUT),
|
|
63
|
+
):
|
|
64
|
+
port_layer = layout.layer(layer_info)
|
|
65
|
+
for index, point in enumerate(_port_points(transforms)):
|
|
66
|
+
top.shapes(port_layer).insert(
|
|
67
|
+
kdb.DBox(
|
|
68
|
+
point.x - box_size,
|
|
69
|
+
point.y + box_size,
|
|
70
|
+
point.x + box_size,
|
|
71
|
+
point.y - box_size,
|
|
72
|
+
)
|
|
73
|
+
)
|
|
74
|
+
top.shapes(port_layer).insert(
|
|
75
|
+
_text_region(
|
|
76
|
+
layout,
|
|
77
|
+
f"{prefix}_{index}",
|
|
78
|
+
mag=text_mag,
|
|
79
|
+
position=point + text_offset,
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _load_structure_modules() -> None:
|
|
85
|
+
structure_dir = Path(__file__).with_name("structure")
|
|
86
|
+
for _, module_name, _ in pkgutil.iter_modules([str(structure_dir)]):
|
|
87
|
+
if module_name.startswith("_"):
|
|
88
|
+
continue
|
|
89
|
+
importlib.import_module(f"{structure_package_name}.{module_name}")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _device_subclasses() -> list[type[AbstractDevice]]:
|
|
93
|
+
_load_structure_modules()
|
|
94
|
+
|
|
95
|
+
discovered: list[type[AbstractDevice]] = []
|
|
96
|
+
seen: set[type[AbstractDevice]] = set()
|
|
97
|
+
stack = list(AbstractDevice.__subclasses__())
|
|
98
|
+
|
|
99
|
+
while stack:
|
|
100
|
+
cls = stack.pop()
|
|
101
|
+
if cls in seen:
|
|
102
|
+
continue
|
|
103
|
+
seen.add(cls)
|
|
104
|
+
stack.extend(cls.__subclasses__())
|
|
105
|
+
|
|
106
|
+
if cls.__module__.startswith(structure_package_name):
|
|
107
|
+
discovered.append(cls)
|
|
108
|
+
|
|
109
|
+
return sorted(discovered, key=lambda cls: (cls.__module__, cls.__name__))
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _required_init_args(cls: type[AbstractDevice]) -> list[str]:
|
|
113
|
+
required: list[str] = []
|
|
114
|
+
for param in inspect.signature(cls).parameters.values():
|
|
115
|
+
if param.name == "layout":
|
|
116
|
+
continue
|
|
117
|
+
if param.kind in (param.VAR_POSITIONAL, param.VAR_KEYWORD):
|
|
118
|
+
continue
|
|
119
|
+
if param.default is inspect._empty:
|
|
120
|
+
required.append(param.name)
|
|
121
|
+
return required
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def discover_renderable_devices() -> tuple[list[type[AbstractDevice]], list[SkippedDevice]]:
|
|
125
|
+
renderable: list[type[AbstractDevice]] = []
|
|
126
|
+
skipped: list[SkippedDevice] = []
|
|
127
|
+
|
|
128
|
+
for cls in _device_subclasses():
|
|
129
|
+
qualified_name = f"{cls.__module__}.{cls.__name__}"
|
|
130
|
+
if inspect.isabstract(cls):
|
|
131
|
+
skipped.append(
|
|
132
|
+
SkippedDevice(
|
|
133
|
+
qualified_name=qualified_name,
|
|
134
|
+
reason="class is abstract",
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
continue
|
|
138
|
+
|
|
139
|
+
required = _required_init_args(cls)
|
|
140
|
+
if required:
|
|
141
|
+
skipped.append(
|
|
142
|
+
SkippedDevice(
|
|
143
|
+
qualified_name=qualified_name,
|
|
144
|
+
reason=f"missing default args: {', '.join(required)}",
|
|
145
|
+
)
|
|
146
|
+
)
|
|
147
|
+
continue
|
|
148
|
+
|
|
149
|
+
renderable.append(cls)
|
|
150
|
+
|
|
151
|
+
return renderable, skipped
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def render_device_class(
|
|
155
|
+
cls: type[AbstractDevice],
|
|
156
|
+
output_dir: str | Path,
|
|
157
|
+
*,
|
|
158
|
+
max_pixels: int = 2048,
|
|
159
|
+
) -> RenderedDeviceImage:
|
|
160
|
+
output_path = Path(output_dir)
|
|
161
|
+
output_path.mkdir(parents=True, exist_ok=True)
|
|
162
|
+
|
|
163
|
+
layout, top, tech = new_layout(top_name=cls.__name__)
|
|
164
|
+
device = cls(layout=layout, tech=tech)
|
|
165
|
+
top.insert(kdb.DCellInstArray(device.cell().cell_index(), kdb.DCplxTrans()))
|
|
166
|
+
_annotate_ports(top, device)
|
|
167
|
+
|
|
168
|
+
png_path = output_path / f"{cls.__name__}.png"
|
|
169
|
+
export_layout_png(layout, png_path, max_pixels=max_pixels)
|
|
170
|
+
return RenderedDeviceImage(class_name=cls.__name__, png_path=png_path)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def render_all(
|
|
174
|
+
output_dir: str | Path = "build/render_all",
|
|
175
|
+
*,
|
|
176
|
+
max_pixels: int = 2048,
|
|
177
|
+
) -> RenderAllResult:
|
|
178
|
+
output_path = Path(output_dir)
|
|
179
|
+
output_path.mkdir(parents=True, exist_ok=True)
|
|
180
|
+
|
|
181
|
+
renderable, skipped = discover_renderable_devices()
|
|
182
|
+
rendered: list[RenderedDeviceImage] = []
|
|
183
|
+
|
|
184
|
+
for cls in renderable:
|
|
185
|
+
qualified_name = f"{cls.__module__}.{cls.__name__}"
|
|
186
|
+
try:
|
|
187
|
+
rendered.append(
|
|
188
|
+
render_device_class(cls, output_path, max_pixels=max_pixels)
|
|
189
|
+
)
|
|
190
|
+
except Exception as exc:
|
|
191
|
+
skipped.append(
|
|
192
|
+
SkippedDevice(
|
|
193
|
+
qualified_name=qualified_name,
|
|
194
|
+
reason=f"{type(exc).__name__}: {exc}",
|
|
195
|
+
)
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
return RenderAllResult(output_dir=output_path, rendered=rendered, skipped=skipped)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _build_arg_parser() -> argparse.ArgumentParser:
|
|
202
|
+
parser = argparse.ArgumentParser(
|
|
203
|
+
description=(
|
|
204
|
+
"Render all default-constructible AbstractDevice subclasses under "
|
|
205
|
+
"laylight.structure into PNG images."
|
|
206
|
+
)
|
|
207
|
+
)
|
|
208
|
+
parser.add_argument(
|
|
209
|
+
"--output-dir",
|
|
210
|
+
default="build/render_all",
|
|
211
|
+
help="Directory for rendered PNG files.",
|
|
212
|
+
)
|
|
213
|
+
parser.add_argument(
|
|
214
|
+
"--max-pixels",
|
|
215
|
+
type=int,
|
|
216
|
+
default=2048,
|
|
217
|
+
help="Longest side of each exported PNG.",
|
|
218
|
+
)
|
|
219
|
+
return parser
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def main() -> int:
|
|
223
|
+
args = _build_arg_parser().parse_args()
|
|
224
|
+
result = render_all(args.output_dir, max_pixels=args.max_pixels)
|
|
225
|
+
|
|
226
|
+
print(f"Rendered {len(result.rendered)} device image(s) into {result.output_dir}")
|
|
227
|
+
for item in result.rendered:
|
|
228
|
+
print(f" - {item.class_name}: {item.png_path}")
|
|
229
|
+
|
|
230
|
+
if result.skipped:
|
|
231
|
+
print(f"Skipped {len(result.skipped)} device(s):")
|
|
232
|
+
for item in result.skipped:
|
|
233
|
+
print(f" - {item.qualified_name}: {item.reason}")
|
|
234
|
+
|
|
235
|
+
return 0
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
if __name__ == "__main__":
|
|
239
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# export all structure modules
|
|
2
|
+
|
|
3
|
+
from .coupler import *
|
|
4
|
+
from .cutback import *
|
|
5
|
+
from .connection import *
|
|
6
|
+
from .curve import *
|
|
7
|
+
from .crossing import *
|
|
8
|
+
from .grating import *
|
|
9
|
+
from .metal import *
|
|
10
|
+
from .mmi import *
|
|
11
|
+
from .mzi import *
|
|
12
|
+
from .packaging import *
|
|
13
|
+
from .prober import *
|
|
14
|
+
from .racetrack import *
|
|
15
|
+
from .ring import *
|
|
16
|
+
from .spiral import *
|
|
17
|
+
from .taper import *
|
|
18
|
+
from .transition import *
|
|
19
|
+
from .wg import *
|