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