gsim 0.0.2__py3-none-any.whl → 0.0.4__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 CHANGED
@@ -1,25 +1,332 @@
1
1
  """Base mixin for Palace simulation classes.
2
2
 
3
- Provides common visualization methods shared across all simulation types.
3
+ Provides common methods shared across all simulation types:
4
+ DrivenSim, EigenmodeSim, ElectrostaticSim.
4
5
  """
5
6
 
6
7
  from __future__ import annotations
7
8
 
9
+ import warnings
8
10
  from pathlib import Path
9
- from typing import TYPE_CHECKING
11
+ from typing import TYPE_CHECKING, Any, Literal
10
12
 
11
13
  if TYPE_CHECKING:
12
- pass
14
+ from gdsfactory.component import Component
15
+
16
+ from gsim.common import Geometry, LayerStack
17
+ from gsim.palace.models import MaterialConfig, MeshConfig, NumericalConfig
13
18
 
14
19
 
15
20
  class PalaceSimMixin:
16
21
  """Mixin providing common methods for all Palace simulation classes.
17
22
 
18
- Requires the class to have:
19
- - _output_dir: Path | None (private attribute)
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)
20
30
  """
21
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
22
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
+ # -------------------------------------------------------------------------
23
330
 
24
331
  def plot_mesh(
25
332
  self,
@@ -52,9 +359,7 @@ class PalaceSimMixin:
52
359
 
53
360
  mesh_path = self._output_dir / "palace.msh"
54
361
  if not mesh_path.exists():
55
- raise ValueError(
56
- f"Mesh file not found: {mesh_path}. Call mesh() first."
57
- )
362
+ raise ValueError(f"Mesh file not found: {mesh_path}. Call mesh() first.")
58
363
 
59
364
  # Default output path if not interactive
60
365
  if output is None and not interactive: