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/eigenmode.py CHANGED
@@ -8,11 +8,8 @@ from __future__ import annotations
8
8
 
9
9
  import logging
10
10
  import tempfile
11
- import warnings
12
11
  from pathlib import Path
13
-
14
- logger = logging.getLogger(__name__)
15
- from typing import TYPE_CHECKING, Any, Literal
12
+ from typing import Any, Literal
16
13
 
17
14
  from pydantic import BaseModel, ConfigDict, Field, PrivateAttr
18
15
 
@@ -29,16 +26,14 @@ from gsim.palace.models import (
29
26
  ValidationResult,
30
27
  )
31
28
 
32
- if TYPE_CHECKING:
33
- from gdsfactory.component import Component
29
+ logger = logging.getLogger(__name__)
34
30
 
35
31
 
36
32
  class EigenmodeSim(PalaceSimMixin, BaseModel):
37
33
  """Eigenmode simulation for finding resonant frequencies.
38
34
 
39
35
  This class configures and runs eigenmode simulations to find
40
- resonant frequencies and mode shapes of structures. Uses composition
41
- (no inheritance) with shared Geometry and Stack components from gsim.common.
36
+ resonant frequencies and mode shapes of structures.
42
37
 
43
38
  Example:
44
39
  >>> from gsim.palace import EigenmodeSim
@@ -48,7 +43,8 @@ class EigenmodeSim(PalaceSimMixin, BaseModel):
48
43
  >>> sim.set_stack(air_above=300.0)
49
44
  >>> sim.add_port("o1", layer="topmetal2", length=5.0)
50
45
  >>> sim.set_eigenmode(num_modes=10, target=50e9)
51
- >>> sim.mesh("./sim", preset="default")
46
+ >>> sim.set_output_dir("./sim")
47
+ >>> sim.mesh(preset="default")
52
48
  >>> results = sim.simulate()
53
49
 
54
50
  Attributes:
@@ -89,66 +85,7 @@ class EigenmodeSim(PalaceSimMixin, BaseModel):
89
85
  _configured_ports: bool = PrivateAttr(default=False)
90
86
 
91
87
  # -------------------------------------------------------------------------
92
- # Geometry methods
93
- # -------------------------------------------------------------------------
94
-
95
- def set_geometry(self, component: Component) -> None:
96
- """Set the gdsfactory component for simulation.
97
-
98
- Args:
99
- component: gdsfactory Component to simulate
100
-
101
- Example:
102
- >>> sim.set_geometry(my_component)
103
- """
104
- self.geometry = Geometry(component=component)
105
-
106
- @property
107
- def component(self) -> Component | None:
108
- """Get the current component (for backward compatibility)."""
109
- return self.geometry.component if self.geometry else None
110
-
111
- @property
112
- def _component(self) -> Component | None:
113
- """Internal component access (backward compatibility)."""
114
- return self.component
115
-
116
- # -------------------------------------------------------------------------
117
- # Stack methods
118
- # -------------------------------------------------------------------------
119
-
120
- def set_stack(
121
- self,
122
- *,
123
- yaml_path: str | Path | None = None,
124
- air_above: float = 200.0,
125
- substrate_thickness: float = 2.0,
126
- include_substrate: bool = False,
127
- **kwargs,
128
- ) -> None:
129
- """Configure the layer stack.
130
-
131
- Args:
132
- yaml_path: Path to custom YAML stack file
133
- air_above: Air box height above top metal in um
134
- substrate_thickness: Thickness below z=0 in um
135
- include_substrate: Include lossy silicon substrate
136
- **kwargs: Additional args passed to extract_layer_stack
137
-
138
- Example:
139
- >>> sim.set_stack(air_above=300.0, substrate_thickness=2.0)
140
- """
141
- self._stack_kwargs = {
142
- "yaml_path": yaml_path,
143
- "air_above": air_above,
144
- "substrate_thickness": substrate_thickness,
145
- "include_substrate": include_substrate,
146
- **kwargs,
147
- }
148
- self.stack = None
149
-
150
- # -------------------------------------------------------------------------
151
- # Port methods
88
+ # Port methods (Eigenmode can have ports for Q-factor calculation)
152
89
  # -------------------------------------------------------------------------
153
90
 
154
91
  def add_port(
@@ -259,86 +196,11 @@ class EigenmodeSim(PalaceSimMixin, BaseModel):
259
196
  tolerance=tolerance,
260
197
  )
261
198
 
262
- # -------------------------------------------------------------------------
263
- # Material methods
264
- # -------------------------------------------------------------------------
265
-
266
- def set_material(
267
- self,
268
- name: str,
269
- *,
270
- type: Literal["conductor", "dielectric", "semiconductor"] | None = None,
271
- conductivity: float | None = None,
272
- permittivity: float | None = None,
273
- loss_tangent: float | None = None,
274
- ) -> None:
275
- """Override or add material properties.
276
-
277
- Args:
278
- name: Material name
279
- type: Material type (conductor, dielectric, semiconductor)
280
- conductivity: Conductivity in S/m (for conductors)
281
- permittivity: Relative permittivity (for dielectrics)
282
- loss_tangent: Dielectric loss tangent
283
-
284
- Example:
285
- >>> sim.set_material("aluminum", type="conductor", conductivity=3.8e7)
286
- """
287
- if type is None:
288
- if conductivity is not None and conductivity > 1e4:
289
- type = "conductor"
290
- elif permittivity is not None:
291
- type = "dielectric"
292
- else:
293
- type = "dielectric"
294
-
295
- self.materials[name] = MaterialConfig(
296
- type=type,
297
- conductivity=conductivity,
298
- permittivity=permittivity,
299
- loss_tangent=loss_tangent,
300
- )
301
-
302
- def set_numerical(
303
- self,
304
- *,
305
- order: int = 2,
306
- tolerance: float = 1e-6,
307
- max_iterations: int = 400,
308
- solver_type: Literal["Default", "SuperLU", "STRUMPACK", "MUMPS"] = "Default",
309
- preconditioner: Literal["Default", "AMS", "BoomerAMG"] = "Default",
310
- device: Literal["CPU", "GPU"] = "CPU",
311
- num_processors: int | None = None,
312
- ) -> None:
313
- """Configure numerical solver parameters.
314
-
315
- Args:
316
- order: Finite element order (1-4)
317
- tolerance: Linear solver tolerance
318
- max_iterations: Maximum solver iterations
319
- solver_type: Linear solver type
320
- preconditioner: Preconditioner type
321
- device: Compute device (CPU or GPU)
322
- num_processors: Number of processors (None = auto)
323
-
324
- Example:
325
- >>> sim.set_numerical(order=3, tolerance=1e-8)
326
- """
327
- self.numerical = NumericalConfig(
328
- order=order,
329
- tolerance=tolerance,
330
- max_iterations=max_iterations,
331
- solver_type=solver_type,
332
- preconditioner=preconditioner,
333
- device=device,
334
- num_processors=num_processors,
335
- )
336
-
337
199
  # -------------------------------------------------------------------------
338
200
  # Validation
339
201
  # -------------------------------------------------------------------------
340
202
 
341
- def validate(self) -> ValidationResult:
203
+ def validate_config(self) -> ValidationResult:
342
204
  """Validate the simulation configuration.
343
205
 
344
206
  Returns:
@@ -360,18 +222,19 @@ class EigenmodeSim(PalaceSimMixin, BaseModel):
360
222
  # Eigenmode simulations may not require ports
361
223
  if not self.ports and not self.cpw_ports:
362
224
  warnings_list.append(
363
- "No ports configured. Eigenmode will find all modes without port loading."
225
+ "No ports configured. Eigenmode finds all modes without port loading."
364
226
  )
365
227
 
366
228
  # Validate port configurations
367
229
  for port in self.ports:
368
230
  if port.geometry == "inplane" and port.layer is None:
369
231
  errors.append(f"Port '{port.name}': inplane ports require 'layer'")
370
- if port.geometry == "via":
371
- if port.from_layer is None or port.to_layer is None:
372
- errors.append(
373
- f"Port '{port.name}': via ports require 'from_layer' and 'to_layer'"
374
- )
232
+ if port.geometry == "via" and (
233
+ port.from_layer is None or port.to_layer is None
234
+ ):
235
+ errors.append(
236
+ f"Port '{port.name}': via ports require 'from_layer' and 'to_layer'"
237
+ )
375
238
 
376
239
  valid = len(errors) == 0
377
240
  return ValidationResult(valid=valid, errors=errors, warnings=warnings_list)
@@ -380,22 +243,7 @@ class EigenmodeSim(PalaceSimMixin, BaseModel):
380
243
  # Internal helpers
381
244
  # -------------------------------------------------------------------------
382
245
 
383
- def _resolve_stack(self) -> LayerStack:
384
- """Resolve the layer stack from PDK or YAML."""
385
- from gsim.common.stack import get_stack
386
-
387
- yaml_path = self._stack_kwargs.pop("yaml_path", None)
388
- legacy_stack = get_stack(yaml_path=yaml_path, **self._stack_kwargs)
389
- self._stack_kwargs["yaml_path"] = yaml_path
390
-
391
- for name, props in self.materials.items():
392
- legacy_stack.materials[name] = props.to_dict()
393
-
394
- self.stack = legacy_stack
395
-
396
- return legacy_stack
397
-
398
- def _configure_ports_on_component(self, stack: LayerStack) -> None:
246
+ def _configure_ports_on_component(self, stack: LayerStack) -> None: # noqa: ARG002
399
247
  """Configure ports on the component."""
400
248
  from gsim.palace.ports import (
401
249
  configure_cpw_port,
@@ -423,7 +271,7 @@ class EigenmodeSim(PalaceSimMixin, BaseModel):
423
271
  f"Available: {[p.name for p in component.ports]}"
424
272
  )
425
273
 
426
- if port_config.geometry == "inplane":
274
+ if port_config.geometry == "inplane" and port_config.layer is not None:
427
275
  configure_inplane_port(
428
276
  gf_port,
429
277
  layer=port_config.layer,
@@ -431,7 +279,9 @@ class EigenmodeSim(PalaceSimMixin, BaseModel):
431
279
  impedance=port_config.impedance,
432
280
  excited=port_config.excited,
433
281
  )
434
- elif port_config.geometry == "via":
282
+ elif port_config.geometry == "via" and (
283
+ port_config.from_layer is not None and port_config.to_layer is not None
284
+ ):
435
285
  configure_via_port(
436
286
  gf_port,
437
287
  from_layer=port_config.from_layer,
@@ -466,57 +316,6 @@ class EigenmodeSim(PalaceSimMixin, BaseModel):
466
316
 
467
317
  self._configured_ports = True
468
318
 
469
- def _build_mesh_config(
470
- self,
471
- preset: Literal["coarse", "default", "fine"] | None,
472
- refined_mesh_size: float | None,
473
- max_mesh_size: float | None,
474
- margin: float | None,
475
- air_above: float | None,
476
- fmax: float | None,
477
- show_gui: bool,
478
- ) -> MeshConfig:
479
- """Build mesh config from preset with optional overrides."""
480
- if preset == "coarse":
481
- mesh_config = MeshConfig.coarse()
482
- elif preset == "fine":
483
- mesh_config = MeshConfig.fine()
484
- else:
485
- mesh_config = MeshConfig.default()
486
-
487
- overrides = []
488
- if preset is not None:
489
- if refined_mesh_size is not None:
490
- overrides.append(f"refined_mesh_size={refined_mesh_size}")
491
- if max_mesh_size is not None:
492
- overrides.append(f"max_mesh_size={max_mesh_size}")
493
- if margin is not None:
494
- overrides.append(f"margin={margin}")
495
- if air_above is not None:
496
- overrides.append(f"air_above={air_above}")
497
- if fmax is not None:
498
- overrides.append(f"fmax={fmax}")
499
-
500
- if overrides:
501
- warnings.warn(
502
- f"Preset '{preset}' values overridden by: {', '.join(overrides)}",
503
- stacklevel=4,
504
- )
505
-
506
- if refined_mesh_size is not None:
507
- mesh_config.refined_mesh_size = refined_mesh_size
508
- if max_mesh_size is not None:
509
- mesh_config.max_mesh_size = max_mesh_size
510
- if margin is not None:
511
- mesh_config.margin = margin
512
- if air_above is not None:
513
- mesh_config.air_above = air_above
514
- if fmax is not None:
515
- mesh_config.fmax = fmax
516
- mesh_config.show_gui = show_gui
517
-
518
- return mesh_config
519
-
520
319
  def _generate_mesh_internal(
521
320
  self,
522
321
  output_dir: Path,
@@ -609,11 +408,9 @@ class EigenmodeSim(PalaceSimMixin, BaseModel):
609
408
 
610
409
  component = self.geometry.component if self.geometry else None
611
410
 
612
- validation = self.validate()
411
+ validation = self.validate_config()
613
412
  if not validation.valid:
614
- raise ValueError(
615
- f"Invalid configuration:\n" + "\n".join(validation.errors)
616
- )
413
+ raise ValueError("Invalid configuration:\n" + "\n".join(validation.errors))
617
414
 
618
415
  mesh_config = self._build_mesh_config(
619
416
  preset=preset,
@@ -648,37 +445,12 @@ class EigenmodeSim(PalaceSimMixin, BaseModel):
648
445
  config=legacy_mesh_config,
649
446
  )
650
447
 
651
- # -------------------------------------------------------------------------
652
- # Convenience methods
653
- # -------------------------------------------------------------------------
654
-
655
- def show_stack(self) -> None:
656
- """Print the layer stack table."""
657
- from gsim.common.stack import print_stack_table
658
-
659
- if self.stack is None:
660
- self._resolve_stack()
661
-
662
- if self.stack is not None:
663
- print_stack_table(self.stack)
664
-
665
- def plot_stack(self) -> None:
666
- """Plot the layer stack visualization."""
667
- from gsim.common.stack import plot_stack
668
-
669
- if self.stack is None:
670
- self._resolve_stack()
671
-
672
- if self.stack is not None:
673
- plot_stack(self.stack)
674
-
675
448
  # -------------------------------------------------------------------------
676
449
  # Mesh generation
677
450
  # -------------------------------------------------------------------------
678
451
 
679
452
  def mesh(
680
453
  self,
681
- output_dir: str | Path,
682
454
  *,
683
455
  preset: Literal["coarse", "default", "fine"] | None = None,
684
456
  refined_mesh_size: float | None = None,
@@ -690,25 +462,37 @@ class EigenmodeSim(PalaceSimMixin, BaseModel):
690
462
  model_name: str = "palace",
691
463
  verbose: bool = True,
692
464
  ) -> SimulationResult:
693
- """Generate the mesh and configuration files.
465
+ """Generate the mesh for Palace simulation.
466
+
467
+ Requires set_output_dir() to be called first.
694
468
 
695
469
  Args:
696
- output_dir: Directory for output files
697
470
  preset: Mesh quality preset ("coarse", "default", "fine")
698
- refined_mesh_size: Mesh size near conductors (um)
699
- max_mesh_size: Max mesh size in air/dielectric (um)
700
- margin: XY margin around design (um)
701
- air_above: Air above top metal (um)
702
- fmax: Max frequency for mesh sizing (Hz)
471
+ refined_mesh_size: Mesh size near conductors (um), overrides preset
472
+ max_mesh_size: Max mesh size in air/dielectric (um), overrides preset
473
+ margin: XY margin around design (um), overrides preset
474
+ air_above: Air above top metal (um), overrides preset
475
+ fmax: Max frequency for mesh sizing (Hz), overrides preset
703
476
  show_gui: Show gmsh GUI during meshing
704
477
  model_name: Base name for output files
705
478
  verbose: Print progress messages
706
479
 
707
480
  Returns:
708
- SimulationResult with mesh and config paths
481
+ SimulationResult with mesh path
482
+
483
+ Raises:
484
+ ValueError: If output_dir not set or configuration is invalid
485
+
486
+ Example:
487
+ >>> sim.set_output_dir("./sim")
488
+ >>> result = sim.mesh(preset="fine")
489
+ >>> print(f"Mesh saved to: {result.mesh_path}")
709
490
  """
710
491
  from gsim.palace.ports import extract_ports
711
492
 
493
+ if self._output_dir is None:
494
+ raise ValueError("Output directory not set. Call set_output_dir() first.")
495
+
712
496
  component = self.geometry.component if self.geometry else None
713
497
 
714
498
  mesh_config = self._build_mesh_config(
@@ -721,15 +505,11 @@ class EigenmodeSim(PalaceSimMixin, BaseModel):
721
505
  show_gui=show_gui,
722
506
  )
723
507
 
724
- validation = self.validate()
508
+ validation = self.validate_config()
725
509
  if not validation.valid:
726
- raise ValueError(
727
- f"Invalid configuration:\n" + "\n".join(validation.errors)
728
- )
510
+ raise ValueError("Invalid configuration:\n" + "\n".join(validation.errors))
729
511
 
730
- output_dir = Path(output_dir)
731
- output_dir.mkdir(parents=True, exist_ok=True)
732
- self._output_dir = output_dir
512
+ output_dir = self._output_dir
733
513
 
734
514
  stack = self._resolve_stack()
735
515