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/base.py ADDED
@@ -0,0 +1,373 @@
1
+ """Base mixin for Palace simulation classes.
2
+
3
+ Provides common methods shared across all simulation types:
4
+ DrivenSim, EigenmodeSim, ElectrostaticSim.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import warnings
10
+ from pathlib import Path
11
+ from typing import TYPE_CHECKING, Any, Literal
12
+
13
+ if TYPE_CHECKING:
14
+ from gdsfactory.component import Component
15
+
16
+ from gsim.common import Geometry, LayerStack
17
+ from gsim.palace.models import MaterialConfig, MeshConfig, NumericalConfig
18
+
19
+
20
+ class PalaceSimMixin:
21
+ """Mixin providing common methods for all Palace simulation classes.
22
+
23
+ Subclasses must define these attributes (typically via Pydantic fields):
24
+ - geometry: Geometry | None
25
+ - stack: LayerStack | None
26
+ - materials: dict[str, MaterialConfig]
27
+ - numerical: NumericalConfig
28
+ - _output_dir: Path | None (private)
29
+ - _stack_kwargs: dict[str, Any] (private)
30
+ """
31
+
32
+ # Type hints for required attributes (implemented by subclasses)
33
+ geometry: Geometry | None
34
+ stack: LayerStack | None
35
+ materials: dict[str, MaterialConfig]
36
+ numerical: NumericalConfig
37
+ _output_dir: Path | None
38
+ _stack_kwargs: dict[str, Any]
39
+
40
+ # -------------------------------------------------------------------------
41
+ # Output directory
42
+ # -------------------------------------------------------------------------
43
+
44
+ def set_output_dir(self, path: str | Path) -> None:
45
+ """Set the output directory for mesh and config files.
46
+
47
+ Args:
48
+ path: Directory path for output files
49
+
50
+ Example:
51
+ >>> sim.set_output_dir("./palace-sim")
52
+ """
53
+ self._output_dir = Path(path)
54
+ self._output_dir.mkdir(parents=True, exist_ok=True)
55
+
56
+ @property
57
+ def output_dir(self) -> Path | None:
58
+ """Get the current output directory."""
59
+ return self._output_dir
60
+
61
+ # -------------------------------------------------------------------------
62
+ # Geometry methods
63
+ # -------------------------------------------------------------------------
64
+
65
+ def set_geometry(self, component: Component) -> None:
66
+ """Set the gdsfactory component for simulation.
67
+
68
+ Args:
69
+ component: gdsfactory Component to simulate
70
+
71
+ Example:
72
+ >>> sim.set_geometry(my_component)
73
+ """
74
+ from gsim.common import Geometry
75
+
76
+ self.geometry = Geometry(component=component)
77
+
78
+ @property
79
+ def component(self) -> Component | None:
80
+ """Get the current component (for backward compatibility)."""
81
+ return self.geometry.component if self.geometry else None
82
+
83
+ # Backward compatibility alias
84
+ @property
85
+ def _component(self) -> Component | None:
86
+ """Internal component access (backward compatibility)."""
87
+ return self.component
88
+
89
+ # -------------------------------------------------------------------------
90
+ # Stack methods
91
+ # -------------------------------------------------------------------------
92
+
93
+ def set_stack(
94
+ self,
95
+ *,
96
+ yaml_path: str | Path | None = None,
97
+ air_above: float = 200.0,
98
+ substrate_thickness: float = 2.0,
99
+ include_substrate: bool = False,
100
+ **kwargs,
101
+ ) -> None:
102
+ """Configure the layer stack.
103
+
104
+ If yaml_path is provided, loads stack from YAML file.
105
+ Otherwise, extracts from active PDK with given parameters.
106
+
107
+ Args:
108
+ yaml_path: Path to custom YAML stack file
109
+ air_above: Air box height above top metal in um
110
+ substrate_thickness: Thickness below z=0 in um
111
+ include_substrate: Include lossy silicon substrate
112
+ **kwargs: Additional args passed to extract_layer_stack
113
+
114
+ Example:
115
+ >>> sim.set_stack(air_above=300.0, substrate_thickness=2.0)
116
+ """
117
+ self._stack_kwargs = {
118
+ "yaml_path": yaml_path,
119
+ "air_above": air_above,
120
+ "substrate_thickness": substrate_thickness,
121
+ "include_substrate": include_substrate,
122
+ **kwargs,
123
+ }
124
+ # Stack will be resolved lazily during mesh() or simulate()
125
+ self.stack = None
126
+
127
+ # -------------------------------------------------------------------------
128
+ # Material methods
129
+ # -------------------------------------------------------------------------
130
+
131
+ def set_material(
132
+ self,
133
+ name: str,
134
+ *,
135
+ material_type: Literal["conductor", "dielectric", "semiconductor"]
136
+ | None = None,
137
+ conductivity: float | None = None,
138
+ permittivity: float | None = None,
139
+ loss_tangent: float | None = None,
140
+ ) -> None:
141
+ """Override or add material properties.
142
+
143
+ Args:
144
+ name: Material name
145
+ material_type: Material type (conductor, dielectric, semiconductor)
146
+ conductivity: Conductivity in S/m (for conductors)
147
+ permittivity: Relative permittivity (for dielectrics)
148
+ loss_tangent: Dielectric loss tangent
149
+
150
+ Example:
151
+ >>> sim.set_material(
152
+ ... "aluminum", material_type="conductor", conductivity=3.8e7
153
+ ... )
154
+ >>> sim.set_material("sio2", material_type="dielectric", permittivity=3.9)
155
+ """
156
+ from gsim.palace.models import MaterialConfig
157
+
158
+ # Determine type if not provided
159
+ resolved_type = material_type
160
+ if resolved_type is None:
161
+ if conductivity is not None and conductivity > 1e4:
162
+ resolved_type = "conductor"
163
+ elif permittivity is not None:
164
+ resolved_type = "dielectric"
165
+ else:
166
+ resolved_type = "dielectric"
167
+
168
+ self.materials[name] = MaterialConfig(
169
+ type=resolved_type,
170
+ conductivity=conductivity,
171
+ permittivity=permittivity,
172
+ loss_tangent=loss_tangent,
173
+ )
174
+
175
+ def set_numerical(
176
+ self,
177
+ *,
178
+ order: int = 2,
179
+ tolerance: float = 1e-6,
180
+ max_iterations: int = 400,
181
+ solver_type: Literal["Default", "SuperLU", "STRUMPACK", "MUMPS"] = "Default",
182
+ preconditioner: Literal["Default", "AMS", "BoomerAMG"] = "Default",
183
+ device: Literal["CPU", "GPU"] = "CPU",
184
+ num_processors: int | None = None,
185
+ ) -> None:
186
+ """Configure numerical solver parameters.
187
+
188
+ Args:
189
+ order: Finite element order (1-4)
190
+ tolerance: Linear solver tolerance
191
+ max_iterations: Maximum solver iterations
192
+ solver_type: Linear solver type
193
+ preconditioner: Preconditioner type
194
+ device: Compute device (CPU or GPU)
195
+ num_processors: Number of processors (None = auto)
196
+
197
+ Example:
198
+ >>> sim.set_numerical(order=3, tolerance=1e-8)
199
+ """
200
+ from gsim.palace.models import NumericalConfig
201
+
202
+ self.numerical = NumericalConfig(
203
+ order=order,
204
+ tolerance=tolerance,
205
+ max_iterations=max_iterations,
206
+ solver_type=solver_type,
207
+ preconditioner=preconditioner,
208
+ device=device,
209
+ num_processors=num_processors,
210
+ )
211
+
212
+ # -------------------------------------------------------------------------
213
+ # Internal helpers
214
+ # -------------------------------------------------------------------------
215
+
216
+ def _resolve_stack(self) -> LayerStack:
217
+ """Resolve the layer stack from PDK or YAML.
218
+
219
+ Returns:
220
+ Legacy LayerStack object for mesh generation
221
+ """
222
+ from gsim.common.stack import get_stack
223
+
224
+ yaml_path = self._stack_kwargs.pop("yaml_path", None)
225
+ legacy_stack = get_stack(yaml_path=yaml_path, **self._stack_kwargs)
226
+
227
+ # Restore yaml_path for potential re-resolution
228
+ self._stack_kwargs["yaml_path"] = yaml_path
229
+
230
+ # Apply material overrides
231
+ for name, props in self.materials.items():
232
+ legacy_stack.materials[name] = props.to_dict()
233
+
234
+ # Store the LayerStack
235
+ self.stack = legacy_stack
236
+
237
+ return legacy_stack
238
+
239
+ def _build_mesh_config(
240
+ self,
241
+ preset: Literal["coarse", "default", "fine"] | None,
242
+ refined_mesh_size: float | None,
243
+ max_mesh_size: float | None,
244
+ margin: float | None,
245
+ air_above: float | None,
246
+ fmax: float | None,
247
+ show_gui: bool,
248
+ ) -> MeshConfig:
249
+ """Build mesh config from preset with optional overrides."""
250
+ from gsim.palace.models import MeshConfig
251
+
252
+ # Build mesh config from preset
253
+ if preset == "coarse":
254
+ mesh_config = MeshConfig.coarse()
255
+ elif preset == "fine":
256
+ mesh_config = MeshConfig.fine()
257
+ else:
258
+ mesh_config = MeshConfig.default()
259
+
260
+ # Track overrides for warning
261
+ overrides = []
262
+ if preset is not None:
263
+ if refined_mesh_size is not None:
264
+ overrides.append(f"refined_mesh_size={refined_mesh_size}")
265
+ if max_mesh_size is not None:
266
+ overrides.append(f"max_mesh_size={max_mesh_size}")
267
+ if margin is not None:
268
+ overrides.append(f"margin={margin}")
269
+ if air_above is not None:
270
+ overrides.append(f"air_above={air_above}")
271
+ if fmax is not None:
272
+ overrides.append(f"fmax={fmax}")
273
+
274
+ if overrides:
275
+ warnings.warn(
276
+ f"Preset '{preset}' values overridden by: {', '.join(overrides)}",
277
+ stacklevel=4,
278
+ )
279
+
280
+ # Apply overrides
281
+ if refined_mesh_size is not None:
282
+ mesh_config.refined_mesh_size = refined_mesh_size
283
+ if max_mesh_size is not None:
284
+ mesh_config.max_mesh_size = max_mesh_size
285
+ if margin is not None:
286
+ mesh_config.margin = margin
287
+ if air_above is not None:
288
+ mesh_config.air_above = air_above
289
+ if fmax is not None:
290
+ mesh_config.fmax = fmax
291
+ mesh_config.show_gui = show_gui
292
+
293
+ return mesh_config
294
+
295
+ # -------------------------------------------------------------------------
296
+ # Convenience methods
297
+ # -------------------------------------------------------------------------
298
+
299
+ def show_stack(self) -> None:
300
+ """Print the layer stack table.
301
+
302
+ Example:
303
+ >>> sim.show_stack()
304
+ """
305
+ from gsim.common.stack import print_stack_table
306
+
307
+ if self.stack is None:
308
+ self._resolve_stack()
309
+
310
+ if self.stack is not None:
311
+ print_stack_table(self.stack)
312
+
313
+ def plot_stack(self) -> None:
314
+ """Plot the layer stack visualization.
315
+
316
+ Example:
317
+ >>> sim.plot_stack()
318
+ """
319
+ from gsim.common.stack import plot_stack
320
+
321
+ if self.stack is None:
322
+ self._resolve_stack()
323
+
324
+ if self.stack is not None:
325
+ plot_stack(self.stack)
326
+
327
+ # -------------------------------------------------------------------------
328
+ # Visualization
329
+ # -------------------------------------------------------------------------
330
+
331
+ def plot_mesh(
332
+ self,
333
+ output: str | Path | None = None,
334
+ show_groups: list[str] | None = None,
335
+ interactive: bool = True,
336
+ ) -> None:
337
+ """Plot the mesh wireframe using PyVista.
338
+
339
+ Requires mesh() to be called first.
340
+
341
+ Args:
342
+ output: Output PNG path (only used if interactive=False)
343
+ show_groups: List of group name patterns to show (None = all).
344
+ Example: ["metal", "P"] to show metal layers and ports.
345
+ interactive: If True, open interactive 3D viewer.
346
+ If False, save static PNG to output path.
347
+
348
+ Raises:
349
+ ValueError: If output_dir not set or mesh file doesn't exist
350
+
351
+ Example:
352
+ >>> sim.mesh(preset="default")
353
+ >>> sim.plot_mesh(show_groups=["metal", "P"])
354
+ """
355
+ from gsim.viz import plot_mesh as _plot_mesh
356
+
357
+ if self._output_dir is None:
358
+ raise ValueError("Output directory not set. Call set_output_dir() first.")
359
+
360
+ mesh_path = self._output_dir / "palace.msh"
361
+ if not mesh_path.exists():
362
+ raise ValueError(f"Mesh file not found: {mesh_path}. Call mesh() first.")
363
+
364
+ # Default output path if not interactive
365
+ if output is None and not interactive:
366
+ output = self._output_dir / "mesh.png"
367
+
368
+ _plot_mesh(
369
+ msh_path=mesh_path,
370
+ output=output,
371
+ show_groups=show_groups,
372
+ interactive=interactive,
373
+ )