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,484 @@
|
|
|
1
|
+
"""Gmsh utility functions for Palace mesh generation.
|
|
2
|
+
|
|
3
|
+
Extracted from gds2palace/util_simulation_setup.py and adapted
|
|
4
|
+
to work directly with palace-api data structures.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import math
|
|
10
|
+
|
|
11
|
+
import gmsh
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def create_box(
|
|
16
|
+
kernel,
|
|
17
|
+
xmin: float,
|
|
18
|
+
ymin: float,
|
|
19
|
+
zmin: float,
|
|
20
|
+
xmax: float,
|
|
21
|
+
ymax: float,
|
|
22
|
+
zmax: float,
|
|
23
|
+
meshseed: float = 0,
|
|
24
|
+
) -> int:
|
|
25
|
+
"""Create a 3D box volume in gmsh.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
kernel: gmsh.model.occ kernel
|
|
29
|
+
xmin, ymin, zmin: minimum coordinates
|
|
30
|
+
xmax, ymax, zmax: maximum coordinates
|
|
31
|
+
meshseed: mesh seed size at corners (0 = auto)
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Volume tag of created box
|
|
35
|
+
"""
|
|
36
|
+
if meshseed == 0:
|
|
37
|
+
# Use simple addBox
|
|
38
|
+
return kernel.addBox(xmin, ymin, zmin, xmax - xmin, ymax - ymin, zmax - zmin)
|
|
39
|
+
|
|
40
|
+
# Create box with explicit mesh seed at corners
|
|
41
|
+
pt1 = kernel.addPoint(xmin, ymin, zmin, meshseed, -1)
|
|
42
|
+
pt2 = kernel.addPoint(xmin, ymax, zmin, meshseed, -1)
|
|
43
|
+
pt3 = kernel.addPoint(xmax, ymax, zmin, meshseed, -1)
|
|
44
|
+
pt4 = kernel.addPoint(xmax, ymin, zmin, meshseed, -1)
|
|
45
|
+
|
|
46
|
+
line1 = kernel.addLine(pt1, pt2, -1)
|
|
47
|
+
line2 = kernel.addLine(pt2, pt3, -1)
|
|
48
|
+
line3 = kernel.addLine(pt3, pt4, -1)
|
|
49
|
+
line4 = kernel.addLine(pt4, pt1, -1)
|
|
50
|
+
linetaglist = [line1, line2, line3, line4]
|
|
51
|
+
|
|
52
|
+
curvetag = kernel.addCurveLoop(linetaglist, tag=-1)
|
|
53
|
+
surfacetag = kernel.addPlaneSurface([curvetag], tag=-1)
|
|
54
|
+
returnval = kernel.extrude([(2, surfacetag)], 0, 0, zmax - zmin)
|
|
55
|
+
volumetag = returnval[1][1]
|
|
56
|
+
|
|
57
|
+
return volumetag
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def create_polygon_surface(
|
|
61
|
+
kernel,
|
|
62
|
+
pts_x: list[float],
|
|
63
|
+
pts_y: list[float],
|
|
64
|
+
z: float,
|
|
65
|
+
meshseed: float = 0,
|
|
66
|
+
) -> int | None:
|
|
67
|
+
"""Create a planar surface from polygon vertices at z height.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
kernel: gmsh.model.occ kernel
|
|
71
|
+
pts_x: list of x coordinates
|
|
72
|
+
pts_y: list of y coordinates
|
|
73
|
+
z: z coordinate of the surface
|
|
74
|
+
meshseed: mesh seed size at vertices (0 = auto)
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Surface tag, or None if polygon is invalid
|
|
78
|
+
"""
|
|
79
|
+
numvertices = len(pts_x)
|
|
80
|
+
if numvertices < 3:
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
linetaglist = []
|
|
84
|
+
vertextaglist = []
|
|
85
|
+
|
|
86
|
+
# Create vertices
|
|
87
|
+
for v in range(numvertices):
|
|
88
|
+
vertextag = kernel.addPoint(pts_x[v], pts_y[v], z, meshseed, -1)
|
|
89
|
+
vertextaglist.append(vertextag)
|
|
90
|
+
|
|
91
|
+
# Create lines connecting vertices
|
|
92
|
+
for v in range(numvertices):
|
|
93
|
+
pt_start = vertextaglist[v]
|
|
94
|
+
pt_end = vertextaglist[(v + 1) % numvertices]
|
|
95
|
+
try:
|
|
96
|
+
linetag = kernel.addLine(pt_start, pt_end, -1)
|
|
97
|
+
linetaglist.append(linetag)
|
|
98
|
+
except Exception:
|
|
99
|
+
pass # Skip degenerate lines
|
|
100
|
+
|
|
101
|
+
if len(linetaglist) < 3:
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
# Create surface
|
|
105
|
+
curvetag = kernel.addCurveLoop(linetaglist, tag=-1)
|
|
106
|
+
surfacetag = kernel.addPlaneSurface([curvetag], tag=-1)
|
|
107
|
+
|
|
108
|
+
return surfacetag
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def extrude_polygon(
|
|
112
|
+
kernel,
|
|
113
|
+
pts_x: list[float],
|
|
114
|
+
pts_y: list[float],
|
|
115
|
+
zmin: float,
|
|
116
|
+
thickness: float,
|
|
117
|
+
meshseed: float = 0,
|
|
118
|
+
) -> int | None:
|
|
119
|
+
"""Create an extruded polygon volume (for vias, metals).
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
kernel: gmsh.model.occ kernel
|
|
123
|
+
pts_x: list of x coordinates
|
|
124
|
+
pts_y: list of y coordinates
|
|
125
|
+
zmin: base z coordinate
|
|
126
|
+
thickness: extrusion height
|
|
127
|
+
meshseed: mesh seed size at vertices
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Volume tag if thickness > 0, surface tag if thickness == 0, or None if invalid
|
|
131
|
+
"""
|
|
132
|
+
surfacetag = create_polygon_surface(kernel, pts_x, pts_y, zmin, meshseed)
|
|
133
|
+
if surfacetag is None:
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
if thickness > 0:
|
|
137
|
+
result = kernel.extrude([(2, surfacetag)], 0, 0, thickness)
|
|
138
|
+
# result[1] contains the volume (dim=3, tag)
|
|
139
|
+
return result[1][1]
|
|
140
|
+
|
|
141
|
+
return surfacetag
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def create_port_rectangle(
|
|
145
|
+
kernel,
|
|
146
|
+
xmin: float,
|
|
147
|
+
ymin: float,
|
|
148
|
+
zmin: float,
|
|
149
|
+
xmax: float,
|
|
150
|
+
ymax: float,
|
|
151
|
+
zmax: float,
|
|
152
|
+
meshseed: float = 0,
|
|
153
|
+
) -> int:
|
|
154
|
+
"""Create a rectangular surface for a port.
|
|
155
|
+
|
|
156
|
+
Handles both horizontal (z-plane) and vertical port surfaces.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
kernel: gmsh.model.occ kernel
|
|
160
|
+
xmin, ymin, zmin: minimum coordinates
|
|
161
|
+
xmax, ymax, zmax: maximum coordinates
|
|
162
|
+
meshseed: mesh seed size at corners
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
Surface tag of created port rectangle
|
|
166
|
+
"""
|
|
167
|
+
# Determine port orientation
|
|
168
|
+
dx = xmax - xmin
|
|
169
|
+
dz = zmax - zmin
|
|
170
|
+
|
|
171
|
+
if dz < 1e-6:
|
|
172
|
+
# Horizontal port (in xy plane)
|
|
173
|
+
pt1 = kernel.addPoint(xmin, ymin, zmin, meshseed, -1)
|
|
174
|
+
pt2 = kernel.addPoint(xmin, ymax, zmin, meshseed, -1)
|
|
175
|
+
pt3 = kernel.addPoint(xmax, ymax, zmin, meshseed, -1)
|
|
176
|
+
pt4 = kernel.addPoint(xmax, ymin, zmin, meshseed, -1)
|
|
177
|
+
elif dx < 1e-6:
|
|
178
|
+
# Vertical port in yz plane
|
|
179
|
+
pt1 = kernel.addPoint(xmin, ymin, zmin, meshseed, -1)
|
|
180
|
+
pt2 = kernel.addPoint(xmin, ymax, zmin, meshseed, -1)
|
|
181
|
+
pt3 = kernel.addPoint(xmin, ymax, zmax, meshseed, -1)
|
|
182
|
+
pt4 = kernel.addPoint(xmin, ymin, zmax, meshseed, -1)
|
|
183
|
+
else:
|
|
184
|
+
# Vertical port in xz plane
|
|
185
|
+
pt1 = kernel.addPoint(xmin, ymin, zmin, meshseed, -1)
|
|
186
|
+
pt2 = kernel.addPoint(xmin, ymin, zmax, meshseed, -1)
|
|
187
|
+
pt3 = kernel.addPoint(xmax, ymin, zmax, meshseed, -1)
|
|
188
|
+
pt4 = kernel.addPoint(xmax, ymin, zmin, meshseed, -1)
|
|
189
|
+
|
|
190
|
+
line1 = kernel.addLine(pt1, pt2, -1)
|
|
191
|
+
line2 = kernel.addLine(pt2, pt3, -1)
|
|
192
|
+
line3 = kernel.addLine(pt3, pt4, -1)
|
|
193
|
+
line4 = kernel.addLine(pt4, pt1, -1)
|
|
194
|
+
linetaglist = [line1, line2, line3, line4]
|
|
195
|
+
|
|
196
|
+
curvetag = kernel.addCurveLoop(linetaglist, tag=-1)
|
|
197
|
+
surfacetag = kernel.addPlaneSurface([curvetag], tag=-1)
|
|
198
|
+
|
|
199
|
+
return surfacetag
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def fragment_all(kernel) -> tuple[list, list]:
|
|
203
|
+
"""Fragment all geometry to ensure conformal mesh at intersections.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
kernel: gmsh.model.occ kernel
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
(geom_dimtags, geom_map) - original dimtags and mapping to new tags
|
|
210
|
+
"""
|
|
211
|
+
geom_dimtags = [x for x in kernel.getEntities() if x[0] in (2, 3)]
|
|
212
|
+
_, geom_map = kernel.fragment(geom_dimtags, [])
|
|
213
|
+
kernel.synchronize()
|
|
214
|
+
return geom_dimtags, geom_map
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def get_tags_after_fragment(
|
|
218
|
+
original_tags: list[int],
|
|
219
|
+
geom_dimtags: list,
|
|
220
|
+
geom_map: list,
|
|
221
|
+
dimension: int = 2,
|
|
222
|
+
) -> list[int]:
|
|
223
|
+
"""Get new tags after fragmenting, given original tags.
|
|
224
|
+
|
|
225
|
+
Tags change after gmsh fragment operation. This function maps
|
|
226
|
+
original tags to their new values using the fragment mapping.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
original_tags: list of tags before fragmenting
|
|
230
|
+
geom_dimtags: list of all original dimtags before fragmenting
|
|
231
|
+
geom_map: mapping from fragment() function
|
|
232
|
+
dimension: dimension for tags (2=surfaces, 3=volumes)
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
List of new tags after fragmenting
|
|
236
|
+
"""
|
|
237
|
+
if isinstance(original_tags, int):
|
|
238
|
+
original_tags = [original_tags]
|
|
239
|
+
|
|
240
|
+
indices = [
|
|
241
|
+
i
|
|
242
|
+
for i, x in enumerate(geom_dimtags)
|
|
243
|
+
if x[0] == dimension and (x[1] in original_tags)
|
|
244
|
+
]
|
|
245
|
+
raw = [geom_map[i] for i in indices]
|
|
246
|
+
flat = [item for sublist in raw for item in sublist]
|
|
247
|
+
newtags = [s[-1] for s in flat]
|
|
248
|
+
|
|
249
|
+
return newtags
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def assign_physical_group(
|
|
253
|
+
dim: int,
|
|
254
|
+
tags: list[int],
|
|
255
|
+
name: str,
|
|
256
|
+
) -> int:
|
|
257
|
+
"""Assign tags to a physical group with a name.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
dim: dimension (2=surfaces, 3=volumes)
|
|
261
|
+
tags: list of entity tags
|
|
262
|
+
name: physical group name
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
Physical group tag
|
|
266
|
+
"""
|
|
267
|
+
if not tags:
|
|
268
|
+
return -1
|
|
269
|
+
phys_group = gmsh.model.addPhysicalGroup(dim, tags, tag=-1)
|
|
270
|
+
gmsh.model.setPhysicalName(dim, phys_group, name)
|
|
271
|
+
return phys_group
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def get_surface_normal(surface_tag: int) -> np.ndarray:
|
|
275
|
+
"""Get the normal vector of a surface.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
surface_tag: surface entity tag
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
Normal vector as numpy array [nx, ny, nz]
|
|
282
|
+
"""
|
|
283
|
+
# Get the boundary of the surface
|
|
284
|
+
boundary_lines = gmsh.model.getBoundary([(2, surface_tag)], oriented=True)
|
|
285
|
+
|
|
286
|
+
# Get points from these lines
|
|
287
|
+
points = []
|
|
288
|
+
seen_points = set()
|
|
289
|
+
|
|
290
|
+
for _dim, line_tag in boundary_lines:
|
|
291
|
+
line_points = gmsh.model.getBoundary([(1, line_tag)], oriented=True)
|
|
292
|
+
for _pdim, ptag in line_points:
|
|
293
|
+
if ptag not in seen_points:
|
|
294
|
+
coord = gmsh.model.getValue(0, ptag, [])
|
|
295
|
+
points.append(np.array(coord))
|
|
296
|
+
seen_points.add(ptag)
|
|
297
|
+
if len(points) == 3:
|
|
298
|
+
break
|
|
299
|
+
if len(points) == 3:
|
|
300
|
+
break
|
|
301
|
+
|
|
302
|
+
if len(points) < 3:
|
|
303
|
+
return np.array([0, 0, 1]) # Default to z-normal
|
|
304
|
+
|
|
305
|
+
# Compute surface normal using cross product
|
|
306
|
+
v1 = points[1] - points[0]
|
|
307
|
+
v2 = points[2] - points[0]
|
|
308
|
+
normal = np.cross(v1, v2)
|
|
309
|
+
norm = np.linalg.norm(normal)
|
|
310
|
+
if norm > 0:
|
|
311
|
+
normal = normal / norm
|
|
312
|
+
return normal
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def is_vertical_surface(surface_tag: int) -> bool:
|
|
316
|
+
"""Check if a surface is vertical (not in xy plane).
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
surface_tag: surface entity tag
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
True if surface is vertical (z component of normal is ~0)
|
|
323
|
+
"""
|
|
324
|
+
normal = get_surface_normal(surface_tag)
|
|
325
|
+
n = normal[2]
|
|
326
|
+
if not np.isnan(n):
|
|
327
|
+
return int(abs(n)) == 0
|
|
328
|
+
return False
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def get_volumes_at_z_range(
|
|
332
|
+
zmin: float,
|
|
333
|
+
zmax: float,
|
|
334
|
+
delta: float = 0.001,
|
|
335
|
+
) -> list[tuple[int, int]]:
|
|
336
|
+
"""Get all volumes within a z-coordinate range.
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
zmin: minimum z coordinate
|
|
340
|
+
zmax: maximum z coordinate
|
|
341
|
+
delta: tolerance for z comparison
|
|
342
|
+
|
|
343
|
+
Returns:
|
|
344
|
+
List of (dim, tag) tuples for volumes in the z range
|
|
345
|
+
"""
|
|
346
|
+
volumes_in_bbox = gmsh.model.getEntitiesInBoundingBox(
|
|
347
|
+
-math.inf,
|
|
348
|
+
-math.inf,
|
|
349
|
+
zmin - delta / 2,
|
|
350
|
+
math.inf,
|
|
351
|
+
math.inf,
|
|
352
|
+
zmax + delta / 2,
|
|
353
|
+
3,
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
volume_list = []
|
|
357
|
+
for volume in volumes_in_bbox:
|
|
358
|
+
volume_tag = volume[1]
|
|
359
|
+
_, _, vzmin, _, _, vzmax = gmsh.model.getBoundingBox(3, volume_tag)
|
|
360
|
+
if (
|
|
361
|
+
abs(vzmin - (zmin - delta / 2)) < delta
|
|
362
|
+
and abs(vzmax - (zmax + delta / 2)) < delta
|
|
363
|
+
):
|
|
364
|
+
volume_list.append(volume)
|
|
365
|
+
|
|
366
|
+
return volume_list
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def get_surfaces_at_z(z: float, delta: float = 0.001) -> list[tuple[int, int]]:
|
|
370
|
+
"""Get all surfaces at a specific z coordinate.
|
|
371
|
+
|
|
372
|
+
Args:
|
|
373
|
+
z: z coordinate
|
|
374
|
+
delta: tolerance for z comparison
|
|
375
|
+
|
|
376
|
+
Returns:
|
|
377
|
+
List of (dim, tag) tuples for surfaces at z
|
|
378
|
+
"""
|
|
379
|
+
return gmsh.model.getEntitiesInBoundingBox(
|
|
380
|
+
-math.inf,
|
|
381
|
+
-math.inf,
|
|
382
|
+
z - delta / 2,
|
|
383
|
+
math.inf,
|
|
384
|
+
math.inf,
|
|
385
|
+
z + delta / 2,
|
|
386
|
+
2,
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def get_boundary_lines(surface_tag: int, kernel) -> list[int]:
|
|
391
|
+
"""Get all boundary line tags of a surface.
|
|
392
|
+
|
|
393
|
+
Args:
|
|
394
|
+
surface_tag: surface entity tag
|
|
395
|
+
kernel: gmsh.model.occ kernel
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
List of curve/line tags forming the surface boundary
|
|
399
|
+
"""
|
|
400
|
+
_clt, ct = kernel.getCurveLoops(surface_tag)
|
|
401
|
+
lines = []
|
|
402
|
+
for curvetag in ct:
|
|
403
|
+
lines.extend(curvetag)
|
|
404
|
+
return lines
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def setup_mesh_refinement(
|
|
408
|
+
boundary_line_tags: list[int],
|
|
409
|
+
refined_cellsize: float,
|
|
410
|
+
max_cellsize: float,
|
|
411
|
+
) -> int:
|
|
412
|
+
"""Set up mesh refinement near boundary lines.
|
|
413
|
+
|
|
414
|
+
Args:
|
|
415
|
+
boundary_line_tags: list of curve tags for refinement
|
|
416
|
+
refined_cellsize: mesh size near boundaries
|
|
417
|
+
max_cellsize: mesh size far from boundaries
|
|
418
|
+
|
|
419
|
+
Returns:
|
|
420
|
+
Field ID for the minimum field
|
|
421
|
+
"""
|
|
422
|
+
# Distance field from boundary curves
|
|
423
|
+
gmsh.model.mesh.field.add("Distance", 1)
|
|
424
|
+
gmsh.model.mesh.field.setNumbers(1, "CurvesList", boundary_line_tags)
|
|
425
|
+
gmsh.model.mesh.field.setNumber(1, "Sampling", 200)
|
|
426
|
+
|
|
427
|
+
# Threshold field for gradual size transition
|
|
428
|
+
gmsh.model.mesh.field.add("Threshold", 2)
|
|
429
|
+
gmsh.model.mesh.field.setNumber(2, "InField", 1)
|
|
430
|
+
gmsh.model.mesh.field.setNumber(2, "SizeMin", refined_cellsize)
|
|
431
|
+
gmsh.model.mesh.field.setNumber(2, "SizeMax", max_cellsize)
|
|
432
|
+
gmsh.model.mesh.field.setNumber(2, "DistMin", 0)
|
|
433
|
+
gmsh.model.mesh.field.setNumber(2, "DistMax", max_cellsize)
|
|
434
|
+
|
|
435
|
+
return 2
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def setup_box_refinement(
|
|
439
|
+
field_id: int,
|
|
440
|
+
xmin: float,
|
|
441
|
+
ymin: float,
|
|
442
|
+
zmin: float,
|
|
443
|
+
xmax: float,
|
|
444
|
+
ymax: float,
|
|
445
|
+
zmax: float,
|
|
446
|
+
size_in: float,
|
|
447
|
+
size_out: float,
|
|
448
|
+
) -> None:
|
|
449
|
+
"""Set up box-based mesh refinement.
|
|
450
|
+
|
|
451
|
+
Args:
|
|
452
|
+
field_id: field ID to use
|
|
453
|
+
xmin, ymin, zmin: box minimum coordinates
|
|
454
|
+
xmax, ymax, zmax: box maximum coordinates
|
|
455
|
+
size_in: mesh size inside box
|
|
456
|
+
size_out: mesh size outside box
|
|
457
|
+
"""
|
|
458
|
+
gmsh.model.mesh.field.add("Box", field_id)
|
|
459
|
+
gmsh.model.mesh.field.setNumber(field_id, "VIn", size_in)
|
|
460
|
+
gmsh.model.mesh.field.setNumber(field_id, "VOut", size_out)
|
|
461
|
+
gmsh.model.mesh.field.setNumber(field_id, "XMin", xmin)
|
|
462
|
+
gmsh.model.mesh.field.setNumber(field_id, "XMax", xmax)
|
|
463
|
+
gmsh.model.mesh.field.setNumber(field_id, "YMin", ymin)
|
|
464
|
+
gmsh.model.mesh.field.setNumber(field_id, "YMax", ymax)
|
|
465
|
+
gmsh.model.mesh.field.setNumber(field_id, "ZMin", zmin)
|
|
466
|
+
gmsh.model.mesh.field.setNumber(field_id, "ZMax", zmax)
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def finalize_mesh_fields(field_ids: list[int]) -> None:
|
|
470
|
+
"""Finalize mesh fields by setting up minimum field.
|
|
471
|
+
|
|
472
|
+
Args:
|
|
473
|
+
field_ids: list of field IDs to combine
|
|
474
|
+
"""
|
|
475
|
+
min_field_id = max(field_ids) + 1
|
|
476
|
+
gmsh.model.mesh.field.add("Min", min_field_id)
|
|
477
|
+
gmsh.model.mesh.field.setNumbers(min_field_id, "FieldsList", field_ids)
|
|
478
|
+
gmsh.model.mesh.field.setAsBackgroundMesh(min_field_id)
|
|
479
|
+
|
|
480
|
+
# Disable other mesh size sources
|
|
481
|
+
gmsh.option.setNumber("Mesh.MeshSizeExtendFromBoundary", 0)
|
|
482
|
+
gmsh.option.setNumber("Mesh.MeshSizeFromPoints", 0)
|
|
483
|
+
gmsh.option.setNumber("Mesh.MeshSizeFromCurvature", 0)
|
|
484
|
+
gmsh.option.setNumber("Mesh.Algorithm", 5) # Delaunay algorithm
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""Mesh generation pipeline for Palace EM simulation.
|
|
2
|
+
|
|
3
|
+
This module provides the main entry point for generating meshes from
|
|
4
|
+
gdsfactory components. Uses the new generator module internally.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from enum import Enum
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from gsim.palace.ports.config import PalacePort
|
|
16
|
+
from gsim.palace.stack.extractor import LayerStack
|
|
17
|
+
|
|
18
|
+
from gsim.palace.mesh.generator import generate_mesh as gen_mesh
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MeshPreset(Enum):
|
|
22
|
+
"""Mesh quality presets based on COMSOL guidelines.
|
|
23
|
+
|
|
24
|
+
COMSOL uses 2nd order elements with ~5 elements per wavelength as default.
|
|
25
|
+
Wavelength in dielectric: λ = c / (f * √εᵣ)
|
|
26
|
+
At 100 GHz in SiO2 (εᵣ≈4): λ ≈ 1500 µm
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
COARSE = "coarse" # ~2.5 elements/λ - fast iteration
|
|
30
|
+
DEFAULT = "default" # ~5 elements/λ - COMSOL default
|
|
31
|
+
FINE = "fine" # ~10 elements/λ - high accuracy
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# Preset configurations: (refined_mesh_size, max_mesh_size, cells_per_wavelength)
|
|
35
|
+
_MESH_PRESETS = {
|
|
36
|
+
MeshPreset.COARSE: (10.0, 600.0, 5),
|
|
37
|
+
MeshPreset.DEFAULT: (5.0, 300.0, 10),
|
|
38
|
+
MeshPreset.FINE: (2.0, 70.0, 20),
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class GroundPlane:
|
|
44
|
+
"""Ground plane configuration for microstrip structures."""
|
|
45
|
+
|
|
46
|
+
layer_name: str # Name of metal layer for ground (e.g., "metal1")
|
|
47
|
+
oversize: float = 50.0 # How much to extend beyond signal geometry (um)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class MeshConfig:
|
|
52
|
+
"""Configuration for mesh generation.
|
|
53
|
+
|
|
54
|
+
Use class methods for quick presets:
|
|
55
|
+
MeshConfig.coarse() - Fast iteration (~2.5 elem/λ)
|
|
56
|
+
MeshConfig.default() - Balanced (COMSOL default, ~5 elem/λ)
|
|
57
|
+
MeshConfig.fine() - High accuracy (~10 elem/λ)
|
|
58
|
+
|
|
59
|
+
Or customize directly:
|
|
60
|
+
MeshConfig(refined_mesh_size=3.0, max_mesh_size=200.0)
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
# Mesh size control
|
|
64
|
+
refined_mesh_size: float = 5.0 # Mesh size near conductors (um)
|
|
65
|
+
max_mesh_size: float = 300.0 # Max mesh size in air/dielectric (um)
|
|
66
|
+
cells_per_wavelength: int = 10 # Mesh cells per wavelength
|
|
67
|
+
|
|
68
|
+
# Geometry margins
|
|
69
|
+
margin: float = 50.0 # XY margin around design (um)
|
|
70
|
+
air_above: float = 100.0 # Air above top metal (um)
|
|
71
|
+
|
|
72
|
+
# Ground plane (optional - for microstrip structures)
|
|
73
|
+
ground_plane: GroundPlane | None = None
|
|
74
|
+
|
|
75
|
+
# Frequency for wavelength calculation (optional)
|
|
76
|
+
fmax: float = 100e9 # Max frequency for mesh sizing (Hz)
|
|
77
|
+
|
|
78
|
+
# Boundary conditions for 6 faces: [xmin, xmax, ymin, ymax, zmin, zmax]
|
|
79
|
+
# Options: 'ABC' (absorbing), 'PEC' (perfect electric conductor), 'PMC'
|
|
80
|
+
boundary_conditions: list[str] | None = None
|
|
81
|
+
|
|
82
|
+
# GUI control
|
|
83
|
+
show_gui: bool = False # Show gmsh GUI during meshing
|
|
84
|
+
preview_only: bool = False # Show geometry without meshing
|
|
85
|
+
|
|
86
|
+
def __post_init__(self) -> None:
|
|
87
|
+
if self.boundary_conditions is None:
|
|
88
|
+
# Default: ABC everywhere
|
|
89
|
+
self.boundary_conditions = ["ABC", "ABC", "ABC", "ABC", "ABC", "ABC"]
|
|
90
|
+
|
|
91
|
+
@classmethod
|
|
92
|
+
def coarse(cls, **kwargs) -> MeshConfig:
|
|
93
|
+
"""Fast mesh for quick iteration (~2.5 elements per wavelength)."""
|
|
94
|
+
refined, max_size, cpw = _MESH_PRESETS[MeshPreset.COARSE]
|
|
95
|
+
return cls(
|
|
96
|
+
refined_mesh_size=refined,
|
|
97
|
+
max_mesh_size=max_size,
|
|
98
|
+
cells_per_wavelength=cpw,
|
|
99
|
+
**kwargs,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
@classmethod
|
|
103
|
+
def default(cls, **kwargs) -> MeshConfig:
|
|
104
|
+
"""Balanced mesh matching COMSOL defaults (~5 elements per wavelength)."""
|
|
105
|
+
refined, max_size, cpw = _MESH_PRESETS[MeshPreset.DEFAULT]
|
|
106
|
+
return cls(
|
|
107
|
+
refined_mesh_size=refined,
|
|
108
|
+
max_mesh_size=max_size,
|
|
109
|
+
cells_per_wavelength=cpw,
|
|
110
|
+
**kwargs,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
@classmethod
|
|
114
|
+
def fine(cls, **kwargs) -> MeshConfig:
|
|
115
|
+
"""High accuracy mesh (~10 elements per wavelength)."""
|
|
116
|
+
refined, max_size, cpw = _MESH_PRESETS[MeshPreset.FINE]
|
|
117
|
+
return cls(
|
|
118
|
+
refined_mesh_size=refined,
|
|
119
|
+
max_mesh_size=max_size,
|
|
120
|
+
cells_per_wavelength=cpw,
|
|
121
|
+
**kwargs,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@dataclass
|
|
126
|
+
class MeshResult:
|
|
127
|
+
"""Result from mesh generation."""
|
|
128
|
+
|
|
129
|
+
mesh_path: Path
|
|
130
|
+
config_path: Path | None = None # Palace config.json if generated
|
|
131
|
+
|
|
132
|
+
# Physical group info for Palace
|
|
133
|
+
conductor_groups: dict = field(default_factory=dict)
|
|
134
|
+
dielectric_groups: dict = field(default_factory=dict)
|
|
135
|
+
port_groups: dict = field(default_factory=dict)
|
|
136
|
+
boundary_groups: dict = field(default_factory=dict)
|
|
137
|
+
|
|
138
|
+
# Port metadata
|
|
139
|
+
port_info: list = field(default_factory=list)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def generate_mesh(
|
|
143
|
+
component,
|
|
144
|
+
stack: LayerStack,
|
|
145
|
+
ports: list[PalacePort],
|
|
146
|
+
output_dir: str | Path,
|
|
147
|
+
config: MeshConfig | None = None,
|
|
148
|
+
model_name: str = "palace",
|
|
149
|
+
) -> MeshResult:
|
|
150
|
+
"""Generate mesh for Palace EM simulation.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
component: gdsfactory Component
|
|
154
|
+
stack: LayerStack from palace-api
|
|
155
|
+
ports: List of PalacePort objects (single and multi-element)
|
|
156
|
+
output_dir: Directory for output files
|
|
157
|
+
config: MeshConfig with mesh parameters
|
|
158
|
+
model_name: Base name for output files (default: "mesh" -> mesh.msh)
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
MeshResult with mesh path and metadata
|
|
162
|
+
"""
|
|
163
|
+
if config is None:
|
|
164
|
+
config = MeshConfig()
|
|
165
|
+
|
|
166
|
+
output_dir = Path(output_dir)
|
|
167
|
+
|
|
168
|
+
# Use new generator
|
|
169
|
+
result = gen_mesh(
|
|
170
|
+
component=component,
|
|
171
|
+
stack=stack,
|
|
172
|
+
ports=ports,
|
|
173
|
+
output_dir=output_dir,
|
|
174
|
+
model_name=model_name,
|
|
175
|
+
refined_mesh_size=config.refined_mesh_size,
|
|
176
|
+
max_mesh_size=config.max_mesh_size,
|
|
177
|
+
margin=config.margin,
|
|
178
|
+
air_margin=config.margin,
|
|
179
|
+
fmax=config.fmax,
|
|
180
|
+
show_gui=config.show_gui,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# Convert to pipeline's MeshResult format
|
|
184
|
+
return MeshResult(
|
|
185
|
+
mesh_path=result.mesh_path,
|
|
186
|
+
config_path=result.config_path,
|
|
187
|
+
port_info=result.port_info,
|
|
188
|
+
)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Port definition for Palace EM simulation.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
from gsim.palace.ports import configure_port, extract_ports
|
|
5
|
+
|
|
6
|
+
# Configure ports on a component
|
|
7
|
+
c = gf.get_component("straight_metal")
|
|
8
|
+
configure_port(c.ports['o1'], type='lumped', layer='topmetal2')
|
|
9
|
+
configure_port(c.ports['o2'], type='lumped', layer='topmetal2')
|
|
10
|
+
|
|
11
|
+
# Extract ports for simulation
|
|
12
|
+
ports = extract_ports(c, stack)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from gsim.palace.ports.config import (
|
|
18
|
+
PalacePort,
|
|
19
|
+
PortGeometry,
|
|
20
|
+
PortType,
|
|
21
|
+
configure_cpw_port,
|
|
22
|
+
configure_inplane_port,
|
|
23
|
+
configure_via_port,
|
|
24
|
+
extract_ports,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"PalacePort",
|
|
29
|
+
"PortGeometry",
|
|
30
|
+
"PortType",
|
|
31
|
+
"configure_cpw_port",
|
|
32
|
+
"configure_inplane_port",
|
|
33
|
+
"configure_via_port",
|
|
34
|
+
"extract_ports",
|
|
35
|
+
]
|