gsim 0.0.0__py3-none-any.whl → 0.0.2__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 +1 -1
- gsim/common/__init__.py +61 -0
- gsim/common/geometry.py +76 -0
- gsim/{palace → common}/stack/__init__.py +8 -5
- gsim/{palace → common}/stack/extractor.py +29 -103
- gsim/{palace → common}/stack/materials.py +27 -11
- gsim/palace/__init__.py +94 -43
- gsim/palace/base.py +68 -0
- gsim/palace/driven.py +1004 -0
- gsim/palace/eigenmode.py +777 -0
- gsim/palace/electrostatic.py +622 -0
- gsim/palace/mesh/generator.py +201 -20
- gsim/palace/mesh/pipeline.py +22 -1
- gsim/palace/models/__init__.py +60 -0
- gsim/palace/models/geometry.py +34 -0
- gsim/palace/models/mesh.py +95 -0
- gsim/palace/models/numerical.py +66 -0
- gsim/palace/models/ports.py +138 -0
- gsim/palace/models/problems.py +195 -0
- gsim/palace/models/results.py +159 -0
- gsim/palace/models/stack.py +59 -0
- gsim/palace/ports/config.py +1 -1
- {gsim-0.0.0.dist-info → gsim-0.0.2.dist-info}/METADATA +5 -3
- gsim-0.0.2.dist-info/RECORD +32 -0
- gsim-0.0.0.dist-info/RECORD +0 -18
- /gsim/{palace → common}/stack/visualization.py +0 -0
- {gsim-0.0.0.dist-info → gsim-0.0.2.dist-info}/WHEEL +0 -0
- {gsim-0.0.0.dist-info → gsim-0.0.2.dist-info}/top_level.txt +0 -0
gsim/__init__.py
CHANGED
gsim/common/__init__.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Common components shared between Palace and FDTD solvers.
|
|
2
|
+
|
|
3
|
+
This module provides shared data models and utilities that can be used
|
|
4
|
+
across different electromagnetic solvers (Palace, FDTD, etc.).
|
|
5
|
+
|
|
6
|
+
Classes:
|
|
7
|
+
Geometry: Wrapper for gdsfactory Component with computed properties
|
|
8
|
+
LayerStack: Layer stack data model with extraction from PDK
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from gsim.common.geometry import Geometry
|
|
14
|
+
from gsim.common.stack import (
|
|
15
|
+
MATERIALS_DB,
|
|
16
|
+
Layer,
|
|
17
|
+
LayerStack,
|
|
18
|
+
MaterialProperties,
|
|
19
|
+
StackLayer,
|
|
20
|
+
ValidationResult,
|
|
21
|
+
extract_from_pdk,
|
|
22
|
+
extract_layer_stack,
|
|
23
|
+
get_material_properties,
|
|
24
|
+
get_stack,
|
|
25
|
+
load_stack_yaml,
|
|
26
|
+
material_is_conductor,
|
|
27
|
+
material_is_dielectric,
|
|
28
|
+
parse_layer_stack,
|
|
29
|
+
plot_stack,
|
|
30
|
+
print_stack,
|
|
31
|
+
print_stack_table,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# Alias for backward compatibility
|
|
35
|
+
Stack = LayerStack
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
# Geometry
|
|
39
|
+
"Geometry",
|
|
40
|
+
# Stack (LayerStack is the primary name, Stack is an alias)
|
|
41
|
+
"LayerStack",
|
|
42
|
+
"Stack",
|
|
43
|
+
"Layer",
|
|
44
|
+
"StackLayer",
|
|
45
|
+
"ValidationResult",
|
|
46
|
+
# Stack functions
|
|
47
|
+
"get_stack",
|
|
48
|
+
"load_stack_yaml",
|
|
49
|
+
"extract_from_pdk",
|
|
50
|
+
"extract_layer_stack",
|
|
51
|
+
"parse_layer_stack",
|
|
52
|
+
"print_stack",
|
|
53
|
+
"print_stack_table",
|
|
54
|
+
"plot_stack",
|
|
55
|
+
# Materials
|
|
56
|
+
"MATERIALS_DB",
|
|
57
|
+
"MaterialProperties",
|
|
58
|
+
"get_material_properties",
|
|
59
|
+
"material_is_conductor",
|
|
60
|
+
"material_is_dielectric",
|
|
61
|
+
]
|
gsim/common/geometry.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Geometry wrapper for gdsfactory components.
|
|
2
|
+
|
|
3
|
+
This module provides a Geometry class that wraps gdsfactory Components
|
|
4
|
+
with computed properties useful for simulation setup.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from functools import cached_property
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from pydantic import BaseModel, ConfigDict
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Geometry(BaseModel):
|
|
16
|
+
"""Shared geometry wrapper for gdsfactory Component.
|
|
17
|
+
|
|
18
|
+
This class wraps a gdsfactory Component and provides computed properties
|
|
19
|
+
that are useful for simulation setup (bounds, ports, etc.).
|
|
20
|
+
|
|
21
|
+
Attributes:
|
|
22
|
+
component: The wrapped gdsfactory Component
|
|
23
|
+
|
|
24
|
+
Example:
|
|
25
|
+
>>> from gdsfactory.components import straight
|
|
26
|
+
>>> c = straight(length=100)
|
|
27
|
+
>>> geom = Geometry(component=c)
|
|
28
|
+
>>> print(geom.bounds)
|
|
29
|
+
(0.0, -0.25, 100.0, 0.25)
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
33
|
+
|
|
34
|
+
component: Any # gdsfactory Component (Any to avoid import issues)
|
|
35
|
+
|
|
36
|
+
@cached_property
|
|
37
|
+
def bounds(self) -> tuple[float, float, float, float]:
|
|
38
|
+
"""Get bounding box (xmin, ymin, xmax, ymax) in um."""
|
|
39
|
+
bbox = self.component.dbbox()
|
|
40
|
+
return (bbox.left, bbox.bottom, bbox.right, bbox.top)
|
|
41
|
+
|
|
42
|
+
@cached_property
|
|
43
|
+
def ports(self) -> list:
|
|
44
|
+
"""Get list of ports on the component."""
|
|
45
|
+
return list(self.component.ports)
|
|
46
|
+
|
|
47
|
+
@cached_property
|
|
48
|
+
def port_names(self) -> list[str]:
|
|
49
|
+
"""Get list of port names."""
|
|
50
|
+
return [p.name for p in self.component.ports]
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def width(self) -> float:
|
|
54
|
+
"""Get width (x-extent) of geometry in um."""
|
|
55
|
+
xmin, _, xmax, _ = self.bounds
|
|
56
|
+
return xmax - xmin
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def height(self) -> float:
|
|
60
|
+
"""Get height (y-extent) of geometry in um."""
|
|
61
|
+
_, ymin, _, ymax = self.bounds
|
|
62
|
+
return ymax - ymin
|
|
63
|
+
|
|
64
|
+
def get_port(self, name: str) -> Any:
|
|
65
|
+
"""Get a port by name.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
name: Port name to find
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Port object if found, None otherwise
|
|
72
|
+
"""
|
|
73
|
+
for port in self.component.ports:
|
|
74
|
+
if port.name == name:
|
|
75
|
+
return port
|
|
76
|
+
return None
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
"""Layer stack extraction and parsing for
|
|
1
|
+
"""Layer stack extraction and parsing for EM simulation.
|
|
2
|
+
|
|
3
|
+
This module provides stack extraction functionality that can be shared
|
|
4
|
+
between different solvers (Palace, FDTD, etc.).
|
|
2
5
|
|
|
3
6
|
Usage:
|
|
4
7
|
# From PDK module (preferred, no file needed)
|
|
5
|
-
from gsim.
|
|
8
|
+
from gsim.common.stack import get_stack
|
|
6
9
|
stack = get_stack(pdk=ihp)
|
|
7
10
|
|
|
8
11
|
# From YAML file (for custom/tweaked stacks)
|
|
@@ -19,21 +22,21 @@ from pathlib import Path
|
|
|
19
22
|
import gdsfactory as gf
|
|
20
23
|
import yaml
|
|
21
24
|
|
|
22
|
-
from gsim.
|
|
25
|
+
from gsim.common.stack.extractor import (
|
|
23
26
|
Layer,
|
|
24
27
|
LayerStack,
|
|
25
28
|
ValidationResult,
|
|
26
29
|
extract_from_pdk,
|
|
27
30
|
extract_layer_stack,
|
|
28
31
|
)
|
|
29
|
-
from gsim.
|
|
32
|
+
from gsim.common.stack.materials import (
|
|
30
33
|
MATERIALS_DB,
|
|
31
34
|
MaterialProperties,
|
|
32
35
|
get_material_properties,
|
|
33
36
|
material_is_conductor,
|
|
34
37
|
material_is_dielectric,
|
|
35
38
|
)
|
|
36
|
-
from gsim.
|
|
39
|
+
from gsim.common.stack.visualization import (
|
|
37
40
|
StackLayer,
|
|
38
41
|
parse_layer_stack,
|
|
39
42
|
plot_stack,
|
|
@@ -7,15 +7,15 @@ that can be used for Palace EM simulation.
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
import logging
|
|
10
|
-
from dataclasses import dataclass, field
|
|
11
10
|
from pathlib import Path
|
|
12
|
-
from typing import Any
|
|
11
|
+
from typing import Any, Literal
|
|
13
12
|
|
|
14
13
|
import yaml
|
|
15
14
|
from gdsfactory.technology import LayerLevel
|
|
16
15
|
from gdsfactory.technology import LayerStack as GfLayerStack
|
|
16
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
17
17
|
|
|
18
|
-
from gsim.
|
|
18
|
+
from gsim.common.stack.materials import (
|
|
19
19
|
MATERIALS_DB,
|
|
20
20
|
get_material_properties,
|
|
21
21
|
)
|
|
@@ -23,20 +23,18 @@ from gsim.palace.stack.materials import (
|
|
|
23
23
|
logger = logging.getLogger(__name__)
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
class Layer:
|
|
26
|
+
class Layer(BaseModel):
|
|
28
27
|
"""Layer information for Palace simulation."""
|
|
29
28
|
|
|
29
|
+
model_config = ConfigDict(validate_assignment=True)
|
|
30
|
+
|
|
30
31
|
name: str
|
|
31
32
|
gds_layer: tuple[int, int] # (layer, datatype)
|
|
32
33
|
zmin: float # um
|
|
33
34
|
zmax: float # um
|
|
34
35
|
thickness: float # um
|
|
35
36
|
material: str
|
|
36
|
-
layer_type:
|
|
37
|
-
|
|
38
|
-
# Mesh resolution control
|
|
39
|
-
# Options: "fine", "medium", "coarse" or a float in um
|
|
37
|
+
layer_type: Literal["conductor", "via", "dielectric", "substrate"]
|
|
40
38
|
mesh_resolution: str | float = "medium"
|
|
41
39
|
|
|
42
40
|
def get_mesh_size(self, base_size: float = 1.0) -> float:
|
|
@@ -56,7 +54,7 @@ class Layer:
|
|
|
56
54
|
"medium": base_size,
|
|
57
55
|
"coarse": base_size * 2.0,
|
|
58
56
|
}
|
|
59
|
-
return resolution_map.get(self.mesh_resolution, base_size)
|
|
57
|
+
return resolution_map.get(str(self.mesh_resolution), base_size)
|
|
60
58
|
|
|
61
59
|
def to_dict(self) -> dict:
|
|
62
60
|
"""Convert to dictionary for YAML output."""
|
|
@@ -71,13 +69,14 @@ class Layer:
|
|
|
71
69
|
}
|
|
72
70
|
|
|
73
71
|
|
|
74
|
-
|
|
75
|
-
class ValidationResult:
|
|
72
|
+
class ValidationResult(BaseModel):
|
|
76
73
|
"""Result of stack validation."""
|
|
77
74
|
|
|
75
|
+
model_config = ConfigDict(validate_assignment=True)
|
|
76
|
+
|
|
78
77
|
valid: bool
|
|
79
|
-
errors: list[str] =
|
|
80
|
-
warnings: list[str] =
|
|
78
|
+
errors: list[str] = Field(default_factory=list)
|
|
79
|
+
warnings: list[str] = Field(default_factory=list)
|
|
81
80
|
|
|
82
81
|
def __bool__(self) -> bool:
|
|
83
82
|
return self.valid
|
|
@@ -97,18 +96,19 @@ class ValidationResult:
|
|
|
97
96
|
return "\n".join(lines)
|
|
98
97
|
|
|
99
98
|
|
|
100
|
-
|
|
101
|
-
class LayerStack:
|
|
99
|
+
class LayerStack(BaseModel):
|
|
102
100
|
"""Complete layer stack for Palace simulation."""
|
|
103
101
|
|
|
104
|
-
|
|
102
|
+
model_config = ConfigDict(validate_assignment=True)
|
|
103
|
+
|
|
104
|
+
pdk_name: str = "unknown"
|
|
105
105
|
units: str = "um"
|
|
106
|
-
layers: dict[str, Layer] =
|
|
107
|
-
materials: dict[str, dict] =
|
|
108
|
-
dielectrics: list[dict] =
|
|
109
|
-
simulation: dict =
|
|
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
110
|
|
|
111
|
-
def
|
|
111
|
+
def validate_stack(self, tolerance: float = 0.001) -> ValidationResult:
|
|
112
112
|
"""Validate the layer stack for simulation readiness.
|
|
113
113
|
|
|
114
114
|
Checks:
|
|
@@ -132,7 +132,6 @@ class LayerStack:
|
|
|
132
132
|
# Collect materials from layers
|
|
133
133
|
for name, layer in self.layers.items():
|
|
134
134
|
materials_used.add(layer.material)
|
|
135
|
-
# Check for invalid thickness
|
|
136
135
|
if layer.thickness < 0:
|
|
137
136
|
errors.append(
|
|
138
137
|
f"Layer '{name}' has negative thickness: {layer.thickness}"
|
|
@@ -165,10 +164,8 @@ class LayerStack:
|
|
|
165
164
|
|
|
166
165
|
# 2. Check z-axis continuity of dielectrics
|
|
167
166
|
if self.dielectrics:
|
|
168
|
-
# Sort dielectrics by zmin
|
|
169
167
|
sorted_dielectrics = sorted(self.dielectrics, key=lambda d: d["zmin"])
|
|
170
168
|
|
|
171
|
-
# Check for gaps between dielectric regions
|
|
172
169
|
for i in range(len(sorted_dielectrics) - 1):
|
|
173
170
|
current = sorted_dielectrics[i]
|
|
174
171
|
next_d = sorted_dielectrics[i + 1]
|
|
@@ -181,13 +178,11 @@ class LayerStack:
|
|
|
181
178
|
f"(zmin={next_d['zmin']:.4f}): gap={gap:.4f} um"
|
|
182
179
|
)
|
|
183
180
|
elif gap < -tolerance:
|
|
184
|
-
# Overlap - this might be intentional
|
|
185
181
|
warnings.append(
|
|
186
182
|
f"Z-axis overlap between '{current['name']}' and "
|
|
187
183
|
f"'{next_d['name']}': overlap={-gap:.4f} um"
|
|
188
184
|
)
|
|
189
185
|
|
|
190
|
-
# Get overall dielectric envelope
|
|
191
186
|
z_min_dielectric = sorted_dielectrics[0]["zmin"]
|
|
192
187
|
z_max_dielectric = sorted_dielectrics[-1]["zmax"]
|
|
193
188
|
else:
|
|
@@ -281,61 +276,43 @@ class LayerStack:
|
|
|
281
276
|
|
|
282
277
|
|
|
283
278
|
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
|
-
"""
|
|
279
|
+
"""Extract GDS layer tuple from LayerLevel."""
|
|
292
280
|
layer: Any = layer_level.layer
|
|
293
281
|
|
|
294
|
-
# Handle different layer specifications
|
|
295
282
|
if isinstance(layer, tuple):
|
|
296
283
|
return (int(layer[0]), int(layer[1]))
|
|
297
284
|
|
|
298
285
|
if isinstance(layer, int):
|
|
299
286
|
return (int(layer), 0)
|
|
300
287
|
|
|
301
|
-
# Handle LogicalLayer (gdsfactory.technology.layer_stack.LogicalLayer)
|
|
302
|
-
# Structure: layer_level.layer (LogicalLayer) -> .layer (Enum) -> .layer/.datatype
|
|
303
|
-
# (int)
|
|
304
288
|
if hasattr(layer, "layer"):
|
|
305
289
|
inner = layer.layer
|
|
306
|
-
# Check if inner has layer/datatype (kfactory/gdsfactory enum)
|
|
307
290
|
if hasattr(inner, "layer") and hasattr(inner, "datatype"):
|
|
308
291
|
return (int(inner.layer), int(inner.datatype))
|
|
309
|
-
# Check if inner itself has the values
|
|
310
292
|
if isinstance(inner, int):
|
|
311
293
|
datatype = getattr(layer, "datatype", 0)
|
|
312
294
|
return (int(inner), int(datatype) if datatype else 0)
|
|
313
|
-
# Recurse one more level if needed
|
|
314
295
|
if hasattr(inner, "layer"):
|
|
315
296
|
innermost = inner.layer
|
|
316
297
|
if isinstance(innermost, int):
|
|
317
298
|
datatype = getattr(inner, "datatype", 0)
|
|
318
299
|
return (int(innermost), int(datatype) if datatype else 0)
|
|
319
300
|
|
|
320
|
-
# Direct enum with layer/datatype
|
|
321
301
|
if hasattr(layer, "layer") and hasattr(layer, "datatype"):
|
|
322
302
|
return (int(layer.layer), int(layer.datatype))
|
|
323
303
|
|
|
324
|
-
# Enum with tuple value
|
|
325
304
|
if hasattr(layer, "value"):
|
|
326
305
|
if isinstance(layer.value, tuple):
|
|
327
306
|
return (int(layer.value[0]), int(layer.value[1]))
|
|
328
307
|
if isinstance(layer.value, int):
|
|
329
308
|
return (int(layer.value), 0)
|
|
330
309
|
|
|
331
|
-
# String format "8/0"
|
|
332
310
|
if isinstance(layer, str):
|
|
333
311
|
if "/" in layer:
|
|
334
312
|
parts = layer.split("/")
|
|
335
313
|
return (int(parts[0]), int(parts[1]))
|
|
336
314
|
return (0, 0)
|
|
337
315
|
|
|
338
|
-
# Fallback
|
|
339
316
|
try:
|
|
340
317
|
return (int(layer), 0)
|
|
341
318
|
except (TypeError, ValueError):
|
|
@@ -343,34 +320,24 @@ def _get_gds_layer_tuple(layer_level: LayerLevel) -> tuple[int, int]:
|
|
|
343
320
|
return (0, 0)
|
|
344
321
|
|
|
345
322
|
|
|
346
|
-
def _classify_layer_type(
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
layer_name: Name of the layer (e.g., "metal1", "via1", "substrate")
|
|
351
|
-
material: Material name
|
|
352
|
-
|
|
353
|
-
Returns:
|
|
354
|
-
Layer type string
|
|
355
|
-
"""
|
|
323
|
+
def _classify_layer_type(
|
|
324
|
+
layer_name: str, material: str
|
|
325
|
+
) -> Literal["conductor", "via", "dielectric", "substrate"]:
|
|
326
|
+
"""Classify a layer as conductor, via, dielectric, or substrate."""
|
|
356
327
|
name_lower = layer_name.lower()
|
|
357
328
|
material_lower = material.lower()
|
|
358
329
|
|
|
359
|
-
# Check for via layers
|
|
360
330
|
if "via" in name_lower:
|
|
361
331
|
return "via"
|
|
362
332
|
|
|
363
|
-
# Check for metal layers
|
|
364
333
|
if any(
|
|
365
334
|
m in name_lower for m in ["metal", "topmetal", "m1", "m2", "m3", "m4", "m5"]
|
|
366
335
|
):
|
|
367
336
|
return "conductor"
|
|
368
337
|
|
|
369
|
-
# Check for substrate
|
|
370
338
|
if "substrate" in name_lower or name_lower == "sub":
|
|
371
339
|
return "substrate"
|
|
372
340
|
|
|
373
|
-
# Check by material
|
|
374
341
|
props = get_material_properties(material)
|
|
375
342
|
if props:
|
|
376
343
|
if props.type == "conductor":
|
|
@@ -380,15 +347,12 @@ def _classify_layer_type(layer_name: str, material: str) -> str:
|
|
|
380
347
|
if props.type == "dielectric":
|
|
381
348
|
return "dielectric"
|
|
382
349
|
|
|
383
|
-
# Check for common conductor materials
|
|
384
350
|
if material_lower in ["aluminum", "copper", "tungsten", "gold", "al", "cu", "w"]:
|
|
385
351
|
return "conductor"
|
|
386
352
|
|
|
387
|
-
# Check for poly
|
|
388
353
|
if "poly" in name_lower:
|
|
389
354
|
return "conductor"
|
|
390
355
|
|
|
391
|
-
# Default to dielectric
|
|
392
356
|
return "dielectric"
|
|
393
357
|
|
|
394
358
|
|
|
@@ -408,41 +372,28 @@ def extract_layer_stack(
|
|
|
408
372
|
substrate_thickness: Thickness of substrate in um (default: 2.0)
|
|
409
373
|
air_above: Height of air box above top metal in um (default: 200)
|
|
410
374
|
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.
|
|
375
|
+
include_substrate: Whether to include lossy substrate (default: False)
|
|
413
376
|
|
|
414
377
|
Returns:
|
|
415
378
|
LayerStack object for Palace simulation
|
|
416
379
|
"""
|
|
417
380
|
stack = LayerStack(pdk_name=pdk_name)
|
|
418
381
|
|
|
419
|
-
# Track z-range for dielectric regions
|
|
420
382
|
z_min_overall = float("inf")
|
|
421
383
|
z_max_overall = float("-inf")
|
|
422
|
-
|
|
423
|
-
# Collect materials used
|
|
424
384
|
materials_used: set[str] = set()
|
|
425
385
|
|
|
426
|
-
# Extract each layer
|
|
427
386
|
for layer_name, layer_level in gf_layer_stack.layers.items():
|
|
428
|
-
# Get layer properties
|
|
429
387
|
zmin = layer_level.zmin if layer_level.zmin is not None else 0.0
|
|
430
388
|
thickness = layer_level.thickness if layer_level.thickness is not None else 0.0
|
|
431
389
|
zmax = zmin + thickness
|
|
432
390
|
material = layer_level.material if layer_level.material else "unknown"
|
|
433
|
-
|
|
434
|
-
# Get GDS layer tuple
|
|
435
391
|
gds_layer = _get_gds_layer_tuple(layer_level)
|
|
436
|
-
|
|
437
|
-
# Classify layer type
|
|
438
392
|
layer_type = _classify_layer_type(layer_name, material)
|
|
439
393
|
|
|
440
|
-
# Skip substrate layers when include_substrate=False (matches gds2palace
|
|
441
|
-
# "nosub").
|
|
442
394
|
if layer_type == "substrate" and not include_substrate:
|
|
443
395
|
continue
|
|
444
396
|
|
|
445
|
-
# Create layer
|
|
446
397
|
layer = Layer(
|
|
447
398
|
name=layer_name,
|
|
448
399
|
gds_layer=gds_layer,
|
|
@@ -456,34 +407,27 @@ def extract_layer_stack(
|
|
|
456
407
|
stack.layers[layer_name] = layer
|
|
457
408
|
materials_used.add(material)
|
|
458
409
|
|
|
459
|
-
# Update z-range (only for non-substrate layers to get metal stack extent)
|
|
460
410
|
if layer_type != "substrate":
|
|
461
411
|
z_min_overall = min(z_min_overall, zmin)
|
|
462
412
|
z_max_overall = max(z_max_overall, zmax)
|
|
463
413
|
|
|
464
|
-
# Add material properties
|
|
465
414
|
for material in materials_used:
|
|
466
415
|
props = get_material_properties(material)
|
|
467
416
|
if props:
|
|
468
417
|
stack.materials[material] = props.to_dict()
|
|
469
418
|
else:
|
|
470
|
-
# Unknown material - add placeholder
|
|
471
419
|
stack.materials[material] = {
|
|
472
420
|
"type": "unknown",
|
|
473
421
|
"note": "Material not in database, please add properties manually",
|
|
474
422
|
}
|
|
475
423
|
|
|
476
|
-
# Always add air for the air box
|
|
477
424
|
if "air" not in stack.materials:
|
|
478
425
|
stack.materials["air"] = MATERIALS_DB["air"].to_dict()
|
|
479
426
|
|
|
480
|
-
# Add SiO2 for inter-layer dielectric if not present
|
|
481
427
|
if "SiO2" not in stack.materials:
|
|
482
428
|
stack.materials["SiO2"] = MATERIALS_DB["SiO2"].to_dict()
|
|
483
429
|
|
|
484
|
-
# Generate dielectric regions
|
|
485
430
|
if include_substrate:
|
|
486
|
-
# 1. Substrate below z=0 (lossy silicon)
|
|
487
431
|
stack.dielectrics.append(
|
|
488
432
|
{
|
|
489
433
|
"name": "substrate",
|
|
@@ -496,12 +440,8 @@ def extract_layer_stack(
|
|
|
496
440
|
stack.materials["silicon"] = MATERIALS_DB["silicon"].to_dict()
|
|
497
441
|
oxide_zmin = 0.0
|
|
498
442
|
else:
|
|
499
|
-
# No substrate - extend oxide down slightly (matches gds2palace "nosub")
|
|
500
|
-
# This provides a dielectric spacing below the bottom metal
|
|
501
443
|
oxide_zmin = -substrate_thickness
|
|
502
444
|
|
|
503
|
-
# 2. Inter-layer dielectric (simplified: one big oxide region)
|
|
504
|
-
# In reality, should fill gaps between metals
|
|
505
445
|
stack.dielectrics.append(
|
|
506
446
|
{
|
|
507
447
|
"name": "oxide",
|
|
@@ -511,8 +451,7 @@ def extract_layer_stack(
|
|
|
511
451
|
}
|
|
512
452
|
)
|
|
513
453
|
|
|
514
|
-
|
|
515
|
-
passive_thickness = 0.4 # um, from gds2palace XML
|
|
454
|
+
passive_thickness = 0.4
|
|
516
455
|
stack.dielectrics.append(
|
|
517
456
|
{
|
|
518
457
|
"name": "passive",
|
|
@@ -524,7 +463,6 @@ def extract_layer_stack(
|
|
|
524
463
|
if "passive" not in stack.materials:
|
|
525
464
|
stack.materials["passive"] = MATERIALS_DB["passive"].to_dict()
|
|
526
465
|
|
|
527
|
-
# 4. Air above passivation
|
|
528
466
|
stack.dielectrics.append(
|
|
529
467
|
{
|
|
530
468
|
"name": "air_box",
|
|
@@ -534,7 +472,6 @@ def extract_layer_stack(
|
|
|
534
472
|
}
|
|
535
473
|
)
|
|
536
474
|
|
|
537
|
-
# Simulation settings
|
|
538
475
|
stack.simulation = {
|
|
539
476
|
"boundary_margin": boundary_margin,
|
|
540
477
|
"air_above": air_above,
|
|
@@ -560,42 +497,31 @@ def extract_from_pdk(
|
|
|
560
497
|
Returns:
|
|
561
498
|
LayerStack object for Palace simulation
|
|
562
499
|
"""
|
|
563
|
-
# Get PDK name - handle both module and Pdk object
|
|
564
500
|
pdk_name = "unknown"
|
|
565
501
|
|
|
566
|
-
# Direct .name attribute (gdsfactory Pdk object)
|
|
567
502
|
if hasattr(pdk_module, "name") and isinstance(pdk_module.name, str):
|
|
568
503
|
pdk_name = pdk_module.name
|
|
569
|
-
# Module with PDK.name
|
|
570
504
|
elif hasattr(pdk_module, "PDK") and hasattr(pdk_module.PDK, "name"):
|
|
571
505
|
pdk_name = pdk_module.PDK.name
|
|
572
|
-
# Module __name__
|
|
573
506
|
elif hasattr(pdk_module, "__name__"):
|
|
574
507
|
pdk_name = pdk_module.__name__
|
|
575
508
|
|
|
576
|
-
# Get layer stack from PDK - handle both module and Pdk object
|
|
577
509
|
gf_layer_stack = None
|
|
578
510
|
|
|
579
|
-
# Direct layer_stack attribute (gdsfactory Pdk object)
|
|
580
511
|
if hasattr(pdk_module, "layer_stack") and pdk_module.layer_stack is not None:
|
|
581
512
|
gf_layer_stack = pdk_module.layer_stack
|
|
582
|
-
# Module with LAYER_STACK
|
|
583
513
|
elif hasattr(pdk_module, "LAYER_STACK"):
|
|
584
514
|
gf_layer_stack = pdk_module.LAYER_STACK
|
|
585
|
-
# Module with get_layer_stack()
|
|
586
515
|
elif hasattr(pdk_module, "get_layer_stack"):
|
|
587
516
|
gf_layer_stack = pdk_module.get_layer_stack()
|
|
588
|
-
# Module with PDK.layer_stack
|
|
589
517
|
elif hasattr(pdk_module, "PDK") and hasattr(pdk_module.PDK, "layer_stack"):
|
|
590
518
|
gf_layer_stack = pdk_module.PDK.layer_stack
|
|
591
519
|
|
|
592
520
|
if gf_layer_stack is None:
|
|
593
521
|
raise ValueError(f"Could not find layer stack in PDK: {pdk_module}")
|
|
594
522
|
|
|
595
|
-
# Extract
|
|
596
523
|
stack = extract_layer_stack(gf_layer_stack, pdk_name=pdk_name, **kwargs)
|
|
597
524
|
|
|
598
|
-
# Write to file if path provided
|
|
599
525
|
if output_path:
|
|
600
526
|
stack.to_yaml(output_path)
|
|
601
527
|
|
|
@@ -6,17 +6,20 @@ This database provides the EM properties needed for Palace simulation.
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
-
from
|
|
9
|
+
from typing import Literal
|
|
10
10
|
|
|
11
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
class MaterialProperties:
|
|
13
|
+
|
|
14
|
+
class MaterialProperties(BaseModel):
|
|
14
15
|
"""EM properties for a material."""
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
model_config = ConfigDict(validate_assignment=True)
|
|
18
|
+
|
|
19
|
+
type: Literal["conductor", "dielectric", "semiconductor"]
|
|
20
|
+
conductivity: float | None = Field(default=None, ge=0) # S/m (for conductors)
|
|
21
|
+
permittivity: float | None = Field(default=None, ge=1.0) # relative permittivity
|
|
22
|
+
loss_tangent: float | None = Field(default=None, ge=0, le=1)
|
|
20
23
|
|
|
21
24
|
def to_dict(self) -> dict[str, object]:
|
|
22
25
|
"""Convert to dictionary for YAML output."""
|
|
@@ -29,6 +32,20 @@ class MaterialProperties:
|
|
|
29
32
|
d["loss_tangent"] = self.loss_tangent
|
|
30
33
|
return d
|
|
31
34
|
|
|
35
|
+
@classmethod
|
|
36
|
+
def conductor(cls, conductivity: float = 5.8e7) -> "MaterialProperties":
|
|
37
|
+
"""Create a conductor material."""
|
|
38
|
+
return cls(type="conductor", conductivity=conductivity)
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def dielectric(
|
|
42
|
+
cls, permittivity: float, loss_tangent: float = 0.0
|
|
43
|
+
) -> "MaterialProperties":
|
|
44
|
+
"""Create a dielectric material."""
|
|
45
|
+
return cls(
|
|
46
|
+
type="dielectric", permittivity=permittivity, loss_tangent=loss_tangent
|
|
47
|
+
)
|
|
48
|
+
|
|
32
49
|
|
|
33
50
|
# Material properties database
|
|
34
51
|
# Sources:
|
|
@@ -38,7 +55,7 @@ MATERIALS_DB: dict[str, MaterialProperties] = {
|
|
|
38
55
|
# Conductors (conductivity in S/m)
|
|
39
56
|
"aluminum": MaterialProperties(
|
|
40
57
|
type="conductor",
|
|
41
|
-
conductivity=3.77e7,
|
|
58
|
+
conductivity=3.77e7,
|
|
42
59
|
),
|
|
43
60
|
"copper": MaterialProperties(
|
|
44
61
|
type="conductor",
|
|
@@ -64,7 +81,7 @@ MATERIALS_DB: dict[str, MaterialProperties] = {
|
|
|
64
81
|
"SiO2": MaterialProperties(
|
|
65
82
|
type="dielectric",
|
|
66
83
|
permittivity=4.1, # Matches gds2palace IHP SG13G2
|
|
67
|
-
loss_tangent=0.0,
|
|
84
|
+
loss_tangent=0.0,
|
|
68
85
|
),
|
|
69
86
|
"passive": MaterialProperties(
|
|
70
87
|
type="dielectric",
|
|
@@ -100,7 +117,7 @@ MATERIALS_DB: dict[str, MaterialProperties] = {
|
|
|
100
117
|
"si": MaterialProperties(
|
|
101
118
|
type="semiconductor",
|
|
102
119
|
permittivity=11.9,
|
|
103
|
-
conductivity=2.0,
|
|
120
|
+
conductivity=2.0,
|
|
104
121
|
),
|
|
105
122
|
}
|
|
106
123
|
|
|
@@ -130,7 +147,6 @@ def get_material_properties(material_name: str) -> MaterialProperties | None:
|
|
|
130
147
|
Returns:
|
|
131
148
|
MaterialProperties if found, None otherwise
|
|
132
149
|
"""
|
|
133
|
-
# Normalize name
|
|
134
150
|
name_lower = material_name.lower().strip()
|
|
135
151
|
|
|
136
152
|
# Check direct match
|