gsim 0.0.0__py3-none-any.whl → 0.0.3__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 +57 -0
- gsim/common/geometry.py +76 -0
- gsim/{palace → common}/stack/__init__.py +8 -5
- gsim/{palace → common}/stack/extractor.py +33 -107
- gsim/{palace → common}/stack/materials.py +27 -11
- gsim/{palace → common}/stack/visualization.py +3 -3
- gsim/gcloud.py +78 -25
- gsim/palace/__init__.py +85 -45
- gsim/palace/base.py +373 -0
- gsim/palace/driven.py +728 -0
- gsim/palace/eigenmode.py +557 -0
- gsim/palace/electrostatic.py +398 -0
- gsim/palace/mesh/__init__.py +13 -1
- gsim/palace/mesh/config_generator.py +367 -0
- gsim/palace/mesh/generator.py +66 -744
- gsim/palace/mesh/geometry.py +472 -0
- gsim/palace/mesh/groups.py +170 -0
- gsim/palace/mesh/pipeline.py +22 -1
- gsim/palace/models/__init__.py +53 -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 +137 -0
- gsim/palace/models/problems.py +195 -0
- gsim/palace/models/results.py +160 -0
- gsim/palace/models/stack.py +59 -0
- gsim/palace/ports/config.py +1 -1
- gsim/viz.py +9 -6
- {gsim-0.0.0.dist-info → gsim-0.0.3.dist-info}/METADATA +9 -6
- gsim-0.0.3.dist-info/RECORD +35 -0
- {gsim-0.0.0.dist-info → gsim-0.0.3.dist-info}/WHEEL +1 -1
- gsim-0.0.0.dist-info/RECORD +0 -18
- {gsim-0.0.0.dist-info → gsim-0.0.3.dist-info}/top_level.txt +0 -0
gsim/palace/mesh/generator.py
CHANGED
|
@@ -6,7 +6,6 @@ replacing the gds2palace backend with a cleaner implementation.
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
-
import json
|
|
10
9
|
import logging
|
|
11
10
|
import math
|
|
12
11
|
from dataclasses import dataclass, field
|
|
@@ -16,25 +15,28 @@ from typing import TYPE_CHECKING
|
|
|
16
15
|
import gmsh
|
|
17
16
|
|
|
18
17
|
from . import gmsh_utils
|
|
18
|
+
from .config_generator import (
|
|
19
|
+
collect_mesh_stats,
|
|
20
|
+
generate_palace_config,
|
|
21
|
+
write_config,
|
|
22
|
+
)
|
|
23
|
+
from .geometry import (
|
|
24
|
+
GeometryData,
|
|
25
|
+
add_dielectrics,
|
|
26
|
+
add_metals,
|
|
27
|
+
add_ports,
|
|
28
|
+
extract_geometry,
|
|
29
|
+
)
|
|
30
|
+
from .groups import assign_physical_groups
|
|
19
31
|
|
|
20
32
|
if TYPE_CHECKING:
|
|
33
|
+
from gsim.common.stack import LayerStack
|
|
34
|
+
from gsim.palace.models import DrivenConfig
|
|
21
35
|
from gsim.palace.ports.config import PalacePort
|
|
22
|
-
from gsim.palace.stack import LayerStack
|
|
23
|
-
|
|
24
|
-
from gsim.palace.ports.config import PortGeometry
|
|
25
36
|
|
|
26
37
|
logger = logging.getLogger(__name__)
|
|
27
38
|
|
|
28
39
|
|
|
29
|
-
@dataclass
|
|
30
|
-
class GeometryData:
|
|
31
|
-
"""Container for geometry data extracted from component."""
|
|
32
|
-
|
|
33
|
-
polygons: list # List of (layer_num, pts_x, pts_y) tuples
|
|
34
|
-
bbox: tuple[float, float, float, float] # (xmin, ymin, xmax, ymax)
|
|
35
|
-
layer_bboxes: dict # layer_num -> (xmin, ymin, xmax, ymax)
|
|
36
|
-
|
|
37
|
-
|
|
38
40
|
@dataclass
|
|
39
41
|
class MeshResult:
|
|
40
42
|
"""Result from mesh generation."""
|
|
@@ -42,567 +44,12 @@ class MeshResult:
|
|
|
42
44
|
mesh_path: Path
|
|
43
45
|
config_path: Path | None = None
|
|
44
46
|
port_info: list = field(default_factory=list)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
component: gdsfactory Component
|
|
52
|
-
stack: LayerStack for layer mapping
|
|
53
|
-
|
|
54
|
-
Returns:
|
|
55
|
-
GeometryData with polygons and bounding boxes
|
|
56
|
-
"""
|
|
57
|
-
polygons = []
|
|
58
|
-
global_bbox = [math.inf, math.inf, -math.inf, -math.inf]
|
|
59
|
-
layer_bboxes = {}
|
|
60
|
-
|
|
61
|
-
# Get polygons from component
|
|
62
|
-
polygons_by_index = component.get_polygons()
|
|
63
|
-
|
|
64
|
-
# Build layer_index -> GDS tuple mapping
|
|
65
|
-
layout = component.kcl.layout
|
|
66
|
-
index_to_gds = {}
|
|
67
|
-
for layer_index in range(layout.layers()):
|
|
68
|
-
if layout.is_valid_layer(layer_index):
|
|
69
|
-
info = layout.get_info(layer_index)
|
|
70
|
-
index_to_gds[layer_index] = (info.layer, info.datatype)
|
|
71
|
-
|
|
72
|
-
# Build GDS tuple -> layer number mapping
|
|
73
|
-
gds_to_layernum = {}
|
|
74
|
-
for layer_data in stack.layers.values():
|
|
75
|
-
gds_tuple = tuple(layer_data.gds_layer)
|
|
76
|
-
gds_to_layernum[gds_tuple] = gds_tuple[0]
|
|
77
|
-
|
|
78
|
-
# Convert polygons
|
|
79
|
-
for layer_index, polys in polygons_by_index.items():
|
|
80
|
-
gds_tuple = index_to_gds.get(layer_index)
|
|
81
|
-
if gds_tuple is None:
|
|
82
|
-
continue
|
|
83
|
-
|
|
84
|
-
layernum = gds_to_layernum.get(gds_tuple)
|
|
85
|
-
if layernum is None:
|
|
86
|
-
continue
|
|
87
|
-
|
|
88
|
-
for poly in polys:
|
|
89
|
-
# Convert klayout polygon to lists (nm -> um)
|
|
90
|
-
points = list(poly.each_point_hull())
|
|
91
|
-
if len(points) < 3:
|
|
92
|
-
continue
|
|
93
|
-
|
|
94
|
-
pts_x = [pt.x / 1000.0 for pt in points]
|
|
95
|
-
pts_y = [pt.y / 1000.0 for pt in points]
|
|
96
|
-
|
|
97
|
-
polygons.append((layernum, pts_x, pts_y))
|
|
98
|
-
|
|
99
|
-
# Update bounding boxes
|
|
100
|
-
xmin, xmax = min(pts_x), max(pts_x)
|
|
101
|
-
ymin, ymax = min(pts_y), max(pts_y)
|
|
102
|
-
|
|
103
|
-
global_bbox[0] = min(global_bbox[0], xmin)
|
|
104
|
-
global_bbox[1] = min(global_bbox[1], ymin)
|
|
105
|
-
global_bbox[2] = max(global_bbox[2], xmax)
|
|
106
|
-
global_bbox[3] = max(global_bbox[3], ymax)
|
|
107
|
-
|
|
108
|
-
if layernum not in layer_bboxes:
|
|
109
|
-
layer_bboxes[layernum] = [xmin, ymin, xmax, ymax]
|
|
110
|
-
else:
|
|
111
|
-
bbox = layer_bboxes[layernum]
|
|
112
|
-
bbox[0] = min(bbox[0], xmin)
|
|
113
|
-
bbox[1] = min(bbox[1], ymin)
|
|
114
|
-
bbox[2] = max(bbox[2], xmax)
|
|
115
|
-
bbox[3] = max(bbox[3], ymax)
|
|
116
|
-
|
|
117
|
-
return GeometryData(
|
|
118
|
-
polygons=polygons,
|
|
119
|
-
bbox=(global_bbox[0], global_bbox[1], global_bbox[2], global_bbox[3]),
|
|
120
|
-
layer_bboxes=layer_bboxes,
|
|
121
|
-
)
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
def _get_layer_info(stack: LayerStack, gds_layer: int) -> dict | None:
|
|
125
|
-
"""Get layer info from stack by GDS layer number."""
|
|
126
|
-
for name, layer in stack.layers.items():
|
|
127
|
-
if layer.gds_layer[0] == gds_layer:
|
|
128
|
-
return {
|
|
129
|
-
"name": name,
|
|
130
|
-
"zmin": layer.zmin,
|
|
131
|
-
"zmax": layer.zmax,
|
|
132
|
-
"thickness": layer.zmax - layer.zmin,
|
|
133
|
-
"material": layer.material,
|
|
134
|
-
"type": layer.layer_type,
|
|
135
|
-
}
|
|
136
|
-
return None
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
def _add_metals(
|
|
140
|
-
kernel,
|
|
141
|
-
geometry: GeometryData,
|
|
142
|
-
stack: LayerStack,
|
|
143
|
-
) -> dict:
|
|
144
|
-
"""Add metal and via geometries to gmsh.
|
|
145
|
-
|
|
146
|
-
Creates extruded volumes for vias and shells (surfaces) for conductors.
|
|
147
|
-
|
|
148
|
-
Returns:
|
|
149
|
-
Dict with layer_name -> list of (surface_tags_xy, surface_tags_z) for
|
|
150
|
-
conductors, or volume_tags for vias.
|
|
151
|
-
"""
|
|
152
|
-
# layer_name -> {"volumes": [], "surfaces_xy": [], "surfaces_z": []}
|
|
153
|
-
metal_tags = {}
|
|
154
|
-
|
|
155
|
-
# Group polygons by layer
|
|
156
|
-
polygons_by_layer = {}
|
|
157
|
-
for layernum, pts_x, pts_y in geometry.polygons:
|
|
158
|
-
if layernum not in polygons_by_layer:
|
|
159
|
-
polygons_by_layer[layernum] = []
|
|
160
|
-
polygons_by_layer[layernum].append((pts_x, pts_y))
|
|
161
|
-
|
|
162
|
-
# Process each layer
|
|
163
|
-
for layernum, polys in polygons_by_layer.items():
|
|
164
|
-
layer_info = _get_layer_info(stack, layernum)
|
|
165
|
-
if layer_info is None:
|
|
166
|
-
continue
|
|
167
|
-
|
|
168
|
-
layer_name = layer_info["name"]
|
|
169
|
-
layer_type = layer_info["type"]
|
|
170
|
-
zmin = layer_info["zmin"]
|
|
171
|
-
thickness = layer_info["thickness"]
|
|
172
|
-
|
|
173
|
-
if layer_type not in ("conductor", "via"):
|
|
174
|
-
continue
|
|
175
|
-
|
|
176
|
-
if layer_name not in metal_tags:
|
|
177
|
-
metal_tags[layer_name] = {
|
|
178
|
-
"volumes": [],
|
|
179
|
-
"surfaces_xy": [],
|
|
180
|
-
"surfaces_z": [],
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
for pts_x, pts_y in polys:
|
|
184
|
-
# Create extruded polygon
|
|
185
|
-
surfacetag = gmsh_utils.create_polygon_surface(kernel, pts_x, pts_y, zmin)
|
|
186
|
-
if surfacetag is None:
|
|
187
|
-
continue
|
|
188
|
-
|
|
189
|
-
if thickness > 0:
|
|
190
|
-
result = kernel.extrude([(2, surfacetag)], 0, 0, thickness)
|
|
191
|
-
volumetag = result[1][1]
|
|
192
|
-
|
|
193
|
-
if layer_type == "via":
|
|
194
|
-
# Keep vias as volumes
|
|
195
|
-
metal_tags[layer_name]["volumes"].append(volumetag)
|
|
196
|
-
else:
|
|
197
|
-
# For conductors, get shell surfaces and remove volume
|
|
198
|
-
_, surfaceloops = kernel.getSurfaceLoops(volumetag)
|
|
199
|
-
if surfaceloops:
|
|
200
|
-
metal_tags[layer_name]["volumes"].append(
|
|
201
|
-
(volumetag, surfaceloops[0])
|
|
202
|
-
)
|
|
203
|
-
kernel.remove([(3, volumetag)])
|
|
204
|
-
|
|
205
|
-
kernel.removeAllDuplicates()
|
|
206
|
-
kernel.synchronize()
|
|
207
|
-
|
|
208
|
-
return metal_tags
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
def _add_dielectrics(
|
|
212
|
-
kernel,
|
|
213
|
-
geometry: GeometryData,
|
|
214
|
-
stack: LayerStack,
|
|
215
|
-
margin: float,
|
|
216
|
-
air_margin: float,
|
|
217
|
-
) -> dict:
|
|
218
|
-
"""Add dielectric boxes and airbox to gmsh.
|
|
219
|
-
|
|
220
|
-
Returns:
|
|
221
|
-
Dict with material_name -> list of volume_tags
|
|
222
|
-
"""
|
|
223
|
-
dielectric_tags = {}
|
|
224
|
-
|
|
225
|
-
# Get overall geometry bounds
|
|
226
|
-
xmin, ymin, xmax, ymax = geometry.bbox
|
|
227
|
-
xmin -= margin
|
|
228
|
-
ymin -= margin
|
|
229
|
-
xmax += margin
|
|
230
|
-
ymax += margin
|
|
231
|
-
|
|
232
|
-
# Track overall z range
|
|
233
|
-
z_min_all = math.inf
|
|
234
|
-
z_max_all = -math.inf
|
|
235
|
-
|
|
236
|
-
# Sort dielectrics by z (top to bottom for correct layering)
|
|
237
|
-
sorted_dielectrics = sorted(
|
|
238
|
-
stack.dielectrics, key=lambda d: d["zmax"], reverse=True
|
|
239
|
-
)
|
|
240
|
-
|
|
241
|
-
# Add dielectric boxes
|
|
242
|
-
offset = 0
|
|
243
|
-
offset_delta = margin / 20
|
|
244
|
-
|
|
245
|
-
for dielectric in sorted_dielectrics:
|
|
246
|
-
material = dielectric["material"]
|
|
247
|
-
d_zmin = dielectric["zmin"]
|
|
248
|
-
d_zmax = dielectric["zmax"]
|
|
249
|
-
|
|
250
|
-
z_min_all = min(z_min_all, d_zmin)
|
|
251
|
-
z_max_all = max(z_max_all, d_zmax)
|
|
252
|
-
|
|
253
|
-
if material not in dielectric_tags:
|
|
254
|
-
dielectric_tags[material] = []
|
|
255
|
-
|
|
256
|
-
# Create box with slight offset to avoid mesh issues
|
|
257
|
-
box_tag = gmsh_utils.create_box(
|
|
258
|
-
kernel,
|
|
259
|
-
xmin - offset,
|
|
260
|
-
ymin - offset,
|
|
261
|
-
d_zmin,
|
|
262
|
-
xmax + offset,
|
|
263
|
-
ymax + offset,
|
|
264
|
-
d_zmax,
|
|
265
|
-
)
|
|
266
|
-
dielectric_tags[material].append(box_tag)
|
|
267
|
-
|
|
268
|
-
# Alternate offset to avoid coincident faces
|
|
269
|
-
offset = offset_delta if offset == 0 else 0
|
|
270
|
-
|
|
271
|
-
# Add surrounding airbox
|
|
272
|
-
air_xmin = xmin - air_margin
|
|
273
|
-
air_ymin = ymin - air_margin
|
|
274
|
-
air_xmax = xmax + air_margin
|
|
275
|
-
air_ymax = ymax + air_margin
|
|
276
|
-
air_zmin = z_min_all - air_margin
|
|
277
|
-
air_zmax = z_max_all + air_margin
|
|
278
|
-
|
|
279
|
-
airbox_tag = kernel.addBox(
|
|
280
|
-
air_xmin,
|
|
281
|
-
air_ymin,
|
|
282
|
-
air_zmin,
|
|
283
|
-
air_xmax - air_xmin,
|
|
284
|
-
air_ymax - air_ymin,
|
|
285
|
-
air_zmax - air_zmin,
|
|
286
|
-
)
|
|
287
|
-
dielectric_tags["airbox"] = [airbox_tag]
|
|
288
|
-
|
|
289
|
-
kernel.synchronize()
|
|
290
|
-
|
|
291
|
-
return dielectric_tags
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
def _add_ports(
|
|
295
|
-
kernel,
|
|
296
|
-
ports: list[PalacePort],
|
|
297
|
-
stack: LayerStack,
|
|
298
|
-
) -> tuple[dict, list]:
|
|
299
|
-
"""Add port surfaces to gmsh.
|
|
300
|
-
|
|
301
|
-
Args:
|
|
302
|
-
kernel: gmsh kernel
|
|
303
|
-
ports: List of PalacePort objects (single or multi-element)
|
|
304
|
-
stack: Layer stack
|
|
305
|
-
|
|
306
|
-
Returns:
|
|
307
|
-
(port_tags dict, port_info list)
|
|
308
|
-
|
|
309
|
-
For single-element ports: port_tags["P{num}"] = [surface_tag]
|
|
310
|
-
For multi-element ports: port_tags["P{num}"] = [surface_tag, surface_tag, ...]
|
|
311
|
-
"""
|
|
312
|
-
port_tags = {} # "P{num}" -> [surface_tag(s)]
|
|
313
|
-
port_info = []
|
|
314
|
-
port_num = 1
|
|
315
|
-
|
|
316
|
-
for port in ports:
|
|
317
|
-
if port.multi_element:
|
|
318
|
-
# Multi-element port (CPW)
|
|
319
|
-
if port.layer is None or port.centers is None or port.directions is None:
|
|
320
|
-
continue
|
|
321
|
-
target_layer = stack.layers.get(port.layer)
|
|
322
|
-
if target_layer is None:
|
|
323
|
-
continue
|
|
324
|
-
|
|
325
|
-
z = target_layer.zmin
|
|
326
|
-
hw = port.width / 2
|
|
327
|
-
hl = (port.length or port.width) / 2
|
|
328
|
-
|
|
329
|
-
# Determine axis from orientation
|
|
330
|
-
angle = port.orientation % 360
|
|
331
|
-
is_y_axis = 45 <= angle < 135 or 225 <= angle < 315
|
|
332
|
-
|
|
333
|
-
surfaces = []
|
|
334
|
-
for cx, cy in port.centers:
|
|
335
|
-
if is_y_axis:
|
|
336
|
-
surf = gmsh_utils.create_port_rectangle(
|
|
337
|
-
kernel, cx - hw, cy - hl, z, cx + hw, cy + hl, z
|
|
338
|
-
)
|
|
339
|
-
else:
|
|
340
|
-
surf = gmsh_utils.create_port_rectangle(
|
|
341
|
-
kernel, cx - hl, cy - hw, z, cx + hl, cy + hw, z
|
|
342
|
-
)
|
|
343
|
-
surfaces.append(surf)
|
|
344
|
-
|
|
345
|
-
port_tags[f"P{port_num}"] = surfaces
|
|
346
|
-
|
|
347
|
-
port_info.append(
|
|
348
|
-
{
|
|
349
|
-
"portnumber": port_num,
|
|
350
|
-
"Z0": port.impedance,
|
|
351
|
-
"type": "cpw",
|
|
352
|
-
"elements": [
|
|
353
|
-
{"surface_idx": i, "direction": port.directions[i]}
|
|
354
|
-
for i in range(len(port.centers))
|
|
355
|
-
],
|
|
356
|
-
"width": port.width,
|
|
357
|
-
"length": port.length or port.width,
|
|
358
|
-
"zmin": z,
|
|
359
|
-
"zmax": z,
|
|
360
|
-
}
|
|
361
|
-
)
|
|
362
|
-
|
|
363
|
-
elif port.geometry == PortGeometry.VIA:
|
|
364
|
-
# Via port: vertical between two layers
|
|
365
|
-
if port.from_layer is None or port.to_layer is None:
|
|
366
|
-
continue
|
|
367
|
-
from_layer = stack.layers.get(port.from_layer)
|
|
368
|
-
to_layer = stack.layers.get(port.to_layer)
|
|
369
|
-
if from_layer is None or to_layer is None:
|
|
370
|
-
continue
|
|
371
|
-
|
|
372
|
-
x, y = port.center
|
|
373
|
-
hw = port.width / 2
|
|
374
|
-
|
|
375
|
-
if from_layer.zmin < to_layer.zmin:
|
|
376
|
-
zmin = from_layer.zmax
|
|
377
|
-
zmax = to_layer.zmin
|
|
378
|
-
else:
|
|
379
|
-
zmin = to_layer.zmax
|
|
380
|
-
zmax = from_layer.zmin
|
|
381
|
-
|
|
382
|
-
# Create vertical port surface
|
|
383
|
-
if port.direction in ("x", "-x"):
|
|
384
|
-
surfacetag = gmsh_utils.create_port_rectangle(
|
|
385
|
-
kernel, x, y - hw, zmin, x, y + hw, zmax
|
|
386
|
-
)
|
|
387
|
-
else:
|
|
388
|
-
surfacetag = gmsh_utils.create_port_rectangle(
|
|
389
|
-
kernel, x - hw, y, zmin, x + hw, y, zmax
|
|
390
|
-
)
|
|
391
|
-
|
|
392
|
-
port_tags[f"P{port_num}"] = [surfacetag]
|
|
393
|
-
port_info.append(
|
|
394
|
-
{
|
|
395
|
-
"portnumber": port_num,
|
|
396
|
-
"Z0": port.impedance,
|
|
397
|
-
"type": "via",
|
|
398
|
-
"direction": "Z",
|
|
399
|
-
"length": zmax - zmin,
|
|
400
|
-
"width": port.width,
|
|
401
|
-
"xmin": x - hw if port.direction in ("y", "-y") else x,
|
|
402
|
-
"xmax": x + hw if port.direction in ("y", "-y") else x,
|
|
403
|
-
"ymin": y - hw if port.direction in ("x", "-x") else y,
|
|
404
|
-
"ymax": y + hw if port.direction in ("x", "-x") else y,
|
|
405
|
-
"zmin": zmin,
|
|
406
|
-
"zmax": zmax,
|
|
407
|
-
}
|
|
408
|
-
)
|
|
409
|
-
|
|
410
|
-
else:
|
|
411
|
-
# Inplane port: horizontal on single layer
|
|
412
|
-
if port.layer is None:
|
|
413
|
-
continue
|
|
414
|
-
target_layer = stack.layers.get(port.layer)
|
|
415
|
-
if target_layer is None:
|
|
416
|
-
continue
|
|
417
|
-
|
|
418
|
-
x, y = port.center
|
|
419
|
-
hw = port.width / 2
|
|
420
|
-
z = target_layer.zmin
|
|
421
|
-
|
|
422
|
-
hl = (port.length or port.width) / 2
|
|
423
|
-
if port.direction in ("x", "-x"):
|
|
424
|
-
surfacetag = gmsh_utils.create_port_rectangle(
|
|
425
|
-
kernel, x - hl, y - hw, z, x + hl, y + hw, z
|
|
426
|
-
)
|
|
427
|
-
length = 2 * hl
|
|
428
|
-
width = port.width
|
|
429
|
-
else:
|
|
430
|
-
surfacetag = gmsh_utils.create_port_rectangle(
|
|
431
|
-
kernel, x - hw, y - hl, z, x + hw, y + hl, z
|
|
432
|
-
)
|
|
433
|
-
length = port.width
|
|
434
|
-
width = 2 * hl
|
|
435
|
-
|
|
436
|
-
port_tags[f"P{port_num}"] = [surfacetag]
|
|
437
|
-
port_info.append(
|
|
438
|
-
{
|
|
439
|
-
"portnumber": port_num,
|
|
440
|
-
"Z0": port.impedance,
|
|
441
|
-
"type": "lumped",
|
|
442
|
-
"direction": port.direction.upper(),
|
|
443
|
-
"length": length,
|
|
444
|
-
"width": width,
|
|
445
|
-
"xmin": x - hl if port.direction in ("x", "-x") else x - hw,
|
|
446
|
-
"xmax": x + hl if port.direction in ("x", "-x") else x + hw,
|
|
447
|
-
"ymin": y - hw if port.direction in ("x", "-x") else y - hl,
|
|
448
|
-
"ymax": y + hw if port.direction in ("x", "-x") else y + hl,
|
|
449
|
-
"zmin": z,
|
|
450
|
-
"zmax": z,
|
|
451
|
-
}
|
|
452
|
-
)
|
|
453
|
-
|
|
454
|
-
port_num += 1
|
|
455
|
-
|
|
456
|
-
kernel.synchronize()
|
|
457
|
-
|
|
458
|
-
return port_tags, port_info
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
def _assign_physical_groups(
|
|
462
|
-
kernel,
|
|
463
|
-
metal_tags: dict,
|
|
464
|
-
dielectric_tags: dict,
|
|
465
|
-
port_tags: dict,
|
|
466
|
-
port_info: list,
|
|
467
|
-
geom_dimtags: list,
|
|
468
|
-
geom_map: list,
|
|
469
|
-
_stack: LayerStack,
|
|
470
|
-
) -> dict:
|
|
471
|
-
"""Assign physical groups after fragmenting.
|
|
472
|
-
|
|
473
|
-
Args:
|
|
474
|
-
kernel: gmsh kernel
|
|
475
|
-
metal_tags: Metal layer tags
|
|
476
|
-
dielectric_tags: Dielectric material tags
|
|
477
|
-
port_tags: Port surface tags (may have multiple surfaces for CPW)
|
|
478
|
-
port_info: Port metadata including type info
|
|
479
|
-
geom_dimtags: Dimension tags from fragmentation
|
|
480
|
-
geom_map: Geometry map from fragmentation
|
|
481
|
-
_stack: Layer stack (unused; reserved for future material metadata)
|
|
482
|
-
|
|
483
|
-
Returns:
|
|
484
|
-
Dict with group info for config file generation
|
|
485
|
-
"""
|
|
486
|
-
groups = {
|
|
487
|
-
"volumes": {},
|
|
488
|
-
"conductor_surfaces": {},
|
|
489
|
-
"port_surfaces": {},
|
|
490
|
-
"boundary_surfaces": {},
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
# Assign volume groups for dielectrics
|
|
494
|
-
for material_name, tags in dielectric_tags.items():
|
|
495
|
-
new_tags = gmsh_utils.get_tags_after_fragment(
|
|
496
|
-
tags, geom_dimtags, geom_map, dimension=3
|
|
497
|
-
)
|
|
498
|
-
if new_tags:
|
|
499
|
-
# Only take first N tags (same as original count)
|
|
500
|
-
new_tags = new_tags[: len(tags)]
|
|
501
|
-
phys_group = gmsh_utils.assign_physical_group(3, new_tags, material_name)
|
|
502
|
-
groups["volumes"][material_name] = {
|
|
503
|
-
"phys_group": phys_group,
|
|
504
|
-
"tags": new_tags,
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
# Assign surface groups for conductors
|
|
508
|
-
for layer_name, tag_info in metal_tags.items():
|
|
509
|
-
if tag_info["volumes"]:
|
|
510
|
-
all_xy_tags = []
|
|
511
|
-
all_z_tags = []
|
|
512
|
-
|
|
513
|
-
for item in tag_info["volumes"]:
|
|
514
|
-
if isinstance(item, tuple):
|
|
515
|
-
_volumetag, surface_tags = item
|
|
516
|
-
# Get updated surface tags after fragment
|
|
517
|
-
new_surface_tags = gmsh_utils.get_tags_after_fragment(
|
|
518
|
-
surface_tags, geom_dimtags, geom_map, dimension=2
|
|
519
|
-
)
|
|
520
|
-
|
|
521
|
-
# Separate xy and z surfaces
|
|
522
|
-
for tag in new_surface_tags:
|
|
523
|
-
if gmsh_utils.is_vertical_surface(tag):
|
|
524
|
-
all_z_tags.append(tag)
|
|
525
|
-
else:
|
|
526
|
-
all_xy_tags.append(tag)
|
|
527
|
-
|
|
528
|
-
if all_xy_tags:
|
|
529
|
-
phys_group = gmsh_utils.assign_physical_group(
|
|
530
|
-
2, all_xy_tags, f"{layer_name}_xy"
|
|
531
|
-
)
|
|
532
|
-
groups["conductor_surfaces"][f"{layer_name}_xy"] = {
|
|
533
|
-
"phys_group": phys_group,
|
|
534
|
-
"tags": all_xy_tags,
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
if all_z_tags:
|
|
538
|
-
phys_group = gmsh_utils.assign_physical_group(
|
|
539
|
-
2, all_z_tags, f"{layer_name}_z"
|
|
540
|
-
)
|
|
541
|
-
groups["conductor_surfaces"][f"{layer_name}_z"] = {
|
|
542
|
-
"phys_group": phys_group,
|
|
543
|
-
"tags": all_z_tags,
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
# Assign port surface groups
|
|
547
|
-
for port_name, tags in port_tags.items():
|
|
548
|
-
# Find corresponding port_info entry
|
|
549
|
-
port_num = int(port_name[1:]) # "P1" -> 1
|
|
550
|
-
info = next((p for p in port_info if p["portnumber"] == port_num), None)
|
|
551
|
-
|
|
552
|
-
if info and info.get("type") == "cpw":
|
|
553
|
-
# CPW port: create separate physical group for each element
|
|
554
|
-
element_phys_groups = []
|
|
555
|
-
for i, tag in enumerate(tags):
|
|
556
|
-
new_tag_list = gmsh_utils.get_tags_after_fragment(
|
|
557
|
-
[tag], geom_dimtags, geom_map, dimension=2
|
|
558
|
-
)
|
|
559
|
-
if new_tag_list:
|
|
560
|
-
elem_name = f"{port_name}_E{i}"
|
|
561
|
-
phys_group = gmsh_utils.assign_physical_group(
|
|
562
|
-
2, new_tag_list, elem_name
|
|
563
|
-
)
|
|
564
|
-
element_phys_groups.append(
|
|
565
|
-
{
|
|
566
|
-
"phys_group": phys_group,
|
|
567
|
-
"tags": new_tag_list,
|
|
568
|
-
"direction": info["elements"][i]["direction"],
|
|
569
|
-
}
|
|
570
|
-
)
|
|
571
|
-
|
|
572
|
-
groups["port_surfaces"][port_name] = {
|
|
573
|
-
"type": "cpw",
|
|
574
|
-
"elements": element_phys_groups,
|
|
575
|
-
}
|
|
576
|
-
else:
|
|
577
|
-
# Regular single-element port
|
|
578
|
-
new_tags = gmsh_utils.get_tags_after_fragment(
|
|
579
|
-
tags, geom_dimtags, geom_map, dimension=2
|
|
580
|
-
)
|
|
581
|
-
if new_tags:
|
|
582
|
-
phys_group = gmsh_utils.assign_physical_group(2, new_tags, port_name)
|
|
583
|
-
groups["port_surfaces"][port_name] = {
|
|
584
|
-
"phys_group": phys_group,
|
|
585
|
-
"tags": new_tags,
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
# Assign boundary surfaces (from airbox)
|
|
589
|
-
if "airbox" in groups["volumes"]:
|
|
590
|
-
airbox_tags = groups["volumes"]["airbox"]["tags"]
|
|
591
|
-
if airbox_tags:
|
|
592
|
-
_, simulation_boundary = kernel.getSurfaceLoops(airbox_tags[0])
|
|
593
|
-
if simulation_boundary:
|
|
594
|
-
boundary_tags = list(next(iter(simulation_boundary)))
|
|
595
|
-
phys_group = gmsh_utils.assign_physical_group(
|
|
596
|
-
2, boundary_tags, "Absorbing_boundary"
|
|
597
|
-
)
|
|
598
|
-
groups["boundary_surfaces"]["absorbing"] = {
|
|
599
|
-
"phys_group": phys_group,
|
|
600
|
-
"tags": boundary_tags,
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
kernel.synchronize()
|
|
604
|
-
|
|
605
|
-
return groups
|
|
47
|
+
mesh_stats: dict = field(default_factory=dict)
|
|
48
|
+
# Data needed for deferred config generation
|
|
49
|
+
groups: dict = field(default_factory=dict)
|
|
50
|
+
output_dir: Path | None = None
|
|
51
|
+
model_name: str = "palace"
|
|
52
|
+
fmax: float = 100e9
|
|
606
53
|
|
|
607
54
|
|
|
608
55
|
def _setup_mesh_fields(
|
|
@@ -613,7 +60,16 @@ def _setup_mesh_fields(
|
|
|
613
60
|
refined_cellsize: float,
|
|
614
61
|
max_cellsize: float,
|
|
615
62
|
) -> None:
|
|
616
|
-
"""Set up mesh refinement fields.
|
|
63
|
+
"""Set up mesh refinement fields.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
kernel: gmsh OCC kernel
|
|
67
|
+
groups: Physical group information
|
|
68
|
+
geometry: Extracted geometry data
|
|
69
|
+
stack: LayerStack with material properties
|
|
70
|
+
refined_cellsize: Fine mesh size near conductors (um)
|
|
71
|
+
max_cellsize: Coarse mesh size in air/dielectric (um)
|
|
72
|
+
"""
|
|
617
73
|
# Collect boundary lines from conductor surfaces
|
|
618
74
|
boundary_lines = []
|
|
619
75
|
for surface_info in groups["conductor_surfaces"].values():
|
|
@@ -672,165 +128,6 @@ def _setup_mesh_fields(
|
|
|
672
128
|
gmsh_utils.finalize_mesh_fields(field_ids)
|
|
673
129
|
|
|
674
130
|
|
|
675
|
-
def _generate_palace_config(
|
|
676
|
-
groups: dict,
|
|
677
|
-
ports: list[PalacePort],
|
|
678
|
-
port_info: list,
|
|
679
|
-
stack: LayerStack,
|
|
680
|
-
output_path: Path,
|
|
681
|
-
model_name: str,
|
|
682
|
-
fmax: float,
|
|
683
|
-
) -> Path:
|
|
684
|
-
"""Generate Palace config.json file."""
|
|
685
|
-
config: dict[str, object] = {
|
|
686
|
-
"Problem": {
|
|
687
|
-
"Type": "Driven",
|
|
688
|
-
"Verbose": 3,
|
|
689
|
-
"Output": f"output/{model_name}",
|
|
690
|
-
},
|
|
691
|
-
"Model": {
|
|
692
|
-
"Mesh": f"{model_name}.msh",
|
|
693
|
-
"L0": 1e-6, # um
|
|
694
|
-
"Refinement": {
|
|
695
|
-
"UniformLevels": 0,
|
|
696
|
-
"Tol": 1e-2,
|
|
697
|
-
"MaxIts": 0,
|
|
698
|
-
},
|
|
699
|
-
},
|
|
700
|
-
"Solver": {
|
|
701
|
-
"Linear": {
|
|
702
|
-
"Type": "Default",
|
|
703
|
-
"KSPType": "GMRES",
|
|
704
|
-
"Tol": 1e-6,
|
|
705
|
-
"MaxIts": 400,
|
|
706
|
-
},
|
|
707
|
-
"Order": 2,
|
|
708
|
-
"Device": "CPU",
|
|
709
|
-
"Driven": {
|
|
710
|
-
"Samples": [
|
|
711
|
-
{
|
|
712
|
-
"Type": "Linear",
|
|
713
|
-
"MinFreq": 1e9 / 1e9,
|
|
714
|
-
"MaxFreq": fmax / 1e9,
|
|
715
|
-
"FreqStep": fmax / 40e9,
|
|
716
|
-
"SaveStep": 0,
|
|
717
|
-
}
|
|
718
|
-
],
|
|
719
|
-
"AdaptiveTol": 2e-2,
|
|
720
|
-
},
|
|
721
|
-
},
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
# Build domains section
|
|
725
|
-
materials: list[dict[str, object]] = []
|
|
726
|
-
for material_name, info in groups["volumes"].items():
|
|
727
|
-
mat_props = stack.materials.get(material_name, {})
|
|
728
|
-
mat_entry: dict[str, object] = {"Attributes": [info["phys_group"]]}
|
|
729
|
-
|
|
730
|
-
if material_name == "airbox":
|
|
731
|
-
mat_entry["Permittivity"] = 1.0
|
|
732
|
-
mat_entry["LossTan"] = 0.0
|
|
733
|
-
else:
|
|
734
|
-
mat_entry["Permittivity"] = mat_props.get("permittivity", 1.0)
|
|
735
|
-
sigma = mat_props.get("conductivity", 0.0)
|
|
736
|
-
if sigma > 0:
|
|
737
|
-
mat_entry["Conductivity"] = sigma
|
|
738
|
-
else:
|
|
739
|
-
mat_entry["LossTan"] = mat_props.get("loss_tangent", 0.0)
|
|
740
|
-
|
|
741
|
-
materials.append(mat_entry)
|
|
742
|
-
|
|
743
|
-
config["Domains"] = {
|
|
744
|
-
"Materials": materials,
|
|
745
|
-
"Postprocessing": {"Energy": [], "Probe": []},
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
# Build boundaries section
|
|
749
|
-
conductors: list[dict[str, object]] = []
|
|
750
|
-
for name, info in groups["conductor_surfaces"].items():
|
|
751
|
-
# Extract layer name from "layer_xy" or "layer_z"
|
|
752
|
-
layer_name = name.rsplit("_", 1)[0]
|
|
753
|
-
layer = stack.layers.get(layer_name)
|
|
754
|
-
if layer:
|
|
755
|
-
mat_props = stack.materials.get(layer.material, {})
|
|
756
|
-
conductors.append(
|
|
757
|
-
{
|
|
758
|
-
"Attributes": [info["phys_group"]],
|
|
759
|
-
"Conductivity": mat_props.get("conductivity", 5.8e7),
|
|
760
|
-
"Thickness": layer.zmax - layer.zmin,
|
|
761
|
-
}
|
|
762
|
-
)
|
|
763
|
-
|
|
764
|
-
lumped_ports: list[dict[str, object]] = []
|
|
765
|
-
port_idx = 1
|
|
766
|
-
|
|
767
|
-
for port in ports:
|
|
768
|
-
port_key = f"P{port_idx}"
|
|
769
|
-
if port_key in groups["port_surfaces"]:
|
|
770
|
-
port_group = groups["port_surfaces"][port_key]
|
|
771
|
-
|
|
772
|
-
if port.multi_element:
|
|
773
|
-
# Multi-element port (CPW)
|
|
774
|
-
if port_group.get("type") == "cpw":
|
|
775
|
-
elements = [
|
|
776
|
-
{
|
|
777
|
-
"Attributes": [elem["phys_group"]],
|
|
778
|
-
"Direction": elem["direction"],
|
|
779
|
-
}
|
|
780
|
-
for elem in port_group["elements"]
|
|
781
|
-
]
|
|
782
|
-
|
|
783
|
-
lumped_ports.append(
|
|
784
|
-
{
|
|
785
|
-
"Index": port_idx,
|
|
786
|
-
"R": port.impedance,
|
|
787
|
-
"Excitation": port_idx if port.excited else False,
|
|
788
|
-
"Elements": elements,
|
|
789
|
-
}
|
|
790
|
-
)
|
|
791
|
-
else:
|
|
792
|
-
# Single-element port
|
|
793
|
-
direction = (
|
|
794
|
-
"Z" if port.geometry == PortGeometry.VIA else port.direction.upper()
|
|
795
|
-
)
|
|
796
|
-
lumped_ports.append(
|
|
797
|
-
{
|
|
798
|
-
"Index": port_idx,
|
|
799
|
-
"R": port.impedance,
|
|
800
|
-
"Direction": direction,
|
|
801
|
-
"Excitation": port_idx if port.excited else False,
|
|
802
|
-
"Attributes": [port_group["phys_group"]],
|
|
803
|
-
}
|
|
804
|
-
)
|
|
805
|
-
port_idx += 1
|
|
806
|
-
|
|
807
|
-
boundaries: dict[str, object] = {
|
|
808
|
-
"Conductivity": conductors,
|
|
809
|
-
"LumpedPort": lumped_ports,
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
if "absorbing" in groups["boundary_surfaces"]:
|
|
813
|
-
boundaries["Absorbing"] = {
|
|
814
|
-
"Attributes": [groups["boundary_surfaces"]["absorbing"]["phys_group"]],
|
|
815
|
-
"Order": 2,
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
config["Boundaries"] = boundaries
|
|
819
|
-
|
|
820
|
-
# Write config file
|
|
821
|
-
config_path = output_path / "config.json"
|
|
822
|
-
with config_path.open("w") as f:
|
|
823
|
-
json.dump(config, f, indent=4)
|
|
824
|
-
|
|
825
|
-
# Write port information file
|
|
826
|
-
port_info_path = output_path / "port_information.json"
|
|
827
|
-
port_info_struct = {"ports": port_info, "unit": 1e-6, "name": model_name}
|
|
828
|
-
with port_info_path.open("w") as f:
|
|
829
|
-
json.dump(port_info_struct, f, indent=4)
|
|
830
|
-
|
|
831
|
-
return config_path
|
|
832
|
-
|
|
833
|
-
|
|
834
131
|
def generate_mesh(
|
|
835
132
|
component,
|
|
836
133
|
stack: LayerStack,
|
|
@@ -843,6 +140,8 @@ def generate_mesh(
|
|
|
843
140
|
air_margin: float = 50.0,
|
|
844
141
|
fmax: float = 100e9,
|
|
845
142
|
show_gui: bool = False,
|
|
143
|
+
driven_config: DrivenConfig | None = None,
|
|
144
|
+
write_config: bool = True,
|
|
846
145
|
) -> MeshResult:
|
|
847
146
|
"""Generate mesh for Palace EM simulation.
|
|
848
147
|
|
|
@@ -858,6 +157,8 @@ def generate_mesh(
|
|
|
858
157
|
air_margin: Air box margin (um)
|
|
859
158
|
fmax: Max frequency for config (Hz)
|
|
860
159
|
show_gui: Show gmsh GUI during meshing
|
|
160
|
+
driven_config: Optional DrivenConfig for frequency sweep settings
|
|
161
|
+
write_config: Whether to write config.json (default True)
|
|
861
162
|
|
|
862
163
|
Returns:
|
|
863
164
|
MeshResult with paths and metadata
|
|
@@ -889,13 +190,13 @@ def generate_mesh(
|
|
|
889
190
|
try:
|
|
890
191
|
# Add geometry
|
|
891
192
|
logger.info("Adding metals...")
|
|
892
|
-
metal_tags =
|
|
193
|
+
metal_tags = add_metals(kernel, geometry, stack)
|
|
893
194
|
|
|
894
195
|
logger.info("Adding ports...")
|
|
895
|
-
port_tags, port_info =
|
|
196
|
+
port_tags, port_info = add_ports(kernel, ports, stack)
|
|
896
197
|
|
|
897
198
|
logger.info("Adding dielectrics...")
|
|
898
|
-
dielectric_tags =
|
|
199
|
+
dielectric_tags = add_dielectrics(kernel, geometry, stack, margin, air_margin)
|
|
899
200
|
|
|
900
201
|
# Fragment geometry
|
|
901
202
|
logger.info("Fragmenting geometry...")
|
|
@@ -903,7 +204,7 @@ def generate_mesh(
|
|
|
903
204
|
|
|
904
205
|
# Assign physical groups
|
|
905
206
|
logger.info("Assigning physical groups...")
|
|
906
|
-
groups =
|
|
207
|
+
groups = assign_physical_groups(
|
|
907
208
|
kernel,
|
|
908
209
|
metal_tags,
|
|
909
210
|
dielectric_tags,
|
|
@@ -928,6 +229,9 @@ def generate_mesh(
|
|
|
928
229
|
logger.info("Generating mesh...")
|
|
929
230
|
gmsh.model.mesh.generate(3)
|
|
930
231
|
|
|
232
|
+
# Collect mesh statistics
|
|
233
|
+
mesh_stats = collect_mesh_stats()
|
|
234
|
+
|
|
931
235
|
# Save mesh
|
|
932
236
|
gmsh.option.setNumber("Mesh.Binary", 0)
|
|
933
237
|
gmsh.option.setNumber("Mesh.SaveAll", 0)
|
|
@@ -936,21 +240,39 @@ def generate_mesh(
|
|
|
936
240
|
|
|
937
241
|
logger.info("Mesh saved: %s", msh_path)
|
|
938
242
|
|
|
939
|
-
# Generate config
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
243
|
+
# Generate config if requested
|
|
244
|
+
config_path = None
|
|
245
|
+
if write_config:
|
|
246
|
+
logger.info("Generating Palace config...")
|
|
247
|
+
config_path = generate_palace_config(
|
|
248
|
+
groups,
|
|
249
|
+
ports,
|
|
250
|
+
port_info,
|
|
251
|
+
stack,
|
|
252
|
+
output_dir,
|
|
253
|
+
model_name,
|
|
254
|
+
fmax,
|
|
255
|
+
driven_config,
|
|
256
|
+
)
|
|
944
257
|
|
|
945
258
|
finally:
|
|
946
259
|
gmsh.clear()
|
|
947
260
|
gmsh.finalize()
|
|
948
261
|
|
|
949
|
-
# Build result
|
|
262
|
+
# Build result (store groups for deferred config generation)
|
|
950
263
|
result = MeshResult(
|
|
951
264
|
mesh_path=msh_path,
|
|
952
265
|
config_path=config_path,
|
|
953
266
|
port_info=port_info,
|
|
267
|
+
mesh_stats=mesh_stats,
|
|
268
|
+
groups=groups,
|
|
269
|
+
output_dir=output_dir,
|
|
270
|
+
model_name=model_name,
|
|
271
|
+
fmax=fmax,
|
|
954
272
|
)
|
|
955
273
|
|
|
956
274
|
return result
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
# Re-export write_config from config_generator for backward compatibility
|
|
278
|
+
__all__ = ["GeometryData", "MeshResult", "generate_mesh", "write_config"]
|