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/palace/driven.py ADDED
@@ -0,0 +1,728 @@
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
+ 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
+ DrivenConfig,
21
+ MaterialConfig,
22
+ MeshConfig,
23
+ NumericalConfig,
24
+ PortConfig,
25
+ SimulationResult,
26
+ ValidationResult,
27
+ )
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+
32
+ class DrivenSim(PalaceSimMixin, BaseModel):
33
+ """Frequency-domain driven simulation for S-parameter extraction.
34
+
35
+ This class configures and runs driven simulations that sweep through
36
+ frequencies to compute S-parameters. Uses composition (no inheritance)
37
+ with shared Geometry and Stack components from gsim.common.
38
+
39
+ Example:
40
+ >>> from gsim.palace import DrivenSim
41
+ >>>
42
+ >>> sim = DrivenSim()
43
+ >>> sim.set_geometry(component)
44
+ >>> sim.set_stack(air_above=300.0)
45
+ >>> sim.add_cpw_port("P2", "P1", layer="topmetal2", length=5.0)
46
+ >>> sim.add_cpw_port("P3", "P4", layer="topmetal2", length=5.0)
47
+ >>> sim.set_driven(fmin=1e9, fmax=100e9, num_points=40)
48
+ >>> sim.mesh("./sim", preset="default")
49
+ >>> results = sim.simulate()
50
+
51
+ Attributes:
52
+ geometry: Wrapped gdsfactory Component (from common)
53
+ stack: Layer stack configuration (from common)
54
+ ports: List of single-element port configurations
55
+ cpw_ports: List of CPW (two-element) port configurations
56
+ driven: Driven simulation configuration (frequencies, etc.)
57
+ mesh: Mesh configuration
58
+ materials: Material property overrides
59
+ numerical: Numerical solver configuration
60
+ """
61
+
62
+ model_config = ConfigDict(
63
+ validate_assignment=True,
64
+ arbitrary_types_allowed=True,
65
+ )
66
+
67
+ # Composed objects (from common)
68
+ geometry: Geometry | None = None
69
+ stack: LayerStack | None = None
70
+
71
+ # Port configurations
72
+ ports: list[PortConfig] = Field(default_factory=list)
73
+ cpw_ports: list[CPWPortConfig] = Field(default_factory=list)
74
+
75
+ # Driven simulation config
76
+ driven: DrivenConfig = Field(default_factory=DrivenConfig)
77
+
78
+ # Mesh config
79
+ mesh_config: MeshConfig = Field(default_factory=MeshConfig.default)
80
+
81
+ # Material overrides and numerical config
82
+ materials: dict[str, MaterialConfig] = Field(default_factory=dict)
83
+ numerical: NumericalConfig = Field(default_factory=NumericalConfig)
84
+
85
+ # Stack configuration (stored as kwargs until resolved)
86
+ _stack_kwargs: dict[str, Any] = PrivateAttr(default_factory=dict)
87
+
88
+ # Internal state
89
+ _output_dir: Path | None = PrivateAttr(default=None)
90
+ _configured_ports: bool = PrivateAttr(default=False)
91
+ _last_mesh_result: Any = PrivateAttr(default=None)
92
+ _last_ports: list = PrivateAttr(default_factory=list)
93
+
94
+ # -------------------------------------------------------------------------
95
+ # Port methods
96
+ # -------------------------------------------------------------------------
97
+
98
+ def add_port(
99
+ self,
100
+ name: str,
101
+ *,
102
+ layer: str | None = None,
103
+ from_layer: str | None = None,
104
+ to_layer: str | None = None,
105
+ length: float | None = None,
106
+ impedance: float = 50.0,
107
+ excited: bool = True,
108
+ geometry: Literal["inplane", "via"] = "inplane",
109
+ ) -> None:
110
+ """Add a single-element lumped port.
111
+
112
+ Args:
113
+ name: Port name (must match component port name)
114
+ layer: Target layer for inplane ports
115
+ from_layer: Bottom layer for via ports
116
+ to_layer: Top layer for via ports
117
+ length: Port extent along direction (um)
118
+ impedance: Port impedance (Ohms)
119
+ excited: Whether this port is excited
120
+ geometry: Port geometry type ("inplane" or "via")
121
+
122
+ Example:
123
+ >>> sim.add_port("o1", layer="topmetal2", length=5.0)
124
+ >>> sim.add_port(
125
+ ... "feed", from_layer="metal1", to_layer="topmetal2", geometry="via"
126
+ ... )
127
+ """
128
+ # Remove existing config for this port if any
129
+ self.ports = [p for p in self.ports if p.name != name]
130
+
131
+ self.ports.append(
132
+ PortConfig(
133
+ name=name,
134
+ layer=layer,
135
+ from_layer=from_layer,
136
+ to_layer=to_layer,
137
+ length=length,
138
+ impedance=impedance,
139
+ excited=excited,
140
+ geometry=geometry,
141
+ )
142
+ )
143
+
144
+ def add_cpw_port(
145
+ self,
146
+ upper: str,
147
+ lower: str,
148
+ *,
149
+ layer: str,
150
+ length: float,
151
+ impedance: float = 50.0,
152
+ excited: bool = True,
153
+ name: str | None = None,
154
+ ) -> None:
155
+ """Add a coplanar waveguide (CPW) port.
156
+
157
+ CPW ports consist of two elements (upper and lower gaps) that are
158
+ excited with opposite E-field directions to create the CPW mode.
159
+
160
+ Args:
161
+ upper: Name of the upper gap port on the component
162
+ lower: Name of the lower gap port on the component
163
+ layer: Target conductor layer (e.g., "topmetal2")
164
+ length: Port extent along direction (um)
165
+ impedance: Port impedance (Ohms)
166
+ excited: Whether this port is excited
167
+ name: Optional name for the CPW port (default: "cpw_{lower}")
168
+
169
+ Example:
170
+ >>> sim.add_cpw_port("P2", "P1", layer="topmetal2", length=5.0)
171
+ """
172
+ # Remove existing CPW port with same elements if any
173
+ self.cpw_ports = [
174
+ p for p in self.cpw_ports if not (p.upper == upper and p.lower == lower)
175
+ ]
176
+
177
+ self.cpw_ports.append(
178
+ CPWPortConfig(
179
+ upper=upper,
180
+ lower=lower,
181
+ layer=layer,
182
+ length=length,
183
+ impedance=impedance,
184
+ excited=excited,
185
+ name=name,
186
+ )
187
+ )
188
+
189
+ # -------------------------------------------------------------------------
190
+ # Driven configuration
191
+ # -------------------------------------------------------------------------
192
+
193
+ def set_driven(
194
+ self,
195
+ *,
196
+ fmin: float = 1e9,
197
+ fmax: float = 100e9,
198
+ num_points: int = 40,
199
+ scale: Literal["linear", "log"] = "linear",
200
+ adaptive_tol: float = 0.02,
201
+ adaptive_max_samples: int = 20,
202
+ compute_s_params: bool = True,
203
+ reference_impedance: float = 50.0,
204
+ excitation_port: str | None = None,
205
+ ) -> None:
206
+ """Configure driven (frequency sweep) simulation.
207
+
208
+ Args:
209
+ fmin: Minimum frequency in Hz
210
+ fmax: Maximum frequency in Hz
211
+ num_points: Number of frequency points
212
+ scale: "linear" or "log" frequency spacing
213
+ adaptive_tol: Adaptive frequency tolerance (0 disables adaptive)
214
+ adaptive_max_samples: Max samples for adaptive refinement
215
+ compute_s_params: Compute S-parameters
216
+ reference_impedance: Reference impedance for S-params (Ohms)
217
+ excitation_port: Port to excite (None = first port)
218
+
219
+ Example:
220
+ >>> sim.set_driven(fmin=1e9, fmax=100e9, num_points=40)
221
+ """
222
+ self.driven = DrivenConfig(
223
+ fmin=fmin,
224
+ fmax=fmax,
225
+ num_points=num_points,
226
+ scale=scale,
227
+ adaptive_tol=adaptive_tol,
228
+ adaptive_max_samples=adaptive_max_samples,
229
+ compute_s_params=compute_s_params,
230
+ reference_impedance=reference_impedance,
231
+ excitation_port=excitation_port,
232
+ )
233
+
234
+ # -------------------------------------------------------------------------
235
+ # Validation
236
+ # -------------------------------------------------------------------------
237
+
238
+ def validate_config(self) -> ValidationResult:
239
+ """Validate the simulation configuration.
240
+
241
+ Returns:
242
+ ValidationResult with validation status and messages
243
+ """
244
+ errors = []
245
+ warnings_list = []
246
+
247
+ # Check geometry
248
+ if self.geometry is None:
249
+ errors.append("No component set. Call set_geometry(component) first.")
250
+
251
+ # Check stack
252
+ if self.stack is None and not self._stack_kwargs:
253
+ warnings_list.append(
254
+ "No stack configured. Will use active PDK with defaults."
255
+ )
256
+
257
+ # Check ports
258
+ has_ports = bool(self.ports) or bool(self.cpw_ports)
259
+ if not has_ports:
260
+ warnings_list.append(
261
+ "No ports configured. Call add_port() or add_cpw_port()."
262
+ )
263
+ else:
264
+ # Validate port configurations
265
+ for port in self.ports:
266
+ if port.geometry == "inplane" and port.layer is None:
267
+ errors.append(f"Port '{port.name}': inplane ports require 'layer'")
268
+ if port.geometry == "via" and (
269
+ port.from_layer is None or port.to_layer is None
270
+ ):
271
+ errors.append(
272
+ f"Port '{port.name}': via ports require "
273
+ "'from_layer' and 'to_layer'"
274
+ )
275
+
276
+ # Validate CPW ports
277
+ errors.extend(
278
+ f"CPW port ({cpw.upper}, {cpw.lower}): 'layer' is required"
279
+ for cpw in self.cpw_ports
280
+ if not cpw.layer
281
+ )
282
+
283
+ # Validate excitation port if specified
284
+ if self.driven.excitation_port is not None:
285
+ port_names = [p.name for p in self.ports]
286
+ cpw_names = [cpw.effective_name for cpw in self.cpw_ports]
287
+ all_port_names = port_names + cpw_names
288
+ if self.driven.excitation_port not in all_port_names:
289
+ errors.append(
290
+ f"Excitation port '{self.driven.excitation_port}' not found. "
291
+ f"Available: {all_port_names}"
292
+ )
293
+
294
+ valid = len(errors) == 0
295
+ return ValidationResult(valid=valid, errors=errors, warnings=warnings_list)
296
+
297
+ # -------------------------------------------------------------------------
298
+ # Internal helpers
299
+ # -------------------------------------------------------------------------
300
+
301
+ def _configure_ports_on_component(self, stack: LayerStack) -> None: # noqa: ARG002
302
+ """Configure ports on the component using legacy functions."""
303
+ from gsim.palace.ports import (
304
+ configure_cpw_port,
305
+ configure_inplane_port,
306
+ configure_via_port,
307
+ )
308
+
309
+ component = self.geometry.component if self.geometry else None
310
+ if component is None:
311
+ raise ValueError("No component set")
312
+
313
+ # Configure regular ports
314
+ for port_config in self.ports:
315
+ if port_config.name is None:
316
+ continue
317
+
318
+ # Find matching gdsfactory port
319
+ gf_port = None
320
+ for p in component.ports:
321
+ if p.name == port_config.name:
322
+ gf_port = p
323
+ break
324
+
325
+ if gf_port is None:
326
+ raise ValueError(
327
+ f"Port '{port_config.name}' not found on component. "
328
+ f"Available ports: {[p.name for p in component.ports]}"
329
+ )
330
+
331
+ if port_config.geometry == "inplane" and port_config.layer is not None:
332
+ configure_inplane_port(
333
+ gf_port,
334
+ layer=port_config.layer,
335
+ length=port_config.length or gf_port.width,
336
+ impedance=port_config.impedance,
337
+ excited=port_config.excited,
338
+ )
339
+ elif port_config.geometry == "via" and (
340
+ port_config.from_layer is not None and port_config.to_layer is not None
341
+ ):
342
+ configure_via_port(
343
+ gf_port,
344
+ from_layer=port_config.from_layer,
345
+ to_layer=port_config.to_layer,
346
+ impedance=port_config.impedance,
347
+ excited=port_config.excited,
348
+ )
349
+
350
+ # Configure CPW ports
351
+ for cpw_config in self.cpw_ports:
352
+ # Find upper port
353
+ port_upper = None
354
+ for p in component.ports:
355
+ if p.name == cpw_config.upper:
356
+ port_upper = p
357
+ break
358
+ if port_upper is None:
359
+ raise ValueError(
360
+ f"CPW upper port '{cpw_config.upper}' not found. "
361
+ f"Available: {[p.name for p in component.ports]}"
362
+ )
363
+
364
+ # Find lower port
365
+ port_lower = None
366
+ for p in component.ports:
367
+ if p.name == cpw_config.lower:
368
+ port_lower = p
369
+ break
370
+ if port_lower is None:
371
+ raise ValueError(
372
+ f"CPW lower port '{cpw_config.lower}' not found. "
373
+ f"Available: {[p.name for p in component.ports]}"
374
+ )
375
+
376
+ configure_cpw_port(
377
+ port_upper=port_upper,
378
+ port_lower=port_lower,
379
+ layer=cpw_config.layer,
380
+ length=cpw_config.length,
381
+ impedance=cpw_config.impedance,
382
+ excited=cpw_config.excited,
383
+ cpw_name=cpw_config.name,
384
+ )
385
+
386
+ self._configured_ports = True
387
+
388
+ def _generate_mesh_internal(
389
+ self,
390
+ output_dir: Path,
391
+ mesh_config: MeshConfig,
392
+ ports: list,
393
+ driven_config: Any,
394
+ model_name: str,
395
+ verbose: bool,
396
+ write_config: bool = True,
397
+ ) -> SimulationResult:
398
+ """Internal mesh generation."""
399
+ from gsim.palace.mesh import MeshConfig as LegacyMeshConfig
400
+ from gsim.palace.mesh import generate_mesh
401
+
402
+ component = self.geometry.component if self.geometry else None
403
+
404
+ # Get effective fmax from driven config if mesh doesn't specify
405
+ effective_fmax = mesh_config.fmax
406
+ if driven_config is not None and mesh_config.fmax == 100e9:
407
+ effective_fmax = driven_config.fmax
408
+
409
+ legacy_mesh_config = LegacyMeshConfig(
410
+ refined_mesh_size=mesh_config.refined_mesh_size,
411
+ max_mesh_size=mesh_config.max_mesh_size,
412
+ cells_per_wavelength=mesh_config.cells_per_wavelength,
413
+ margin=mesh_config.margin,
414
+ air_above=mesh_config.air_above,
415
+ fmax=effective_fmax,
416
+ show_gui=mesh_config.show_gui,
417
+ preview_only=mesh_config.preview_only,
418
+ )
419
+
420
+ # Resolve stack
421
+ stack = self._resolve_stack()
422
+
423
+ if verbose:
424
+ logger.info("Generating mesh in %s", output_dir)
425
+
426
+ mesh_result = generate_mesh(
427
+ component=component,
428
+ stack=stack,
429
+ ports=ports,
430
+ output_dir=output_dir,
431
+ config=legacy_mesh_config,
432
+ model_name=model_name,
433
+ driven_config=driven_config,
434
+ write_config=write_config,
435
+ )
436
+
437
+ # Store mesh_result for deferred config generation
438
+ self._last_mesh_result = mesh_result
439
+ self._last_ports = ports
440
+
441
+ return SimulationResult(
442
+ mesh_path=mesh_result.mesh_path,
443
+ output_dir=output_dir,
444
+ config_path=mesh_result.config_path,
445
+ port_info=mesh_result.port_info,
446
+ mesh_stats=mesh_result.mesh_stats,
447
+ )
448
+
449
+ def _get_ports_for_preview(self, stack: LayerStack) -> list:
450
+ """Get ports for preview."""
451
+ from gsim.palace.ports import extract_ports
452
+
453
+ component = self.geometry.component if self.geometry else None
454
+ self._configure_ports_on_component(stack)
455
+ return extract_ports(component, stack)
456
+
457
+ # -------------------------------------------------------------------------
458
+ # Preview
459
+ # -------------------------------------------------------------------------
460
+
461
+ def preview(
462
+ self,
463
+ *,
464
+ preset: Literal["coarse", "default", "fine"] | None = None,
465
+ refined_mesh_size: float | None = None,
466
+ max_mesh_size: float | None = None,
467
+ margin: float | None = None,
468
+ air_above: float | None = None,
469
+ fmax: float | None = None,
470
+ show_gui: bool = True,
471
+ ) -> None:
472
+ """Preview the mesh without running simulation.
473
+
474
+ Opens the gmsh GUI to visualize the mesh interactively.
475
+
476
+ Args:
477
+ preset: Mesh quality preset ("coarse", "default", "fine")
478
+ refined_mesh_size: Mesh size near conductors (um)
479
+ max_mesh_size: Max mesh size in air/dielectric (um)
480
+ margin: XY margin around design (um)
481
+ air_above: Air above top metal (um)
482
+ fmax: Max frequency for mesh sizing (Hz)
483
+ show_gui: Show gmsh GUI for interactive preview
484
+
485
+ Example:
486
+ >>> sim.preview(preset="fine", show_gui=True)
487
+ """
488
+ from gsim.palace.mesh import MeshConfig as LegacyMeshConfig
489
+ from gsim.palace.mesh import generate_mesh
490
+
491
+ component = self.geometry.component if self.geometry else None
492
+
493
+ # Validate configuration
494
+ validation = self.validate_config()
495
+ if not validation.valid:
496
+ raise ValueError("Invalid configuration:\n" + "\n".join(validation.errors))
497
+
498
+ # Build mesh config
499
+ mesh_config = self._build_mesh_config(
500
+ preset=preset,
501
+ refined_mesh_size=refined_mesh_size,
502
+ max_mesh_size=max_mesh_size,
503
+ margin=margin,
504
+ air_above=air_above,
505
+ fmax=fmax,
506
+ show_gui=show_gui,
507
+ )
508
+
509
+ # Resolve stack
510
+ stack = self._resolve_stack()
511
+
512
+ # Get ports
513
+ ports = self._get_ports_for_preview(stack)
514
+
515
+ # Build legacy mesh config with preview mode
516
+ legacy_mesh_config = LegacyMeshConfig(
517
+ refined_mesh_size=mesh_config.refined_mesh_size,
518
+ max_mesh_size=mesh_config.max_mesh_size,
519
+ cells_per_wavelength=mesh_config.cells_per_wavelength,
520
+ margin=mesh_config.margin,
521
+ air_above=mesh_config.air_above,
522
+ fmax=mesh_config.fmax,
523
+ show_gui=show_gui,
524
+ preview_only=True,
525
+ )
526
+
527
+ # Generate mesh in temp directory
528
+ with tempfile.TemporaryDirectory() as tmpdir:
529
+ generate_mesh(
530
+ component=component,
531
+ stack=stack,
532
+ ports=ports,
533
+ output_dir=tmpdir,
534
+ config=legacy_mesh_config,
535
+ )
536
+
537
+ # -------------------------------------------------------------------------
538
+ # Mesh generation
539
+ # -------------------------------------------------------------------------
540
+
541
+ def mesh(
542
+ self,
543
+ *,
544
+ preset: Literal["coarse", "default", "fine"] | None = None,
545
+ refined_mesh_size: float | None = None,
546
+ max_mesh_size: float | None = None,
547
+ margin: float | None = None,
548
+ air_above: float | None = None,
549
+ fmax: float | None = None,
550
+ show_gui: bool = False,
551
+ model_name: str = "palace",
552
+ verbose: bool = True,
553
+ ) -> SimulationResult:
554
+ """Generate the mesh for Palace simulation.
555
+
556
+ Only generates the mesh file (palace.msh). Config is generated
557
+ separately with write_config().
558
+
559
+ Requires set_output_dir() to be called first.
560
+
561
+ Args:
562
+ preset: Mesh quality preset ("coarse", "default", "fine")
563
+ refined_mesh_size: Mesh size near conductors (um), overrides preset
564
+ max_mesh_size: Max mesh size in air/dielectric (um), overrides preset
565
+ margin: XY margin around design (um), overrides preset
566
+ air_above: Air above top metal (um), overrides preset
567
+ fmax: Max frequency for mesh sizing (Hz), overrides preset
568
+ show_gui: Show gmsh GUI during meshing
569
+ model_name: Base name for output files
570
+ verbose: Print progress messages
571
+
572
+ Returns:
573
+ SimulationResult with mesh path
574
+
575
+ Raises:
576
+ ValueError: If output_dir not set or configuration is invalid
577
+
578
+ Example:
579
+ >>> sim.set_output_dir("./sim")
580
+ >>> result = sim.mesh(preset="fine")
581
+ >>> print(f"Mesh saved to: {result.mesh_path}")
582
+ """
583
+ from gsim.palace.ports import extract_ports
584
+
585
+ if self._output_dir is None:
586
+ raise ValueError("Output directory not set. Call set_output_dir() first.")
587
+
588
+ component = self.geometry.component if self.geometry else None
589
+
590
+ # Build mesh config
591
+ mesh_config = self._build_mesh_config(
592
+ preset=preset,
593
+ refined_mesh_size=refined_mesh_size,
594
+ max_mesh_size=max_mesh_size,
595
+ margin=margin,
596
+ air_above=air_above,
597
+ fmax=fmax,
598
+ show_gui=show_gui,
599
+ )
600
+
601
+ # Validate configuration
602
+ validation = self.validate_config()
603
+ if not validation.valid:
604
+ raise ValueError("Invalid configuration:\n" + "\n".join(validation.errors))
605
+
606
+ output_dir = self._output_dir
607
+
608
+ # Resolve stack and configure ports
609
+ stack = self._resolve_stack()
610
+ self._configure_ports_on_component(stack)
611
+
612
+ # Extract ports
613
+ palace_ports = extract_ports(component, stack)
614
+
615
+ # Generate mesh (config is written separately by simulate() or write_config())
616
+ return self._generate_mesh_internal(
617
+ output_dir=output_dir,
618
+ mesh_config=mesh_config,
619
+ ports=palace_ports,
620
+ driven_config=self.driven,
621
+ model_name=model_name,
622
+ verbose=verbose,
623
+ write_config=False,
624
+ )
625
+
626
+ def write_config(self) -> Path:
627
+ """Write Palace config.json after mesh generation.
628
+
629
+ Use this when mesh() was called with write_config=False.
630
+
631
+ Returns:
632
+ Path to the generated config.json
633
+
634
+ Raises:
635
+ ValueError: If mesh() hasn't been called yet
636
+
637
+ Example:
638
+ >>> result = sim.mesh("./sim", write_config=False)
639
+ >>> config_path = sim.write_config()
640
+ """
641
+ from gsim.palace.mesh.generator import write_config as gen_write_config
642
+
643
+ if self._last_mesh_result is None:
644
+ raise ValueError("No mesh result. Call mesh() first.")
645
+
646
+ if not self._last_mesh_result.groups:
647
+ raise ValueError(
648
+ "Mesh result has no groups data. "
649
+ "Was mesh() called with write_config=True already?"
650
+ )
651
+
652
+ stack = self._resolve_stack()
653
+ config_path = gen_write_config(
654
+ mesh_result=self._last_mesh_result,
655
+ stack=stack,
656
+ ports=self._last_ports,
657
+ driven_config=self.driven,
658
+ )
659
+
660
+ return config_path
661
+
662
+ # -------------------------------------------------------------------------
663
+ # Simulation
664
+ # -------------------------------------------------------------------------
665
+
666
+ def simulate(
667
+ self,
668
+ *,
669
+ verbose: bool = True,
670
+ ) -> dict[str, Path]:
671
+ """Run simulation on GDSFactory+ cloud.
672
+
673
+ Requires mesh() and write_config() to be called first.
674
+
675
+ Args:
676
+ verbose: Print progress messages
677
+
678
+ Returns:
679
+ Dict mapping result filenames to local paths
680
+
681
+ Raises:
682
+ ValueError: If output_dir not set
683
+ FileNotFoundError: If mesh or config files don't exist
684
+ RuntimeError: If simulation fails
685
+
686
+ Example:
687
+ >>> results = sim.simulate()
688
+ >>> print(f"S-params saved to: {results['port-S.csv']}")
689
+ """
690
+ from gsim.gcloud import run_simulation
691
+
692
+ if self._output_dir is None:
693
+ raise ValueError("Output directory not set. Call set_output_dir() first.")
694
+
695
+ output_dir = self._output_dir
696
+
697
+ return run_simulation(
698
+ output_dir,
699
+ job_type="palace",
700
+ verbose=verbose,
701
+ )
702
+
703
+ def simulate_local(
704
+ self,
705
+ *,
706
+ verbose: bool = True,
707
+ ) -> dict[str, Path]:
708
+ """Run simulation locally using Palace.
709
+
710
+ Requires mesh() and write_config() to be called first,
711
+ and Palace to be installed locally.
712
+
713
+ Args:
714
+ verbose: Print progress messages
715
+
716
+ Returns:
717
+ Dict mapping result filenames to local paths
718
+
719
+ Raises:
720
+ NotImplementedError: Local simulation is not yet implemented
721
+ """
722
+ raise NotImplementedError(
723
+ "Local simulation is not yet implemented. "
724
+ "Use simulate() to run on GDSFactory+ cloud."
725
+ )
726
+
727
+
728
+ __all__ = ["DrivenSim"]