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,622 @@
1
+ """Electrostatic simulation class for capacitance extraction.
2
+
3
+ This module provides the ElectrostaticSim class for extracting
4
+ capacitance matrices between terminals.
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
+ ElectrostaticConfig,
23
+ MaterialConfig,
24
+ MeshConfig,
25
+ NumericalConfig,
26
+ SimulationResult,
27
+ TerminalConfig,
28
+ ValidationResult,
29
+ )
30
+
31
+ if TYPE_CHECKING:
32
+ from gdsfactory.component import Component
33
+
34
+
35
+ class ElectrostaticSim(PalaceSimMixin, BaseModel):
36
+ """Electrostatic simulation for capacitance matrix extraction.
37
+
38
+ This class configures and runs electrostatic simulations to extract
39
+ the capacitance matrix between conductor terminals. Unlike driven
40
+ and eigenmode simulations, this does not use ports. Uses composition
41
+ (no inheritance) with shared Geometry and Stack components from gsim.common.
42
+
43
+ Example:
44
+ >>> from gsim.palace import ElectrostaticSim
45
+ >>>
46
+ >>> sim = ElectrostaticSim()
47
+ >>> sim.set_geometry(component)
48
+ >>> sim.set_stack(air_above=300.0)
49
+ >>> sim.add_terminal("T1", layer="topmetal2")
50
+ >>> sim.add_terminal("T2", layer="topmetal2")
51
+ >>> sim.set_electrostatic()
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
+ terminals: List of terminal configurations
59
+ electrostatic: Electrostatic 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
+ # Terminal configurations (no ports in electrostatic)
74
+ terminals: list[TerminalConfig] = Field(default_factory=list)
75
+
76
+ # Electrostatic simulation config
77
+ electrostatic: ElectrostaticConfig = Field(default_factory=ElectrostaticConfig)
78
+
79
+ # Material overrides and numerical config
80
+ materials: dict[str, MaterialConfig] = Field(default_factory=dict)
81
+ numerical: NumericalConfig = Field(default_factory=NumericalConfig)
82
+
83
+ # Stack configuration (stored as kwargs until resolved)
84
+ _stack_kwargs: dict[str, Any] = PrivateAttr(default_factory=dict)
85
+
86
+ # Internal state
87
+ _output_dir: Path | None = PrivateAttr(default=None)
88
+ _configured_terminals: bool = PrivateAttr(default=False)
89
+
90
+ # -------------------------------------------------------------------------
91
+ # Geometry methods
92
+ # -------------------------------------------------------------------------
93
+
94
+ def set_geometry(self, component: Component) -> None:
95
+ """Set the gdsfactory component for simulation.
96
+
97
+ Args:
98
+ component: gdsfactory Component to simulate
99
+
100
+ Example:
101
+ >>> sim.set_geometry(my_component)
102
+ """
103
+ self.geometry = Geometry(component=component)
104
+
105
+ @property
106
+ def component(self) -> Component | None:
107
+ """Get the current component (for backward compatibility)."""
108
+ return self.geometry.component if self.geometry else None
109
+
110
+ @property
111
+ def _component(self) -> Component | None:
112
+ """Internal component access (backward compatibility)."""
113
+ return self.component
114
+
115
+ # -------------------------------------------------------------------------
116
+ # Stack methods
117
+ # -------------------------------------------------------------------------
118
+
119
+ def set_stack(
120
+ self,
121
+ *,
122
+ yaml_path: str | Path | None = None,
123
+ air_above: float = 200.0,
124
+ substrate_thickness: float = 2.0,
125
+ include_substrate: bool = False,
126
+ **kwargs,
127
+ ) -> None:
128
+ """Configure the layer stack.
129
+
130
+ Args:
131
+ yaml_path: Path to custom YAML stack file
132
+ air_above: Air box height above top metal in um
133
+ substrate_thickness: Thickness below z=0 in um
134
+ include_substrate: Include lossy silicon substrate
135
+ **kwargs: Additional args passed to extract_layer_stack
136
+
137
+ Example:
138
+ >>> sim.set_stack(air_above=300.0, substrate_thickness=2.0)
139
+ """
140
+ self._stack_kwargs = {
141
+ "yaml_path": yaml_path,
142
+ "air_above": air_above,
143
+ "substrate_thickness": substrate_thickness,
144
+ "include_substrate": include_substrate,
145
+ **kwargs,
146
+ }
147
+ self.stack = None
148
+
149
+ # -------------------------------------------------------------------------
150
+ # Terminal methods
151
+ # -------------------------------------------------------------------------
152
+
153
+ def add_terminal(
154
+ self,
155
+ name: str,
156
+ *,
157
+ layer: str,
158
+ ) -> None:
159
+ """Add a terminal for capacitance extraction.
160
+
161
+ Terminals define conductor surfaces for capacitance matrix extraction.
162
+
163
+ Args:
164
+ name: Terminal name
165
+ layer: Target conductor layer
166
+
167
+ Example:
168
+ >>> sim.add_terminal("T1", layer="topmetal2")
169
+ >>> sim.add_terminal("T2", layer="topmetal2")
170
+ """
171
+ # Remove existing terminal with same name
172
+ self.terminals = [t for t in self.terminals if t.name != name]
173
+ self.terminals.append(
174
+ TerminalConfig(
175
+ name=name,
176
+ layer=layer,
177
+ )
178
+ )
179
+
180
+ # -------------------------------------------------------------------------
181
+ # Electrostatic configuration
182
+ # -------------------------------------------------------------------------
183
+
184
+ def set_electrostatic(
185
+ self,
186
+ *,
187
+ save_fields: int = 0,
188
+ ) -> None:
189
+ """Configure electrostatic simulation.
190
+
191
+ Args:
192
+ save_fields: Number of field solutions to save
193
+
194
+ Example:
195
+ >>> sim.set_electrostatic(save_fields=1)
196
+ """
197
+ self.electrostatic = ElectrostaticConfig(
198
+ save_fields=save_fields,
199
+ )
200
+
201
+ # -------------------------------------------------------------------------
202
+ # Material methods
203
+ # -------------------------------------------------------------------------
204
+
205
+ def set_material(
206
+ self,
207
+ name: str,
208
+ *,
209
+ type: Literal["conductor", "dielectric", "semiconductor"] | None = None,
210
+ conductivity: float | None = None,
211
+ permittivity: float | None = None,
212
+ loss_tangent: float | None = None,
213
+ ) -> None:
214
+ """Override or add material properties.
215
+
216
+ Args:
217
+ name: Material name
218
+ type: Material type (conductor, dielectric, semiconductor)
219
+ conductivity: Conductivity in S/m (for conductors)
220
+ permittivity: Relative permittivity (for dielectrics)
221
+ loss_tangent: Dielectric loss tangent
222
+
223
+ Example:
224
+ >>> sim.set_material("aluminum", type="conductor", conductivity=3.8e7)
225
+ """
226
+ if type is None:
227
+ if conductivity is not None and conductivity > 1e4:
228
+ type = "conductor"
229
+ elif permittivity is not None:
230
+ type = "dielectric"
231
+ else:
232
+ type = "dielectric"
233
+
234
+ self.materials[name] = MaterialConfig(
235
+ type=type,
236
+ conductivity=conductivity,
237
+ permittivity=permittivity,
238
+ loss_tangent=loss_tangent,
239
+ )
240
+
241
+ def set_numerical(
242
+ self,
243
+ *,
244
+ order: int = 2,
245
+ tolerance: float = 1e-6,
246
+ max_iterations: int = 400,
247
+ solver_type: Literal["Default", "SuperLU", "STRUMPACK", "MUMPS"] = "Default",
248
+ preconditioner: Literal["Default", "AMS", "BoomerAMG"] = "Default",
249
+ device: Literal["CPU", "GPU"] = "CPU",
250
+ num_processors: int | None = None,
251
+ ) -> None:
252
+ """Configure numerical solver parameters.
253
+
254
+ Args:
255
+ order: Finite element order (1-4)
256
+ tolerance: Linear solver tolerance
257
+ max_iterations: Maximum solver iterations
258
+ solver_type: Linear solver type
259
+ preconditioner: Preconditioner type
260
+ device: Compute device (CPU or GPU)
261
+ num_processors: Number of processors (None = auto)
262
+
263
+ Example:
264
+ >>> sim.set_numerical(order=3, tolerance=1e-8)
265
+ """
266
+ self.numerical = NumericalConfig(
267
+ order=order,
268
+ tolerance=tolerance,
269
+ max_iterations=max_iterations,
270
+ solver_type=solver_type,
271
+ preconditioner=preconditioner,
272
+ device=device,
273
+ num_processors=num_processors,
274
+ )
275
+
276
+ # -------------------------------------------------------------------------
277
+ # Validation
278
+ # -------------------------------------------------------------------------
279
+
280
+ def validate(self) -> ValidationResult:
281
+ """Validate the simulation configuration.
282
+
283
+ Returns:
284
+ ValidationResult with validation status and messages
285
+ """
286
+ errors = []
287
+ warnings_list = []
288
+
289
+ # Check geometry
290
+ if self.geometry is None:
291
+ errors.append("No component set. Call set_geometry(component) first.")
292
+
293
+ # Check stack
294
+ if self.stack is None and not self._stack_kwargs:
295
+ warnings_list.append(
296
+ "No stack configured. Will use active PDK with defaults."
297
+ )
298
+
299
+ # Electrostatic requires at least 2 terminals
300
+ if len(self.terminals) < 2:
301
+ errors.append(
302
+ "Electrostatic simulation requires at least 2 terminals. "
303
+ "Call add_terminal() to add terminals."
304
+ )
305
+
306
+ # Validate terminal configurations
307
+ for terminal in self.terminals:
308
+ if not terminal.layer:
309
+ errors.append(f"Terminal '{terminal.name}': 'layer' is required")
310
+
311
+ valid = len(errors) == 0
312
+ return ValidationResult(valid=valid, errors=errors, warnings=warnings_list)
313
+
314
+ # -------------------------------------------------------------------------
315
+ # Internal helpers
316
+ # -------------------------------------------------------------------------
317
+
318
+ def _resolve_stack(self) -> LayerStack:
319
+ """Resolve the layer stack from PDK or YAML."""
320
+ from gsim.common.stack import get_stack
321
+
322
+ yaml_path = self._stack_kwargs.pop("yaml_path", None)
323
+ stack = get_stack(yaml_path=yaml_path, **self._stack_kwargs)
324
+ self._stack_kwargs["yaml_path"] = yaml_path
325
+
326
+ for name, props in self.materials.items():
327
+ stack.materials[name] = props.to_dict()
328
+
329
+ self.stack = stack
330
+ return stack
331
+
332
+ def _build_mesh_config(
333
+ self,
334
+ preset: Literal["coarse", "default", "fine"] | None,
335
+ refined_mesh_size: float | None,
336
+ max_mesh_size: float | None,
337
+ margin: float | None,
338
+ air_above: float | None,
339
+ fmax: float | None,
340
+ show_gui: bool,
341
+ ) -> MeshConfig:
342
+ """Build mesh config from preset with optional overrides."""
343
+ if preset == "coarse":
344
+ mesh_config = MeshConfig.coarse()
345
+ elif preset == "fine":
346
+ mesh_config = MeshConfig.fine()
347
+ else:
348
+ mesh_config = MeshConfig.default()
349
+
350
+ overrides = []
351
+ if preset is not None:
352
+ if refined_mesh_size is not None:
353
+ overrides.append(f"refined_mesh_size={refined_mesh_size}")
354
+ if max_mesh_size is not None:
355
+ overrides.append(f"max_mesh_size={max_mesh_size}")
356
+ if margin is not None:
357
+ overrides.append(f"margin={margin}")
358
+ if air_above is not None:
359
+ overrides.append(f"air_above={air_above}")
360
+ if fmax is not None:
361
+ overrides.append(f"fmax={fmax}")
362
+
363
+ if overrides:
364
+ warnings.warn(
365
+ f"Preset '{preset}' values overridden by: {', '.join(overrides)}",
366
+ stacklevel=4,
367
+ )
368
+
369
+ if refined_mesh_size is not None:
370
+ mesh_config.refined_mesh_size = refined_mesh_size
371
+ if max_mesh_size is not None:
372
+ mesh_config.max_mesh_size = max_mesh_size
373
+ if margin is not None:
374
+ mesh_config.margin = margin
375
+ if air_above is not None:
376
+ mesh_config.air_above = air_above
377
+ if fmax is not None:
378
+ mesh_config.fmax = fmax
379
+ mesh_config.show_gui = show_gui
380
+
381
+ return mesh_config
382
+
383
+ def _generate_mesh_internal(
384
+ self,
385
+ output_dir: Path,
386
+ mesh_config: MeshConfig,
387
+ model_name: str,
388
+ verbose: bool,
389
+ ) -> SimulationResult:
390
+ """Internal mesh generation."""
391
+ from gsim.palace.mesh import MeshConfig as LegacyMeshConfig
392
+ from gsim.palace.mesh import generate_mesh
393
+
394
+ component = self.geometry.component if self.geometry else None
395
+
396
+ legacy_mesh_config = LegacyMeshConfig(
397
+ refined_mesh_size=mesh_config.refined_mesh_size,
398
+ max_mesh_size=mesh_config.max_mesh_size,
399
+ cells_per_wavelength=mesh_config.cells_per_wavelength,
400
+ margin=mesh_config.margin,
401
+ air_above=mesh_config.air_above,
402
+ fmax=mesh_config.fmax,
403
+ show_gui=mesh_config.show_gui,
404
+ preview_only=mesh_config.preview_only,
405
+ )
406
+
407
+ stack = self._resolve_stack()
408
+
409
+ if verbose:
410
+ logger.info("Generating mesh in %s", output_dir)
411
+
412
+ mesh_result = generate_mesh(
413
+ component=component,
414
+ stack=stack,
415
+ ports=[], # No ports for electrostatic
416
+ output_dir=output_dir,
417
+ config=legacy_mesh_config,
418
+ model_name=model_name,
419
+ driven_config=None, # No driven config for electrostatic
420
+ )
421
+
422
+ return SimulationResult(
423
+ mesh_path=mesh_result.mesh_path,
424
+ output_dir=output_dir,
425
+ config_path=mesh_result.config_path,
426
+ port_info=mesh_result.port_info,
427
+ mesh_stats=mesh_result.mesh_stats,
428
+ )
429
+
430
+ def _get_ports_for_preview(self, stack: LayerStack) -> list:
431
+ """Get ports for preview (none for electrostatic)."""
432
+ return []
433
+
434
+ # -------------------------------------------------------------------------
435
+ # Preview
436
+ # -------------------------------------------------------------------------
437
+
438
+ def preview(
439
+ self,
440
+ *,
441
+ preset: Literal["coarse", "default", "fine"] | None = None,
442
+ refined_mesh_size: float | None = None,
443
+ max_mesh_size: float | None = None,
444
+ margin: float | None = None,
445
+ air_above: float | None = None,
446
+ fmax: float | None = None,
447
+ show_gui: bool = True,
448
+ ) -> None:
449
+ """Preview the mesh without running simulation.
450
+
451
+ Args:
452
+ preset: Mesh quality preset ("coarse", "default", "fine")
453
+ refined_mesh_size: Mesh size near conductors (um)
454
+ max_mesh_size: Max mesh size in air/dielectric (um)
455
+ margin: XY margin around design (um)
456
+ air_above: Air above top metal (um)
457
+ fmax: Max frequency for mesh sizing (Hz)
458
+ show_gui: Show gmsh GUI for interactive preview
459
+
460
+ Example:
461
+ >>> sim.preview(preset="fine", show_gui=True)
462
+ """
463
+ from gsim.palace.mesh import MeshConfig as LegacyMeshConfig
464
+ from gsim.palace.mesh import generate_mesh
465
+
466
+ component = self.geometry.component if self.geometry else None
467
+
468
+ validation = self.validate()
469
+ if not validation.valid:
470
+ raise ValueError(
471
+ f"Invalid configuration:\n" + "\n".join(validation.errors)
472
+ )
473
+
474
+ mesh_config = self._build_mesh_config(
475
+ preset=preset,
476
+ refined_mesh_size=refined_mesh_size,
477
+ max_mesh_size=max_mesh_size,
478
+ margin=margin,
479
+ air_above=air_above,
480
+ fmax=fmax,
481
+ show_gui=show_gui,
482
+ )
483
+
484
+ stack = self._resolve_stack()
485
+
486
+ legacy_mesh_config = LegacyMeshConfig(
487
+ refined_mesh_size=mesh_config.refined_mesh_size,
488
+ max_mesh_size=mesh_config.max_mesh_size,
489
+ cells_per_wavelength=mesh_config.cells_per_wavelength,
490
+ margin=mesh_config.margin,
491
+ air_above=mesh_config.air_above,
492
+ fmax=mesh_config.fmax,
493
+ show_gui=show_gui,
494
+ preview_only=True,
495
+ )
496
+
497
+ with tempfile.TemporaryDirectory() as tmpdir:
498
+ generate_mesh(
499
+ component=component,
500
+ stack=stack,
501
+ ports=[],
502
+ output_dir=tmpdir,
503
+ config=legacy_mesh_config,
504
+ )
505
+
506
+ # -------------------------------------------------------------------------
507
+ # Convenience methods
508
+ # -------------------------------------------------------------------------
509
+
510
+ def show_stack(self) -> None:
511
+ """Print the layer stack table."""
512
+ from gsim.common.stack import print_stack_table
513
+
514
+ if self.stack is None:
515
+ self._resolve_stack()
516
+
517
+ if self.stack is not None:
518
+ print_stack_table(self.stack)
519
+
520
+ def plot_stack(self) -> None:
521
+ """Plot the layer stack visualization."""
522
+ from gsim.common.stack import plot_stack
523
+
524
+ if self.stack is None:
525
+ self._resolve_stack()
526
+
527
+ if self.stack is not None:
528
+ plot_stack(self.stack)
529
+
530
+ # -------------------------------------------------------------------------
531
+ # Mesh generation
532
+ # -------------------------------------------------------------------------
533
+
534
+ def mesh(
535
+ self,
536
+ output_dir: str | Path,
537
+ *,
538
+ preset: Literal["coarse", "default", "fine"] | None = None,
539
+ refined_mesh_size: float | None = None,
540
+ max_mesh_size: float | None = None,
541
+ margin: float | None = None,
542
+ air_above: float | None = None,
543
+ fmax: float | None = None,
544
+ show_gui: bool = False,
545
+ model_name: str = "palace",
546
+ verbose: bool = True,
547
+ ) -> SimulationResult:
548
+ """Generate the mesh and configuration files.
549
+
550
+ Args:
551
+ output_dir: Directory for output files
552
+ preset: Mesh quality preset ("coarse", "default", "fine")
553
+ refined_mesh_size: Mesh size near conductors (um)
554
+ max_mesh_size: Max mesh size in air/dielectric (um)
555
+ margin: XY margin around design (um)
556
+ air_above: Air above top metal (um)
557
+ fmax: Max frequency for mesh sizing (Hz) - less relevant for electrostatic
558
+ show_gui: Show gmsh GUI during meshing
559
+ model_name: Base name for output files
560
+ verbose: Print progress messages
561
+
562
+ Returns:
563
+ SimulationResult with mesh and config paths
564
+ """
565
+ mesh_config = self._build_mesh_config(
566
+ preset=preset,
567
+ refined_mesh_size=refined_mesh_size,
568
+ max_mesh_size=max_mesh_size,
569
+ margin=margin,
570
+ air_above=air_above,
571
+ fmax=fmax,
572
+ show_gui=show_gui,
573
+ )
574
+
575
+ validation = self.validate()
576
+ if not validation.valid:
577
+ raise ValueError(
578
+ f"Invalid configuration:\n" + "\n".join(validation.errors)
579
+ )
580
+
581
+ output_dir = Path(output_dir)
582
+ output_dir.mkdir(parents=True, exist_ok=True)
583
+ self._output_dir = output_dir
584
+
585
+ self._resolve_stack()
586
+
587
+ return self._generate_mesh_internal(
588
+ output_dir=output_dir,
589
+ mesh_config=mesh_config,
590
+ model_name=model_name,
591
+ verbose=verbose,
592
+ )
593
+
594
+ # -------------------------------------------------------------------------
595
+ # Simulation
596
+ # -------------------------------------------------------------------------
597
+
598
+ def simulate(
599
+ self,
600
+ output_dir: str | Path | None = None,
601
+ *,
602
+ verbose: bool = True,
603
+ ) -> dict[str, Path]:
604
+ """Run electrostatic simulation on GDSFactory+ cloud.
605
+
606
+ Args:
607
+ output_dir: Directory containing mesh files
608
+ verbose: Print progress messages
609
+
610
+ Returns:
611
+ Dict mapping result filenames to local paths
612
+
613
+ Raises:
614
+ NotImplementedError: Electrostatic is not yet fully implemented
615
+ """
616
+ raise NotImplementedError(
617
+ "Electrostatic simulation is not yet fully implemented on cloud. "
618
+ "Use DrivenSim for S-parameter extraction."
619
+ )
620
+
621
+
622
+ __all__ = ["ElectrostaticSim"]