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.
@@ -0,0 +1,777 @@
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
+ 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
+ EigenmodeConfig,
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 EigenmodeSim(PalaceSimMixin, BaseModel):
37
+ """Eigenmode simulation for finding resonant frequencies.
38
+
39
+ This class configures and runs eigenmode simulations to find
40
+ resonant frequencies and mode shapes of structures. Uses composition
41
+ (no inheritance) with shared Geometry and Stack components from gsim.common.
42
+
43
+ Example:
44
+ >>> from gsim.palace import EigenmodeSim
45
+ >>>
46
+ >>> sim = EigenmodeSim()
47
+ >>> sim.set_geometry(component)
48
+ >>> sim.set_stack(air_above=300.0)
49
+ >>> sim.add_port("o1", layer="topmetal2", length=5.0)
50
+ >>> sim.set_eigenmode(num_modes=10, target=50e9)
51
+ >>> sim.mesh("./sim", preset="default")
52
+ >>> results = sim.simulate()
53
+
54
+ Attributes:
55
+ geometry: Wrapped gdsfactory Component (from common)
56
+ stack: Layer stack configuration (from common)
57
+ ports: List of single-element port configurations
58
+ cpw_ports: List of CPW (two-element) port configurations
59
+ eigenmode: Eigenmode simulation configuration
60
+ materials: Material property overrides
61
+ numerical: Numerical solver configuration
62
+ """
63
+
64
+ model_config = ConfigDict(
65
+ validate_assignment=True,
66
+ arbitrary_types_allowed=True,
67
+ )
68
+
69
+ # Composed objects (from common)
70
+ geometry: Geometry | None = None
71
+ stack: LayerStack | None = None
72
+
73
+ # Port configurations (eigenmode can have ports for Q-factor calculation)
74
+ ports: list[PortConfig] = Field(default_factory=list)
75
+ cpw_ports: list[CPWPortConfig] = Field(default_factory=list)
76
+
77
+ # Eigenmode simulation config
78
+ eigenmode: EigenmodeConfig = Field(default_factory=EigenmodeConfig)
79
+
80
+ # Material overrides and numerical config
81
+ materials: dict[str, MaterialConfig] = Field(default_factory=dict)
82
+ numerical: NumericalConfig = Field(default_factory=NumericalConfig)
83
+
84
+ # Stack configuration (stored as kwargs until resolved)
85
+ _stack_kwargs: dict[str, Any] = PrivateAttr(default_factory=dict)
86
+
87
+ # Internal state
88
+ _output_dir: Path | None = PrivateAttr(default=None)
89
+ _configured_ports: bool = PrivateAttr(default=False)
90
+
91
+ # -------------------------------------------------------------------------
92
+ # Geometry methods
93
+ # -------------------------------------------------------------------------
94
+
95
+ def set_geometry(self, component: Component) -> None:
96
+ """Set the gdsfactory component for simulation.
97
+
98
+ Args:
99
+ component: gdsfactory Component to simulate
100
+
101
+ Example:
102
+ >>> sim.set_geometry(my_component)
103
+ """
104
+ self.geometry = Geometry(component=component)
105
+
106
+ @property
107
+ def component(self) -> Component | None:
108
+ """Get the current component (for backward compatibility)."""
109
+ return self.geometry.component if self.geometry else None
110
+
111
+ @property
112
+ def _component(self) -> Component | None:
113
+ """Internal component access (backward compatibility)."""
114
+ return self.component
115
+
116
+ # -------------------------------------------------------------------------
117
+ # Stack methods
118
+ # -------------------------------------------------------------------------
119
+
120
+ def set_stack(
121
+ self,
122
+ *,
123
+ yaml_path: str | Path | None = None,
124
+ air_above: float = 200.0,
125
+ substrate_thickness: float = 2.0,
126
+ include_substrate: bool = False,
127
+ **kwargs,
128
+ ) -> None:
129
+ """Configure the layer stack.
130
+
131
+ Args:
132
+ yaml_path: Path to custom YAML stack file
133
+ air_above: Air box height above top metal in um
134
+ substrate_thickness: Thickness below z=0 in um
135
+ include_substrate: Include lossy silicon substrate
136
+ **kwargs: Additional args passed to extract_layer_stack
137
+
138
+ Example:
139
+ >>> sim.set_stack(air_above=300.0, substrate_thickness=2.0)
140
+ """
141
+ self._stack_kwargs = {
142
+ "yaml_path": yaml_path,
143
+ "air_above": air_above,
144
+ "substrate_thickness": substrate_thickness,
145
+ "include_substrate": include_substrate,
146
+ **kwargs,
147
+ }
148
+ self.stack = None
149
+
150
+ # -------------------------------------------------------------------------
151
+ # Port methods
152
+ # -------------------------------------------------------------------------
153
+
154
+ def add_port(
155
+ self,
156
+ name: str,
157
+ *,
158
+ layer: str | None = None,
159
+ from_layer: str | None = None,
160
+ to_layer: str | None = None,
161
+ length: float | None = None,
162
+ impedance: float = 50.0,
163
+ excited: bool = True,
164
+ geometry: Literal["inplane", "via"] = "inplane",
165
+ ) -> None:
166
+ """Add a single-element lumped port.
167
+
168
+ Args:
169
+ name: Port name (must match component port name)
170
+ layer: Target layer for inplane ports
171
+ from_layer: Bottom layer for via ports
172
+ to_layer: Top layer for via ports
173
+ length: Port extent along direction (um)
174
+ impedance: Port impedance (Ohms)
175
+ excited: Whether this port is excited
176
+ geometry: Port geometry type ("inplane" or "via")
177
+
178
+ Example:
179
+ >>> sim.add_port("o1", layer="topmetal2", length=5.0)
180
+ """
181
+ self.ports = [p for p in self.ports if p.name != name]
182
+ self.ports.append(
183
+ PortConfig(
184
+ name=name,
185
+ layer=layer,
186
+ from_layer=from_layer,
187
+ to_layer=to_layer,
188
+ length=length,
189
+ impedance=impedance,
190
+ excited=excited,
191
+ geometry=geometry,
192
+ )
193
+ )
194
+
195
+ def add_cpw_port(
196
+ self,
197
+ upper: str,
198
+ lower: str,
199
+ *,
200
+ layer: str,
201
+ length: float,
202
+ impedance: float = 50.0,
203
+ excited: bool = True,
204
+ name: str | None = None,
205
+ ) -> None:
206
+ """Add a coplanar waveguide (CPW) port.
207
+
208
+ Args:
209
+ upper: Name of the upper gap port on the component
210
+ lower: Name of the lower gap port on the component
211
+ layer: Target conductor layer
212
+ length: Port extent along direction (um)
213
+ impedance: Port impedance (Ohms)
214
+ excited: Whether this port is excited
215
+ name: Optional name for the CPW port
216
+
217
+ Example:
218
+ >>> sim.add_cpw_port("P2", "P1", layer="topmetal2", length=5.0)
219
+ """
220
+ self.cpw_ports = [
221
+ p for p in self.cpw_ports if not (p.upper == upper and p.lower == lower)
222
+ ]
223
+ self.cpw_ports.append(
224
+ CPWPortConfig(
225
+ upper=upper,
226
+ lower=lower,
227
+ layer=layer,
228
+ length=length,
229
+ impedance=impedance,
230
+ excited=excited,
231
+ name=name,
232
+ )
233
+ )
234
+
235
+ # -------------------------------------------------------------------------
236
+ # Eigenmode configuration
237
+ # -------------------------------------------------------------------------
238
+
239
+ def set_eigenmode(
240
+ self,
241
+ *,
242
+ num_modes: int = 10,
243
+ target: float | None = None,
244
+ tolerance: float = 1e-6,
245
+ ) -> None:
246
+ """Configure eigenmode simulation.
247
+
248
+ Args:
249
+ num_modes: Number of modes to find
250
+ target: Target frequency in Hz for mode search
251
+ tolerance: Eigenvalue solver tolerance
252
+
253
+ Example:
254
+ >>> sim.set_eigenmode(num_modes=10, target=50e9)
255
+ """
256
+ self.eigenmode = EigenmodeConfig(
257
+ num_modes=num_modes,
258
+ target=target,
259
+ tolerance=tolerance,
260
+ )
261
+
262
+ # -------------------------------------------------------------------------
263
+ # Material methods
264
+ # -------------------------------------------------------------------------
265
+
266
+ def set_material(
267
+ self,
268
+ name: str,
269
+ *,
270
+ type: Literal["conductor", "dielectric", "semiconductor"] | None = None,
271
+ conductivity: float | None = None,
272
+ permittivity: float | None = None,
273
+ loss_tangent: float | None = None,
274
+ ) -> None:
275
+ """Override or add material properties.
276
+
277
+ Args:
278
+ name: Material name
279
+ type: Material type (conductor, dielectric, semiconductor)
280
+ conductivity: Conductivity in S/m (for conductors)
281
+ permittivity: Relative permittivity (for dielectrics)
282
+ loss_tangent: Dielectric loss tangent
283
+
284
+ Example:
285
+ >>> sim.set_material("aluminum", type="conductor", conductivity=3.8e7)
286
+ """
287
+ if type is None:
288
+ if conductivity is not None and conductivity > 1e4:
289
+ type = "conductor"
290
+ elif permittivity is not None:
291
+ type = "dielectric"
292
+ else:
293
+ type = "dielectric"
294
+
295
+ self.materials[name] = MaterialConfig(
296
+ type=type,
297
+ conductivity=conductivity,
298
+ permittivity=permittivity,
299
+ loss_tangent=loss_tangent,
300
+ )
301
+
302
+ def set_numerical(
303
+ self,
304
+ *,
305
+ order: int = 2,
306
+ tolerance: float = 1e-6,
307
+ max_iterations: int = 400,
308
+ solver_type: Literal["Default", "SuperLU", "STRUMPACK", "MUMPS"] = "Default",
309
+ preconditioner: Literal["Default", "AMS", "BoomerAMG"] = "Default",
310
+ device: Literal["CPU", "GPU"] = "CPU",
311
+ num_processors: int | None = None,
312
+ ) -> None:
313
+ """Configure numerical solver parameters.
314
+
315
+ Args:
316
+ order: Finite element order (1-4)
317
+ tolerance: Linear solver tolerance
318
+ max_iterations: Maximum solver iterations
319
+ solver_type: Linear solver type
320
+ preconditioner: Preconditioner type
321
+ device: Compute device (CPU or GPU)
322
+ num_processors: Number of processors (None = auto)
323
+
324
+ Example:
325
+ >>> sim.set_numerical(order=3, tolerance=1e-8)
326
+ """
327
+ self.numerical = NumericalConfig(
328
+ order=order,
329
+ tolerance=tolerance,
330
+ max_iterations=max_iterations,
331
+ solver_type=solver_type,
332
+ preconditioner=preconditioner,
333
+ device=device,
334
+ num_processors=num_processors,
335
+ )
336
+
337
+ # -------------------------------------------------------------------------
338
+ # Validation
339
+ # -------------------------------------------------------------------------
340
+
341
+ def validate(self) -> ValidationResult:
342
+ """Validate the simulation configuration.
343
+
344
+ Returns:
345
+ ValidationResult with validation status and messages
346
+ """
347
+ errors = []
348
+ warnings_list = []
349
+
350
+ # Check geometry
351
+ if self.geometry is None:
352
+ errors.append("No component set. Call set_geometry(component) first.")
353
+
354
+ # Check stack
355
+ if self.stack is None and not self._stack_kwargs:
356
+ warnings_list.append(
357
+ "No stack configured. Will use active PDK with defaults."
358
+ )
359
+
360
+ # Eigenmode simulations may not require ports
361
+ if not self.ports and not self.cpw_ports:
362
+ warnings_list.append(
363
+ "No ports configured. Eigenmode will find all modes without port loading."
364
+ )
365
+
366
+ # Validate port configurations
367
+ for port in self.ports:
368
+ if port.geometry == "inplane" and port.layer is None:
369
+ errors.append(f"Port '{port.name}': inplane ports require 'layer'")
370
+ if port.geometry == "via":
371
+ if port.from_layer is None or port.to_layer is None:
372
+ errors.append(
373
+ f"Port '{port.name}': via ports require 'from_layer' and 'to_layer'"
374
+ )
375
+
376
+ valid = len(errors) == 0
377
+ return ValidationResult(valid=valid, errors=errors, warnings=warnings_list)
378
+
379
+ # -------------------------------------------------------------------------
380
+ # Internal helpers
381
+ # -------------------------------------------------------------------------
382
+
383
+ def _resolve_stack(self) -> LayerStack:
384
+ """Resolve the layer stack from PDK or YAML."""
385
+ from gsim.common.stack import get_stack
386
+
387
+ yaml_path = self._stack_kwargs.pop("yaml_path", None)
388
+ legacy_stack = get_stack(yaml_path=yaml_path, **self._stack_kwargs)
389
+ self._stack_kwargs["yaml_path"] = yaml_path
390
+
391
+ for name, props in self.materials.items():
392
+ legacy_stack.materials[name] = props.to_dict()
393
+
394
+ self.stack = legacy_stack
395
+
396
+ return legacy_stack
397
+
398
+ def _configure_ports_on_component(self, stack: LayerStack) -> None:
399
+ """Configure ports on the component."""
400
+ from gsim.palace.ports import (
401
+ configure_cpw_port,
402
+ configure_inplane_port,
403
+ configure_via_port,
404
+ )
405
+
406
+ component = self.geometry.component if self.geometry else None
407
+ if component is None:
408
+ raise ValueError("No component set")
409
+
410
+ for port_config in self.ports:
411
+ if port_config.name is None:
412
+ continue
413
+
414
+ gf_port = None
415
+ for p in component.ports:
416
+ if p.name == port_config.name:
417
+ gf_port = p
418
+ break
419
+
420
+ if gf_port is None:
421
+ raise ValueError(
422
+ f"Port '{port_config.name}' not found on component. "
423
+ f"Available: {[p.name for p in component.ports]}"
424
+ )
425
+
426
+ if port_config.geometry == "inplane":
427
+ configure_inplane_port(
428
+ gf_port,
429
+ layer=port_config.layer,
430
+ length=port_config.length or gf_port.width,
431
+ impedance=port_config.impedance,
432
+ excited=port_config.excited,
433
+ )
434
+ elif port_config.geometry == "via":
435
+ configure_via_port(
436
+ gf_port,
437
+ from_layer=port_config.from_layer,
438
+ to_layer=port_config.to_layer,
439
+ impedance=port_config.impedance,
440
+ excited=port_config.excited,
441
+ )
442
+
443
+ for cpw_config in self.cpw_ports:
444
+ port_upper = None
445
+ port_lower = None
446
+ for p in component.ports:
447
+ if p.name == cpw_config.upper:
448
+ port_upper = p
449
+ if p.name == cpw_config.lower:
450
+ port_lower = p
451
+
452
+ if port_upper is None:
453
+ raise ValueError(f"CPW upper port '{cpw_config.upper}' not found.")
454
+ if port_lower is None:
455
+ raise ValueError(f"CPW lower port '{cpw_config.lower}' not found.")
456
+
457
+ configure_cpw_port(
458
+ port_upper=port_upper,
459
+ port_lower=port_lower,
460
+ layer=cpw_config.layer,
461
+ length=cpw_config.length,
462
+ impedance=cpw_config.impedance,
463
+ excited=cpw_config.excited,
464
+ cpw_name=cpw_config.name,
465
+ )
466
+
467
+ self._configured_ports = True
468
+
469
+ def _build_mesh_config(
470
+ self,
471
+ preset: Literal["coarse", "default", "fine"] | None,
472
+ refined_mesh_size: float | None,
473
+ max_mesh_size: float | None,
474
+ margin: float | None,
475
+ air_above: float | None,
476
+ fmax: float | None,
477
+ show_gui: bool,
478
+ ) -> MeshConfig:
479
+ """Build mesh config from preset with optional overrides."""
480
+ if preset == "coarse":
481
+ mesh_config = MeshConfig.coarse()
482
+ elif preset == "fine":
483
+ mesh_config = MeshConfig.fine()
484
+ else:
485
+ mesh_config = MeshConfig.default()
486
+
487
+ overrides = []
488
+ if preset is not None:
489
+ if refined_mesh_size is not None:
490
+ overrides.append(f"refined_mesh_size={refined_mesh_size}")
491
+ if max_mesh_size is not None:
492
+ overrides.append(f"max_mesh_size={max_mesh_size}")
493
+ if margin is not None:
494
+ overrides.append(f"margin={margin}")
495
+ if air_above is not None:
496
+ overrides.append(f"air_above={air_above}")
497
+ if fmax is not None:
498
+ overrides.append(f"fmax={fmax}")
499
+
500
+ if overrides:
501
+ warnings.warn(
502
+ f"Preset '{preset}' values overridden by: {', '.join(overrides)}",
503
+ stacklevel=4,
504
+ )
505
+
506
+ if refined_mesh_size is not None:
507
+ mesh_config.refined_mesh_size = refined_mesh_size
508
+ if max_mesh_size is not None:
509
+ mesh_config.max_mesh_size = max_mesh_size
510
+ if margin is not None:
511
+ mesh_config.margin = margin
512
+ if air_above is not None:
513
+ mesh_config.air_above = air_above
514
+ if fmax is not None:
515
+ mesh_config.fmax = fmax
516
+ mesh_config.show_gui = show_gui
517
+
518
+ return mesh_config
519
+
520
+ def _generate_mesh_internal(
521
+ self,
522
+ output_dir: Path,
523
+ mesh_config: MeshConfig,
524
+ ports: list,
525
+ model_name: str,
526
+ verbose: bool,
527
+ ) -> SimulationResult:
528
+ """Internal mesh generation."""
529
+ from gsim.palace.mesh import MeshConfig as LegacyMeshConfig
530
+ from gsim.palace.mesh import generate_mesh
531
+
532
+ component = self.geometry.component if self.geometry else None
533
+
534
+ legacy_mesh_config = LegacyMeshConfig(
535
+ refined_mesh_size=mesh_config.refined_mesh_size,
536
+ max_mesh_size=mesh_config.max_mesh_size,
537
+ cells_per_wavelength=mesh_config.cells_per_wavelength,
538
+ margin=mesh_config.margin,
539
+ air_above=mesh_config.air_above,
540
+ fmax=mesh_config.fmax,
541
+ show_gui=mesh_config.show_gui,
542
+ preview_only=mesh_config.preview_only,
543
+ )
544
+
545
+ stack = self._resolve_stack()
546
+
547
+ if verbose:
548
+ logger.info("Generating mesh in %s", output_dir)
549
+
550
+ mesh_result = generate_mesh(
551
+ component=component,
552
+ stack=stack,
553
+ ports=ports,
554
+ output_dir=output_dir,
555
+ config=legacy_mesh_config,
556
+ model_name=model_name,
557
+ driven_config=None, # Eigenmode doesn't use driven config
558
+ )
559
+
560
+ return SimulationResult(
561
+ mesh_path=mesh_result.mesh_path,
562
+ output_dir=output_dir,
563
+ config_path=mesh_result.config_path,
564
+ port_info=mesh_result.port_info,
565
+ mesh_stats=mesh_result.mesh_stats,
566
+ )
567
+
568
+ def _get_ports_for_preview(self, stack: LayerStack) -> list:
569
+ """Get ports for preview."""
570
+ from gsim.palace.ports import extract_ports
571
+
572
+ component = self.geometry.component if self.geometry else None
573
+ if self.ports or self.cpw_ports:
574
+ self._configure_ports_on_component(stack)
575
+ return extract_ports(component, stack)
576
+ return []
577
+
578
+ # -------------------------------------------------------------------------
579
+ # Preview
580
+ # -------------------------------------------------------------------------
581
+
582
+ def preview(
583
+ self,
584
+ *,
585
+ preset: Literal["coarse", "default", "fine"] | None = None,
586
+ refined_mesh_size: float | None = None,
587
+ max_mesh_size: float | None = None,
588
+ margin: float | None = None,
589
+ air_above: float | None = None,
590
+ fmax: float | None = None,
591
+ show_gui: bool = True,
592
+ ) -> None:
593
+ """Preview the mesh without running simulation.
594
+
595
+ Args:
596
+ preset: Mesh quality preset ("coarse", "default", "fine")
597
+ refined_mesh_size: Mesh size near conductors (um)
598
+ max_mesh_size: Max mesh size in air/dielectric (um)
599
+ margin: XY margin around design (um)
600
+ air_above: Air above top metal (um)
601
+ fmax: Max frequency for mesh sizing (Hz)
602
+ show_gui: Show gmsh GUI for interactive preview
603
+
604
+ Example:
605
+ >>> sim.preview(preset="fine", show_gui=True)
606
+ """
607
+ from gsim.palace.mesh import MeshConfig as LegacyMeshConfig
608
+ from gsim.palace.mesh import generate_mesh
609
+
610
+ component = self.geometry.component if self.geometry else None
611
+
612
+ validation = self.validate()
613
+ if not validation.valid:
614
+ raise ValueError(
615
+ f"Invalid configuration:\n" + "\n".join(validation.errors)
616
+ )
617
+
618
+ mesh_config = self._build_mesh_config(
619
+ preset=preset,
620
+ refined_mesh_size=refined_mesh_size,
621
+ max_mesh_size=max_mesh_size,
622
+ margin=margin,
623
+ air_above=air_above,
624
+ fmax=fmax,
625
+ show_gui=show_gui,
626
+ )
627
+
628
+ stack = self._resolve_stack()
629
+ ports = self._get_ports_for_preview(stack)
630
+
631
+ legacy_mesh_config = LegacyMeshConfig(
632
+ refined_mesh_size=mesh_config.refined_mesh_size,
633
+ max_mesh_size=mesh_config.max_mesh_size,
634
+ cells_per_wavelength=mesh_config.cells_per_wavelength,
635
+ margin=mesh_config.margin,
636
+ air_above=mesh_config.air_above,
637
+ fmax=mesh_config.fmax,
638
+ show_gui=show_gui,
639
+ preview_only=True,
640
+ )
641
+
642
+ with tempfile.TemporaryDirectory() as tmpdir:
643
+ generate_mesh(
644
+ component=component,
645
+ stack=stack,
646
+ ports=ports,
647
+ output_dir=tmpdir,
648
+ config=legacy_mesh_config,
649
+ )
650
+
651
+ # -------------------------------------------------------------------------
652
+ # Convenience methods
653
+ # -------------------------------------------------------------------------
654
+
655
+ def show_stack(self) -> None:
656
+ """Print the layer stack table."""
657
+ from gsim.common.stack import print_stack_table
658
+
659
+ if self.stack is None:
660
+ self._resolve_stack()
661
+
662
+ if self.stack is not None:
663
+ print_stack_table(self.stack)
664
+
665
+ def plot_stack(self) -> None:
666
+ """Plot the layer stack visualization."""
667
+ from gsim.common.stack import plot_stack
668
+
669
+ if self.stack is None:
670
+ self._resolve_stack()
671
+
672
+ if self.stack is not None:
673
+ plot_stack(self.stack)
674
+
675
+ # -------------------------------------------------------------------------
676
+ # Mesh generation
677
+ # -------------------------------------------------------------------------
678
+
679
+ def mesh(
680
+ self,
681
+ output_dir: str | Path,
682
+ *,
683
+ preset: Literal["coarse", "default", "fine"] | None = None,
684
+ refined_mesh_size: float | None = None,
685
+ max_mesh_size: float | None = None,
686
+ margin: float | None = None,
687
+ air_above: float | None = None,
688
+ fmax: float | None = None,
689
+ show_gui: bool = False,
690
+ model_name: str = "palace",
691
+ verbose: bool = True,
692
+ ) -> SimulationResult:
693
+ """Generate the mesh and configuration files.
694
+
695
+ Args:
696
+ output_dir: Directory for output files
697
+ preset: Mesh quality preset ("coarse", "default", "fine")
698
+ refined_mesh_size: Mesh size near conductors (um)
699
+ max_mesh_size: Max mesh size in air/dielectric (um)
700
+ margin: XY margin around design (um)
701
+ air_above: Air above top metal (um)
702
+ fmax: Max frequency for mesh sizing (Hz)
703
+ show_gui: Show gmsh GUI during meshing
704
+ model_name: Base name for output files
705
+ verbose: Print progress messages
706
+
707
+ Returns:
708
+ SimulationResult with mesh and config paths
709
+ """
710
+ from gsim.palace.ports import extract_ports
711
+
712
+ component = self.geometry.component if self.geometry else None
713
+
714
+ mesh_config = self._build_mesh_config(
715
+ preset=preset,
716
+ refined_mesh_size=refined_mesh_size,
717
+ max_mesh_size=max_mesh_size,
718
+ margin=margin,
719
+ air_above=air_above,
720
+ fmax=fmax,
721
+ show_gui=show_gui,
722
+ )
723
+
724
+ validation = self.validate()
725
+ if not validation.valid:
726
+ raise ValueError(
727
+ f"Invalid configuration:\n" + "\n".join(validation.errors)
728
+ )
729
+
730
+ output_dir = Path(output_dir)
731
+ output_dir.mkdir(parents=True, exist_ok=True)
732
+ self._output_dir = output_dir
733
+
734
+ stack = self._resolve_stack()
735
+
736
+ palace_ports = []
737
+ if self.ports or self.cpw_ports:
738
+ self._configure_ports_on_component(stack)
739
+ palace_ports = extract_ports(component, stack)
740
+
741
+ return self._generate_mesh_internal(
742
+ output_dir=output_dir,
743
+ mesh_config=mesh_config,
744
+ ports=palace_ports,
745
+ model_name=model_name,
746
+ verbose=verbose,
747
+ )
748
+
749
+ # -------------------------------------------------------------------------
750
+ # Simulation
751
+ # -------------------------------------------------------------------------
752
+
753
+ def simulate(
754
+ self,
755
+ output_dir: str | Path | None = None,
756
+ *,
757
+ verbose: bool = True,
758
+ ) -> dict[str, Path]:
759
+ """Run eigenmode simulation on GDSFactory+ cloud.
760
+
761
+ Args:
762
+ output_dir: Directory containing mesh files
763
+ verbose: Print progress messages
764
+
765
+ Returns:
766
+ Dict mapping result filenames to local paths
767
+
768
+ Raises:
769
+ NotImplementedError: Eigenmode is not yet fully implemented
770
+ """
771
+ raise NotImplementedError(
772
+ "Eigenmode simulation is not yet fully implemented on cloud. "
773
+ "Use DrivenSim for S-parameter extraction."
774
+ )
775
+
776
+
777
+ __all__ = ["EigenmodeSim"]