gsim 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.
- gsim/__init__.py +16 -0
- gsim/gcloud.py +169 -0
- gsim/palace/__init__.py +120 -0
- gsim/palace/mesh/__init__.py +50 -0
- gsim/palace/mesh/generator.py +956 -0
- gsim/palace/mesh/gmsh_utils.py +484 -0
- gsim/palace/mesh/pipeline.py +188 -0
- gsim/palace/ports/__init__.py +35 -0
- gsim/palace/ports/config.py +363 -0
- gsim/palace/stack/__init__.py +149 -0
- gsim/palace/stack/extractor.py +602 -0
- gsim/palace/stack/materials.py +161 -0
- gsim/palace/stack/visualization.py +630 -0
- gsim/viz.py +86 -0
- gsim-0.0.0.dist-info/METADATA +128 -0
- gsim-0.0.0.dist-info/RECORD +18 -0
- gsim-0.0.0.dist-info/WHEEL +5 -0
- gsim-0.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
"""Port configuration for Palace EM simulation.
|
|
2
|
+
|
|
3
|
+
Ports define where excitation and measurement occur in the simulation.
|
|
4
|
+
This module provides helpers to configure gdsfactory ports with Palace metadata.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from enum import Enum
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from gsim.palace.stack import LayerStack
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PortType(Enum):
|
|
18
|
+
"""Palace port types (maps to Palace config)."""
|
|
19
|
+
|
|
20
|
+
LUMPED = "lumped" # LumpedPort: internal boundary with circuit impedance
|
|
21
|
+
WAVEPORT = "waveport" # WavePort: domain boundary, modal port
|
|
22
|
+
# SURFACE_CURRENT = "surface_current" # Future: inductance matrix extraction
|
|
23
|
+
# TERMINAL = "terminal" # Future: capacitance matrix extraction (electrostatics)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class PortGeometry(Enum):
|
|
27
|
+
"""Internal geometry type for mesh generation."""
|
|
28
|
+
|
|
29
|
+
INPLANE = "inplane" # Horizontal surface on single metal layer (Direction: +X, +Y)
|
|
30
|
+
VIA = "via" # Vertical surface between two metal layers (Direction: +Z)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class PalacePort:
|
|
35
|
+
"""Port definition for Palace simulation."""
|
|
36
|
+
|
|
37
|
+
name: str
|
|
38
|
+
port_type: PortType = PortType.LUMPED # Palace port type
|
|
39
|
+
geometry: PortGeometry = PortGeometry.INPLANE # Mesh geometry type
|
|
40
|
+
center: tuple[float, float] = (0.0, 0.0) # (x, y) in um
|
|
41
|
+
width: float = 0.0 # um
|
|
42
|
+
orientation: float = 0.0 # degrees (0=east, 90=north, 180=west, 270=south)
|
|
43
|
+
|
|
44
|
+
# Z coordinates (filled from stack)
|
|
45
|
+
zmin: float = 0.0
|
|
46
|
+
zmax: float = 0.0
|
|
47
|
+
|
|
48
|
+
# Layer info
|
|
49
|
+
layer: str | None = None # For inplane: target layer
|
|
50
|
+
from_layer: str | None = None # For via: bottom layer
|
|
51
|
+
to_layer: str | None = None # For via: top layer
|
|
52
|
+
|
|
53
|
+
# Port geometry
|
|
54
|
+
length: float | None = None # Port extent along direction (um)
|
|
55
|
+
|
|
56
|
+
# Multi-element support (for CPW)
|
|
57
|
+
multi_element: bool = False
|
|
58
|
+
centers: list[tuple[float, float]] | None = None # Multiple centers for CPW
|
|
59
|
+
directions: list[str] | None = (
|
|
60
|
+
None # Direction per element for CPW (e.g., ["+Y", "-Y"])
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Electrical properties
|
|
64
|
+
impedance: float = 50.0 # Ohms
|
|
65
|
+
excited: bool = True # Whether this port is excited (vs just measured)
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def direction(self) -> str:
|
|
69
|
+
"""Get direction from orientation."""
|
|
70
|
+
# Normalize orientation to 0-360
|
|
71
|
+
angle = self.orientation % 360
|
|
72
|
+
if angle < 45 or angle >= 315:
|
|
73
|
+
return "x" # East
|
|
74
|
+
if 45 <= angle < 135:
|
|
75
|
+
return "y" # North
|
|
76
|
+
if 135 <= angle < 225:
|
|
77
|
+
return "-x" # West
|
|
78
|
+
return "-y" # South
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def configure_inplane_port(
|
|
82
|
+
ports,
|
|
83
|
+
layer: str,
|
|
84
|
+
length: float,
|
|
85
|
+
impedance: float = 50.0,
|
|
86
|
+
excited: bool = True,
|
|
87
|
+
):
|
|
88
|
+
"""Configure gdsfactory port(s) as inplane (lumped) ports for Palace simulation.
|
|
89
|
+
|
|
90
|
+
Inplane ports are horizontal ports on a single metal layer, used for CPW gaps
|
|
91
|
+
or similar structures where excitation occurs in the XY plane.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
ports: Single gdsfactory Port or iterable of Ports (e.g., c.ports)
|
|
95
|
+
layer: Target conductor layer name (e.g., 'topmetal2')
|
|
96
|
+
length: Port extent along direction in um (perpendicular to port width)
|
|
97
|
+
impedance: Port impedance in Ohms (default: 50)
|
|
98
|
+
excited: Whether port is excited vs just measured (default: True)
|
|
99
|
+
|
|
100
|
+
Examples:
|
|
101
|
+
```python
|
|
102
|
+
configure_inplane_port(c.ports["o1"], layer="topmetal2", length=5.0)
|
|
103
|
+
configure_inplane_port(c.ports, layer="topmetal2", length=5.0) # all ports
|
|
104
|
+
```
|
|
105
|
+
"""
|
|
106
|
+
# Handle single port or iterable
|
|
107
|
+
port_list = [ports] if hasattr(ports, "info") else ports
|
|
108
|
+
|
|
109
|
+
for port in port_list:
|
|
110
|
+
port.info["palace_type"] = "lumped"
|
|
111
|
+
port.info["layer"] = layer
|
|
112
|
+
port.info["length"] = length
|
|
113
|
+
port.info["impedance"] = impedance
|
|
114
|
+
port.info["excited"] = excited
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def configure_via_port(
|
|
118
|
+
ports,
|
|
119
|
+
from_layer: str,
|
|
120
|
+
to_layer: str,
|
|
121
|
+
impedance: float = 50.0,
|
|
122
|
+
excited: bool = True,
|
|
123
|
+
):
|
|
124
|
+
"""Configure gdsfactory port(s) as via (vertical) lumped ports.
|
|
125
|
+
|
|
126
|
+
Via ports are vertical lumped ports between two metal layers, used for microstrip
|
|
127
|
+
feed structures where excitation occurs in the Z direction.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
ports: Single gdsfactory Port or iterable of Ports (e.g., c.ports)
|
|
131
|
+
from_layer: Bottom conductor layer name (e.g., 'metal1')
|
|
132
|
+
to_layer: Top conductor layer name (e.g., 'topmetal2')
|
|
133
|
+
impedance: Port impedance in Ohms (default: 50)
|
|
134
|
+
excited: Whether port is excited vs just measured (default: True)
|
|
135
|
+
|
|
136
|
+
Examples:
|
|
137
|
+
```python
|
|
138
|
+
configure_via_port(c.ports["o1"], from_layer="metal1", to_layer="topmetal2")
|
|
139
|
+
configure_via_port(
|
|
140
|
+
c.ports, from_layer="metal1", to_layer="topmetal2"
|
|
141
|
+
) # all ports
|
|
142
|
+
```
|
|
143
|
+
"""
|
|
144
|
+
# Handle single port or iterable
|
|
145
|
+
port_list = [ports] if hasattr(ports, "info") else ports
|
|
146
|
+
|
|
147
|
+
for port in port_list:
|
|
148
|
+
port.info["palace_type"] = "lumped"
|
|
149
|
+
port.info["from_layer"] = from_layer
|
|
150
|
+
port.info["to_layer"] = to_layer
|
|
151
|
+
port.info["impedance"] = impedance
|
|
152
|
+
port.info["excited"] = excited
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def configure_cpw_port(
|
|
156
|
+
port_upper,
|
|
157
|
+
port_lower,
|
|
158
|
+
layer: str,
|
|
159
|
+
length: float,
|
|
160
|
+
impedance: float = 50.0,
|
|
161
|
+
excited: bool = True,
|
|
162
|
+
cpw_name: str | None = None,
|
|
163
|
+
):
|
|
164
|
+
"""Configure two gdsfactory ports as a CPW (multi-element) lumped port.
|
|
165
|
+
|
|
166
|
+
In CPW (Ground-Signal-Ground), E-fields are opposite in the two gaps.
|
|
167
|
+
This function links two ports to form one multi-element lumped port
|
|
168
|
+
that Palace will excite with proper CPW mode.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
port_upper: gdsfactory Port for upper gap (signal-to-ground2)
|
|
172
|
+
port_lower: gdsfactory Port for lower gap (ground1-to-signal)
|
|
173
|
+
layer: Target conductor layer name (e.g., 'topmetal2')
|
|
174
|
+
length: Port extent along direction (um)
|
|
175
|
+
impedance: Port impedance in Ohms (default: 50)
|
|
176
|
+
excited: Whether port is excited (default: True)
|
|
177
|
+
cpw_name: Optional name for the CPW port (default: uses port_lower.name)
|
|
178
|
+
|
|
179
|
+
Examples:
|
|
180
|
+
```python
|
|
181
|
+
configure_cpw_port(
|
|
182
|
+
port_upper=c.ports["gap_upper"],
|
|
183
|
+
port_lower=c.ports["gap_lower"],
|
|
184
|
+
layer="topmetal2",
|
|
185
|
+
length=5.0,
|
|
186
|
+
)
|
|
187
|
+
```
|
|
188
|
+
"""
|
|
189
|
+
# Generate unique CPW group ID
|
|
190
|
+
cpw_group_id = cpw_name or f"cpw_{port_lower.name}"
|
|
191
|
+
|
|
192
|
+
# Auto-detect directions based on positions
|
|
193
|
+
upper_y = float(port_upper.center[1])
|
|
194
|
+
lower_y = float(port_lower.center[1])
|
|
195
|
+
|
|
196
|
+
# The port farther from origin in Y gets negative direction (E toward signal)
|
|
197
|
+
# The port closer to origin gets positive direction (E toward signal)
|
|
198
|
+
if upper_y > lower_y:
|
|
199
|
+
upper_direction = "-Y"
|
|
200
|
+
lower_direction = "+Y"
|
|
201
|
+
else:
|
|
202
|
+
upper_direction = "+Y"
|
|
203
|
+
lower_direction = "-Y"
|
|
204
|
+
|
|
205
|
+
# Store metadata on BOTH ports, marking them as CPW elements
|
|
206
|
+
for port, direction in [
|
|
207
|
+
(port_upper, upper_direction),
|
|
208
|
+
(port_lower, lower_direction),
|
|
209
|
+
]:
|
|
210
|
+
port.info["palace_type"] = "cpw_element"
|
|
211
|
+
port.info["cpw_group"] = cpw_group_id
|
|
212
|
+
port.info["cpw_direction"] = direction
|
|
213
|
+
port.info["layer"] = layer
|
|
214
|
+
port.info["length"] = length
|
|
215
|
+
port.info["impedance"] = impedance
|
|
216
|
+
port.info["excited"] = excited
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def extract_ports(component, stack: LayerStack) -> list[PalacePort]:
|
|
220
|
+
"""Extract Palace ports from a gdsfactory component.
|
|
221
|
+
|
|
222
|
+
Handles all port types: inplane, via, and CPW (multi-element).
|
|
223
|
+
CPW ports are automatically grouped by their cpw_group ID.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
component: gdsfactory Component with configured ports
|
|
227
|
+
stack: LayerStack from stack module
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
List of PalacePort objects ready for simulation
|
|
231
|
+
"""
|
|
232
|
+
palace_ports = []
|
|
233
|
+
|
|
234
|
+
# First, collect CPW elements grouped by cpw_group
|
|
235
|
+
cpw_groups: dict[str, list] = {}
|
|
236
|
+
|
|
237
|
+
for port in component.ports:
|
|
238
|
+
info = port.info
|
|
239
|
+
palace_type = info.get("palace_type")
|
|
240
|
+
|
|
241
|
+
if palace_type is None:
|
|
242
|
+
continue
|
|
243
|
+
|
|
244
|
+
if palace_type == "cpw_element":
|
|
245
|
+
group_id = info.get("cpw_group")
|
|
246
|
+
if group_id:
|
|
247
|
+
if group_id not in cpw_groups:
|
|
248
|
+
cpw_groups[group_id] = []
|
|
249
|
+
cpw_groups[group_id].append(port)
|
|
250
|
+
continue
|
|
251
|
+
|
|
252
|
+
# Handle single-element ports (lumped, waveport)
|
|
253
|
+
center = (float(port.center[0]), float(port.center[1]))
|
|
254
|
+
width = float(port.width)
|
|
255
|
+
orientation = float(port.orientation) if port.orientation is not None else 0.0
|
|
256
|
+
|
|
257
|
+
zmin, zmax = 0.0, 0.0
|
|
258
|
+
from_layer = info.get("from_layer")
|
|
259
|
+
to_layer = info.get("to_layer")
|
|
260
|
+
layer_name = info.get("layer")
|
|
261
|
+
|
|
262
|
+
if palace_type == "lumped":
|
|
263
|
+
port_type = PortType.LUMPED
|
|
264
|
+
if from_layer and to_layer:
|
|
265
|
+
geometry = PortGeometry.VIA
|
|
266
|
+
if from_layer in stack.layers:
|
|
267
|
+
zmin = stack.layers[from_layer].zmin
|
|
268
|
+
if to_layer in stack.layers:
|
|
269
|
+
zmax = stack.layers[to_layer].zmax
|
|
270
|
+
elif layer_name:
|
|
271
|
+
geometry = PortGeometry.INPLANE
|
|
272
|
+
if layer_name in stack.layers:
|
|
273
|
+
layer = stack.layers[layer_name]
|
|
274
|
+
zmin = layer.zmin
|
|
275
|
+
zmax = layer.zmax
|
|
276
|
+
else:
|
|
277
|
+
raise ValueError(f"Lumped port '{port.name}' missing layer info")
|
|
278
|
+
|
|
279
|
+
elif palace_type == "waveport":
|
|
280
|
+
port_type = PortType.WAVEPORT
|
|
281
|
+
geometry = PortGeometry.INPLANE # Waveport geometry TBD
|
|
282
|
+
zmin, zmax = stack.get_z_range()
|
|
283
|
+
|
|
284
|
+
else:
|
|
285
|
+
raise ValueError(f"Unknown port type: {palace_type}")
|
|
286
|
+
|
|
287
|
+
palace_port = PalacePort(
|
|
288
|
+
name=port.name,
|
|
289
|
+
port_type=port_type,
|
|
290
|
+
geometry=geometry,
|
|
291
|
+
center=center,
|
|
292
|
+
width=width,
|
|
293
|
+
orientation=orientation,
|
|
294
|
+
zmin=zmin,
|
|
295
|
+
zmax=zmax,
|
|
296
|
+
layer=layer_name,
|
|
297
|
+
from_layer=from_layer,
|
|
298
|
+
to_layer=to_layer,
|
|
299
|
+
length=info.get("length"),
|
|
300
|
+
impedance=info.get("impedance", 50.0),
|
|
301
|
+
excited=info.get("excited", True),
|
|
302
|
+
)
|
|
303
|
+
palace_ports.append(palace_port)
|
|
304
|
+
|
|
305
|
+
# Now process CPW groups into multi-element PalacePort objects
|
|
306
|
+
for group_id, ports in cpw_groups.items():
|
|
307
|
+
if len(ports) != 2:
|
|
308
|
+
raise ValueError(
|
|
309
|
+
f"CPW group '{group_id}' must have exactly 2 ports, got {len(ports)}"
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
# Sort by Y position to get consistent ordering
|
|
313
|
+
ports_sorted = sorted(ports, key=lambda p: p.center[1], reverse=True)
|
|
314
|
+
port_upper, port_lower = ports_sorted[0], ports_sorted[1]
|
|
315
|
+
|
|
316
|
+
info = port_lower.info
|
|
317
|
+
layer_name = info.get("layer")
|
|
318
|
+
|
|
319
|
+
# Get z coordinates from stack
|
|
320
|
+
zmin, zmax = 0.0, 0.0
|
|
321
|
+
if layer_name and layer_name in stack.layers:
|
|
322
|
+
layer = stack.layers[layer_name]
|
|
323
|
+
zmin = layer.zmin
|
|
324
|
+
zmax = layer.zmax
|
|
325
|
+
|
|
326
|
+
# Get centers and directions
|
|
327
|
+
centers = [
|
|
328
|
+
(float(port_upper.center[0]), float(port_upper.center[1])),
|
|
329
|
+
(float(port_lower.center[0]), float(port_lower.center[1])),
|
|
330
|
+
]
|
|
331
|
+
directions = [
|
|
332
|
+
port_upper.info.get("cpw_direction", "-Y"),
|
|
333
|
+
port_lower.info.get("cpw_direction", "+Y"),
|
|
334
|
+
]
|
|
335
|
+
|
|
336
|
+
# Use average center for the main center field
|
|
337
|
+
avg_center = (
|
|
338
|
+
(centers[0][0] + centers[1][0]) / 2,
|
|
339
|
+
(centers[0][1] + centers[1][1]) / 2,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
cpw_port = PalacePort(
|
|
343
|
+
name=group_id,
|
|
344
|
+
port_type=PortType.LUMPED,
|
|
345
|
+
geometry=PortGeometry.INPLANE,
|
|
346
|
+
center=avg_center,
|
|
347
|
+
width=float(port_lower.width),
|
|
348
|
+
orientation=float(port_lower.orientation)
|
|
349
|
+
if port_lower.orientation
|
|
350
|
+
else 0.0,
|
|
351
|
+
zmin=zmin,
|
|
352
|
+
zmax=zmax,
|
|
353
|
+
layer=layer_name,
|
|
354
|
+
length=info.get("length"),
|
|
355
|
+
multi_element=True,
|
|
356
|
+
centers=centers,
|
|
357
|
+
directions=directions,
|
|
358
|
+
impedance=info.get("impedance", 50.0),
|
|
359
|
+
excited=info.get("excited", True),
|
|
360
|
+
)
|
|
361
|
+
palace_ports.append(cpw_port)
|
|
362
|
+
|
|
363
|
+
return palace_ports
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Layer stack extraction and parsing for Palace EM simulation.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
# From PDK module (preferred, no file needed)
|
|
5
|
+
from gsim.palace.stack import get_stack
|
|
6
|
+
stack = get_stack(pdk=ihp)
|
|
7
|
+
|
|
8
|
+
# From YAML file (for custom/tweaked stacks)
|
|
9
|
+
stack = get_stack(yaml_path="my_stack.yaml")
|
|
10
|
+
|
|
11
|
+
# Export to YAML for manual editing
|
|
12
|
+
stack.to_yaml("my_stack.yaml")
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
import gdsfactory as gf
|
|
20
|
+
import yaml
|
|
21
|
+
|
|
22
|
+
from gsim.palace.stack.extractor import (
|
|
23
|
+
Layer,
|
|
24
|
+
LayerStack,
|
|
25
|
+
ValidationResult,
|
|
26
|
+
extract_from_pdk,
|
|
27
|
+
extract_layer_stack,
|
|
28
|
+
)
|
|
29
|
+
from gsim.palace.stack.materials import (
|
|
30
|
+
MATERIALS_DB,
|
|
31
|
+
MaterialProperties,
|
|
32
|
+
get_material_properties,
|
|
33
|
+
material_is_conductor,
|
|
34
|
+
material_is_dielectric,
|
|
35
|
+
)
|
|
36
|
+
from gsim.palace.stack.visualization import (
|
|
37
|
+
StackLayer,
|
|
38
|
+
parse_layer_stack,
|
|
39
|
+
plot_stack,
|
|
40
|
+
print_stack,
|
|
41
|
+
print_stack_table,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_stack(
|
|
46
|
+
yaml_path: str | Path | None = None,
|
|
47
|
+
**kwargs,
|
|
48
|
+
) -> LayerStack:
|
|
49
|
+
"""Get layer stack from active PDK or YAML file.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
yaml_path: Path to custom YAML stack file. If None, uses active PDK.
|
|
53
|
+
**kwargs: Additional args passed to extract_layer_stack:
|
|
54
|
+
- substrate_thickness: Thickness below z=0 in um (default: 2.0)
|
|
55
|
+
- air_above: Air box height above top metal in um (default: 200)
|
|
56
|
+
- include_substrate: Include lossy silicon substrate (default: False).
|
|
57
|
+
When False, matches gds2palace "nosub" behavior for RF simulation.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
LayerStack object
|
|
61
|
+
|
|
62
|
+
Examples:
|
|
63
|
+
# From active PDK (after PDK.activate()) - no substrate (recommended for RF)
|
|
64
|
+
stack = get_stack()
|
|
65
|
+
|
|
66
|
+
# With lossy substrate (for substrate coupling studies)
|
|
67
|
+
stack = get_stack(include_substrate=True)
|
|
68
|
+
|
|
69
|
+
# From YAML file
|
|
70
|
+
stack = get_stack(yaml_path="custom_stack.yaml")
|
|
71
|
+
|
|
72
|
+
# With custom settings
|
|
73
|
+
stack = get_stack(air_above=300, substrate_thickness=5.0)
|
|
74
|
+
"""
|
|
75
|
+
if yaml_path is not None:
|
|
76
|
+
return load_stack_yaml(yaml_path)
|
|
77
|
+
|
|
78
|
+
pdk = gf.get_active_pdk()
|
|
79
|
+
if pdk is None:
|
|
80
|
+
raise ValueError("No active PDK found. Call PDK.activate() first.")
|
|
81
|
+
|
|
82
|
+
return extract_from_pdk(pdk, **kwargs)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def load_stack_yaml(yaml_path: str | Path) -> LayerStack:
|
|
86
|
+
"""Load layer stack from YAML file.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
yaml_path: Path to YAML file
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
LayerStack object
|
|
93
|
+
"""
|
|
94
|
+
yaml_path = Path(yaml_path)
|
|
95
|
+
with open(yaml_path) as f:
|
|
96
|
+
data = yaml.safe_load(f)
|
|
97
|
+
|
|
98
|
+
# Reconstruct LayerStack from dict
|
|
99
|
+
stack = LayerStack(
|
|
100
|
+
pdk_name=data.get("pdk", "unknown"),
|
|
101
|
+
units=data.get("units", "um"),
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Load materials
|
|
105
|
+
stack.materials = data.get("materials", {})
|
|
106
|
+
|
|
107
|
+
# Load layers
|
|
108
|
+
for name, layer_data in data.get("layers", {}).items():
|
|
109
|
+
stack.layers[name] = Layer(
|
|
110
|
+
name=name,
|
|
111
|
+
gds_layer=tuple(layer_data["gds_layer"]),
|
|
112
|
+
zmin=layer_data["zmin"],
|
|
113
|
+
zmax=layer_data["zmax"],
|
|
114
|
+
thickness=layer_data.get(
|
|
115
|
+
"thickness", layer_data["zmax"] - layer_data["zmin"]
|
|
116
|
+
),
|
|
117
|
+
material=layer_data["material"],
|
|
118
|
+
layer_type=layer_data["type"],
|
|
119
|
+
mesh_resolution=layer_data.get("mesh_resolution", "medium"),
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Load dielectrics
|
|
123
|
+
stack.dielectrics = data.get("dielectrics", [])
|
|
124
|
+
|
|
125
|
+
# Load simulation settings
|
|
126
|
+
stack.simulation = data.get("simulation", {})
|
|
127
|
+
|
|
128
|
+
return stack
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
__all__ = [
|
|
132
|
+
"MATERIALS_DB",
|
|
133
|
+
"Layer",
|
|
134
|
+
"LayerStack",
|
|
135
|
+
"MaterialProperties",
|
|
136
|
+
"StackLayer",
|
|
137
|
+
"ValidationResult",
|
|
138
|
+
"extract_from_pdk",
|
|
139
|
+
"extract_layer_stack",
|
|
140
|
+
"get_material_properties",
|
|
141
|
+
"get_stack",
|
|
142
|
+
"load_stack_yaml",
|
|
143
|
+
"material_is_conductor",
|
|
144
|
+
"material_is_dielectric",
|
|
145
|
+
"parse_layer_stack",
|
|
146
|
+
"plot_stack",
|
|
147
|
+
"print_stack",
|
|
148
|
+
"print_stack_table",
|
|
149
|
+
]
|