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,602 @@
|
|
|
1
|
+
"""Extract layer stack from gdsfactory PDK and convert to YAML format.
|
|
2
|
+
|
|
3
|
+
This module reads a PDK's LayerStack and generates a YAML stack file
|
|
4
|
+
that can be used for Palace EM simulation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import yaml
|
|
15
|
+
from gdsfactory.technology import LayerLevel
|
|
16
|
+
from gdsfactory.technology import LayerStack as GfLayerStack
|
|
17
|
+
|
|
18
|
+
from gsim.palace.stack.materials import (
|
|
19
|
+
MATERIALS_DB,
|
|
20
|
+
get_material_properties,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class Layer:
|
|
28
|
+
"""Layer information for Palace simulation."""
|
|
29
|
+
|
|
30
|
+
name: str
|
|
31
|
+
gds_layer: tuple[int, int] # (layer, datatype)
|
|
32
|
+
zmin: float # um
|
|
33
|
+
zmax: float # um
|
|
34
|
+
thickness: float # um
|
|
35
|
+
material: str
|
|
36
|
+
layer_type: str # "conductor", "via", "dielectric", "substrate"
|
|
37
|
+
|
|
38
|
+
# Mesh resolution control
|
|
39
|
+
# Options: "fine", "medium", "coarse" or a float in um
|
|
40
|
+
mesh_resolution: str | float = "medium"
|
|
41
|
+
|
|
42
|
+
def get_mesh_size(self, base_size: float = 1.0) -> float:
|
|
43
|
+
"""Get mesh size in um for this layer.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
base_size: Base mesh size for "medium" resolution
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Mesh size in um
|
|
50
|
+
"""
|
|
51
|
+
if isinstance(self.mesh_resolution, int | float):
|
|
52
|
+
return float(self.mesh_resolution)
|
|
53
|
+
|
|
54
|
+
resolution_map = {
|
|
55
|
+
"fine": base_size * 0.5,
|
|
56
|
+
"medium": base_size,
|
|
57
|
+
"coarse": base_size * 2.0,
|
|
58
|
+
}
|
|
59
|
+
return resolution_map.get(self.mesh_resolution, base_size)
|
|
60
|
+
|
|
61
|
+
def to_dict(self) -> dict:
|
|
62
|
+
"""Convert to dictionary for YAML output."""
|
|
63
|
+
return {
|
|
64
|
+
"gds_layer": list(self.gds_layer),
|
|
65
|
+
"zmin": round(self.zmin, 4),
|
|
66
|
+
"zmax": round(self.zmax, 4),
|
|
67
|
+
"thickness": round(self.thickness, 4),
|
|
68
|
+
"material": self.material,
|
|
69
|
+
"type": self.layer_type,
|
|
70
|
+
"mesh_resolution": self.mesh_resolution,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class ValidationResult:
|
|
76
|
+
"""Result of stack validation."""
|
|
77
|
+
|
|
78
|
+
valid: bool
|
|
79
|
+
errors: list[str] = field(default_factory=list)
|
|
80
|
+
warnings: list[str] = field(default_factory=list)
|
|
81
|
+
|
|
82
|
+
def __bool__(self) -> bool:
|
|
83
|
+
return self.valid
|
|
84
|
+
|
|
85
|
+
def __str__(self) -> str:
|
|
86
|
+
lines = []
|
|
87
|
+
if self.valid:
|
|
88
|
+
lines.append("Stack validation: PASSED")
|
|
89
|
+
else:
|
|
90
|
+
lines.append("Stack validation: FAILED")
|
|
91
|
+
if self.errors:
|
|
92
|
+
lines.append("Errors:")
|
|
93
|
+
lines.extend([f" - {e}" for e in self.errors])
|
|
94
|
+
if self.warnings:
|
|
95
|
+
lines.append("Warnings:")
|
|
96
|
+
lines.extend([f" - {w}" for w in self.warnings])
|
|
97
|
+
return "\n".join(lines)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass
|
|
101
|
+
class LayerStack:
|
|
102
|
+
"""Complete layer stack for Palace simulation."""
|
|
103
|
+
|
|
104
|
+
pdk_name: str
|
|
105
|
+
units: str = "um"
|
|
106
|
+
layers: dict[str, Layer] = field(default_factory=dict)
|
|
107
|
+
materials: dict[str, dict] = field(default_factory=dict)
|
|
108
|
+
dielectrics: list[dict] = field(default_factory=list)
|
|
109
|
+
simulation: dict = field(default_factory=dict)
|
|
110
|
+
|
|
111
|
+
def validate(self, tolerance: float = 0.001) -> ValidationResult:
|
|
112
|
+
"""Validate the layer stack for simulation readiness.
|
|
113
|
+
|
|
114
|
+
Checks:
|
|
115
|
+
1. Z-axis continuity: no gaps in dielectric regions
|
|
116
|
+
2. Material coverage: all materials have properties defined
|
|
117
|
+
3. Layer coverage: all conductor/via layers are within dielectric envelope
|
|
118
|
+
4. No negative thicknesses
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
tolerance: Tolerance for z-coordinate comparisons (um)
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
ValidationResult with valid flag, errors, and warnings
|
|
125
|
+
"""
|
|
126
|
+
errors = []
|
|
127
|
+
warnings = []
|
|
128
|
+
|
|
129
|
+
# 1. Check all materials have required properties
|
|
130
|
+
materials_used = set()
|
|
131
|
+
|
|
132
|
+
# Collect materials from layers
|
|
133
|
+
for name, layer in self.layers.items():
|
|
134
|
+
materials_used.add(layer.material)
|
|
135
|
+
# Check for invalid thickness
|
|
136
|
+
if layer.thickness < 0:
|
|
137
|
+
errors.append(
|
|
138
|
+
f"Layer '{name}' has negative thickness: {layer.thickness}"
|
|
139
|
+
)
|
|
140
|
+
if layer.thickness == 0:
|
|
141
|
+
warnings.append(f"Layer '{name}' has zero thickness")
|
|
142
|
+
|
|
143
|
+
# Collect materials from dielectrics
|
|
144
|
+
for d in self.dielectrics:
|
|
145
|
+
materials_used.add(d["material"])
|
|
146
|
+
|
|
147
|
+
# Check each material has properties
|
|
148
|
+
for mat in materials_used:
|
|
149
|
+
if mat not in self.materials:
|
|
150
|
+
errors.append(
|
|
151
|
+
f"Material '{mat}' used but not defined in materials dict"
|
|
152
|
+
)
|
|
153
|
+
else:
|
|
154
|
+
props = self.materials[mat]
|
|
155
|
+
mat_type = props.get("type", "unknown")
|
|
156
|
+
if mat_type == "unknown":
|
|
157
|
+
warnings.append(f"Material '{mat}' has unknown type")
|
|
158
|
+
elif mat_type == "conductor":
|
|
159
|
+
if "conductivity" not in props:
|
|
160
|
+
errors.append(
|
|
161
|
+
f"Conductor material '{mat}' missing conductivity"
|
|
162
|
+
)
|
|
163
|
+
elif mat_type == "dielectric" and "permittivity" not in props:
|
|
164
|
+
errors.append(f"Dielectric material '{mat}' missing permittivity")
|
|
165
|
+
|
|
166
|
+
# 2. Check z-axis continuity of dielectrics
|
|
167
|
+
if self.dielectrics:
|
|
168
|
+
# Sort dielectrics by zmin
|
|
169
|
+
sorted_dielectrics = sorted(self.dielectrics, key=lambda d: d["zmin"])
|
|
170
|
+
|
|
171
|
+
# Check for gaps between dielectric regions
|
|
172
|
+
for i in range(len(sorted_dielectrics) - 1):
|
|
173
|
+
current = sorted_dielectrics[i]
|
|
174
|
+
next_d = sorted_dielectrics[i + 1]
|
|
175
|
+
|
|
176
|
+
gap = next_d["zmin"] - current["zmax"]
|
|
177
|
+
if gap > tolerance:
|
|
178
|
+
errors.append(
|
|
179
|
+
f"Z-axis gap between '{current['name']}' "
|
|
180
|
+
f"(zmax={current['zmax']:.4f}) and '{next_d['name']}' "
|
|
181
|
+
f"(zmin={next_d['zmin']:.4f}): gap={gap:.4f} um"
|
|
182
|
+
)
|
|
183
|
+
elif gap < -tolerance:
|
|
184
|
+
# Overlap - this might be intentional
|
|
185
|
+
warnings.append(
|
|
186
|
+
f"Z-axis overlap between '{current['name']}' and "
|
|
187
|
+
f"'{next_d['name']}': overlap={-gap:.4f} um"
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# Get overall dielectric envelope
|
|
191
|
+
z_min_dielectric = sorted_dielectrics[0]["zmin"]
|
|
192
|
+
z_max_dielectric = sorted_dielectrics[-1]["zmax"]
|
|
193
|
+
else:
|
|
194
|
+
errors.append("No dielectric regions defined")
|
|
195
|
+
z_min_dielectric = 0
|
|
196
|
+
z_max_dielectric = 0
|
|
197
|
+
|
|
198
|
+
# 3. Check all conductor/via layers are within dielectric envelope
|
|
199
|
+
for name, layer in self.layers.items():
|
|
200
|
+
if layer.layer_type in ("conductor", "via"):
|
|
201
|
+
if layer.zmin < z_min_dielectric - tolerance:
|
|
202
|
+
errors.append(
|
|
203
|
+
f"Layer '{name}' extends below dielectric envelope: "
|
|
204
|
+
f"layer zmin={layer.zmin:.4f}, dielectric "
|
|
205
|
+
f"zmin={z_min_dielectric:.4f}"
|
|
206
|
+
)
|
|
207
|
+
if layer.zmax > z_max_dielectric + tolerance:
|
|
208
|
+
errors.append(
|
|
209
|
+
f"Layer '{name}' extends above dielectric envelope: "
|
|
210
|
+
f"layer zmax={layer.zmax:.4f}, dielectric "
|
|
211
|
+
f"zmax={z_max_dielectric:.4f}"
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
# 4. Check we have at least substrate, oxide, and air
|
|
215
|
+
dielectric_names = {d["name"] for d in self.dielectrics}
|
|
216
|
+
if "substrate" not in dielectric_names:
|
|
217
|
+
warnings.append("No 'substrate' dielectric region defined")
|
|
218
|
+
if "air_box" not in dielectric_names:
|
|
219
|
+
warnings.append("No 'air_box' dielectric region defined")
|
|
220
|
+
|
|
221
|
+
valid = len(errors) == 0
|
|
222
|
+
return ValidationResult(valid=valid, errors=errors, warnings=warnings)
|
|
223
|
+
|
|
224
|
+
def get_z_range(self) -> tuple[float, float]:
|
|
225
|
+
"""Get the full z-range of the stack (substrate bottom to air top)."""
|
|
226
|
+
if not self.dielectrics:
|
|
227
|
+
return (0.0, 0.0)
|
|
228
|
+
z_min = min(d["zmin"] for d in self.dielectrics)
|
|
229
|
+
z_max = max(d["zmax"] for d in self.dielectrics)
|
|
230
|
+
return (z_min, z_max)
|
|
231
|
+
|
|
232
|
+
def get_conductor_layers(self) -> dict[str, Layer]:
|
|
233
|
+
"""Get all conductor layers."""
|
|
234
|
+
return {
|
|
235
|
+
n: layer
|
|
236
|
+
for n, layer in self.layers.items()
|
|
237
|
+
if layer.layer_type == "conductor"
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
def get_via_layers(self) -> dict[str, Layer]:
|
|
241
|
+
"""Get all via layers."""
|
|
242
|
+
return {
|
|
243
|
+
n: layer for n, layer in self.layers.items() if layer.layer_type == "via"
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
def to_dict(self) -> dict:
|
|
247
|
+
"""Convert to dictionary for YAML output."""
|
|
248
|
+
return {
|
|
249
|
+
"version": "1.0",
|
|
250
|
+
"pdk": self.pdk_name,
|
|
251
|
+
"units": self.units,
|
|
252
|
+
"materials": self.materials,
|
|
253
|
+
"layers": {name: layer.to_dict() for name, layer in self.layers.items()},
|
|
254
|
+
"dielectrics": self.dielectrics,
|
|
255
|
+
"simulation": self.simulation,
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
def to_yaml(self, path: Path | None = None) -> str:
|
|
259
|
+
"""Convert to YAML string and optionally write to file.
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
path: Optional path to write YAML file
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
YAML string
|
|
266
|
+
"""
|
|
267
|
+
yaml_str = yaml.dump(
|
|
268
|
+
self.to_dict(),
|
|
269
|
+
default_flow_style=False,
|
|
270
|
+
sort_keys=False,
|
|
271
|
+
allow_unicode=True,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
if path:
|
|
275
|
+
path = Path(path)
|
|
276
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
277
|
+
path.write_text(yaml_str)
|
|
278
|
+
logger.info("Stack written to: %s", path)
|
|
279
|
+
|
|
280
|
+
return yaml_str
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _get_gds_layer_tuple(layer_level: LayerLevel) -> tuple[int, int]:
|
|
284
|
+
"""Extract GDS layer tuple from LayerLevel.
|
|
285
|
+
|
|
286
|
+
The layer can be specified in various ways in gdsfactory:
|
|
287
|
+
- tuple: (8, 0)
|
|
288
|
+
- int: 8
|
|
289
|
+
- LogicalLayer with nested layer enum
|
|
290
|
+
- kfactory LayerEnum with .layer and .datatype
|
|
291
|
+
"""
|
|
292
|
+
layer: Any = layer_level.layer
|
|
293
|
+
|
|
294
|
+
# Handle different layer specifications
|
|
295
|
+
if isinstance(layer, tuple):
|
|
296
|
+
return (int(layer[0]), int(layer[1]))
|
|
297
|
+
|
|
298
|
+
if isinstance(layer, int):
|
|
299
|
+
return (int(layer), 0)
|
|
300
|
+
|
|
301
|
+
# Handle LogicalLayer (gdsfactory.technology.layer_stack.LogicalLayer)
|
|
302
|
+
# Structure: layer_level.layer (LogicalLayer) -> .layer (Enum) -> .layer/.datatype
|
|
303
|
+
# (int)
|
|
304
|
+
if hasattr(layer, "layer"):
|
|
305
|
+
inner = layer.layer
|
|
306
|
+
# Check if inner has layer/datatype (kfactory/gdsfactory enum)
|
|
307
|
+
if hasattr(inner, "layer") and hasattr(inner, "datatype"):
|
|
308
|
+
return (int(inner.layer), int(inner.datatype))
|
|
309
|
+
# Check if inner itself has the values
|
|
310
|
+
if isinstance(inner, int):
|
|
311
|
+
datatype = getattr(layer, "datatype", 0)
|
|
312
|
+
return (int(inner), int(datatype) if datatype else 0)
|
|
313
|
+
# Recurse one more level if needed
|
|
314
|
+
if hasattr(inner, "layer"):
|
|
315
|
+
innermost = inner.layer
|
|
316
|
+
if isinstance(innermost, int):
|
|
317
|
+
datatype = getattr(inner, "datatype", 0)
|
|
318
|
+
return (int(innermost), int(datatype) if datatype else 0)
|
|
319
|
+
|
|
320
|
+
# Direct enum with layer/datatype
|
|
321
|
+
if hasattr(layer, "layer") and hasattr(layer, "datatype"):
|
|
322
|
+
return (int(layer.layer), int(layer.datatype))
|
|
323
|
+
|
|
324
|
+
# Enum with tuple value
|
|
325
|
+
if hasattr(layer, "value"):
|
|
326
|
+
if isinstance(layer.value, tuple):
|
|
327
|
+
return (int(layer.value[0]), int(layer.value[1]))
|
|
328
|
+
if isinstance(layer.value, int):
|
|
329
|
+
return (int(layer.value), 0)
|
|
330
|
+
|
|
331
|
+
# String format "8/0"
|
|
332
|
+
if isinstance(layer, str):
|
|
333
|
+
if "/" in layer:
|
|
334
|
+
parts = layer.split("/")
|
|
335
|
+
return (int(parts[0]), int(parts[1]))
|
|
336
|
+
return (0, 0)
|
|
337
|
+
|
|
338
|
+
# Fallback
|
|
339
|
+
try:
|
|
340
|
+
return (int(layer), 0)
|
|
341
|
+
except (TypeError, ValueError):
|
|
342
|
+
logger.warning("Could not parse layer %s, using (0, 0)", layer)
|
|
343
|
+
return (0, 0)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _classify_layer_type(layer_name: str, material: str) -> str:
|
|
347
|
+
"""Classify a layer as conductor, via, dielectric, or substrate.
|
|
348
|
+
|
|
349
|
+
Args:
|
|
350
|
+
layer_name: Name of the layer (e.g., "metal1", "via1", "substrate")
|
|
351
|
+
material: Material name
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
Layer type string
|
|
355
|
+
"""
|
|
356
|
+
name_lower = layer_name.lower()
|
|
357
|
+
material_lower = material.lower()
|
|
358
|
+
|
|
359
|
+
# Check for via layers
|
|
360
|
+
if "via" in name_lower:
|
|
361
|
+
return "via"
|
|
362
|
+
|
|
363
|
+
# Check for metal layers
|
|
364
|
+
if any(
|
|
365
|
+
m in name_lower for m in ["metal", "topmetal", "m1", "m2", "m3", "m4", "m5"]
|
|
366
|
+
):
|
|
367
|
+
return "conductor"
|
|
368
|
+
|
|
369
|
+
# Check for substrate
|
|
370
|
+
if "substrate" in name_lower or name_lower == "sub":
|
|
371
|
+
return "substrate"
|
|
372
|
+
|
|
373
|
+
# Check by material
|
|
374
|
+
props = get_material_properties(material)
|
|
375
|
+
if props:
|
|
376
|
+
if props.type == "conductor":
|
|
377
|
+
return "conductor"
|
|
378
|
+
if props.type == "semiconductor" and "substrate" in name_lower:
|
|
379
|
+
return "substrate"
|
|
380
|
+
if props.type == "dielectric":
|
|
381
|
+
return "dielectric"
|
|
382
|
+
|
|
383
|
+
# Check for common conductor materials
|
|
384
|
+
if material_lower in ["aluminum", "copper", "tungsten", "gold", "al", "cu", "w"]:
|
|
385
|
+
return "conductor"
|
|
386
|
+
|
|
387
|
+
# Check for poly
|
|
388
|
+
if "poly" in name_lower:
|
|
389
|
+
return "conductor"
|
|
390
|
+
|
|
391
|
+
# Default to dielectric
|
|
392
|
+
return "dielectric"
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def extract_layer_stack(
|
|
396
|
+
gf_layer_stack: GfLayerStack,
|
|
397
|
+
pdk_name: str = "unknown",
|
|
398
|
+
substrate_thickness: float = 2.0,
|
|
399
|
+
air_above: float = 200.0,
|
|
400
|
+
boundary_margin: float = 30.0,
|
|
401
|
+
include_substrate: bool = False,
|
|
402
|
+
) -> LayerStack:
|
|
403
|
+
"""Extract layer stack from a gdsfactory LayerStack.
|
|
404
|
+
|
|
405
|
+
Args:
|
|
406
|
+
gf_layer_stack: gdsfactory LayerStack object
|
|
407
|
+
pdk_name: Name of the PDK (for documentation)
|
|
408
|
+
substrate_thickness: Thickness of substrate in um (default: 2.0)
|
|
409
|
+
air_above: Height of air box above top metal in um (default: 200)
|
|
410
|
+
boundary_margin: Lateral margin from GDS bbox in um (default: 30)
|
|
411
|
+
include_substrate: Whether to include lossy substrate (default: False).
|
|
412
|
+
When False, matches gds2palace "nosub" behavior for RF simulation.
|
|
413
|
+
|
|
414
|
+
Returns:
|
|
415
|
+
LayerStack object for Palace simulation
|
|
416
|
+
"""
|
|
417
|
+
stack = LayerStack(pdk_name=pdk_name)
|
|
418
|
+
|
|
419
|
+
# Track z-range for dielectric regions
|
|
420
|
+
z_min_overall = float("inf")
|
|
421
|
+
z_max_overall = float("-inf")
|
|
422
|
+
|
|
423
|
+
# Collect materials used
|
|
424
|
+
materials_used: set[str] = set()
|
|
425
|
+
|
|
426
|
+
# Extract each layer
|
|
427
|
+
for layer_name, layer_level in gf_layer_stack.layers.items():
|
|
428
|
+
# Get layer properties
|
|
429
|
+
zmin = layer_level.zmin if layer_level.zmin is not None else 0.0
|
|
430
|
+
thickness = layer_level.thickness if layer_level.thickness is not None else 0.0
|
|
431
|
+
zmax = zmin + thickness
|
|
432
|
+
material = layer_level.material if layer_level.material else "unknown"
|
|
433
|
+
|
|
434
|
+
# Get GDS layer tuple
|
|
435
|
+
gds_layer = _get_gds_layer_tuple(layer_level)
|
|
436
|
+
|
|
437
|
+
# Classify layer type
|
|
438
|
+
layer_type = _classify_layer_type(layer_name, material)
|
|
439
|
+
|
|
440
|
+
# Skip substrate layers when include_substrate=False (matches gds2palace
|
|
441
|
+
# "nosub").
|
|
442
|
+
if layer_type == "substrate" and not include_substrate:
|
|
443
|
+
continue
|
|
444
|
+
|
|
445
|
+
# Create layer
|
|
446
|
+
layer = Layer(
|
|
447
|
+
name=layer_name,
|
|
448
|
+
gds_layer=gds_layer,
|
|
449
|
+
zmin=zmin,
|
|
450
|
+
zmax=zmax,
|
|
451
|
+
thickness=thickness,
|
|
452
|
+
material=material,
|
|
453
|
+
layer_type=layer_type,
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
stack.layers[layer_name] = layer
|
|
457
|
+
materials_used.add(material)
|
|
458
|
+
|
|
459
|
+
# Update z-range (only for non-substrate layers to get metal stack extent)
|
|
460
|
+
if layer_type != "substrate":
|
|
461
|
+
z_min_overall = min(z_min_overall, zmin)
|
|
462
|
+
z_max_overall = max(z_max_overall, zmax)
|
|
463
|
+
|
|
464
|
+
# Add material properties
|
|
465
|
+
for material in materials_used:
|
|
466
|
+
props = get_material_properties(material)
|
|
467
|
+
if props:
|
|
468
|
+
stack.materials[material] = props.to_dict()
|
|
469
|
+
else:
|
|
470
|
+
# Unknown material - add placeholder
|
|
471
|
+
stack.materials[material] = {
|
|
472
|
+
"type": "unknown",
|
|
473
|
+
"note": "Material not in database, please add properties manually",
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
# Always add air for the air box
|
|
477
|
+
if "air" not in stack.materials:
|
|
478
|
+
stack.materials["air"] = MATERIALS_DB["air"].to_dict()
|
|
479
|
+
|
|
480
|
+
# Add SiO2 for inter-layer dielectric if not present
|
|
481
|
+
if "SiO2" not in stack.materials:
|
|
482
|
+
stack.materials["SiO2"] = MATERIALS_DB["SiO2"].to_dict()
|
|
483
|
+
|
|
484
|
+
# Generate dielectric regions
|
|
485
|
+
if include_substrate:
|
|
486
|
+
# 1. Substrate below z=0 (lossy silicon)
|
|
487
|
+
stack.dielectrics.append(
|
|
488
|
+
{
|
|
489
|
+
"name": "substrate",
|
|
490
|
+
"zmin": -substrate_thickness,
|
|
491
|
+
"zmax": 0.0,
|
|
492
|
+
"material": "silicon",
|
|
493
|
+
}
|
|
494
|
+
)
|
|
495
|
+
if "silicon" not in stack.materials:
|
|
496
|
+
stack.materials["silicon"] = MATERIALS_DB["silicon"].to_dict()
|
|
497
|
+
oxide_zmin = 0.0
|
|
498
|
+
else:
|
|
499
|
+
# No substrate - extend oxide down slightly (matches gds2palace "nosub")
|
|
500
|
+
# This provides a dielectric spacing below the bottom metal
|
|
501
|
+
oxide_zmin = -substrate_thickness
|
|
502
|
+
|
|
503
|
+
# 2. Inter-layer dielectric (simplified: one big oxide region)
|
|
504
|
+
# In reality, should fill gaps between metals
|
|
505
|
+
stack.dielectrics.append(
|
|
506
|
+
{
|
|
507
|
+
"name": "oxide",
|
|
508
|
+
"zmin": oxide_zmin,
|
|
509
|
+
"zmax": z_max_overall,
|
|
510
|
+
"material": "SiO2",
|
|
511
|
+
}
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
# 3. Passivation layer on top of oxide (matches gds2palace IHP SG13G2)
|
|
515
|
+
passive_thickness = 0.4 # um, from gds2palace XML
|
|
516
|
+
stack.dielectrics.append(
|
|
517
|
+
{
|
|
518
|
+
"name": "passive",
|
|
519
|
+
"zmin": z_max_overall,
|
|
520
|
+
"zmax": z_max_overall + passive_thickness,
|
|
521
|
+
"material": "passive",
|
|
522
|
+
}
|
|
523
|
+
)
|
|
524
|
+
if "passive" not in stack.materials:
|
|
525
|
+
stack.materials["passive"] = MATERIALS_DB["passive"].to_dict()
|
|
526
|
+
|
|
527
|
+
# 4. Air above passivation
|
|
528
|
+
stack.dielectrics.append(
|
|
529
|
+
{
|
|
530
|
+
"name": "air_box",
|
|
531
|
+
"zmin": z_max_overall + passive_thickness,
|
|
532
|
+
"zmax": z_max_overall + passive_thickness + air_above,
|
|
533
|
+
"material": "air",
|
|
534
|
+
}
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
# Simulation settings
|
|
538
|
+
stack.simulation = {
|
|
539
|
+
"boundary_margin": boundary_margin,
|
|
540
|
+
"air_above": air_above,
|
|
541
|
+
"substrate_thickness": substrate_thickness,
|
|
542
|
+
"include_substrate": include_substrate,
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
return stack
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def extract_from_pdk(
|
|
549
|
+
pdk_module,
|
|
550
|
+
output_path: Path | None = None,
|
|
551
|
+
**kwargs,
|
|
552
|
+
) -> LayerStack:
|
|
553
|
+
"""Extract layer stack from a PDK module or PDK object.
|
|
554
|
+
|
|
555
|
+
Args:
|
|
556
|
+
pdk_module: PDK module (e.g., ihp, sky130) or gdsfactory Pdk object
|
|
557
|
+
output_path: Optional path to write YAML file
|
|
558
|
+
**kwargs: Additional arguments passed to extract_layer_stack
|
|
559
|
+
|
|
560
|
+
Returns:
|
|
561
|
+
LayerStack object for Palace simulation
|
|
562
|
+
"""
|
|
563
|
+
# Get PDK name - handle both module and Pdk object
|
|
564
|
+
pdk_name = "unknown"
|
|
565
|
+
|
|
566
|
+
# Direct .name attribute (gdsfactory Pdk object)
|
|
567
|
+
if hasattr(pdk_module, "name") and isinstance(pdk_module.name, str):
|
|
568
|
+
pdk_name = pdk_module.name
|
|
569
|
+
# Module with PDK.name
|
|
570
|
+
elif hasattr(pdk_module, "PDK") and hasattr(pdk_module.PDK, "name"):
|
|
571
|
+
pdk_name = pdk_module.PDK.name
|
|
572
|
+
# Module __name__
|
|
573
|
+
elif hasattr(pdk_module, "__name__"):
|
|
574
|
+
pdk_name = pdk_module.__name__
|
|
575
|
+
|
|
576
|
+
# Get layer stack from PDK - handle both module and Pdk object
|
|
577
|
+
gf_layer_stack = None
|
|
578
|
+
|
|
579
|
+
# Direct layer_stack attribute (gdsfactory Pdk object)
|
|
580
|
+
if hasattr(pdk_module, "layer_stack") and pdk_module.layer_stack is not None:
|
|
581
|
+
gf_layer_stack = pdk_module.layer_stack
|
|
582
|
+
# Module with LAYER_STACK
|
|
583
|
+
elif hasattr(pdk_module, "LAYER_STACK"):
|
|
584
|
+
gf_layer_stack = pdk_module.LAYER_STACK
|
|
585
|
+
# Module with get_layer_stack()
|
|
586
|
+
elif hasattr(pdk_module, "get_layer_stack"):
|
|
587
|
+
gf_layer_stack = pdk_module.get_layer_stack()
|
|
588
|
+
# Module with PDK.layer_stack
|
|
589
|
+
elif hasattr(pdk_module, "PDK") and hasattr(pdk_module.PDK, "layer_stack"):
|
|
590
|
+
gf_layer_stack = pdk_module.PDK.layer_stack
|
|
591
|
+
|
|
592
|
+
if gf_layer_stack is None:
|
|
593
|
+
raise ValueError(f"Could not find layer stack in PDK: {pdk_module}")
|
|
594
|
+
|
|
595
|
+
# Extract
|
|
596
|
+
stack = extract_layer_stack(gf_layer_stack, pdk_name=pdk_name, **kwargs)
|
|
597
|
+
|
|
598
|
+
# Write to file if path provided
|
|
599
|
+
if output_path:
|
|
600
|
+
stack.to_yaml(output_path)
|
|
601
|
+
|
|
602
|
+
return stack
|