gsim 0.0.2__py3-none-any.whl → 0.0.4__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 +80 -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.4.dist-info}/METADATA +7 -6
- gsim-0.0.4.dist-info/RECORD +35 -0
- {gsim-0.0.2.dist-info → gsim-0.0.4.dist-info}/WHEEL +1 -1
- gsim-0.0.2.dist-info/RECORD +0 -32
- {gsim-0.0.2.dist-info → gsim-0.0.4.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
"""Geometry extraction and creation for Palace mesh generation.
|
|
2
|
+
|
|
3
|
+
This module handles extracting polygons from gdsfactory components
|
|
4
|
+
and creating 3D geometry in gmsh.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import math
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
from . import gmsh_utils
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from gsim.common.stack import LayerStack
|
|
17
|
+
from gsim.palace.ports.config import PalacePort
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class GeometryData:
|
|
22
|
+
"""Container for geometry data extracted from component."""
|
|
23
|
+
|
|
24
|
+
polygons: list # List of (layer_num, pts_x, pts_y) tuples
|
|
25
|
+
bbox: tuple[float, float, float, float] # (xmin, ymin, xmax, ymax)
|
|
26
|
+
layer_bboxes: dict # layer_num -> (xmin, ymin, xmax, ymax)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def extract_geometry(component, stack: LayerStack) -> GeometryData:
|
|
30
|
+
"""Extract polygon geometry from a gdsfactory component.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
component: gdsfactory Component
|
|
34
|
+
stack: LayerStack for layer mapping
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
GeometryData with polygons and bounding boxes
|
|
38
|
+
"""
|
|
39
|
+
polygons = []
|
|
40
|
+
global_bbox = [math.inf, math.inf, -math.inf, -math.inf]
|
|
41
|
+
layer_bboxes = {}
|
|
42
|
+
|
|
43
|
+
# Get polygons from component
|
|
44
|
+
polygons_by_index = component.get_polygons()
|
|
45
|
+
|
|
46
|
+
# Build layer_index -> GDS tuple mapping
|
|
47
|
+
layout = component.kcl.layout
|
|
48
|
+
index_to_gds = {}
|
|
49
|
+
for layer_index in range(layout.layers()):
|
|
50
|
+
if layout.is_valid_layer(layer_index):
|
|
51
|
+
info = layout.get_info(layer_index)
|
|
52
|
+
index_to_gds[layer_index] = (info.layer, info.datatype)
|
|
53
|
+
|
|
54
|
+
# Build GDS tuple -> layer number mapping
|
|
55
|
+
gds_to_layernum = {}
|
|
56
|
+
for layer_data in stack.layers.values():
|
|
57
|
+
gds_tuple = tuple(layer_data.gds_layer)
|
|
58
|
+
gds_to_layernum[gds_tuple] = gds_tuple[0]
|
|
59
|
+
|
|
60
|
+
# Convert polygons
|
|
61
|
+
for layer_index, polys in polygons_by_index.items():
|
|
62
|
+
gds_tuple = index_to_gds.get(layer_index)
|
|
63
|
+
if gds_tuple is None:
|
|
64
|
+
continue
|
|
65
|
+
|
|
66
|
+
layernum = gds_to_layernum.get(gds_tuple)
|
|
67
|
+
if layernum is None:
|
|
68
|
+
continue
|
|
69
|
+
|
|
70
|
+
for poly in polys:
|
|
71
|
+
# Convert klayout polygon to lists (nm -> um)
|
|
72
|
+
points = list(poly.each_point_hull())
|
|
73
|
+
if len(points) < 3:
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
pts_x = [pt.x / 1000.0 for pt in points]
|
|
77
|
+
pts_y = [pt.y / 1000.0 for pt in points]
|
|
78
|
+
|
|
79
|
+
polygons.append((layernum, pts_x, pts_y))
|
|
80
|
+
|
|
81
|
+
# Update bounding boxes
|
|
82
|
+
xmin, xmax = min(pts_x), max(pts_x)
|
|
83
|
+
ymin, ymax = min(pts_y), max(pts_y)
|
|
84
|
+
|
|
85
|
+
global_bbox[0] = min(global_bbox[0], xmin)
|
|
86
|
+
global_bbox[1] = min(global_bbox[1], ymin)
|
|
87
|
+
global_bbox[2] = max(global_bbox[2], xmax)
|
|
88
|
+
global_bbox[3] = max(global_bbox[3], ymax)
|
|
89
|
+
|
|
90
|
+
if layernum not in layer_bboxes:
|
|
91
|
+
layer_bboxes[layernum] = [xmin, ymin, xmax, ymax]
|
|
92
|
+
else:
|
|
93
|
+
bbox = layer_bboxes[layernum]
|
|
94
|
+
bbox[0] = min(bbox[0], xmin)
|
|
95
|
+
bbox[1] = min(bbox[1], ymin)
|
|
96
|
+
bbox[2] = max(bbox[2], xmax)
|
|
97
|
+
bbox[3] = max(bbox[3], ymax)
|
|
98
|
+
|
|
99
|
+
return GeometryData(
|
|
100
|
+
polygons=polygons,
|
|
101
|
+
bbox=(global_bbox[0], global_bbox[1], global_bbox[2], global_bbox[3]),
|
|
102
|
+
layer_bboxes=layer_bboxes,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def get_layer_info(stack: LayerStack, gds_layer: int) -> dict | None:
|
|
107
|
+
"""Get layer info from stack by GDS layer number.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
stack: LayerStack with layer definitions
|
|
111
|
+
gds_layer: GDS layer number
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
Dict with layer info or None if not found
|
|
115
|
+
"""
|
|
116
|
+
for name, layer in stack.layers.items():
|
|
117
|
+
if layer.gds_layer[0] == gds_layer:
|
|
118
|
+
return {
|
|
119
|
+
"name": name,
|
|
120
|
+
"zmin": layer.zmin,
|
|
121
|
+
"zmax": layer.zmax,
|
|
122
|
+
"thickness": layer.zmax - layer.zmin,
|
|
123
|
+
"material": layer.material,
|
|
124
|
+
"type": layer.layer_type,
|
|
125
|
+
}
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def add_metals(
|
|
130
|
+
kernel,
|
|
131
|
+
geometry: GeometryData,
|
|
132
|
+
stack: LayerStack,
|
|
133
|
+
) -> dict:
|
|
134
|
+
"""Add metal and via geometries to gmsh.
|
|
135
|
+
|
|
136
|
+
Creates extruded volumes for vias and shells (surfaces) for conductors.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
kernel: gmsh OCC kernel
|
|
140
|
+
geometry: Extracted geometry data
|
|
141
|
+
stack: LayerStack with layer definitions
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Dict with layer_name -> list of (surface_tags_xy, surface_tags_z) for
|
|
145
|
+
conductors, or volume_tags for vias.
|
|
146
|
+
"""
|
|
147
|
+
# layer_name -> {"volumes": [], "surfaces_xy": [], "surfaces_z": []}
|
|
148
|
+
metal_tags = {}
|
|
149
|
+
|
|
150
|
+
# Group polygons by layer
|
|
151
|
+
polygons_by_layer = {}
|
|
152
|
+
for layernum, pts_x, pts_y in geometry.polygons:
|
|
153
|
+
if layernum not in polygons_by_layer:
|
|
154
|
+
polygons_by_layer[layernum] = []
|
|
155
|
+
polygons_by_layer[layernum].append((pts_x, pts_y))
|
|
156
|
+
|
|
157
|
+
# Process each layer
|
|
158
|
+
for layernum, polys in polygons_by_layer.items():
|
|
159
|
+
layer_info = get_layer_info(stack, layernum)
|
|
160
|
+
if layer_info is None:
|
|
161
|
+
continue
|
|
162
|
+
|
|
163
|
+
layer_name = layer_info["name"]
|
|
164
|
+
layer_type = layer_info["type"]
|
|
165
|
+
zmin = layer_info["zmin"]
|
|
166
|
+
thickness = layer_info["thickness"]
|
|
167
|
+
|
|
168
|
+
if layer_type not in ("conductor", "via"):
|
|
169
|
+
continue
|
|
170
|
+
|
|
171
|
+
if layer_name not in metal_tags:
|
|
172
|
+
metal_tags[layer_name] = {
|
|
173
|
+
"volumes": [],
|
|
174
|
+
"surfaces_xy": [],
|
|
175
|
+
"surfaces_z": [],
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
for pts_x, pts_y in polys:
|
|
179
|
+
# Create extruded polygon
|
|
180
|
+
surfacetag = gmsh_utils.create_polygon_surface(kernel, pts_x, pts_y, zmin)
|
|
181
|
+
if surfacetag is None:
|
|
182
|
+
continue
|
|
183
|
+
|
|
184
|
+
if thickness > 0:
|
|
185
|
+
result = kernel.extrude([(2, surfacetag)], 0, 0, thickness)
|
|
186
|
+
volumetag = result[1][1]
|
|
187
|
+
|
|
188
|
+
if layer_type == "via":
|
|
189
|
+
# Keep vias as volumes
|
|
190
|
+
metal_tags[layer_name]["volumes"].append(volumetag)
|
|
191
|
+
else:
|
|
192
|
+
# For conductors, get shell surfaces and remove volume
|
|
193
|
+
_, surfaceloops = kernel.getSurfaceLoops(volumetag)
|
|
194
|
+
if surfaceloops:
|
|
195
|
+
metal_tags[layer_name]["volumes"].append(
|
|
196
|
+
(volumetag, surfaceloops[0])
|
|
197
|
+
)
|
|
198
|
+
kernel.remove([(3, volumetag)])
|
|
199
|
+
|
|
200
|
+
kernel.removeAllDuplicates()
|
|
201
|
+
kernel.synchronize()
|
|
202
|
+
|
|
203
|
+
return metal_tags
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def add_dielectrics(
|
|
207
|
+
kernel,
|
|
208
|
+
geometry: GeometryData,
|
|
209
|
+
stack: LayerStack,
|
|
210
|
+
margin: float,
|
|
211
|
+
air_margin: float,
|
|
212
|
+
) -> dict:
|
|
213
|
+
"""Add dielectric boxes and airbox to gmsh.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
kernel: gmsh OCC kernel
|
|
217
|
+
geometry: Extracted geometry data
|
|
218
|
+
stack: LayerStack with dielectric definitions
|
|
219
|
+
margin: XY margin around design (um)
|
|
220
|
+
air_margin: Air box margin (um)
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
Dict with material_name -> list of volume_tags
|
|
224
|
+
"""
|
|
225
|
+
dielectric_tags = {}
|
|
226
|
+
|
|
227
|
+
# Get overall geometry bounds
|
|
228
|
+
xmin, ymin, xmax, ymax = geometry.bbox
|
|
229
|
+
xmin -= margin
|
|
230
|
+
ymin -= margin
|
|
231
|
+
xmax += margin
|
|
232
|
+
ymax += margin
|
|
233
|
+
|
|
234
|
+
# Track overall z range
|
|
235
|
+
z_min_all = math.inf
|
|
236
|
+
z_max_all = -math.inf
|
|
237
|
+
|
|
238
|
+
# Sort dielectrics by z (top to bottom for correct layering)
|
|
239
|
+
sorted_dielectrics = sorted(
|
|
240
|
+
stack.dielectrics, key=lambda d: d["zmax"], reverse=True
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
# Add dielectric boxes
|
|
244
|
+
offset = 0
|
|
245
|
+
offset_delta = margin / 20
|
|
246
|
+
|
|
247
|
+
for dielectric in sorted_dielectrics:
|
|
248
|
+
material = dielectric["material"]
|
|
249
|
+
d_zmin = dielectric["zmin"]
|
|
250
|
+
d_zmax = dielectric["zmax"]
|
|
251
|
+
|
|
252
|
+
z_min_all = min(z_min_all, d_zmin)
|
|
253
|
+
z_max_all = max(z_max_all, d_zmax)
|
|
254
|
+
|
|
255
|
+
if material not in dielectric_tags:
|
|
256
|
+
dielectric_tags[material] = []
|
|
257
|
+
|
|
258
|
+
# Create box with slight offset to avoid mesh issues
|
|
259
|
+
box_tag = gmsh_utils.create_box(
|
|
260
|
+
kernel,
|
|
261
|
+
xmin - offset,
|
|
262
|
+
ymin - offset,
|
|
263
|
+
d_zmin,
|
|
264
|
+
xmax + offset,
|
|
265
|
+
ymax + offset,
|
|
266
|
+
d_zmax,
|
|
267
|
+
)
|
|
268
|
+
dielectric_tags[material].append(box_tag)
|
|
269
|
+
|
|
270
|
+
# Alternate offset to avoid coincident faces
|
|
271
|
+
offset = offset_delta if offset == 0 else 0
|
|
272
|
+
|
|
273
|
+
# Add surrounding airbox
|
|
274
|
+
air_xmin = xmin - air_margin
|
|
275
|
+
air_ymin = ymin - air_margin
|
|
276
|
+
air_xmax = xmax + air_margin
|
|
277
|
+
air_ymax = ymax + air_margin
|
|
278
|
+
air_zmin = z_min_all - air_margin
|
|
279
|
+
air_zmax = z_max_all + air_margin
|
|
280
|
+
|
|
281
|
+
airbox_tag = kernel.addBox(
|
|
282
|
+
air_xmin,
|
|
283
|
+
air_ymin,
|
|
284
|
+
air_zmin,
|
|
285
|
+
air_xmax - air_xmin,
|
|
286
|
+
air_ymax - air_ymin,
|
|
287
|
+
air_zmax - air_zmin,
|
|
288
|
+
)
|
|
289
|
+
dielectric_tags["airbox"] = [airbox_tag]
|
|
290
|
+
|
|
291
|
+
kernel.synchronize()
|
|
292
|
+
|
|
293
|
+
return dielectric_tags
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def add_ports(
|
|
297
|
+
kernel,
|
|
298
|
+
ports: list[PalacePort],
|
|
299
|
+
stack: LayerStack,
|
|
300
|
+
) -> tuple[dict, list]:
|
|
301
|
+
"""Add port surfaces to gmsh.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
kernel: gmsh OCC kernel
|
|
305
|
+
ports: List of PalacePort objects (single or multi-element)
|
|
306
|
+
stack: Layer stack
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
(port_tags dict, port_info list)
|
|
310
|
+
|
|
311
|
+
For single-element ports: port_tags["P{num}"] = [surface_tag]
|
|
312
|
+
For multi-element ports: port_tags["P{num}"] = [surface_tag, surface_tag, ...]
|
|
313
|
+
"""
|
|
314
|
+
from gsim.palace.ports.config import PortGeometry
|
|
315
|
+
|
|
316
|
+
port_tags = {} # "P{num}" -> [surface_tag(s)]
|
|
317
|
+
port_info = []
|
|
318
|
+
port_num = 1
|
|
319
|
+
|
|
320
|
+
for port in ports:
|
|
321
|
+
if port.multi_element:
|
|
322
|
+
# Multi-element port (CPW)
|
|
323
|
+
if port.layer is None or port.centers is None or port.directions is None:
|
|
324
|
+
continue
|
|
325
|
+
target_layer = stack.layers.get(port.layer)
|
|
326
|
+
if target_layer is None:
|
|
327
|
+
continue
|
|
328
|
+
|
|
329
|
+
z = target_layer.zmin
|
|
330
|
+
hw = port.width / 2
|
|
331
|
+
hl = (port.length or port.width) / 2
|
|
332
|
+
|
|
333
|
+
# Determine axis from orientation
|
|
334
|
+
angle = port.orientation % 360
|
|
335
|
+
is_y_axis = 45 <= angle < 135 or 225 <= angle < 315
|
|
336
|
+
|
|
337
|
+
surfaces = []
|
|
338
|
+
for cx, cy in port.centers:
|
|
339
|
+
if is_y_axis:
|
|
340
|
+
surf = gmsh_utils.create_port_rectangle(
|
|
341
|
+
kernel, cx - hw, cy - hl, z, cx + hw, cy + hl, z
|
|
342
|
+
)
|
|
343
|
+
else:
|
|
344
|
+
surf = gmsh_utils.create_port_rectangle(
|
|
345
|
+
kernel, cx - hl, cy - hw, z, cx + hl, cy + hw, z
|
|
346
|
+
)
|
|
347
|
+
surfaces.append(surf)
|
|
348
|
+
|
|
349
|
+
port_tags[f"P{port_num}"] = surfaces
|
|
350
|
+
|
|
351
|
+
port_info.append(
|
|
352
|
+
{
|
|
353
|
+
"portnumber": port_num,
|
|
354
|
+
"Z0": port.impedance,
|
|
355
|
+
"type": "cpw",
|
|
356
|
+
"elements": [
|
|
357
|
+
{"surface_idx": i, "direction": port.directions[i]}
|
|
358
|
+
for i in range(len(port.centers))
|
|
359
|
+
],
|
|
360
|
+
"width": port.width,
|
|
361
|
+
"length": port.length or port.width,
|
|
362
|
+
"zmin": z,
|
|
363
|
+
"zmax": z,
|
|
364
|
+
}
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
elif port.geometry == PortGeometry.VIA:
|
|
368
|
+
# Via port: vertical between two layers
|
|
369
|
+
if port.from_layer is None or port.to_layer is None:
|
|
370
|
+
continue
|
|
371
|
+
from_layer = stack.layers.get(port.from_layer)
|
|
372
|
+
to_layer = stack.layers.get(port.to_layer)
|
|
373
|
+
if from_layer is None or to_layer is None:
|
|
374
|
+
continue
|
|
375
|
+
|
|
376
|
+
x, y = port.center
|
|
377
|
+
hw = port.width / 2
|
|
378
|
+
|
|
379
|
+
if from_layer.zmin < to_layer.zmin:
|
|
380
|
+
zmin = from_layer.zmax
|
|
381
|
+
zmax = to_layer.zmin
|
|
382
|
+
else:
|
|
383
|
+
zmin = to_layer.zmax
|
|
384
|
+
zmax = from_layer.zmin
|
|
385
|
+
|
|
386
|
+
# Create vertical port surface
|
|
387
|
+
if port.direction in ("x", "-x"):
|
|
388
|
+
surfacetag = gmsh_utils.create_port_rectangle(
|
|
389
|
+
kernel, x, y - hw, zmin, x, y + hw, zmax
|
|
390
|
+
)
|
|
391
|
+
else:
|
|
392
|
+
surfacetag = gmsh_utils.create_port_rectangle(
|
|
393
|
+
kernel, x - hw, y, zmin, x + hw, y, zmax
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
port_tags[f"P{port_num}"] = [surfacetag]
|
|
397
|
+
port_info.append(
|
|
398
|
+
{
|
|
399
|
+
"portnumber": port_num,
|
|
400
|
+
"Z0": port.impedance,
|
|
401
|
+
"type": "via",
|
|
402
|
+
"direction": "Z",
|
|
403
|
+
"length": zmax - zmin,
|
|
404
|
+
"width": port.width,
|
|
405
|
+
"xmin": x - hw if port.direction in ("y", "-y") else x,
|
|
406
|
+
"xmax": x + hw if port.direction in ("y", "-y") else x,
|
|
407
|
+
"ymin": y - hw if port.direction in ("x", "-x") else y,
|
|
408
|
+
"ymax": y + hw if port.direction in ("x", "-x") else y,
|
|
409
|
+
"zmin": zmin,
|
|
410
|
+
"zmax": zmax,
|
|
411
|
+
}
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
else:
|
|
415
|
+
# Inplane port: horizontal on single layer
|
|
416
|
+
if port.layer is None:
|
|
417
|
+
continue
|
|
418
|
+
target_layer = stack.layers.get(port.layer)
|
|
419
|
+
if target_layer is None:
|
|
420
|
+
continue
|
|
421
|
+
|
|
422
|
+
x, y = port.center
|
|
423
|
+
hw = port.width / 2
|
|
424
|
+
z = target_layer.zmin
|
|
425
|
+
|
|
426
|
+
hl = (port.length or port.width) / 2
|
|
427
|
+
if port.direction in ("x", "-x"):
|
|
428
|
+
surfacetag = gmsh_utils.create_port_rectangle(
|
|
429
|
+
kernel, x - hl, y - hw, z, x + hl, y + hw, z
|
|
430
|
+
)
|
|
431
|
+
length = 2 * hl
|
|
432
|
+
width = port.width
|
|
433
|
+
else:
|
|
434
|
+
surfacetag = gmsh_utils.create_port_rectangle(
|
|
435
|
+
kernel, x - hw, y - hl, z, x + hw, y + hl, z
|
|
436
|
+
)
|
|
437
|
+
length = port.width
|
|
438
|
+
width = 2 * hl
|
|
439
|
+
|
|
440
|
+
port_tags[f"P{port_num}"] = [surfacetag]
|
|
441
|
+
port_info.append(
|
|
442
|
+
{
|
|
443
|
+
"portnumber": port_num,
|
|
444
|
+
"Z0": port.impedance,
|
|
445
|
+
"type": "lumped",
|
|
446
|
+
"direction": port.direction.upper(),
|
|
447
|
+
"length": length,
|
|
448
|
+
"width": width,
|
|
449
|
+
"xmin": x - hl if port.direction in ("x", "-x") else x - hw,
|
|
450
|
+
"xmax": x + hl if port.direction in ("x", "-x") else x + hw,
|
|
451
|
+
"ymin": y - hw if port.direction in ("x", "-x") else y - hl,
|
|
452
|
+
"ymax": y + hw if port.direction in ("x", "-x") else y + hl,
|
|
453
|
+
"zmin": z,
|
|
454
|
+
"zmax": z,
|
|
455
|
+
}
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
port_num += 1
|
|
459
|
+
|
|
460
|
+
kernel.synchronize()
|
|
461
|
+
|
|
462
|
+
return port_tags, port_info
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
__all__ = [
|
|
466
|
+
"GeometryData",
|
|
467
|
+
"add_dielectrics",
|
|
468
|
+
"add_metals",
|
|
469
|
+
"add_ports",
|
|
470
|
+
"extract_geometry",
|
|
471
|
+
"get_layer_info",
|
|
472
|
+
]
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Physical group assignment for Palace mesh generation.
|
|
2
|
+
|
|
3
|
+
This module handles assigning gmsh physical groups after geometry fragmentation.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from . import gmsh_utils
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from gsim.common.stack import LayerStack
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def assign_physical_groups(
|
|
17
|
+
kernel,
|
|
18
|
+
metal_tags: dict,
|
|
19
|
+
dielectric_tags: dict,
|
|
20
|
+
port_tags: dict,
|
|
21
|
+
port_info: list,
|
|
22
|
+
geom_dimtags: list,
|
|
23
|
+
geom_map: list,
|
|
24
|
+
_stack: LayerStack,
|
|
25
|
+
) -> dict:
|
|
26
|
+
"""Assign physical groups after fragmenting.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
kernel: gmsh OCC kernel
|
|
30
|
+
metal_tags: Metal layer tags from add_metals()
|
|
31
|
+
dielectric_tags: Dielectric material tags from add_dielectrics()
|
|
32
|
+
port_tags: Port surface tags (may have multiple surfaces for CPW)
|
|
33
|
+
port_info: Port metadata including type info
|
|
34
|
+
geom_dimtags: Dimension tags from fragmentation
|
|
35
|
+
geom_map: Geometry map from fragmentation
|
|
36
|
+
_stack: Layer stack (unused; reserved for future material metadata)
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Dict with group info for config file generation:
|
|
40
|
+
{
|
|
41
|
+
"volumes": {material_name: {"phys_group": int, "tags": [int]}},
|
|
42
|
+
"conductor_surfaces": {layer_name: {"phys_group": int, "tags": [int]}},
|
|
43
|
+
"port_surfaces": {port_name: {"phys_group": int, "tags": [int]} or
|
|
44
|
+
{"type": "cpw", "elements": [...]}},
|
|
45
|
+
"boundary_surfaces": {"absorbing": {"phys_group": int, "tags": [int]}}
|
|
46
|
+
}
|
|
47
|
+
"""
|
|
48
|
+
groups = {
|
|
49
|
+
"volumes": {},
|
|
50
|
+
"conductor_surfaces": {},
|
|
51
|
+
"port_surfaces": {},
|
|
52
|
+
"boundary_surfaces": {},
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# Assign volume groups for dielectrics
|
|
56
|
+
for material_name, tags in dielectric_tags.items():
|
|
57
|
+
new_tags = gmsh_utils.get_tags_after_fragment(
|
|
58
|
+
tags, geom_dimtags, geom_map, dimension=3
|
|
59
|
+
)
|
|
60
|
+
if new_tags:
|
|
61
|
+
# Only take first N tags (same as original count)
|
|
62
|
+
new_tags = new_tags[: len(tags)]
|
|
63
|
+
phys_group = gmsh_utils.assign_physical_group(3, new_tags, material_name)
|
|
64
|
+
groups["volumes"][material_name] = {
|
|
65
|
+
"phys_group": phys_group,
|
|
66
|
+
"tags": new_tags,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# Assign surface groups for conductors
|
|
70
|
+
for layer_name, tag_info in metal_tags.items():
|
|
71
|
+
if tag_info["volumes"]:
|
|
72
|
+
all_xy_tags = []
|
|
73
|
+
all_z_tags = []
|
|
74
|
+
|
|
75
|
+
for item in tag_info["volumes"]:
|
|
76
|
+
if isinstance(item, tuple):
|
|
77
|
+
_volumetag, surface_tags = item
|
|
78
|
+
# Get updated surface tags after fragment
|
|
79
|
+
new_surface_tags = gmsh_utils.get_tags_after_fragment(
|
|
80
|
+
surface_tags, geom_dimtags, geom_map, dimension=2
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Separate xy and z surfaces
|
|
84
|
+
for tag in new_surface_tags:
|
|
85
|
+
if gmsh_utils.is_vertical_surface(tag):
|
|
86
|
+
all_z_tags.append(tag)
|
|
87
|
+
else:
|
|
88
|
+
all_xy_tags.append(tag)
|
|
89
|
+
|
|
90
|
+
if all_xy_tags:
|
|
91
|
+
phys_group = gmsh_utils.assign_physical_group(
|
|
92
|
+
2, all_xy_tags, f"{layer_name}_xy"
|
|
93
|
+
)
|
|
94
|
+
groups["conductor_surfaces"][f"{layer_name}_xy"] = {
|
|
95
|
+
"phys_group": phys_group,
|
|
96
|
+
"tags": all_xy_tags,
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if all_z_tags:
|
|
100
|
+
phys_group = gmsh_utils.assign_physical_group(
|
|
101
|
+
2, all_z_tags, f"{layer_name}_z"
|
|
102
|
+
)
|
|
103
|
+
groups["conductor_surfaces"][f"{layer_name}_z"] = {
|
|
104
|
+
"phys_group": phys_group,
|
|
105
|
+
"tags": all_z_tags,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
# Assign port surface groups
|
|
109
|
+
for port_name, tags in port_tags.items():
|
|
110
|
+
# Find corresponding port_info entry
|
|
111
|
+
port_num = int(port_name[1:]) # "P1" -> 1
|
|
112
|
+
info = next((p for p in port_info if p["portnumber"] == port_num), None)
|
|
113
|
+
|
|
114
|
+
if info and info.get("type") == "cpw":
|
|
115
|
+
# CPW port: create separate physical group for each element
|
|
116
|
+
element_phys_groups = []
|
|
117
|
+
for i, tag in enumerate(tags):
|
|
118
|
+
new_tag_list = gmsh_utils.get_tags_after_fragment(
|
|
119
|
+
[tag], geom_dimtags, geom_map, dimension=2
|
|
120
|
+
)
|
|
121
|
+
if new_tag_list:
|
|
122
|
+
elem_name = f"{port_name}_E{i}"
|
|
123
|
+
phys_group = gmsh_utils.assign_physical_group(
|
|
124
|
+
2, new_tag_list, elem_name
|
|
125
|
+
)
|
|
126
|
+
element_phys_groups.append(
|
|
127
|
+
{
|
|
128
|
+
"phys_group": phys_group,
|
|
129
|
+
"tags": new_tag_list,
|
|
130
|
+
"direction": info["elements"][i]["direction"],
|
|
131
|
+
}
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
groups["port_surfaces"][port_name] = {
|
|
135
|
+
"type": "cpw",
|
|
136
|
+
"elements": element_phys_groups,
|
|
137
|
+
}
|
|
138
|
+
else:
|
|
139
|
+
# Regular single-element port
|
|
140
|
+
new_tags = gmsh_utils.get_tags_after_fragment(
|
|
141
|
+
tags, geom_dimtags, geom_map, dimension=2
|
|
142
|
+
)
|
|
143
|
+
if new_tags:
|
|
144
|
+
phys_group = gmsh_utils.assign_physical_group(2, new_tags, port_name)
|
|
145
|
+
groups["port_surfaces"][port_name] = {
|
|
146
|
+
"phys_group": phys_group,
|
|
147
|
+
"tags": new_tags,
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
# Assign boundary surfaces (from airbox)
|
|
151
|
+
if "airbox" in groups["volumes"]:
|
|
152
|
+
airbox_tags = groups["volumes"]["airbox"]["tags"]
|
|
153
|
+
if airbox_tags:
|
|
154
|
+
_, simulation_boundary = kernel.getSurfaceLoops(airbox_tags[0])
|
|
155
|
+
if simulation_boundary:
|
|
156
|
+
boundary_tags = list(next(iter(simulation_boundary)))
|
|
157
|
+
phys_group = gmsh_utils.assign_physical_group(
|
|
158
|
+
2, boundary_tags, "Absorbing_boundary"
|
|
159
|
+
)
|
|
160
|
+
groups["boundary_surfaces"]["absorbing"] = {
|
|
161
|
+
"phys_group": phys_group,
|
|
162
|
+
"tags": boundary_tags,
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
kernel.synchronize()
|
|
166
|
+
|
|
167
|
+
return groups
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
__all__ = ["assign_physical_groups"]
|
gsim/palace/models/__init__.py
CHANGED
|
@@ -35,26 +35,19 @@ from gsim.palace.models.results import SimulationResult, ValidationResult
|
|
|
35
35
|
from gsim.palace.models.stack import MaterialConfig
|
|
36
36
|
|
|
37
37
|
__all__ = [
|
|
38
|
-
# Geometry
|
|
39
|
-
"GeometryConfig",
|
|
40
|
-
# Stack
|
|
41
|
-
"MaterialConfig",
|
|
42
|
-
# Ports
|
|
43
38
|
"CPWPortConfig",
|
|
44
|
-
"PortConfig",
|
|
45
|
-
"TerminalConfig",
|
|
46
|
-
"WavePortConfig",
|
|
47
|
-
# Mesh
|
|
48
|
-
"MeshConfig",
|
|
49
|
-
# Numerical
|
|
50
|
-
"NumericalConfig",
|
|
51
|
-
# Problems
|
|
52
39
|
"DrivenConfig",
|
|
53
40
|
"EigenmodeConfig",
|
|
54
41
|
"ElectrostaticConfig",
|
|
42
|
+
"GeometryConfig",
|
|
55
43
|
"MagnetostaticConfig",
|
|
56
|
-
"
|
|
57
|
-
|
|
44
|
+
"MaterialConfig",
|
|
45
|
+
"MeshConfig",
|
|
46
|
+
"NumericalConfig",
|
|
47
|
+
"PortConfig",
|
|
58
48
|
"SimulationResult",
|
|
49
|
+
"TerminalConfig",
|
|
50
|
+
"TransientConfig",
|
|
59
51
|
"ValidationResult",
|
|
52
|
+
"WavePortConfig",
|
|
60
53
|
]
|