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 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
@@ -0,0 +1,6 @@
1
+ def main():
2
+ print("Hello from core!")
3
+
4
+
5
+ if __name__ == "__main__":
6
+ main()
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 *