gsim 0.0.0__py3-none-any.whl → 0.0.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gsim/__init__.py +1 -1
- gsim/common/__init__.py +57 -0
- gsim/common/geometry.py +76 -0
- gsim/{palace → common}/stack/__init__.py +8 -5
- gsim/{palace → common}/stack/extractor.py +33 -107
- gsim/{palace → common}/stack/materials.py +27 -11
- gsim/{palace → common}/stack/visualization.py +3 -3
- gsim/gcloud.py +78 -25
- gsim/palace/__init__.py +85 -45
- gsim/palace/base.py +373 -0
- gsim/palace/driven.py +728 -0
- gsim/palace/eigenmode.py +557 -0
- gsim/palace/electrostatic.py +398 -0
- gsim/palace/mesh/__init__.py +13 -1
- gsim/palace/mesh/config_generator.py +367 -0
- gsim/palace/mesh/generator.py +66 -744
- gsim/palace/mesh/geometry.py +472 -0
- gsim/palace/mesh/groups.py +170 -0
- gsim/palace/mesh/pipeline.py +22 -1
- gsim/palace/models/__init__.py +53 -0
- gsim/palace/models/geometry.py +34 -0
- gsim/palace/models/mesh.py +95 -0
- gsim/palace/models/numerical.py +66 -0
- gsim/palace/models/ports.py +137 -0
- gsim/palace/models/problems.py +195 -0
- gsim/palace/models/results.py +160 -0
- gsim/palace/models/stack.py +59 -0
- gsim/palace/ports/config.py +1 -1
- gsim/viz.py +9 -6
- {gsim-0.0.0.dist-info → gsim-0.0.3.dist-info}/METADATA +9 -6
- gsim-0.0.3.dist-info/RECORD +35 -0
- {gsim-0.0.0.dist-info → gsim-0.0.3.dist-info}/WHEEL +1 -1
- gsim-0.0.0.dist-info/RECORD +0 -18
- {gsim-0.0.0.dist-info → gsim-0.0.3.dist-info}/top_level.txt +0 -0
gsim/palace/eigenmode.py
ADDED
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
"""Eigenmode simulation class for resonance/mode finding.
|
|
2
|
+
|
|
3
|
+
This module provides the EigenmodeSim class for finding resonant
|
|
4
|
+
frequencies and mode shapes.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import tempfile
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Literal
|
|
13
|
+
|
|
14
|
+
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr
|
|
15
|
+
|
|
16
|
+
from gsim.common import Geometry, LayerStack
|
|
17
|
+
from gsim.palace.base import PalaceSimMixin
|
|
18
|
+
from gsim.palace.models import (
|
|
19
|
+
CPWPortConfig,
|
|
20
|
+
EigenmodeConfig,
|
|
21
|
+
MaterialConfig,
|
|
22
|
+
MeshConfig,
|
|
23
|
+
NumericalConfig,
|
|
24
|
+
PortConfig,
|
|
25
|
+
SimulationResult,
|
|
26
|
+
ValidationResult,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class EigenmodeSim(PalaceSimMixin, BaseModel):
|
|
33
|
+
"""Eigenmode simulation for finding resonant frequencies.
|
|
34
|
+
|
|
35
|
+
This class configures and runs eigenmode simulations to find
|
|
36
|
+
resonant frequencies and mode shapes of structures.
|
|
37
|
+
|
|
38
|
+
Example:
|
|
39
|
+
>>> from gsim.palace import EigenmodeSim
|
|
40
|
+
>>>
|
|
41
|
+
>>> sim = EigenmodeSim()
|
|
42
|
+
>>> sim.set_geometry(component)
|
|
43
|
+
>>> sim.set_stack(air_above=300.0)
|
|
44
|
+
>>> sim.add_port("o1", layer="topmetal2", length=5.0)
|
|
45
|
+
>>> sim.set_eigenmode(num_modes=10, target=50e9)
|
|
46
|
+
>>> sim.set_output_dir("./sim")
|
|
47
|
+
>>> sim.mesh(preset="default")
|
|
48
|
+
>>> results = sim.simulate()
|
|
49
|
+
|
|
50
|
+
Attributes:
|
|
51
|
+
geometry: Wrapped gdsfactory Component (from common)
|
|
52
|
+
stack: Layer stack configuration (from common)
|
|
53
|
+
ports: List of single-element port configurations
|
|
54
|
+
cpw_ports: List of CPW (two-element) port configurations
|
|
55
|
+
eigenmode: Eigenmode simulation configuration
|
|
56
|
+
materials: Material property overrides
|
|
57
|
+
numerical: Numerical solver configuration
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
model_config = ConfigDict(
|
|
61
|
+
validate_assignment=True,
|
|
62
|
+
arbitrary_types_allowed=True,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Composed objects (from common)
|
|
66
|
+
geometry: Geometry | None = None
|
|
67
|
+
stack: LayerStack | None = None
|
|
68
|
+
|
|
69
|
+
# Port configurations (eigenmode can have ports for Q-factor calculation)
|
|
70
|
+
ports: list[PortConfig] = Field(default_factory=list)
|
|
71
|
+
cpw_ports: list[CPWPortConfig] = Field(default_factory=list)
|
|
72
|
+
|
|
73
|
+
# Eigenmode simulation config
|
|
74
|
+
eigenmode: EigenmodeConfig = Field(default_factory=EigenmodeConfig)
|
|
75
|
+
|
|
76
|
+
# Material overrides and numerical config
|
|
77
|
+
materials: dict[str, MaterialConfig] = Field(default_factory=dict)
|
|
78
|
+
numerical: NumericalConfig = Field(default_factory=NumericalConfig)
|
|
79
|
+
|
|
80
|
+
# Stack configuration (stored as kwargs until resolved)
|
|
81
|
+
_stack_kwargs: dict[str, Any] = PrivateAttr(default_factory=dict)
|
|
82
|
+
|
|
83
|
+
# Internal state
|
|
84
|
+
_output_dir: Path | None = PrivateAttr(default=None)
|
|
85
|
+
_configured_ports: bool = PrivateAttr(default=False)
|
|
86
|
+
|
|
87
|
+
# -------------------------------------------------------------------------
|
|
88
|
+
# Port methods (Eigenmode can have ports for Q-factor calculation)
|
|
89
|
+
# -------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
def add_port(
|
|
92
|
+
self,
|
|
93
|
+
name: str,
|
|
94
|
+
*,
|
|
95
|
+
layer: str | None = None,
|
|
96
|
+
from_layer: str | None = None,
|
|
97
|
+
to_layer: str | None = None,
|
|
98
|
+
length: float | None = None,
|
|
99
|
+
impedance: float = 50.0,
|
|
100
|
+
excited: bool = True,
|
|
101
|
+
geometry: Literal["inplane", "via"] = "inplane",
|
|
102
|
+
) -> None:
|
|
103
|
+
"""Add a single-element lumped port.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
name: Port name (must match component port name)
|
|
107
|
+
layer: Target layer for inplane ports
|
|
108
|
+
from_layer: Bottom layer for via ports
|
|
109
|
+
to_layer: Top layer for via ports
|
|
110
|
+
length: Port extent along direction (um)
|
|
111
|
+
impedance: Port impedance (Ohms)
|
|
112
|
+
excited: Whether this port is excited
|
|
113
|
+
geometry: Port geometry type ("inplane" or "via")
|
|
114
|
+
|
|
115
|
+
Example:
|
|
116
|
+
>>> sim.add_port("o1", layer="topmetal2", length=5.0)
|
|
117
|
+
"""
|
|
118
|
+
self.ports = [p for p in self.ports if p.name != name]
|
|
119
|
+
self.ports.append(
|
|
120
|
+
PortConfig(
|
|
121
|
+
name=name,
|
|
122
|
+
layer=layer,
|
|
123
|
+
from_layer=from_layer,
|
|
124
|
+
to_layer=to_layer,
|
|
125
|
+
length=length,
|
|
126
|
+
impedance=impedance,
|
|
127
|
+
excited=excited,
|
|
128
|
+
geometry=geometry,
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
def add_cpw_port(
|
|
133
|
+
self,
|
|
134
|
+
upper: str,
|
|
135
|
+
lower: str,
|
|
136
|
+
*,
|
|
137
|
+
layer: str,
|
|
138
|
+
length: float,
|
|
139
|
+
impedance: float = 50.0,
|
|
140
|
+
excited: bool = True,
|
|
141
|
+
name: str | None = None,
|
|
142
|
+
) -> None:
|
|
143
|
+
"""Add a coplanar waveguide (CPW) port.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
upper: Name of the upper gap port on the component
|
|
147
|
+
lower: Name of the lower gap port on the component
|
|
148
|
+
layer: Target conductor layer
|
|
149
|
+
length: Port extent along direction (um)
|
|
150
|
+
impedance: Port impedance (Ohms)
|
|
151
|
+
excited: Whether this port is excited
|
|
152
|
+
name: Optional name for the CPW port
|
|
153
|
+
|
|
154
|
+
Example:
|
|
155
|
+
>>> sim.add_cpw_port("P2", "P1", layer="topmetal2", length=5.0)
|
|
156
|
+
"""
|
|
157
|
+
self.cpw_ports = [
|
|
158
|
+
p for p in self.cpw_ports if not (p.upper == upper and p.lower == lower)
|
|
159
|
+
]
|
|
160
|
+
self.cpw_ports.append(
|
|
161
|
+
CPWPortConfig(
|
|
162
|
+
upper=upper,
|
|
163
|
+
lower=lower,
|
|
164
|
+
layer=layer,
|
|
165
|
+
length=length,
|
|
166
|
+
impedance=impedance,
|
|
167
|
+
excited=excited,
|
|
168
|
+
name=name,
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
# -------------------------------------------------------------------------
|
|
173
|
+
# Eigenmode configuration
|
|
174
|
+
# -------------------------------------------------------------------------
|
|
175
|
+
|
|
176
|
+
def set_eigenmode(
|
|
177
|
+
self,
|
|
178
|
+
*,
|
|
179
|
+
num_modes: int = 10,
|
|
180
|
+
target: float | None = None,
|
|
181
|
+
tolerance: float = 1e-6,
|
|
182
|
+
) -> None:
|
|
183
|
+
"""Configure eigenmode simulation.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
num_modes: Number of modes to find
|
|
187
|
+
target: Target frequency in Hz for mode search
|
|
188
|
+
tolerance: Eigenvalue solver tolerance
|
|
189
|
+
|
|
190
|
+
Example:
|
|
191
|
+
>>> sim.set_eigenmode(num_modes=10, target=50e9)
|
|
192
|
+
"""
|
|
193
|
+
self.eigenmode = EigenmodeConfig(
|
|
194
|
+
num_modes=num_modes,
|
|
195
|
+
target=target,
|
|
196
|
+
tolerance=tolerance,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# -------------------------------------------------------------------------
|
|
200
|
+
# Validation
|
|
201
|
+
# -------------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
def validate_config(self) -> ValidationResult:
|
|
204
|
+
"""Validate the simulation configuration.
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
ValidationResult with validation status and messages
|
|
208
|
+
"""
|
|
209
|
+
errors = []
|
|
210
|
+
warnings_list = []
|
|
211
|
+
|
|
212
|
+
# Check geometry
|
|
213
|
+
if self.geometry is None:
|
|
214
|
+
errors.append("No component set. Call set_geometry(component) first.")
|
|
215
|
+
|
|
216
|
+
# Check stack
|
|
217
|
+
if self.stack is None and not self._stack_kwargs:
|
|
218
|
+
warnings_list.append(
|
|
219
|
+
"No stack configured. Will use active PDK with defaults."
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
# Eigenmode simulations may not require ports
|
|
223
|
+
if not self.ports and not self.cpw_ports:
|
|
224
|
+
warnings_list.append(
|
|
225
|
+
"No ports configured. Eigenmode finds all modes without port loading."
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
# Validate port configurations
|
|
229
|
+
for port in self.ports:
|
|
230
|
+
if port.geometry == "inplane" and port.layer is None:
|
|
231
|
+
errors.append(f"Port '{port.name}': inplane ports require 'layer'")
|
|
232
|
+
if port.geometry == "via" and (
|
|
233
|
+
port.from_layer is None or port.to_layer is None
|
|
234
|
+
):
|
|
235
|
+
errors.append(
|
|
236
|
+
f"Port '{port.name}': via ports require 'from_layer' and 'to_layer'"
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
valid = len(errors) == 0
|
|
240
|
+
return ValidationResult(valid=valid, errors=errors, warnings=warnings_list)
|
|
241
|
+
|
|
242
|
+
# -------------------------------------------------------------------------
|
|
243
|
+
# Internal helpers
|
|
244
|
+
# -------------------------------------------------------------------------
|
|
245
|
+
|
|
246
|
+
def _configure_ports_on_component(self, stack: LayerStack) -> None: # noqa: ARG002
|
|
247
|
+
"""Configure ports on the component."""
|
|
248
|
+
from gsim.palace.ports import (
|
|
249
|
+
configure_cpw_port,
|
|
250
|
+
configure_inplane_port,
|
|
251
|
+
configure_via_port,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
component = self.geometry.component if self.geometry else None
|
|
255
|
+
if component is None:
|
|
256
|
+
raise ValueError("No component set")
|
|
257
|
+
|
|
258
|
+
for port_config in self.ports:
|
|
259
|
+
if port_config.name is None:
|
|
260
|
+
continue
|
|
261
|
+
|
|
262
|
+
gf_port = None
|
|
263
|
+
for p in component.ports:
|
|
264
|
+
if p.name == port_config.name:
|
|
265
|
+
gf_port = p
|
|
266
|
+
break
|
|
267
|
+
|
|
268
|
+
if gf_port is None:
|
|
269
|
+
raise ValueError(
|
|
270
|
+
f"Port '{port_config.name}' not found on component. "
|
|
271
|
+
f"Available: {[p.name for p in component.ports]}"
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
if port_config.geometry == "inplane" and port_config.layer is not None:
|
|
275
|
+
configure_inplane_port(
|
|
276
|
+
gf_port,
|
|
277
|
+
layer=port_config.layer,
|
|
278
|
+
length=port_config.length or gf_port.width,
|
|
279
|
+
impedance=port_config.impedance,
|
|
280
|
+
excited=port_config.excited,
|
|
281
|
+
)
|
|
282
|
+
elif port_config.geometry == "via" and (
|
|
283
|
+
port_config.from_layer is not None and port_config.to_layer is not None
|
|
284
|
+
):
|
|
285
|
+
configure_via_port(
|
|
286
|
+
gf_port,
|
|
287
|
+
from_layer=port_config.from_layer,
|
|
288
|
+
to_layer=port_config.to_layer,
|
|
289
|
+
impedance=port_config.impedance,
|
|
290
|
+
excited=port_config.excited,
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
for cpw_config in self.cpw_ports:
|
|
294
|
+
port_upper = None
|
|
295
|
+
port_lower = None
|
|
296
|
+
for p in component.ports:
|
|
297
|
+
if p.name == cpw_config.upper:
|
|
298
|
+
port_upper = p
|
|
299
|
+
if p.name == cpw_config.lower:
|
|
300
|
+
port_lower = p
|
|
301
|
+
|
|
302
|
+
if port_upper is None:
|
|
303
|
+
raise ValueError(f"CPW upper port '{cpw_config.upper}' not found.")
|
|
304
|
+
if port_lower is None:
|
|
305
|
+
raise ValueError(f"CPW lower port '{cpw_config.lower}' not found.")
|
|
306
|
+
|
|
307
|
+
configure_cpw_port(
|
|
308
|
+
port_upper=port_upper,
|
|
309
|
+
port_lower=port_lower,
|
|
310
|
+
layer=cpw_config.layer,
|
|
311
|
+
length=cpw_config.length,
|
|
312
|
+
impedance=cpw_config.impedance,
|
|
313
|
+
excited=cpw_config.excited,
|
|
314
|
+
cpw_name=cpw_config.name,
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
self._configured_ports = True
|
|
318
|
+
|
|
319
|
+
def _generate_mesh_internal(
|
|
320
|
+
self,
|
|
321
|
+
output_dir: Path,
|
|
322
|
+
mesh_config: MeshConfig,
|
|
323
|
+
ports: list,
|
|
324
|
+
model_name: str,
|
|
325
|
+
verbose: bool,
|
|
326
|
+
) -> SimulationResult:
|
|
327
|
+
"""Internal mesh generation."""
|
|
328
|
+
from gsim.palace.mesh import MeshConfig as LegacyMeshConfig
|
|
329
|
+
from gsim.palace.mesh import generate_mesh
|
|
330
|
+
|
|
331
|
+
component = self.geometry.component if self.geometry else None
|
|
332
|
+
|
|
333
|
+
legacy_mesh_config = LegacyMeshConfig(
|
|
334
|
+
refined_mesh_size=mesh_config.refined_mesh_size,
|
|
335
|
+
max_mesh_size=mesh_config.max_mesh_size,
|
|
336
|
+
cells_per_wavelength=mesh_config.cells_per_wavelength,
|
|
337
|
+
margin=mesh_config.margin,
|
|
338
|
+
air_above=mesh_config.air_above,
|
|
339
|
+
fmax=mesh_config.fmax,
|
|
340
|
+
show_gui=mesh_config.show_gui,
|
|
341
|
+
preview_only=mesh_config.preview_only,
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
stack = self._resolve_stack()
|
|
345
|
+
|
|
346
|
+
if verbose:
|
|
347
|
+
logger.info("Generating mesh in %s", output_dir)
|
|
348
|
+
|
|
349
|
+
mesh_result = generate_mesh(
|
|
350
|
+
component=component,
|
|
351
|
+
stack=stack,
|
|
352
|
+
ports=ports,
|
|
353
|
+
output_dir=output_dir,
|
|
354
|
+
config=legacy_mesh_config,
|
|
355
|
+
model_name=model_name,
|
|
356
|
+
driven_config=None, # Eigenmode doesn't use driven config
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
return SimulationResult(
|
|
360
|
+
mesh_path=mesh_result.mesh_path,
|
|
361
|
+
output_dir=output_dir,
|
|
362
|
+
config_path=mesh_result.config_path,
|
|
363
|
+
port_info=mesh_result.port_info,
|
|
364
|
+
mesh_stats=mesh_result.mesh_stats,
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
def _get_ports_for_preview(self, stack: LayerStack) -> list:
|
|
368
|
+
"""Get ports for preview."""
|
|
369
|
+
from gsim.palace.ports import extract_ports
|
|
370
|
+
|
|
371
|
+
component = self.geometry.component if self.geometry else None
|
|
372
|
+
if self.ports or self.cpw_ports:
|
|
373
|
+
self._configure_ports_on_component(stack)
|
|
374
|
+
return extract_ports(component, stack)
|
|
375
|
+
return []
|
|
376
|
+
|
|
377
|
+
# -------------------------------------------------------------------------
|
|
378
|
+
# Preview
|
|
379
|
+
# -------------------------------------------------------------------------
|
|
380
|
+
|
|
381
|
+
def preview(
|
|
382
|
+
self,
|
|
383
|
+
*,
|
|
384
|
+
preset: Literal["coarse", "default", "fine"] | None = None,
|
|
385
|
+
refined_mesh_size: float | None = None,
|
|
386
|
+
max_mesh_size: float | None = None,
|
|
387
|
+
margin: float | None = None,
|
|
388
|
+
air_above: float | None = None,
|
|
389
|
+
fmax: float | None = None,
|
|
390
|
+
show_gui: bool = True,
|
|
391
|
+
) -> None:
|
|
392
|
+
"""Preview the mesh without running simulation.
|
|
393
|
+
|
|
394
|
+
Args:
|
|
395
|
+
preset: Mesh quality preset ("coarse", "default", "fine")
|
|
396
|
+
refined_mesh_size: Mesh size near conductors (um)
|
|
397
|
+
max_mesh_size: Max mesh size in air/dielectric (um)
|
|
398
|
+
margin: XY margin around design (um)
|
|
399
|
+
air_above: Air above top metal (um)
|
|
400
|
+
fmax: Max frequency for mesh sizing (Hz)
|
|
401
|
+
show_gui: Show gmsh GUI for interactive preview
|
|
402
|
+
|
|
403
|
+
Example:
|
|
404
|
+
>>> sim.preview(preset="fine", show_gui=True)
|
|
405
|
+
"""
|
|
406
|
+
from gsim.palace.mesh import MeshConfig as LegacyMeshConfig
|
|
407
|
+
from gsim.palace.mesh import generate_mesh
|
|
408
|
+
|
|
409
|
+
component = self.geometry.component if self.geometry else None
|
|
410
|
+
|
|
411
|
+
validation = self.validate_config()
|
|
412
|
+
if not validation.valid:
|
|
413
|
+
raise ValueError("Invalid configuration:\n" + "\n".join(validation.errors))
|
|
414
|
+
|
|
415
|
+
mesh_config = self._build_mesh_config(
|
|
416
|
+
preset=preset,
|
|
417
|
+
refined_mesh_size=refined_mesh_size,
|
|
418
|
+
max_mesh_size=max_mesh_size,
|
|
419
|
+
margin=margin,
|
|
420
|
+
air_above=air_above,
|
|
421
|
+
fmax=fmax,
|
|
422
|
+
show_gui=show_gui,
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
stack = self._resolve_stack()
|
|
426
|
+
ports = self._get_ports_for_preview(stack)
|
|
427
|
+
|
|
428
|
+
legacy_mesh_config = LegacyMeshConfig(
|
|
429
|
+
refined_mesh_size=mesh_config.refined_mesh_size,
|
|
430
|
+
max_mesh_size=mesh_config.max_mesh_size,
|
|
431
|
+
cells_per_wavelength=mesh_config.cells_per_wavelength,
|
|
432
|
+
margin=mesh_config.margin,
|
|
433
|
+
air_above=mesh_config.air_above,
|
|
434
|
+
fmax=mesh_config.fmax,
|
|
435
|
+
show_gui=show_gui,
|
|
436
|
+
preview_only=True,
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
440
|
+
generate_mesh(
|
|
441
|
+
component=component,
|
|
442
|
+
stack=stack,
|
|
443
|
+
ports=ports,
|
|
444
|
+
output_dir=tmpdir,
|
|
445
|
+
config=legacy_mesh_config,
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
# -------------------------------------------------------------------------
|
|
449
|
+
# Mesh generation
|
|
450
|
+
# -------------------------------------------------------------------------
|
|
451
|
+
|
|
452
|
+
def mesh(
|
|
453
|
+
self,
|
|
454
|
+
*,
|
|
455
|
+
preset: Literal["coarse", "default", "fine"] | None = None,
|
|
456
|
+
refined_mesh_size: float | None = None,
|
|
457
|
+
max_mesh_size: float | None = None,
|
|
458
|
+
margin: float | None = None,
|
|
459
|
+
air_above: float | None = None,
|
|
460
|
+
fmax: float | None = None,
|
|
461
|
+
show_gui: bool = False,
|
|
462
|
+
model_name: str = "palace",
|
|
463
|
+
verbose: bool = True,
|
|
464
|
+
) -> SimulationResult:
|
|
465
|
+
"""Generate the mesh for Palace simulation.
|
|
466
|
+
|
|
467
|
+
Requires set_output_dir() to be called first.
|
|
468
|
+
|
|
469
|
+
Args:
|
|
470
|
+
preset: Mesh quality preset ("coarse", "default", "fine")
|
|
471
|
+
refined_mesh_size: Mesh size near conductors (um), overrides preset
|
|
472
|
+
max_mesh_size: Max mesh size in air/dielectric (um), overrides preset
|
|
473
|
+
margin: XY margin around design (um), overrides preset
|
|
474
|
+
air_above: Air above top metal (um), overrides preset
|
|
475
|
+
fmax: Max frequency for mesh sizing (Hz), overrides preset
|
|
476
|
+
show_gui: Show gmsh GUI during meshing
|
|
477
|
+
model_name: Base name for output files
|
|
478
|
+
verbose: Print progress messages
|
|
479
|
+
|
|
480
|
+
Returns:
|
|
481
|
+
SimulationResult with mesh path
|
|
482
|
+
|
|
483
|
+
Raises:
|
|
484
|
+
ValueError: If output_dir not set or configuration is invalid
|
|
485
|
+
|
|
486
|
+
Example:
|
|
487
|
+
>>> sim.set_output_dir("./sim")
|
|
488
|
+
>>> result = sim.mesh(preset="fine")
|
|
489
|
+
>>> print(f"Mesh saved to: {result.mesh_path}")
|
|
490
|
+
"""
|
|
491
|
+
from gsim.palace.ports import extract_ports
|
|
492
|
+
|
|
493
|
+
if self._output_dir is None:
|
|
494
|
+
raise ValueError("Output directory not set. Call set_output_dir() first.")
|
|
495
|
+
|
|
496
|
+
component = self.geometry.component if self.geometry else None
|
|
497
|
+
|
|
498
|
+
mesh_config = self._build_mesh_config(
|
|
499
|
+
preset=preset,
|
|
500
|
+
refined_mesh_size=refined_mesh_size,
|
|
501
|
+
max_mesh_size=max_mesh_size,
|
|
502
|
+
margin=margin,
|
|
503
|
+
air_above=air_above,
|
|
504
|
+
fmax=fmax,
|
|
505
|
+
show_gui=show_gui,
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
validation = self.validate_config()
|
|
509
|
+
if not validation.valid:
|
|
510
|
+
raise ValueError("Invalid configuration:\n" + "\n".join(validation.errors))
|
|
511
|
+
|
|
512
|
+
output_dir = self._output_dir
|
|
513
|
+
|
|
514
|
+
stack = self._resolve_stack()
|
|
515
|
+
|
|
516
|
+
palace_ports = []
|
|
517
|
+
if self.ports or self.cpw_ports:
|
|
518
|
+
self._configure_ports_on_component(stack)
|
|
519
|
+
palace_ports = extract_ports(component, stack)
|
|
520
|
+
|
|
521
|
+
return self._generate_mesh_internal(
|
|
522
|
+
output_dir=output_dir,
|
|
523
|
+
mesh_config=mesh_config,
|
|
524
|
+
ports=palace_ports,
|
|
525
|
+
model_name=model_name,
|
|
526
|
+
verbose=verbose,
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
# -------------------------------------------------------------------------
|
|
530
|
+
# Simulation
|
|
531
|
+
# -------------------------------------------------------------------------
|
|
532
|
+
|
|
533
|
+
def simulate(
|
|
534
|
+
self,
|
|
535
|
+
output_dir: str | Path | None = None,
|
|
536
|
+
*,
|
|
537
|
+
verbose: bool = True,
|
|
538
|
+
) -> dict[str, Path]:
|
|
539
|
+
"""Run eigenmode simulation on GDSFactory+ cloud.
|
|
540
|
+
|
|
541
|
+
Args:
|
|
542
|
+
output_dir: Directory containing mesh files
|
|
543
|
+
verbose: Print progress messages
|
|
544
|
+
|
|
545
|
+
Returns:
|
|
546
|
+
Dict mapping result filenames to local paths
|
|
547
|
+
|
|
548
|
+
Raises:
|
|
549
|
+
NotImplementedError: Eigenmode is not yet fully implemented
|
|
550
|
+
"""
|
|
551
|
+
raise NotImplementedError(
|
|
552
|
+
"Eigenmode simulation is not yet fully implemented on cloud. "
|
|
553
|
+
"Use DrivenSim for S-parameter extraction."
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
__all__ = ["EigenmodeSim"]
|