gsim 0.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gsim/__init__.py +16 -0
- gsim/gcloud.py +169 -0
- gsim/palace/__init__.py +120 -0
- gsim/palace/mesh/__init__.py +50 -0
- gsim/palace/mesh/generator.py +956 -0
- gsim/palace/mesh/gmsh_utils.py +484 -0
- gsim/palace/mesh/pipeline.py +188 -0
- gsim/palace/ports/__init__.py +35 -0
- gsim/palace/ports/config.py +363 -0
- gsim/palace/stack/__init__.py +149 -0
- gsim/palace/stack/extractor.py +602 -0
- gsim/palace/stack/materials.py +161 -0
- gsim/palace/stack/visualization.py +630 -0
- gsim/viz.py +86 -0
- gsim-0.0.0.dist-info/METADATA +128 -0
- gsim-0.0.0.dist-info/RECORD +18 -0
- gsim-0.0.0.dist-info/WHEEL +5 -0
- gsim-0.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,956 @@
|
|
|
1
|
+
"""Mesh generator for Palace EM simulation.
|
|
2
|
+
|
|
3
|
+
This module generates meshes directly from palace-api data structures,
|
|
4
|
+
replacing the gds2palace backend with a cleaner implementation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
import math
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import TYPE_CHECKING
|
|
15
|
+
|
|
16
|
+
import gmsh
|
|
17
|
+
|
|
18
|
+
from . import gmsh_utils
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from gsim.palace.ports.config import PalacePort
|
|
22
|
+
from gsim.palace.stack import LayerStack
|
|
23
|
+
|
|
24
|
+
from gsim.palace.ports.config import PortGeometry
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
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
|
+
@dataclass
|
|
39
|
+
class MeshResult:
|
|
40
|
+
"""Result from mesh generation."""
|
|
41
|
+
|
|
42
|
+
mesh_path: Path
|
|
43
|
+
config_path: Path | None = None
|
|
44
|
+
port_info: list = field(default_factory=list)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def extract_geometry(component, stack: LayerStack) -> GeometryData:
|
|
48
|
+
"""Extract polygon geometry from a gdsfactory component.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
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
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
def _setup_mesh_fields(
|
|
609
|
+
kernel,
|
|
610
|
+
groups: dict,
|
|
611
|
+
geometry: GeometryData,
|
|
612
|
+
stack: LayerStack,
|
|
613
|
+
refined_cellsize: float,
|
|
614
|
+
max_cellsize: float,
|
|
615
|
+
) -> None:
|
|
616
|
+
"""Set up mesh refinement fields."""
|
|
617
|
+
# Collect boundary lines from conductor surfaces
|
|
618
|
+
boundary_lines = []
|
|
619
|
+
for surface_info in groups["conductor_surfaces"].values():
|
|
620
|
+
for tag in surface_info["tags"]:
|
|
621
|
+
lines = gmsh_utils.get_boundary_lines(tag, kernel)
|
|
622
|
+
boundary_lines.extend(lines)
|
|
623
|
+
|
|
624
|
+
# Add port boundaries
|
|
625
|
+
for surface_info in groups["port_surfaces"].values():
|
|
626
|
+
if surface_info.get("type") == "cpw":
|
|
627
|
+
# CPW port: get tags from each element
|
|
628
|
+
for elem in surface_info["elements"]:
|
|
629
|
+
for tag in elem["tags"]:
|
|
630
|
+
lines = gmsh_utils.get_boundary_lines(tag, kernel)
|
|
631
|
+
boundary_lines.extend(lines)
|
|
632
|
+
else:
|
|
633
|
+
# Regular port
|
|
634
|
+
for tag in surface_info["tags"]:
|
|
635
|
+
lines = gmsh_utils.get_boundary_lines(tag, kernel)
|
|
636
|
+
boundary_lines.extend(lines)
|
|
637
|
+
|
|
638
|
+
# Setup main refinement field
|
|
639
|
+
field_ids = []
|
|
640
|
+
if boundary_lines:
|
|
641
|
+
field_id = gmsh_utils.setup_mesh_refinement(
|
|
642
|
+
boundary_lines, refined_cellsize, max_cellsize
|
|
643
|
+
)
|
|
644
|
+
field_ids.append(field_id)
|
|
645
|
+
|
|
646
|
+
# Add box refinement for dielectrics based on permittivity
|
|
647
|
+
xmin, ymin, xmax, ymax = geometry.bbox
|
|
648
|
+
field_counter = 10
|
|
649
|
+
|
|
650
|
+
for dielectric in stack.dielectrics:
|
|
651
|
+
material_name = dielectric["material"]
|
|
652
|
+
material_props = stack.materials.get(material_name, {})
|
|
653
|
+
permittivity = material_props.get("permittivity", 1.0)
|
|
654
|
+
|
|
655
|
+
if permittivity > 1:
|
|
656
|
+
local_max = max_cellsize / math.sqrt(permittivity)
|
|
657
|
+
gmsh_utils.setup_box_refinement(
|
|
658
|
+
field_counter,
|
|
659
|
+
xmin,
|
|
660
|
+
ymin,
|
|
661
|
+
dielectric["zmin"],
|
|
662
|
+
xmax,
|
|
663
|
+
ymax,
|
|
664
|
+
dielectric["zmax"],
|
|
665
|
+
local_max,
|
|
666
|
+
max_cellsize,
|
|
667
|
+
)
|
|
668
|
+
field_ids.append(field_counter)
|
|
669
|
+
field_counter += 1
|
|
670
|
+
|
|
671
|
+
if field_ids:
|
|
672
|
+
gmsh_utils.finalize_mesh_fields(field_ids)
|
|
673
|
+
|
|
674
|
+
|
|
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
|
+
def generate_mesh(
|
|
835
|
+
component,
|
|
836
|
+
stack: LayerStack,
|
|
837
|
+
ports: list[PalacePort],
|
|
838
|
+
output_dir: str | Path,
|
|
839
|
+
model_name: str = "palace",
|
|
840
|
+
refined_mesh_size: float = 5.0,
|
|
841
|
+
max_mesh_size: float = 300.0,
|
|
842
|
+
margin: float = 50.0,
|
|
843
|
+
air_margin: float = 50.0,
|
|
844
|
+
fmax: float = 100e9,
|
|
845
|
+
show_gui: bool = False,
|
|
846
|
+
) -> MeshResult:
|
|
847
|
+
"""Generate mesh for Palace EM simulation.
|
|
848
|
+
|
|
849
|
+
Args:
|
|
850
|
+
component: gdsfactory Component
|
|
851
|
+
stack: LayerStack from palace-api
|
|
852
|
+
ports: List of PalacePort objects (single and multi-element)
|
|
853
|
+
output_dir: Directory for output files
|
|
854
|
+
model_name: Base name for output files
|
|
855
|
+
refined_mesh_size: Mesh size near conductors (um)
|
|
856
|
+
max_mesh_size: Max mesh size in air/dielectric (um)
|
|
857
|
+
margin: XY margin around design (um)
|
|
858
|
+
air_margin: Air box margin (um)
|
|
859
|
+
fmax: Max frequency for config (Hz)
|
|
860
|
+
show_gui: Show gmsh GUI during meshing
|
|
861
|
+
|
|
862
|
+
Returns:
|
|
863
|
+
MeshResult with paths and metadata
|
|
864
|
+
"""
|
|
865
|
+
output_dir = Path(output_dir)
|
|
866
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
867
|
+
|
|
868
|
+
msh_path = output_dir / f"{model_name}.msh"
|
|
869
|
+
|
|
870
|
+
# Extract geometry
|
|
871
|
+
logger.info("Extracting geometry...")
|
|
872
|
+
geometry = extract_geometry(component, stack)
|
|
873
|
+
logger.info(" Polygons: %s", len(geometry.polygons))
|
|
874
|
+
logger.info(" Bbox: %s", geometry.bbox)
|
|
875
|
+
|
|
876
|
+
# Initialize gmsh
|
|
877
|
+
gmsh.initialize()
|
|
878
|
+
gmsh.option.setNumber("General.Verbosity", 3)
|
|
879
|
+
|
|
880
|
+
if "palace_mesh" in gmsh.model.list():
|
|
881
|
+
gmsh.model.setCurrent("palace_mesh")
|
|
882
|
+
gmsh.model.remove()
|
|
883
|
+
gmsh.model.add("palace_mesh")
|
|
884
|
+
|
|
885
|
+
kernel = gmsh.model.occ
|
|
886
|
+
config_path: Path | None = None
|
|
887
|
+
port_info: list = []
|
|
888
|
+
|
|
889
|
+
try:
|
|
890
|
+
# Add geometry
|
|
891
|
+
logger.info("Adding metals...")
|
|
892
|
+
metal_tags = _add_metals(kernel, geometry, stack)
|
|
893
|
+
|
|
894
|
+
logger.info("Adding ports...")
|
|
895
|
+
port_tags, port_info = _add_ports(kernel, ports, stack)
|
|
896
|
+
|
|
897
|
+
logger.info("Adding dielectrics...")
|
|
898
|
+
dielectric_tags = _add_dielectrics(kernel, geometry, stack, margin, air_margin)
|
|
899
|
+
|
|
900
|
+
# Fragment geometry
|
|
901
|
+
logger.info("Fragmenting geometry...")
|
|
902
|
+
geom_dimtags, geom_map = gmsh_utils.fragment_all(kernel)
|
|
903
|
+
|
|
904
|
+
# Assign physical groups
|
|
905
|
+
logger.info("Assigning physical groups...")
|
|
906
|
+
groups = _assign_physical_groups(
|
|
907
|
+
kernel,
|
|
908
|
+
metal_tags,
|
|
909
|
+
dielectric_tags,
|
|
910
|
+
port_tags,
|
|
911
|
+
port_info,
|
|
912
|
+
geom_dimtags,
|
|
913
|
+
geom_map,
|
|
914
|
+
stack,
|
|
915
|
+
)
|
|
916
|
+
|
|
917
|
+
# Setup mesh fields
|
|
918
|
+
logger.info("Setting up mesh refinement...")
|
|
919
|
+
_setup_mesh_fields(
|
|
920
|
+
kernel, groups, geometry, stack, refined_mesh_size, max_mesh_size
|
|
921
|
+
)
|
|
922
|
+
|
|
923
|
+
# Show GUI if requested
|
|
924
|
+
if show_gui:
|
|
925
|
+
gmsh.fltk.run()
|
|
926
|
+
|
|
927
|
+
# Generate mesh
|
|
928
|
+
logger.info("Generating mesh...")
|
|
929
|
+
gmsh.model.mesh.generate(3)
|
|
930
|
+
|
|
931
|
+
# Save mesh
|
|
932
|
+
gmsh.option.setNumber("Mesh.Binary", 0)
|
|
933
|
+
gmsh.option.setNumber("Mesh.SaveAll", 0)
|
|
934
|
+
gmsh.option.setNumber("Mesh.MshFileVersion", 2.2)
|
|
935
|
+
gmsh.write(str(msh_path))
|
|
936
|
+
|
|
937
|
+
logger.info("Mesh saved: %s", msh_path)
|
|
938
|
+
|
|
939
|
+
# Generate config
|
|
940
|
+
logger.info("Generating Palace config...")
|
|
941
|
+
config_path = _generate_palace_config(
|
|
942
|
+
groups, ports, port_info, stack, output_dir, model_name, fmax
|
|
943
|
+
)
|
|
944
|
+
|
|
945
|
+
finally:
|
|
946
|
+
gmsh.clear()
|
|
947
|
+
gmsh.finalize()
|
|
948
|
+
|
|
949
|
+
# Build result
|
|
950
|
+
result = MeshResult(
|
|
951
|
+
mesh_path=msh_path,
|
|
952
|
+
config_path=config_path,
|
|
953
|
+
port_info=port_info,
|
|
954
|
+
)
|
|
955
|
+
|
|
956
|
+
return result
|