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.
@@ -0,0 +1,398 @@
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
+ 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
+ ElectrostaticConfig,
20
+ MaterialConfig,
21
+ MeshConfig,
22
+ NumericalConfig,
23
+ SimulationResult,
24
+ TerminalConfig,
25
+ ValidationResult,
26
+ )
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+
31
+ class ElectrostaticSim(PalaceSimMixin, BaseModel):
32
+ """Electrostatic simulation for capacitance matrix extraction.
33
+
34
+ This class configures and runs electrostatic simulations to extract
35
+ the capacitance matrix between conductor terminals. Unlike driven
36
+ and eigenmode simulations, this does not use ports.
37
+
38
+ Example:
39
+ >>> from gsim.palace import ElectrostaticSim
40
+ >>>
41
+ >>> sim = ElectrostaticSim()
42
+ >>> sim.set_geometry(component)
43
+ >>> sim.set_stack(air_above=300.0)
44
+ >>> sim.add_terminal("T1", layer="topmetal2")
45
+ >>> sim.add_terminal("T2", layer="topmetal2")
46
+ >>> sim.set_electrostatic()
47
+ >>> sim.set_output_dir("./sim")
48
+ >>> sim.mesh(preset="default")
49
+ >>> results = sim.simulate()
50
+
51
+ Attributes:
52
+ geometry: Wrapped gdsfactory Component (from common)
53
+ stack: Layer stack configuration (from common)
54
+ terminals: List of terminal configurations
55
+ electrostatic: Electrostatic simulation configuration
56
+ materials: Material property overrides
57
+ numerical: Numerical solver configuration
58
+ """
59
+
60
+ model_config = ConfigDict(
61
+ validate_assignment=True,
62
+ arbitrary_types_allowed=True,
63
+ )
64
+
65
+ # Composed objects (from common)
66
+ geometry: Geometry | None = None
67
+ stack: LayerStack | None = None
68
+
69
+ # Terminal configurations (no ports in electrostatic)
70
+ terminals: list[TerminalConfig] = Field(default_factory=list)
71
+
72
+ # Electrostatic simulation config
73
+ electrostatic: ElectrostaticConfig = Field(default_factory=ElectrostaticConfig)
74
+
75
+ # Material overrides and numerical config
76
+ materials: dict[str, MaterialConfig] = Field(default_factory=dict)
77
+ numerical: NumericalConfig = Field(default_factory=NumericalConfig)
78
+
79
+ # Stack configuration (stored as kwargs until resolved)
80
+ _stack_kwargs: dict[str, Any] = PrivateAttr(default_factory=dict)
81
+
82
+ # Internal state
83
+ _output_dir: Path | None = PrivateAttr(default=None)
84
+ _configured_terminals: bool = PrivateAttr(default=False)
85
+
86
+ # -------------------------------------------------------------------------
87
+ # Terminal methods
88
+ # -------------------------------------------------------------------------
89
+
90
+ def add_terminal(
91
+ self,
92
+ name: str,
93
+ *,
94
+ layer: str,
95
+ ) -> None:
96
+ """Add a terminal for capacitance extraction.
97
+
98
+ Terminals define conductor surfaces for capacitance matrix extraction.
99
+
100
+ Args:
101
+ name: Terminal name
102
+ layer: Target conductor layer
103
+
104
+ Example:
105
+ >>> sim.add_terminal("T1", layer="topmetal2")
106
+ >>> sim.add_terminal("T2", layer="topmetal2")
107
+ """
108
+ # Remove existing terminal with same name
109
+ self.terminals = [t for t in self.terminals if t.name != name]
110
+ self.terminals.append(
111
+ TerminalConfig(
112
+ name=name,
113
+ layer=layer,
114
+ )
115
+ )
116
+
117
+ # -------------------------------------------------------------------------
118
+ # Electrostatic configuration
119
+ # -------------------------------------------------------------------------
120
+
121
+ def set_electrostatic(
122
+ self,
123
+ *,
124
+ save_fields: int = 0,
125
+ ) -> None:
126
+ """Configure electrostatic simulation.
127
+
128
+ Args:
129
+ save_fields: Number of field solutions to save
130
+
131
+ Example:
132
+ >>> sim.set_electrostatic(save_fields=1)
133
+ """
134
+ self.electrostatic = ElectrostaticConfig(
135
+ save_fields=save_fields,
136
+ )
137
+
138
+ # -------------------------------------------------------------------------
139
+ # Validation
140
+ # -------------------------------------------------------------------------
141
+
142
+ def validate_config(self) -> ValidationResult:
143
+ """Validate the simulation configuration.
144
+
145
+ Returns:
146
+ ValidationResult with validation status and messages
147
+ """
148
+ errors = []
149
+ warnings_list = []
150
+
151
+ # Check geometry
152
+ if self.geometry is None:
153
+ errors.append("No component set. Call set_geometry(component) first.")
154
+
155
+ # Check stack
156
+ if self.stack is None and not self._stack_kwargs:
157
+ warnings_list.append(
158
+ "No stack configured. Will use active PDK with defaults."
159
+ )
160
+
161
+ # Electrostatic requires at least 2 terminals
162
+ if len(self.terminals) < 2:
163
+ errors.append(
164
+ "Electrostatic simulation requires at least 2 terminals. "
165
+ "Call add_terminal() to add terminals."
166
+ )
167
+
168
+ # Validate terminal configurations
169
+ errors.extend(
170
+ f"Terminal '{terminal.name}': 'layer' is required"
171
+ for terminal in self.terminals
172
+ if not terminal.layer
173
+ )
174
+
175
+ valid = len(errors) == 0
176
+ return ValidationResult(valid=valid, errors=errors, warnings=warnings_list)
177
+
178
+ # -------------------------------------------------------------------------
179
+ # Internal helpers
180
+ # -------------------------------------------------------------------------
181
+
182
+ def _generate_mesh_internal(
183
+ self,
184
+ output_dir: Path,
185
+ mesh_config: MeshConfig,
186
+ model_name: str,
187
+ verbose: bool,
188
+ ) -> SimulationResult:
189
+ """Internal mesh generation."""
190
+ from gsim.palace.mesh import MeshConfig as LegacyMeshConfig
191
+ from gsim.palace.mesh import generate_mesh
192
+
193
+ component = self.geometry.component if self.geometry else None
194
+
195
+ legacy_mesh_config = LegacyMeshConfig(
196
+ refined_mesh_size=mesh_config.refined_mesh_size,
197
+ max_mesh_size=mesh_config.max_mesh_size,
198
+ cells_per_wavelength=mesh_config.cells_per_wavelength,
199
+ margin=mesh_config.margin,
200
+ air_above=mesh_config.air_above,
201
+ fmax=mesh_config.fmax,
202
+ show_gui=mesh_config.show_gui,
203
+ preview_only=mesh_config.preview_only,
204
+ )
205
+
206
+ stack = self._resolve_stack()
207
+
208
+ if verbose:
209
+ logger.info("Generating mesh in %s", output_dir)
210
+
211
+ mesh_result = generate_mesh(
212
+ component=component,
213
+ stack=stack,
214
+ ports=[], # No ports for electrostatic
215
+ output_dir=output_dir,
216
+ config=legacy_mesh_config,
217
+ model_name=model_name,
218
+ driven_config=None, # No driven config for electrostatic
219
+ )
220
+
221
+ return SimulationResult(
222
+ mesh_path=mesh_result.mesh_path,
223
+ output_dir=output_dir,
224
+ config_path=mesh_result.config_path,
225
+ port_info=mesh_result.port_info,
226
+ mesh_stats=mesh_result.mesh_stats,
227
+ )
228
+
229
+ # -------------------------------------------------------------------------
230
+ # Preview
231
+ # -------------------------------------------------------------------------
232
+
233
+ def preview(
234
+ self,
235
+ *,
236
+ preset: Literal["coarse", "default", "fine"] | None = None,
237
+ refined_mesh_size: float | None = None,
238
+ max_mesh_size: float | None = None,
239
+ margin: float | None = None,
240
+ air_above: float | None = None,
241
+ fmax: float | None = None,
242
+ show_gui: bool = True,
243
+ ) -> None:
244
+ """Preview the mesh without running simulation.
245
+
246
+ Args:
247
+ preset: Mesh quality preset ("coarse", "default", "fine")
248
+ refined_mesh_size: Mesh size near conductors (um)
249
+ max_mesh_size: Max mesh size in air/dielectric (um)
250
+ margin: XY margin around design (um)
251
+ air_above: Air above top metal (um)
252
+ fmax: Max frequency for mesh sizing (Hz)
253
+ show_gui: Show gmsh GUI for interactive preview
254
+
255
+ Example:
256
+ >>> sim.preview(preset="fine", show_gui=True)
257
+ """
258
+ from gsim.palace.mesh import MeshConfig as LegacyMeshConfig
259
+ from gsim.palace.mesh import generate_mesh
260
+
261
+ component = self.geometry.component if self.geometry else None
262
+
263
+ validation = self.validate_config()
264
+ if not validation.valid:
265
+ raise ValueError("Invalid configuration:\n" + "\n".join(validation.errors))
266
+
267
+ mesh_config = self._build_mesh_config(
268
+ preset=preset,
269
+ refined_mesh_size=refined_mesh_size,
270
+ max_mesh_size=max_mesh_size,
271
+ margin=margin,
272
+ air_above=air_above,
273
+ fmax=fmax,
274
+ show_gui=show_gui,
275
+ )
276
+
277
+ stack = self._resolve_stack()
278
+
279
+ legacy_mesh_config = LegacyMeshConfig(
280
+ refined_mesh_size=mesh_config.refined_mesh_size,
281
+ max_mesh_size=mesh_config.max_mesh_size,
282
+ cells_per_wavelength=mesh_config.cells_per_wavelength,
283
+ margin=mesh_config.margin,
284
+ air_above=mesh_config.air_above,
285
+ fmax=mesh_config.fmax,
286
+ show_gui=show_gui,
287
+ preview_only=True,
288
+ )
289
+
290
+ with tempfile.TemporaryDirectory() as tmpdir:
291
+ generate_mesh(
292
+ component=component,
293
+ stack=stack,
294
+ ports=[],
295
+ output_dir=tmpdir,
296
+ config=legacy_mesh_config,
297
+ )
298
+
299
+ # -------------------------------------------------------------------------
300
+ # Mesh generation
301
+ # -------------------------------------------------------------------------
302
+
303
+ def mesh(
304
+ self,
305
+ *,
306
+ preset: Literal["coarse", "default", "fine"] | None = None,
307
+ refined_mesh_size: float | None = None,
308
+ max_mesh_size: float | None = None,
309
+ margin: float | None = None,
310
+ air_above: float | None = None,
311
+ fmax: float | None = None,
312
+ show_gui: bool = False,
313
+ model_name: str = "palace",
314
+ verbose: bool = True,
315
+ ) -> SimulationResult:
316
+ """Generate the mesh for Palace simulation.
317
+
318
+ Requires set_output_dir() to be called first.
319
+
320
+ Args:
321
+ preset: Mesh quality preset ("coarse", "default", "fine")
322
+ refined_mesh_size: Mesh size near conductors (um), overrides preset
323
+ max_mesh_size: Max mesh size in air/dielectric (um), overrides preset
324
+ margin: XY margin around design (um), overrides preset
325
+ air_above: Air above top metal (um), overrides preset
326
+ fmax: Max frequency for mesh sizing (Hz) - less relevant for electrostatic
327
+ show_gui: Show gmsh GUI during meshing
328
+ model_name: Base name for output files
329
+ verbose: Print progress messages
330
+
331
+ Returns:
332
+ SimulationResult with mesh path
333
+
334
+ Raises:
335
+ ValueError: If output_dir not set or configuration is invalid
336
+
337
+ Example:
338
+ >>> sim.set_output_dir("./sim")
339
+ >>> result = sim.mesh(preset="fine")
340
+ >>> print(f"Mesh saved to: {result.mesh_path}")
341
+ """
342
+ if self._output_dir is None:
343
+ raise ValueError("Output directory not set. Call set_output_dir() first.")
344
+
345
+ mesh_config = self._build_mesh_config(
346
+ preset=preset,
347
+ refined_mesh_size=refined_mesh_size,
348
+ max_mesh_size=max_mesh_size,
349
+ margin=margin,
350
+ air_above=air_above,
351
+ fmax=fmax,
352
+ show_gui=show_gui,
353
+ )
354
+
355
+ validation = self.validate_config()
356
+ if not validation.valid:
357
+ raise ValueError("Invalid configuration:\n" + "\n".join(validation.errors))
358
+
359
+ output_dir = self._output_dir
360
+
361
+ self._resolve_stack()
362
+
363
+ return self._generate_mesh_internal(
364
+ output_dir=output_dir,
365
+ mesh_config=mesh_config,
366
+ model_name=model_name,
367
+ verbose=verbose,
368
+ )
369
+
370
+ # -------------------------------------------------------------------------
371
+ # Simulation
372
+ # -------------------------------------------------------------------------
373
+
374
+ def simulate(
375
+ self,
376
+ output_dir: str | Path | None = None,
377
+ *,
378
+ verbose: bool = True,
379
+ ) -> dict[str, Path]:
380
+ """Run electrostatic simulation on GDSFactory+ cloud.
381
+
382
+ Args:
383
+ output_dir: Directory containing mesh files
384
+ verbose: Print progress messages
385
+
386
+ Returns:
387
+ Dict mapping result filenames to local paths
388
+
389
+ Raises:
390
+ NotImplementedError: Electrostatic is not yet fully implemented
391
+ """
392
+ raise NotImplementedError(
393
+ "Electrostatic simulation is not yet fully implemented on cloud. "
394
+ "Use DrivenSim for S-parameter extraction."
395
+ )
396
+
397
+
398
+ __all__ = ["ElectrostaticSim"]
@@ -28,7 +28,16 @@ Usage:
28
28
 
29
29
  from __future__ import annotations
30
30
 
31
- from gsim.palace.mesh.generator import generate_mesh as generate_mesh_direct
31
+ from gsim.palace.mesh.generator import (
32
+ GeometryData,
33
+ write_config,
34
+ )
35
+ from gsim.palace.mesh.generator import (
36
+ MeshResult as MeshResultDirect,
37
+ )
38
+ from gsim.palace.mesh.generator import (
39
+ generate_mesh as generate_mesh_direct,
40
+ )
32
41
  from gsim.palace.mesh.pipeline import (
33
42
  GroundPlane,
34
43
  MeshConfig,
@@ -40,11 +49,14 @@ from gsim.palace.mesh.pipeline import (
40
49
  from . import gmsh_utils
41
50
 
42
51
  __all__ = [
52
+ "GeometryData",
43
53
  "GroundPlane",
44
54
  "MeshConfig",
45
55
  "MeshPreset",
46
56
  "MeshResult",
57
+ "MeshResultDirect",
47
58
  "generate_mesh",
48
59
  "generate_mesh_direct",
49
60
  "gmsh_utils",
61
+ "write_config",
50
62
  ]