gsim 0.0.0__py3-none-any.whl → 0.0.2__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 +61 -0
- gsim/common/geometry.py +76 -0
- gsim/{palace → common}/stack/__init__.py +8 -5
- gsim/{palace → common}/stack/extractor.py +29 -103
- gsim/{palace → common}/stack/materials.py +27 -11
- gsim/palace/__init__.py +94 -43
- gsim/palace/base.py +68 -0
- gsim/palace/driven.py +1004 -0
- gsim/palace/eigenmode.py +777 -0
- gsim/palace/electrostatic.py +622 -0
- gsim/palace/mesh/generator.py +201 -20
- gsim/palace/mesh/pipeline.py +22 -1
- gsim/palace/models/__init__.py +60 -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 +138 -0
- gsim/palace/models/problems.py +195 -0
- gsim/palace/models/results.py +159 -0
- gsim/palace/models/stack.py +59 -0
- gsim/palace/ports/config.py +1 -1
- {gsim-0.0.0.dist-info → gsim-0.0.2.dist-info}/METADATA +5 -3
- gsim-0.0.2.dist-info/RECORD +32 -0
- gsim-0.0.0.dist-info/RECORD +0 -18
- /gsim/{palace → common}/stack/visualization.py +0 -0
- {gsim-0.0.0.dist-info → gsim-0.0.2.dist-info}/WHEEL +0 -0
- {gsim-0.0.0.dist-info → gsim-0.0.2.dist-info}/top_level.txt +0 -0
gsim/palace/driven.py
ADDED
|
@@ -0,0 +1,1004 @@
|
|
|
1
|
+
"""Driven simulation class for frequency-domain S-parameter extraction.
|
|
2
|
+
|
|
3
|
+
This module provides the DrivenSim class for running frequency-sweep
|
|
4
|
+
simulations to extract S-parameters.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import tempfile
|
|
11
|
+
import warnings
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
16
|
+
|
|
17
|
+
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr
|
|
18
|
+
|
|
19
|
+
from gsim.common import Geometry, LayerStack
|
|
20
|
+
from gsim.palace.base import PalaceSimMixin
|
|
21
|
+
from gsim.palace.models import (
|
|
22
|
+
CPWPortConfig,
|
|
23
|
+
DrivenConfig,
|
|
24
|
+
MaterialConfig,
|
|
25
|
+
MeshConfig,
|
|
26
|
+
NumericalConfig,
|
|
27
|
+
PortConfig,
|
|
28
|
+
SimulationResult,
|
|
29
|
+
ValidationResult,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from gdsfactory.component import Component
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class DrivenSim(PalaceSimMixin, BaseModel):
|
|
37
|
+
"""Frequency-domain driven simulation for S-parameter extraction.
|
|
38
|
+
|
|
39
|
+
This class configures and runs driven simulations that sweep through
|
|
40
|
+
frequencies to compute S-parameters. Uses composition (no inheritance)
|
|
41
|
+
with shared Geometry and Stack components from gsim.common.
|
|
42
|
+
|
|
43
|
+
Example:
|
|
44
|
+
>>> from gsim.palace import DrivenSim
|
|
45
|
+
>>>
|
|
46
|
+
>>> sim = DrivenSim()
|
|
47
|
+
>>> sim.set_geometry(component)
|
|
48
|
+
>>> sim.set_stack(air_above=300.0)
|
|
49
|
+
>>> sim.add_cpw_port("P2", "P1", layer="topmetal2", length=5.0)
|
|
50
|
+
>>> sim.add_cpw_port("P3", "P4", layer="topmetal2", length=5.0)
|
|
51
|
+
>>> sim.set_driven(fmin=1e9, fmax=100e9, num_points=40)
|
|
52
|
+
>>> sim.mesh("./sim", preset="default")
|
|
53
|
+
>>> results = sim.simulate()
|
|
54
|
+
|
|
55
|
+
Attributes:
|
|
56
|
+
geometry: Wrapped gdsfactory Component (from common)
|
|
57
|
+
stack: Layer stack configuration (from common)
|
|
58
|
+
ports: List of single-element port configurations
|
|
59
|
+
cpw_ports: List of CPW (two-element) port configurations
|
|
60
|
+
driven: Driven simulation configuration (frequencies, etc.)
|
|
61
|
+
mesh: Mesh configuration
|
|
62
|
+
materials: Material property overrides
|
|
63
|
+
numerical: Numerical solver configuration
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
model_config = ConfigDict(
|
|
67
|
+
validate_assignment=True,
|
|
68
|
+
arbitrary_types_allowed=True,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Composed objects (from common)
|
|
72
|
+
geometry: Geometry | None = None
|
|
73
|
+
stack: LayerStack | None = None
|
|
74
|
+
|
|
75
|
+
# Port configurations
|
|
76
|
+
ports: list[PortConfig] = Field(default_factory=list)
|
|
77
|
+
cpw_ports: list[CPWPortConfig] = Field(default_factory=list)
|
|
78
|
+
|
|
79
|
+
# Driven simulation config
|
|
80
|
+
driven: DrivenConfig = Field(default_factory=DrivenConfig)
|
|
81
|
+
|
|
82
|
+
# Mesh config
|
|
83
|
+
mesh_config: MeshConfig = Field(default_factory=MeshConfig.default)
|
|
84
|
+
|
|
85
|
+
# Material overrides and numerical config
|
|
86
|
+
materials: dict[str, MaterialConfig] = Field(default_factory=dict)
|
|
87
|
+
numerical: NumericalConfig = Field(default_factory=NumericalConfig)
|
|
88
|
+
|
|
89
|
+
# Stack configuration (stored as kwargs until resolved)
|
|
90
|
+
_stack_kwargs: dict[str, Any] = PrivateAttr(default_factory=dict)
|
|
91
|
+
|
|
92
|
+
# Internal state
|
|
93
|
+
_output_dir: Path | None = PrivateAttr(default=None)
|
|
94
|
+
_configured_ports: bool = PrivateAttr(default=False)
|
|
95
|
+
_last_mesh_result: Any = PrivateAttr(default=None)
|
|
96
|
+
_last_ports: list = PrivateAttr(default_factory=list)
|
|
97
|
+
|
|
98
|
+
# -------------------------------------------------------------------------
|
|
99
|
+
# Output directory
|
|
100
|
+
# -------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
def set_output_dir(self, path: str | Path) -> None:
|
|
103
|
+
"""Set the output directory for mesh and config files.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
path: Directory path for output files
|
|
107
|
+
|
|
108
|
+
Example:
|
|
109
|
+
>>> sim.set_output_dir("./palace-sim")
|
|
110
|
+
"""
|
|
111
|
+
self._output_dir = Path(path)
|
|
112
|
+
self._output_dir.mkdir(parents=True, exist_ok=True)
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def output_dir(self) -> Path | None:
|
|
116
|
+
"""Get the current output directory."""
|
|
117
|
+
return self._output_dir
|
|
118
|
+
|
|
119
|
+
# -------------------------------------------------------------------------
|
|
120
|
+
# Geometry methods
|
|
121
|
+
# -------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
def set_geometry(self, component: Component) -> None:
|
|
124
|
+
"""Set the gdsfactory component for simulation.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
component: gdsfactory Component to simulate
|
|
128
|
+
|
|
129
|
+
Example:
|
|
130
|
+
>>> sim.set_geometry(my_component)
|
|
131
|
+
"""
|
|
132
|
+
self.geometry = Geometry(component=component)
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def component(self) -> Component | None:
|
|
136
|
+
"""Get the current component (for backward compatibility)."""
|
|
137
|
+
return self.geometry.component if self.geometry else None
|
|
138
|
+
|
|
139
|
+
# Backward compatibility alias
|
|
140
|
+
@property
|
|
141
|
+
def _component(self) -> Component | None:
|
|
142
|
+
"""Internal component access (backward compatibility)."""
|
|
143
|
+
return self.component
|
|
144
|
+
|
|
145
|
+
# -------------------------------------------------------------------------
|
|
146
|
+
# Stack methods
|
|
147
|
+
# -------------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
def set_stack(
|
|
150
|
+
self,
|
|
151
|
+
*,
|
|
152
|
+
yaml_path: str | Path | None = None,
|
|
153
|
+
air_above: float = 200.0,
|
|
154
|
+
substrate_thickness: float = 2.0,
|
|
155
|
+
include_substrate: bool = False,
|
|
156
|
+
**kwargs,
|
|
157
|
+
) -> None:
|
|
158
|
+
"""Configure the layer stack.
|
|
159
|
+
|
|
160
|
+
If yaml_path is provided, loads stack from YAML file.
|
|
161
|
+
Otherwise, extracts from active PDK with given parameters.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
yaml_path: Path to custom YAML stack file
|
|
165
|
+
air_above: Air box height above top metal in um
|
|
166
|
+
substrate_thickness: Thickness below z=0 in um
|
|
167
|
+
include_substrate: Include lossy silicon substrate
|
|
168
|
+
**kwargs: Additional args passed to extract_layer_stack
|
|
169
|
+
|
|
170
|
+
Example:
|
|
171
|
+
>>> sim.set_stack(air_above=300.0, substrate_thickness=2.0)
|
|
172
|
+
"""
|
|
173
|
+
self._stack_kwargs = {
|
|
174
|
+
"yaml_path": yaml_path,
|
|
175
|
+
"air_above": air_above,
|
|
176
|
+
"substrate_thickness": substrate_thickness,
|
|
177
|
+
"include_substrate": include_substrate,
|
|
178
|
+
**kwargs,
|
|
179
|
+
}
|
|
180
|
+
# Stack will be resolved lazily during mesh() or simulate()
|
|
181
|
+
self.stack = None
|
|
182
|
+
|
|
183
|
+
# -------------------------------------------------------------------------
|
|
184
|
+
# Port methods
|
|
185
|
+
# -------------------------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
def add_port(
|
|
188
|
+
self,
|
|
189
|
+
name: str,
|
|
190
|
+
*,
|
|
191
|
+
layer: str | None = None,
|
|
192
|
+
from_layer: str | None = None,
|
|
193
|
+
to_layer: str | None = None,
|
|
194
|
+
length: float | None = None,
|
|
195
|
+
impedance: float = 50.0,
|
|
196
|
+
excited: bool = True,
|
|
197
|
+
geometry: Literal["inplane", "via"] = "inplane",
|
|
198
|
+
) -> None:
|
|
199
|
+
"""Add a single-element lumped port.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
name: Port name (must match component port name)
|
|
203
|
+
layer: Target layer for inplane ports
|
|
204
|
+
from_layer: Bottom layer for via ports
|
|
205
|
+
to_layer: Top layer for via ports
|
|
206
|
+
length: Port extent along direction (um)
|
|
207
|
+
impedance: Port impedance (Ohms)
|
|
208
|
+
excited: Whether this port is excited
|
|
209
|
+
geometry: Port geometry type ("inplane" or "via")
|
|
210
|
+
|
|
211
|
+
Example:
|
|
212
|
+
>>> sim.add_port("o1", layer="topmetal2", length=5.0)
|
|
213
|
+
>>> sim.add_port("feed", from_layer="metal1", to_layer="topmetal2", geometry="via")
|
|
214
|
+
"""
|
|
215
|
+
# Remove existing config for this port if any
|
|
216
|
+
self.ports = [p for p in self.ports if p.name != name]
|
|
217
|
+
|
|
218
|
+
self.ports.append(
|
|
219
|
+
PortConfig(
|
|
220
|
+
name=name,
|
|
221
|
+
layer=layer,
|
|
222
|
+
from_layer=from_layer,
|
|
223
|
+
to_layer=to_layer,
|
|
224
|
+
length=length,
|
|
225
|
+
impedance=impedance,
|
|
226
|
+
excited=excited,
|
|
227
|
+
geometry=geometry,
|
|
228
|
+
)
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
def add_cpw_port(
|
|
232
|
+
self,
|
|
233
|
+
upper: str,
|
|
234
|
+
lower: str,
|
|
235
|
+
*,
|
|
236
|
+
layer: str,
|
|
237
|
+
length: float,
|
|
238
|
+
impedance: float = 50.0,
|
|
239
|
+
excited: bool = True,
|
|
240
|
+
name: str | None = None,
|
|
241
|
+
) -> None:
|
|
242
|
+
"""Add a coplanar waveguide (CPW) port.
|
|
243
|
+
|
|
244
|
+
CPW ports consist of two elements (upper and lower gaps) that are
|
|
245
|
+
excited with opposite E-field directions to create the CPW mode.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
upper: Name of the upper gap port on the component
|
|
249
|
+
lower: Name of the lower gap port on the component
|
|
250
|
+
layer: Target conductor layer (e.g., "topmetal2")
|
|
251
|
+
length: Port extent along direction (um)
|
|
252
|
+
impedance: Port impedance (Ohms)
|
|
253
|
+
excited: Whether this port is excited
|
|
254
|
+
name: Optional name for the CPW port (default: "cpw_{lower}")
|
|
255
|
+
|
|
256
|
+
Example:
|
|
257
|
+
>>> sim.add_cpw_port("P2", "P1", layer="topmetal2", length=5.0)
|
|
258
|
+
"""
|
|
259
|
+
# Remove existing CPW port with same elements if any
|
|
260
|
+
self.cpw_ports = [
|
|
261
|
+
p for p in self.cpw_ports if not (p.upper == upper and p.lower == lower)
|
|
262
|
+
]
|
|
263
|
+
|
|
264
|
+
self.cpw_ports.append(
|
|
265
|
+
CPWPortConfig(
|
|
266
|
+
upper=upper,
|
|
267
|
+
lower=lower,
|
|
268
|
+
layer=layer,
|
|
269
|
+
length=length,
|
|
270
|
+
impedance=impedance,
|
|
271
|
+
excited=excited,
|
|
272
|
+
name=name,
|
|
273
|
+
)
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
# -------------------------------------------------------------------------
|
|
277
|
+
# Driven configuration
|
|
278
|
+
# -------------------------------------------------------------------------
|
|
279
|
+
|
|
280
|
+
def set_driven(
|
|
281
|
+
self,
|
|
282
|
+
*,
|
|
283
|
+
fmin: float = 1e9,
|
|
284
|
+
fmax: float = 100e9,
|
|
285
|
+
num_points: int = 40,
|
|
286
|
+
scale: Literal["linear", "log"] = "linear",
|
|
287
|
+
adaptive_tol: float = 0.02,
|
|
288
|
+
adaptive_max_samples: int = 20,
|
|
289
|
+
compute_s_params: bool = True,
|
|
290
|
+
reference_impedance: float = 50.0,
|
|
291
|
+
excitation_port: str | None = None,
|
|
292
|
+
) -> None:
|
|
293
|
+
"""Configure driven (frequency sweep) simulation.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
fmin: Minimum frequency in Hz
|
|
297
|
+
fmax: Maximum frequency in Hz
|
|
298
|
+
num_points: Number of frequency points
|
|
299
|
+
scale: "linear" or "log" frequency spacing
|
|
300
|
+
adaptive_tol: Adaptive frequency tolerance (0 disables adaptive)
|
|
301
|
+
adaptive_max_samples: Max samples for adaptive refinement
|
|
302
|
+
compute_s_params: Compute S-parameters
|
|
303
|
+
reference_impedance: Reference impedance for S-params (Ohms)
|
|
304
|
+
excitation_port: Port to excite (None = first port)
|
|
305
|
+
|
|
306
|
+
Example:
|
|
307
|
+
>>> sim.set_driven(fmin=1e9, fmax=100e9, num_points=40)
|
|
308
|
+
"""
|
|
309
|
+
self.driven = DrivenConfig(
|
|
310
|
+
fmin=fmin,
|
|
311
|
+
fmax=fmax,
|
|
312
|
+
num_points=num_points,
|
|
313
|
+
scale=scale,
|
|
314
|
+
adaptive_tol=adaptive_tol,
|
|
315
|
+
adaptive_max_samples=adaptive_max_samples,
|
|
316
|
+
compute_s_params=compute_s_params,
|
|
317
|
+
reference_impedance=reference_impedance,
|
|
318
|
+
excitation_port=excitation_port,
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
# -------------------------------------------------------------------------
|
|
322
|
+
# Material methods
|
|
323
|
+
# -------------------------------------------------------------------------
|
|
324
|
+
|
|
325
|
+
def set_material(
|
|
326
|
+
self,
|
|
327
|
+
name: str,
|
|
328
|
+
*,
|
|
329
|
+
type: Literal["conductor", "dielectric", "semiconductor"] | None = None,
|
|
330
|
+
conductivity: float | None = None,
|
|
331
|
+
permittivity: float | None = None,
|
|
332
|
+
loss_tangent: float | None = None,
|
|
333
|
+
) -> None:
|
|
334
|
+
"""Override or add material properties.
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
name: Material name
|
|
338
|
+
type: Material type (conductor, dielectric, semiconductor)
|
|
339
|
+
conductivity: Conductivity in S/m (for conductors)
|
|
340
|
+
permittivity: Relative permittivity (for dielectrics)
|
|
341
|
+
loss_tangent: Dielectric loss tangent
|
|
342
|
+
|
|
343
|
+
Example:
|
|
344
|
+
>>> sim.set_material("aluminum", type="conductor", conductivity=3.8e7)
|
|
345
|
+
>>> sim.set_material("sio2", type="dielectric", permittivity=3.9)
|
|
346
|
+
"""
|
|
347
|
+
# Determine type if not provided
|
|
348
|
+
if type is None:
|
|
349
|
+
if conductivity is not None and conductivity > 1e4:
|
|
350
|
+
type = "conductor"
|
|
351
|
+
elif permittivity is not None:
|
|
352
|
+
type = "dielectric"
|
|
353
|
+
else:
|
|
354
|
+
type = "dielectric"
|
|
355
|
+
|
|
356
|
+
self.materials[name] = MaterialConfig(
|
|
357
|
+
type=type,
|
|
358
|
+
conductivity=conductivity,
|
|
359
|
+
permittivity=permittivity,
|
|
360
|
+
loss_tangent=loss_tangent,
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
def set_numerical(
|
|
364
|
+
self,
|
|
365
|
+
*,
|
|
366
|
+
order: int = 2,
|
|
367
|
+
tolerance: float = 1e-6,
|
|
368
|
+
max_iterations: int = 400,
|
|
369
|
+
solver_type: Literal["Default", "SuperLU", "STRUMPACK", "MUMPS"] = "Default",
|
|
370
|
+
preconditioner: Literal["Default", "AMS", "BoomerAMG"] = "Default",
|
|
371
|
+
device: Literal["CPU", "GPU"] = "CPU",
|
|
372
|
+
num_processors: int | None = None,
|
|
373
|
+
) -> None:
|
|
374
|
+
"""Configure numerical solver parameters.
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
order: Finite element order (1-4)
|
|
378
|
+
tolerance: Linear solver tolerance
|
|
379
|
+
max_iterations: Maximum solver iterations
|
|
380
|
+
solver_type: Linear solver type
|
|
381
|
+
preconditioner: Preconditioner type
|
|
382
|
+
device: Compute device (CPU or GPU)
|
|
383
|
+
num_processors: Number of processors (None = auto)
|
|
384
|
+
|
|
385
|
+
Example:
|
|
386
|
+
>>> sim.set_numerical(order=3, tolerance=1e-8)
|
|
387
|
+
"""
|
|
388
|
+
self.numerical = NumericalConfig(
|
|
389
|
+
order=order,
|
|
390
|
+
tolerance=tolerance,
|
|
391
|
+
max_iterations=max_iterations,
|
|
392
|
+
solver_type=solver_type,
|
|
393
|
+
preconditioner=preconditioner,
|
|
394
|
+
device=device,
|
|
395
|
+
num_processors=num_processors,
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
# -------------------------------------------------------------------------
|
|
399
|
+
# Validation
|
|
400
|
+
# -------------------------------------------------------------------------
|
|
401
|
+
|
|
402
|
+
def validate(self) -> ValidationResult:
|
|
403
|
+
"""Validate the simulation configuration.
|
|
404
|
+
|
|
405
|
+
Returns:
|
|
406
|
+
ValidationResult with validation status and messages
|
|
407
|
+
"""
|
|
408
|
+
errors = []
|
|
409
|
+
warnings_list = []
|
|
410
|
+
|
|
411
|
+
# Check geometry
|
|
412
|
+
if self.geometry is None:
|
|
413
|
+
errors.append("No component set. Call set_geometry(component) first.")
|
|
414
|
+
|
|
415
|
+
# Check stack
|
|
416
|
+
if self.stack is None and not self._stack_kwargs:
|
|
417
|
+
warnings_list.append(
|
|
418
|
+
"No stack configured. Will use active PDK with defaults."
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
# Check ports
|
|
422
|
+
has_ports = bool(self.ports) or bool(self.cpw_ports)
|
|
423
|
+
if not has_ports:
|
|
424
|
+
warnings_list.append(
|
|
425
|
+
"No ports configured. Call add_port() or add_cpw_port()."
|
|
426
|
+
)
|
|
427
|
+
else:
|
|
428
|
+
# Validate port configurations
|
|
429
|
+
for port in self.ports:
|
|
430
|
+
if port.geometry == "inplane" and port.layer is None:
|
|
431
|
+
errors.append(
|
|
432
|
+
f"Port '{port.name}': inplane ports require 'layer'"
|
|
433
|
+
)
|
|
434
|
+
if port.geometry == "via":
|
|
435
|
+
if port.from_layer is None or port.to_layer is None:
|
|
436
|
+
errors.append(
|
|
437
|
+
f"Port '{port.name}': via ports require "
|
|
438
|
+
"'from_layer' and 'to_layer'"
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
# Validate CPW ports
|
|
442
|
+
for cpw in self.cpw_ports:
|
|
443
|
+
if not cpw.layer:
|
|
444
|
+
errors.append(
|
|
445
|
+
f"CPW port ({cpw.upper}, {cpw.lower}): 'layer' is required"
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
# Validate excitation port if specified
|
|
449
|
+
if self.driven.excitation_port is not None:
|
|
450
|
+
port_names = [p.name for p in self.ports]
|
|
451
|
+
cpw_names = [cpw.effective_name for cpw in self.cpw_ports]
|
|
452
|
+
all_port_names = port_names + cpw_names
|
|
453
|
+
if self.driven.excitation_port not in all_port_names:
|
|
454
|
+
errors.append(
|
|
455
|
+
f"Excitation port '{self.driven.excitation_port}' not found. "
|
|
456
|
+
f"Available: {all_port_names}"
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
valid = len(errors) == 0
|
|
460
|
+
return ValidationResult(valid=valid, errors=errors, warnings=warnings_list)
|
|
461
|
+
|
|
462
|
+
# -------------------------------------------------------------------------
|
|
463
|
+
# Internal helpers
|
|
464
|
+
# -------------------------------------------------------------------------
|
|
465
|
+
|
|
466
|
+
def _resolve_stack(self) -> LayerStack:
|
|
467
|
+
"""Resolve the layer stack from PDK or YAML.
|
|
468
|
+
|
|
469
|
+
Returns:
|
|
470
|
+
Legacy LayerStack object for mesh generation
|
|
471
|
+
"""
|
|
472
|
+
from gsim.common.stack import get_stack
|
|
473
|
+
|
|
474
|
+
yaml_path = self._stack_kwargs.pop("yaml_path", None)
|
|
475
|
+
legacy_stack = get_stack(yaml_path=yaml_path, **self._stack_kwargs)
|
|
476
|
+
|
|
477
|
+
# Restore yaml_path for potential re-resolution
|
|
478
|
+
self._stack_kwargs["yaml_path"] = yaml_path
|
|
479
|
+
|
|
480
|
+
# Apply material overrides
|
|
481
|
+
for name, props in self.materials.items():
|
|
482
|
+
legacy_stack.materials[name] = props.to_dict()
|
|
483
|
+
|
|
484
|
+
# Store the LayerStack
|
|
485
|
+
self.stack = legacy_stack
|
|
486
|
+
|
|
487
|
+
return legacy_stack
|
|
488
|
+
|
|
489
|
+
def _configure_ports_on_component(self, stack: LayerStack) -> None:
|
|
490
|
+
"""Configure ports on the component using legacy functions."""
|
|
491
|
+
from gsim.palace.ports import (
|
|
492
|
+
configure_cpw_port,
|
|
493
|
+
configure_inplane_port,
|
|
494
|
+
configure_via_port,
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
component = self.geometry.component if self.geometry else None
|
|
498
|
+
if component is None:
|
|
499
|
+
raise ValueError("No component set")
|
|
500
|
+
|
|
501
|
+
# Configure regular ports
|
|
502
|
+
for port_config in self.ports:
|
|
503
|
+
if port_config.name is None:
|
|
504
|
+
continue
|
|
505
|
+
|
|
506
|
+
# Find matching gdsfactory port
|
|
507
|
+
gf_port = None
|
|
508
|
+
for p in component.ports:
|
|
509
|
+
if p.name == port_config.name:
|
|
510
|
+
gf_port = p
|
|
511
|
+
break
|
|
512
|
+
|
|
513
|
+
if gf_port is None:
|
|
514
|
+
raise ValueError(
|
|
515
|
+
f"Port '{port_config.name}' not found on component. "
|
|
516
|
+
f"Available ports: {[p.name for p in component.ports]}"
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
if port_config.geometry == "inplane":
|
|
520
|
+
configure_inplane_port(
|
|
521
|
+
gf_port,
|
|
522
|
+
layer=port_config.layer,
|
|
523
|
+
length=port_config.length or gf_port.width,
|
|
524
|
+
impedance=port_config.impedance,
|
|
525
|
+
excited=port_config.excited,
|
|
526
|
+
)
|
|
527
|
+
elif port_config.geometry == "via":
|
|
528
|
+
configure_via_port(
|
|
529
|
+
gf_port,
|
|
530
|
+
from_layer=port_config.from_layer,
|
|
531
|
+
to_layer=port_config.to_layer,
|
|
532
|
+
impedance=port_config.impedance,
|
|
533
|
+
excited=port_config.excited,
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
# Configure CPW ports
|
|
537
|
+
for cpw_config in self.cpw_ports:
|
|
538
|
+
# Find upper port
|
|
539
|
+
port_upper = None
|
|
540
|
+
for p in component.ports:
|
|
541
|
+
if p.name == cpw_config.upper:
|
|
542
|
+
port_upper = p
|
|
543
|
+
break
|
|
544
|
+
if port_upper is None:
|
|
545
|
+
raise ValueError(
|
|
546
|
+
f"CPW upper port '{cpw_config.upper}' not found. "
|
|
547
|
+
f"Available: {[p.name for p in component.ports]}"
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
# Find lower port
|
|
551
|
+
port_lower = None
|
|
552
|
+
for p in component.ports:
|
|
553
|
+
if p.name == cpw_config.lower:
|
|
554
|
+
port_lower = p
|
|
555
|
+
break
|
|
556
|
+
if port_lower is None:
|
|
557
|
+
raise ValueError(
|
|
558
|
+
f"CPW lower port '{cpw_config.lower}' not found. "
|
|
559
|
+
f"Available: {[p.name for p in component.ports]}"
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
configure_cpw_port(
|
|
563
|
+
port_upper=port_upper,
|
|
564
|
+
port_lower=port_lower,
|
|
565
|
+
layer=cpw_config.layer,
|
|
566
|
+
length=cpw_config.length,
|
|
567
|
+
impedance=cpw_config.impedance,
|
|
568
|
+
excited=cpw_config.excited,
|
|
569
|
+
cpw_name=cpw_config.name,
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
self._configured_ports = True
|
|
573
|
+
|
|
574
|
+
def _build_mesh_config(
|
|
575
|
+
self,
|
|
576
|
+
preset: Literal["coarse", "default", "fine"] | None,
|
|
577
|
+
refined_mesh_size: float | None,
|
|
578
|
+
max_mesh_size: float | None,
|
|
579
|
+
margin: float | None,
|
|
580
|
+
air_above: float | None,
|
|
581
|
+
fmax: float | None,
|
|
582
|
+
show_gui: bool,
|
|
583
|
+
) -> MeshConfig:
|
|
584
|
+
"""Build mesh config from preset with optional overrides."""
|
|
585
|
+
# Build mesh config from preset
|
|
586
|
+
if preset == "coarse":
|
|
587
|
+
mesh_config = MeshConfig.coarse()
|
|
588
|
+
elif preset == "fine":
|
|
589
|
+
mesh_config = MeshConfig.fine()
|
|
590
|
+
else:
|
|
591
|
+
mesh_config = MeshConfig.default()
|
|
592
|
+
|
|
593
|
+
# Track overrides for warning
|
|
594
|
+
overrides = []
|
|
595
|
+
if preset is not None:
|
|
596
|
+
if refined_mesh_size is not None:
|
|
597
|
+
overrides.append(f"refined_mesh_size={refined_mesh_size}")
|
|
598
|
+
if max_mesh_size is not None:
|
|
599
|
+
overrides.append(f"max_mesh_size={max_mesh_size}")
|
|
600
|
+
if margin is not None:
|
|
601
|
+
overrides.append(f"margin={margin}")
|
|
602
|
+
if air_above is not None:
|
|
603
|
+
overrides.append(f"air_above={air_above}")
|
|
604
|
+
if fmax is not None:
|
|
605
|
+
overrides.append(f"fmax={fmax}")
|
|
606
|
+
|
|
607
|
+
if overrides:
|
|
608
|
+
warnings.warn(
|
|
609
|
+
f"Preset '{preset}' values overridden by: {', '.join(overrides)}",
|
|
610
|
+
stacklevel=4,
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
# Apply overrides
|
|
614
|
+
if refined_mesh_size is not None:
|
|
615
|
+
mesh_config.refined_mesh_size = refined_mesh_size
|
|
616
|
+
if max_mesh_size is not None:
|
|
617
|
+
mesh_config.max_mesh_size = max_mesh_size
|
|
618
|
+
if margin is not None:
|
|
619
|
+
mesh_config.margin = margin
|
|
620
|
+
if air_above is not None:
|
|
621
|
+
mesh_config.air_above = air_above
|
|
622
|
+
if fmax is not None:
|
|
623
|
+
mesh_config.fmax = fmax
|
|
624
|
+
mesh_config.show_gui = show_gui
|
|
625
|
+
|
|
626
|
+
return mesh_config
|
|
627
|
+
|
|
628
|
+
def _generate_mesh_internal(
|
|
629
|
+
self,
|
|
630
|
+
output_dir: Path,
|
|
631
|
+
mesh_config: MeshConfig,
|
|
632
|
+
ports: list,
|
|
633
|
+
driven_config: Any,
|
|
634
|
+
model_name: str,
|
|
635
|
+
verbose: bool,
|
|
636
|
+
write_config: bool = True,
|
|
637
|
+
) -> SimulationResult:
|
|
638
|
+
"""Internal mesh generation."""
|
|
639
|
+
from gsim.palace.mesh import MeshConfig as LegacyMeshConfig
|
|
640
|
+
from gsim.palace.mesh import generate_mesh
|
|
641
|
+
|
|
642
|
+
component = self.geometry.component if self.geometry else None
|
|
643
|
+
|
|
644
|
+
# Get effective fmax from driven config if mesh doesn't specify
|
|
645
|
+
effective_fmax = mesh_config.fmax
|
|
646
|
+
if driven_config is not None and mesh_config.fmax == 100e9:
|
|
647
|
+
effective_fmax = driven_config.fmax
|
|
648
|
+
|
|
649
|
+
legacy_mesh_config = LegacyMeshConfig(
|
|
650
|
+
refined_mesh_size=mesh_config.refined_mesh_size,
|
|
651
|
+
max_mesh_size=mesh_config.max_mesh_size,
|
|
652
|
+
cells_per_wavelength=mesh_config.cells_per_wavelength,
|
|
653
|
+
margin=mesh_config.margin,
|
|
654
|
+
air_above=mesh_config.air_above,
|
|
655
|
+
fmax=effective_fmax,
|
|
656
|
+
show_gui=mesh_config.show_gui,
|
|
657
|
+
preview_only=mesh_config.preview_only,
|
|
658
|
+
)
|
|
659
|
+
|
|
660
|
+
# Resolve stack
|
|
661
|
+
stack = self._resolve_stack()
|
|
662
|
+
|
|
663
|
+
if verbose:
|
|
664
|
+
logger.info("Generating mesh in %s", output_dir)
|
|
665
|
+
|
|
666
|
+
mesh_result = generate_mesh(
|
|
667
|
+
component=component,
|
|
668
|
+
stack=stack,
|
|
669
|
+
ports=ports,
|
|
670
|
+
output_dir=output_dir,
|
|
671
|
+
config=legacy_mesh_config,
|
|
672
|
+
model_name=model_name,
|
|
673
|
+
driven_config=driven_config,
|
|
674
|
+
write_config=write_config,
|
|
675
|
+
)
|
|
676
|
+
|
|
677
|
+
# Store mesh_result for deferred config generation
|
|
678
|
+
self._last_mesh_result = mesh_result
|
|
679
|
+
self._last_ports = ports
|
|
680
|
+
|
|
681
|
+
return SimulationResult(
|
|
682
|
+
mesh_path=mesh_result.mesh_path,
|
|
683
|
+
output_dir=output_dir,
|
|
684
|
+
config_path=mesh_result.config_path,
|
|
685
|
+
port_info=mesh_result.port_info,
|
|
686
|
+
mesh_stats=mesh_result.mesh_stats,
|
|
687
|
+
)
|
|
688
|
+
|
|
689
|
+
def _get_ports_for_preview(self, stack: LayerStack) -> list:
|
|
690
|
+
"""Get ports for preview."""
|
|
691
|
+
from gsim.palace.ports import extract_ports
|
|
692
|
+
|
|
693
|
+
component = self.geometry.component if self.geometry else None
|
|
694
|
+
self._configure_ports_on_component(stack)
|
|
695
|
+
return extract_ports(component, stack)
|
|
696
|
+
|
|
697
|
+
# -------------------------------------------------------------------------
|
|
698
|
+
# Preview
|
|
699
|
+
# -------------------------------------------------------------------------
|
|
700
|
+
|
|
701
|
+
def preview(
|
|
702
|
+
self,
|
|
703
|
+
*,
|
|
704
|
+
preset: Literal["coarse", "default", "fine"] | None = None,
|
|
705
|
+
refined_mesh_size: float | None = None,
|
|
706
|
+
max_mesh_size: float | None = None,
|
|
707
|
+
margin: float | None = None,
|
|
708
|
+
air_above: float | None = None,
|
|
709
|
+
fmax: float | None = None,
|
|
710
|
+
show_gui: bool = True,
|
|
711
|
+
) -> None:
|
|
712
|
+
"""Preview the mesh without running simulation.
|
|
713
|
+
|
|
714
|
+
Opens the gmsh GUI to visualize the mesh interactively.
|
|
715
|
+
|
|
716
|
+
Args:
|
|
717
|
+
preset: Mesh quality preset ("coarse", "default", "fine")
|
|
718
|
+
refined_mesh_size: Mesh size near conductors (um)
|
|
719
|
+
max_mesh_size: Max mesh size in air/dielectric (um)
|
|
720
|
+
margin: XY margin around design (um)
|
|
721
|
+
air_above: Air above top metal (um)
|
|
722
|
+
fmax: Max frequency for mesh sizing (Hz)
|
|
723
|
+
show_gui: Show gmsh GUI for interactive preview
|
|
724
|
+
|
|
725
|
+
Example:
|
|
726
|
+
>>> sim.preview(preset="fine", show_gui=True)
|
|
727
|
+
"""
|
|
728
|
+
from gsim.palace.mesh import MeshConfig as LegacyMeshConfig
|
|
729
|
+
from gsim.palace.mesh import generate_mesh
|
|
730
|
+
|
|
731
|
+
component = self.geometry.component if self.geometry else None
|
|
732
|
+
|
|
733
|
+
# Validate configuration
|
|
734
|
+
validation = self.validate()
|
|
735
|
+
if not validation.valid:
|
|
736
|
+
raise ValueError(
|
|
737
|
+
f"Invalid configuration:\n" + "\n".join(validation.errors)
|
|
738
|
+
)
|
|
739
|
+
|
|
740
|
+
# Build mesh config
|
|
741
|
+
mesh_config = self._build_mesh_config(
|
|
742
|
+
preset=preset,
|
|
743
|
+
refined_mesh_size=refined_mesh_size,
|
|
744
|
+
max_mesh_size=max_mesh_size,
|
|
745
|
+
margin=margin,
|
|
746
|
+
air_above=air_above,
|
|
747
|
+
fmax=fmax,
|
|
748
|
+
show_gui=show_gui,
|
|
749
|
+
)
|
|
750
|
+
|
|
751
|
+
# Resolve stack
|
|
752
|
+
stack = self._resolve_stack()
|
|
753
|
+
|
|
754
|
+
# Get ports
|
|
755
|
+
ports = self._get_ports_for_preview(stack)
|
|
756
|
+
|
|
757
|
+
# Build legacy mesh config with preview mode
|
|
758
|
+
legacy_mesh_config = LegacyMeshConfig(
|
|
759
|
+
refined_mesh_size=mesh_config.refined_mesh_size,
|
|
760
|
+
max_mesh_size=mesh_config.max_mesh_size,
|
|
761
|
+
cells_per_wavelength=mesh_config.cells_per_wavelength,
|
|
762
|
+
margin=mesh_config.margin,
|
|
763
|
+
air_above=mesh_config.air_above,
|
|
764
|
+
fmax=mesh_config.fmax,
|
|
765
|
+
show_gui=show_gui,
|
|
766
|
+
preview_only=True,
|
|
767
|
+
)
|
|
768
|
+
|
|
769
|
+
# Generate mesh in temp directory
|
|
770
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
771
|
+
generate_mesh(
|
|
772
|
+
component=component,
|
|
773
|
+
stack=stack,
|
|
774
|
+
ports=ports,
|
|
775
|
+
output_dir=tmpdir,
|
|
776
|
+
config=legacy_mesh_config,
|
|
777
|
+
)
|
|
778
|
+
|
|
779
|
+
# -------------------------------------------------------------------------
|
|
780
|
+
# Convenience methods
|
|
781
|
+
# -------------------------------------------------------------------------
|
|
782
|
+
|
|
783
|
+
def show_stack(self) -> None:
|
|
784
|
+
"""Print the layer stack table.
|
|
785
|
+
|
|
786
|
+
Example:
|
|
787
|
+
>>> sim.show_stack()
|
|
788
|
+
"""
|
|
789
|
+
from gsim.common.stack import print_stack_table
|
|
790
|
+
|
|
791
|
+
if self.stack is None:
|
|
792
|
+
self._resolve_stack()
|
|
793
|
+
|
|
794
|
+
if self.stack is not None:
|
|
795
|
+
print_stack_table(self.stack)
|
|
796
|
+
|
|
797
|
+
def plot_stack(self) -> None:
|
|
798
|
+
"""Plot the layer stack visualization.
|
|
799
|
+
|
|
800
|
+
Example:
|
|
801
|
+
>>> sim.plot_stack()
|
|
802
|
+
"""
|
|
803
|
+
from gsim.common.stack import plot_stack
|
|
804
|
+
|
|
805
|
+
if self.stack is None:
|
|
806
|
+
self._resolve_stack()
|
|
807
|
+
|
|
808
|
+
if self.stack is not None:
|
|
809
|
+
plot_stack(self.stack)
|
|
810
|
+
|
|
811
|
+
# -------------------------------------------------------------------------
|
|
812
|
+
# Mesh generation
|
|
813
|
+
# -------------------------------------------------------------------------
|
|
814
|
+
|
|
815
|
+
def mesh(
|
|
816
|
+
self,
|
|
817
|
+
*,
|
|
818
|
+
preset: Literal["coarse", "default", "fine"] | None = None,
|
|
819
|
+
refined_mesh_size: float | None = None,
|
|
820
|
+
max_mesh_size: float | None = None,
|
|
821
|
+
margin: float | None = None,
|
|
822
|
+
air_above: float | None = None,
|
|
823
|
+
fmax: float | None = None,
|
|
824
|
+
show_gui: bool = False,
|
|
825
|
+
model_name: str = "palace",
|
|
826
|
+
verbose: bool = True,
|
|
827
|
+
) -> SimulationResult:
|
|
828
|
+
"""Generate the mesh for Palace simulation.
|
|
829
|
+
|
|
830
|
+
Only generates the mesh file (palace.msh). Config is generated
|
|
831
|
+
separately with write_config().
|
|
832
|
+
|
|
833
|
+
Requires set_output_dir() to be called first.
|
|
834
|
+
|
|
835
|
+
Args:
|
|
836
|
+
preset: Mesh quality preset ("coarse", "default", "fine")
|
|
837
|
+
refined_mesh_size: Mesh size near conductors (um), overrides preset
|
|
838
|
+
max_mesh_size: Max mesh size in air/dielectric (um), overrides preset
|
|
839
|
+
margin: XY margin around design (um), overrides preset
|
|
840
|
+
air_above: Air above top metal (um), overrides preset
|
|
841
|
+
fmax: Max frequency for mesh sizing (Hz), overrides preset
|
|
842
|
+
show_gui: Show gmsh GUI during meshing
|
|
843
|
+
model_name: Base name for output files
|
|
844
|
+
verbose: Print progress messages
|
|
845
|
+
|
|
846
|
+
Returns:
|
|
847
|
+
SimulationResult with mesh path
|
|
848
|
+
|
|
849
|
+
Raises:
|
|
850
|
+
ValueError: If output_dir not set or configuration is invalid
|
|
851
|
+
|
|
852
|
+
Example:
|
|
853
|
+
>>> sim.set_output_dir("./sim")
|
|
854
|
+
>>> result = sim.mesh(preset="fine")
|
|
855
|
+
>>> print(f"Mesh saved to: {result.mesh_path}")
|
|
856
|
+
"""
|
|
857
|
+
from gsim.palace.ports import extract_ports
|
|
858
|
+
|
|
859
|
+
if self._output_dir is None:
|
|
860
|
+
raise ValueError("Output directory not set. Call set_output_dir() first.")
|
|
861
|
+
|
|
862
|
+
component = self.geometry.component if self.geometry else None
|
|
863
|
+
|
|
864
|
+
# Build mesh config
|
|
865
|
+
mesh_config = self._build_mesh_config(
|
|
866
|
+
preset=preset,
|
|
867
|
+
refined_mesh_size=refined_mesh_size,
|
|
868
|
+
max_mesh_size=max_mesh_size,
|
|
869
|
+
margin=margin,
|
|
870
|
+
air_above=air_above,
|
|
871
|
+
fmax=fmax,
|
|
872
|
+
show_gui=show_gui,
|
|
873
|
+
)
|
|
874
|
+
|
|
875
|
+
# Validate configuration
|
|
876
|
+
validation = self.validate()
|
|
877
|
+
if not validation.valid:
|
|
878
|
+
raise ValueError(
|
|
879
|
+
f"Invalid configuration:\n" + "\n".join(validation.errors)
|
|
880
|
+
)
|
|
881
|
+
|
|
882
|
+
output_dir = self._output_dir
|
|
883
|
+
|
|
884
|
+
# Resolve stack and configure ports
|
|
885
|
+
stack = self._resolve_stack()
|
|
886
|
+
self._configure_ports_on_component(stack)
|
|
887
|
+
|
|
888
|
+
# Extract ports
|
|
889
|
+
palace_ports = extract_ports(component, stack)
|
|
890
|
+
|
|
891
|
+
# Generate mesh (config is written separately by simulate() or write_config())
|
|
892
|
+
return self._generate_mesh_internal(
|
|
893
|
+
output_dir=output_dir,
|
|
894
|
+
mesh_config=mesh_config,
|
|
895
|
+
ports=palace_ports,
|
|
896
|
+
driven_config=self.driven,
|
|
897
|
+
model_name=model_name,
|
|
898
|
+
verbose=verbose,
|
|
899
|
+
write_config=False,
|
|
900
|
+
)
|
|
901
|
+
|
|
902
|
+
def write_config(self) -> Path:
|
|
903
|
+
"""Write Palace config.json after mesh generation.
|
|
904
|
+
|
|
905
|
+
Use this when mesh() was called with write_config=False.
|
|
906
|
+
|
|
907
|
+
Returns:
|
|
908
|
+
Path to the generated config.json
|
|
909
|
+
|
|
910
|
+
Raises:
|
|
911
|
+
ValueError: If mesh() hasn't been called yet
|
|
912
|
+
|
|
913
|
+
Example:
|
|
914
|
+
>>> result = sim.mesh("./sim", write_config=False)
|
|
915
|
+
>>> config_path = sim.write_config()
|
|
916
|
+
"""
|
|
917
|
+
from gsim.palace.mesh.generator import write_config as gen_write_config
|
|
918
|
+
|
|
919
|
+
if self._last_mesh_result is None:
|
|
920
|
+
raise ValueError("No mesh result. Call mesh() first.")
|
|
921
|
+
|
|
922
|
+
if not self._last_mesh_result.groups:
|
|
923
|
+
raise ValueError(
|
|
924
|
+
"Mesh result has no groups data. "
|
|
925
|
+
"Was mesh() called with write_config=True already?"
|
|
926
|
+
)
|
|
927
|
+
|
|
928
|
+
stack = self._resolve_stack()
|
|
929
|
+
config_path = gen_write_config(
|
|
930
|
+
mesh_result=self._last_mesh_result,
|
|
931
|
+
stack=stack,
|
|
932
|
+
ports=self._last_ports,
|
|
933
|
+
driven_config=self.driven,
|
|
934
|
+
)
|
|
935
|
+
|
|
936
|
+
return config_path
|
|
937
|
+
|
|
938
|
+
# -------------------------------------------------------------------------
|
|
939
|
+
# Simulation
|
|
940
|
+
# -------------------------------------------------------------------------
|
|
941
|
+
|
|
942
|
+
def simulate(
|
|
943
|
+
self,
|
|
944
|
+
*,
|
|
945
|
+
verbose: bool = True,
|
|
946
|
+
) -> dict[str, Path]:
|
|
947
|
+
"""Run simulation on GDSFactory+ cloud.
|
|
948
|
+
|
|
949
|
+
Requires mesh() and write_config() to be called first.
|
|
950
|
+
|
|
951
|
+
Args:
|
|
952
|
+
verbose: Print progress messages
|
|
953
|
+
|
|
954
|
+
Returns:
|
|
955
|
+
Dict mapping result filenames to local paths
|
|
956
|
+
|
|
957
|
+
Raises:
|
|
958
|
+
ValueError: If output_dir not set
|
|
959
|
+
FileNotFoundError: If mesh or config files don't exist
|
|
960
|
+
RuntimeError: If simulation fails
|
|
961
|
+
|
|
962
|
+
Example:
|
|
963
|
+
>>> results = sim.simulate()
|
|
964
|
+
>>> print(f"S-params saved to: {results['port-S.csv']}")
|
|
965
|
+
"""
|
|
966
|
+
from gsim.gcloud import run_simulation
|
|
967
|
+
|
|
968
|
+
if self._output_dir is None:
|
|
969
|
+
raise ValueError("Output directory not set. Call set_output_dir() first.")
|
|
970
|
+
|
|
971
|
+
output_dir = self._output_dir
|
|
972
|
+
|
|
973
|
+
return run_simulation(
|
|
974
|
+
output_dir,
|
|
975
|
+
job_type="palace",
|
|
976
|
+
verbose=verbose,
|
|
977
|
+
)
|
|
978
|
+
|
|
979
|
+
def simulate_local(
|
|
980
|
+
self,
|
|
981
|
+
*,
|
|
982
|
+
verbose: bool = True,
|
|
983
|
+
) -> dict[str, Path]:
|
|
984
|
+
"""Run simulation locally using Palace.
|
|
985
|
+
|
|
986
|
+
Requires mesh() and write_config() to be called first,
|
|
987
|
+
and Palace to be installed locally.
|
|
988
|
+
|
|
989
|
+
Args:
|
|
990
|
+
verbose: Print progress messages
|
|
991
|
+
|
|
992
|
+
Returns:
|
|
993
|
+
Dict mapping result filenames to local paths
|
|
994
|
+
|
|
995
|
+
Raises:
|
|
996
|
+
NotImplementedError: Local simulation is not yet implemented
|
|
997
|
+
"""
|
|
998
|
+
raise NotImplementedError(
|
|
999
|
+
"Local simulation is not yet implemented. "
|
|
1000
|
+
"Use simulate() to run on GDSFactory+ cloud."
|
|
1001
|
+
)
|
|
1002
|
+
|
|
1003
|
+
|
|
1004
|
+
__all__ = ["DrivenSim"]
|