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.
@@ -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
+ ]