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,53 @@
1
+ """Pydantic models for Palace EM simulation configuration.
2
+
3
+ This module provides Pydantic v2 models for configuring Palace simulations,
4
+ offering validation, serialization, and a clean API.
5
+
6
+ Submodules:
7
+ - geometry: GeometryConfig
8
+ - stack: MaterialConfig (Layer/Stack are in gsim.common.stack)
9
+ - ports: PortConfig, CPWPortConfig, TerminalConfig, WavePortConfig
10
+ - mesh: MeshConfig
11
+ - numerical: NumericalConfig
12
+ - problems: DrivenConfig, EigenmodeConfig, ElectrostaticConfig, etc.
13
+ - results: SimulationResult, ValidationResult
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from gsim.palace.models.geometry import GeometryConfig
19
+ from gsim.palace.models.mesh import MeshConfig
20
+ from gsim.palace.models.numerical import NumericalConfig
21
+ from gsim.palace.models.ports import (
22
+ CPWPortConfig,
23
+ PortConfig,
24
+ TerminalConfig,
25
+ WavePortConfig,
26
+ )
27
+ from gsim.palace.models.problems import (
28
+ DrivenConfig,
29
+ EigenmodeConfig,
30
+ ElectrostaticConfig,
31
+ MagnetostaticConfig,
32
+ TransientConfig,
33
+ )
34
+ from gsim.palace.models.results import SimulationResult, ValidationResult
35
+ from gsim.palace.models.stack import MaterialConfig
36
+
37
+ __all__ = [
38
+ "CPWPortConfig",
39
+ "DrivenConfig",
40
+ "EigenmodeConfig",
41
+ "ElectrostaticConfig",
42
+ "GeometryConfig",
43
+ "MagnetostaticConfig",
44
+ "MaterialConfig",
45
+ "MeshConfig",
46
+ "NumericalConfig",
47
+ "PortConfig",
48
+ "SimulationResult",
49
+ "TerminalConfig",
50
+ "TransientConfig",
51
+ "ValidationResult",
52
+ "WavePortConfig",
53
+ ]
@@ -0,0 +1,34 @@
1
+ """Geometry configuration models for Palace simulations.
2
+
3
+ This module contains Pydantic models for geometry-related settings.
4
+ Note: The actual gdsfactory Component is stored directly on the simulation
5
+ classes since it's not serializable with Pydantic.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from pydantic import BaseModel, ConfigDict, Field
11
+
12
+
13
+ class GeometryConfig(BaseModel):
14
+ """Configuration for geometry settings.
15
+
16
+ This model stores metadata about the component being simulated.
17
+ The actual Component object is stored separately on simulation classes.
18
+
19
+ Attributes:
20
+ component_name: Name of the component being simulated
21
+ bounds: Bounding box as (xmin, ymin, xmax, ymax)
22
+ """
23
+
24
+ model_config = ConfigDict(validate_assignment=True)
25
+
26
+ component_name: str | None = None
27
+ bounds: tuple[float, float, float, float] | None = Field(
28
+ default=None, description="Bounding box (xmin, ymin, xmax, ymax)"
29
+ )
30
+
31
+
32
+ __all__ = [
33
+ "GeometryConfig",
34
+ ]
@@ -0,0 +1,95 @@
1
+ """Mesh configuration models for Palace simulations.
2
+
3
+ This module contains Pydantic models for mesh generation configuration.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Self
9
+
10
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
11
+
12
+
13
+ class MeshConfig(BaseModel):
14
+ """Configuration for mesh generation with COMSOL-style presets.
15
+
16
+ Attributes:
17
+ refined_mesh_size: Mesh size near conductors (um)
18
+ max_mesh_size: Maximum mesh size in air/dielectric (um)
19
+ cells_per_wavelength: Number of mesh cells per wavelength
20
+ margin: XY margin around design (um)
21
+ air_above: Air above top metal (um)
22
+ fmax: Maximum frequency for mesh sizing (Hz)
23
+ boundary_conditions: List of boundary conditions for each face
24
+ show_gui: Show gmsh GUI during meshing
25
+ preview_only: Generate preview only, don't save mesh
26
+ """
27
+
28
+ model_config = ConfigDict(validate_assignment=True)
29
+
30
+ refined_mesh_size: float = Field(default=5.0, gt=0)
31
+ max_mesh_size: float = Field(default=300.0, gt=0)
32
+ cells_per_wavelength: int = Field(default=10, ge=1)
33
+ margin: float = Field(default=50.0, ge=0)
34
+ air_above: float = Field(default=100.0, ge=0)
35
+ fmax: float = Field(default=100e9, gt=0)
36
+ boundary_conditions: list[str] | None = None
37
+ show_gui: bool = False
38
+ preview_only: bool = False
39
+
40
+ @model_validator(mode="after")
41
+ def set_default_boundary_conditions(self) -> Self:
42
+ """Set default boundary conditions if not provided."""
43
+ if self.boundary_conditions is None:
44
+ self.boundary_conditions = ["ABC", "ABC", "ABC", "ABC", "ABC", "ABC"]
45
+ return self
46
+
47
+ @classmethod
48
+ def coarse(cls, **kwargs: float | bool | list[str] | None) -> Self:
49
+ """Fast mesh for quick iteration (~2.5 elements per wavelength).
50
+
51
+ This preset is suitable for initial debugging and quick checks.
52
+ Not recommended for accurate results.
53
+ """
54
+ defaults: dict[str, float | int | bool | list[str] | None] = {
55
+ "refined_mesh_size": 10.0,
56
+ "max_mesh_size": 600.0,
57
+ "cells_per_wavelength": 5,
58
+ }
59
+ defaults.update(kwargs)
60
+ return cls(**defaults) # type: ignore[arg-type]
61
+
62
+ @classmethod
63
+ def default(cls, **kwargs: float | bool | list[str] | None) -> Self:
64
+ """Balanced mesh matching COMSOL defaults (~5 elements per wavelength).
65
+
66
+ This preset provides a good balance between accuracy and computation time.
67
+ Suitable for most simulations.
68
+ """
69
+ defaults: dict[str, float | int | bool | list[str] | None] = {
70
+ "refined_mesh_size": 5.0,
71
+ "max_mesh_size": 300.0,
72
+ "cells_per_wavelength": 10,
73
+ }
74
+ defaults.update(kwargs)
75
+ return cls(**defaults) # type: ignore[arg-type]
76
+
77
+ @classmethod
78
+ def fine(cls, **kwargs: float | bool | list[str] | None) -> Self:
79
+ """High accuracy mesh (~10 elements per wavelength).
80
+
81
+ This preset provides higher accuracy at the cost of increased
82
+ computation time. Use for final production simulations.
83
+ """
84
+ defaults: dict[str, float | int | bool | list[str] | None] = {
85
+ "refined_mesh_size": 2.0,
86
+ "max_mesh_size": 70.0,
87
+ "cells_per_wavelength": 20,
88
+ }
89
+ defaults.update(kwargs)
90
+ return cls(**defaults) # type: ignore[arg-type]
91
+
92
+
93
+ __all__ = [
94
+ "MeshConfig",
95
+ ]
@@ -0,0 +1,66 @@
1
+ """Numerical solver configuration models for Palace simulations.
2
+
3
+ This module contains Pydantic models for numerical solver settings.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Literal
9
+
10
+ from pydantic import BaseModel, ConfigDict, Field
11
+
12
+
13
+ class NumericalConfig(BaseModel):
14
+ """Numerical solver configuration.
15
+
16
+ Attributes:
17
+ order: Finite element order (1-4)
18
+ tolerance: Linear solver tolerance
19
+ max_iterations: Maximum solver iterations
20
+ solver_type: Linear solver type
21
+ preconditioner: Preconditioner type
22
+ device: Compute device (CPU or GPU)
23
+ num_processors: Number of processors (None = auto)
24
+ """
25
+
26
+ model_config = ConfigDict(validate_assignment=True)
27
+
28
+ # Element order
29
+ order: int = Field(default=2, ge=1, le=4)
30
+
31
+ # Linear solver
32
+ tolerance: float = Field(default=1e-6, gt=0)
33
+ max_iterations: int = Field(default=400, ge=1)
34
+ solver_type: Literal["Default", "SuperLU", "STRUMPACK", "MUMPS"] = "Default"
35
+
36
+ # Preconditioner
37
+ preconditioner: Literal["Default", "AMS", "BoomerAMG"] = "Default"
38
+
39
+ # Device
40
+ device: Literal["CPU", "GPU"] = "CPU"
41
+
42
+ # Partitioning
43
+ num_processors: int | None = None # None = auto
44
+
45
+ def to_palace_config(self) -> dict:
46
+ """Convert to Palace JSON config format."""
47
+ solver_config: dict[str, str | int | float] = {
48
+ "Tolerance": self.tolerance,
49
+ "MaxIterations": self.max_iterations,
50
+ }
51
+
52
+ if self.solver_type != "Default":
53
+ solver_config["Type"] = self.solver_type
54
+
55
+ if self.preconditioner != "Default":
56
+ solver_config["Preconditioner"] = self.preconditioner
57
+
58
+ return {
59
+ "Order": self.order,
60
+ "Solver": solver_config,
61
+ }
62
+
63
+
64
+ __all__ = [
65
+ "NumericalConfig",
66
+ ]
@@ -0,0 +1,137 @@
1
+ """Port configuration models for Palace simulations.
2
+
3
+ This module contains Pydantic models for port definitions:
4
+ - PortConfig: Single-element lumped port configuration
5
+ - CPWPortConfig: Coplanar waveguide (two-element) port configuration
6
+ - TerminalConfig: Terminal for electrostatic simulations
7
+ - WavePortConfig: Wave port (domain boundary with mode solving)
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Literal, Self
13
+
14
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
15
+
16
+
17
+ class PortConfig(BaseModel):
18
+ """Configuration for a single-element lumped port.
19
+
20
+ Lumped ports can be inplane (horizontal, on single layer) or
21
+ via (vertical, between two layers).
22
+
23
+ Attributes:
24
+ name: Port name (must match component port name)
25
+ layer: Target layer for inplane ports
26
+ from_layer: Bottom layer for via ports
27
+ to_layer: Top layer for via ports
28
+ length: Port extent along direction (um)
29
+ impedance: Port impedance (Ohms)
30
+ excited: Whether this port is excited
31
+ geometry: Port geometry type ("inplane" or "via")
32
+ """
33
+
34
+ model_config = ConfigDict(validate_assignment=True)
35
+
36
+ name: str
37
+ layer: str | None = None
38
+ from_layer: str | None = None
39
+ to_layer: str | None = None
40
+ length: float | None = Field(default=None, gt=0)
41
+ impedance: float = Field(default=50.0, gt=0)
42
+ excited: bool = True
43
+ geometry: Literal["inplane", "via"] = "inplane"
44
+
45
+ @model_validator(mode="after")
46
+ def validate_layer_config(self) -> Self:
47
+ """Validate layer configuration based on geometry type."""
48
+ if self.geometry == "inplane" and self.layer is None:
49
+ raise ValueError("Inplane ports require 'layer' to be specified")
50
+ if self.geometry == "via" and (
51
+ self.from_layer is None or self.to_layer is None
52
+ ):
53
+ raise ValueError("Via ports require both 'from_layer' and 'to_layer'")
54
+ return self
55
+
56
+
57
+ class CPWPortConfig(BaseModel):
58
+ """Configuration for a coplanar waveguide (CPW) port.
59
+
60
+ CPW ports consist of two elements (upper and lower gaps) that are
61
+ excited with opposite E-field directions to create the CPW mode.
62
+
63
+ Attributes:
64
+ upper: Name of the upper gap port on the component
65
+ lower: Name of the lower gap port on the component
66
+ layer: Target conductor layer
67
+ length: Port extent along direction (um)
68
+ impedance: Port impedance (Ohms)
69
+ excited: Whether this port is excited
70
+ name: Optional name for the CPW port (default: "cpw_{lower}")
71
+ """
72
+
73
+ model_config = ConfigDict(validate_assignment=True)
74
+
75
+ upper: str = Field(description="Name of upper gap port")
76
+ lower: str = Field(description="Name of lower gap port")
77
+ layer: str = Field(description="Target conductor layer")
78
+ length: float = Field(gt=0, description="Port extent in um")
79
+ impedance: float = Field(default=50.0, gt=0)
80
+ excited: bool = True
81
+ name: str | None = Field(
82
+ default=None, description="CPW port name (default: cpw_{lower})"
83
+ )
84
+
85
+ @property
86
+ def effective_name(self) -> str:
87
+ """Get the effective port name."""
88
+ return self.name if self.name else f"cpw_{self.lower}"
89
+
90
+
91
+ class TerminalConfig(BaseModel):
92
+ """Configuration for a terminal (for electrostatic capacitance extraction).
93
+
94
+ Terminals define conductor surfaces for capacitance matrix extraction
95
+ in electrostatic simulations.
96
+
97
+ Attributes:
98
+ name: Terminal name
99
+ layer: Target conductor layer
100
+ """
101
+
102
+ model_config = ConfigDict(validate_assignment=True)
103
+
104
+ name: str
105
+ layer: str
106
+
107
+
108
+ class WavePortConfig(BaseModel):
109
+ """Configuration for a wave port (domain boundary with mode solving).
110
+
111
+ Wave ports are used for domain-boundary ports where mode solving
112
+ is needed. This is an alternative to lumped ports for more accurate
113
+ S-parameter extraction.
114
+
115
+ Attributes:
116
+ name: Port name (must match component port name)
117
+ layer: Target conductor layer
118
+ mode: Mode number to excite
119
+ excited: Whether this port is excited
120
+ offset: De-embedding distance in um
121
+ """
122
+
123
+ model_config = ConfigDict(validate_assignment=True)
124
+
125
+ name: str
126
+ layer: str
127
+ mode: int = Field(default=1, ge=1, description="Mode number to excite")
128
+ excited: bool = True
129
+ offset: float = Field(default=0.0, ge=0, description="De-embedding distance in um")
130
+
131
+
132
+ __all__ = [
133
+ "CPWPortConfig",
134
+ "PortConfig",
135
+ "TerminalConfig",
136
+ "WavePortConfig",
137
+ ]
@@ -0,0 +1,195 @@
1
+ """Problem-specific configuration models for Palace simulations.
2
+
3
+ This module contains Pydantic models for different simulation types:
4
+ - DrivenConfig: Frequency-domain driven simulation (S-parameters)
5
+ - EigenmodeConfig: Eigenmode/resonance simulation
6
+ - ElectrostaticConfig: Electrostatic capacitance extraction
7
+ - MagnetostaticConfig: Magnetostatic inductance extraction
8
+ - TransientConfig: Time-domain simulation
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from typing import Literal, Self
14
+
15
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
16
+
17
+
18
+ class DrivenConfig(BaseModel):
19
+ """Configuration for driven (frequency sweep) simulation.
20
+
21
+ This is used for S-parameter extraction and frequency response analysis.
22
+
23
+ Attributes:
24
+ fmin: Minimum frequency in Hz
25
+ fmax: Maximum frequency in Hz
26
+ num_points: Number of frequency points
27
+ scale: Frequency spacing ("linear" or "log")
28
+ adaptive_tol: Adaptive tolerance (0 disables adaptive)
29
+ adaptive_max_samples: Maximum samples for adaptive refinement
30
+ compute_s_params: Whether to compute S-parameters
31
+ reference_impedance: Reference impedance for S-params (Ohms)
32
+ excitation_port: Name of port to excite (None = first port)
33
+ """
34
+
35
+ model_config = ConfigDict(validate_assignment=True)
36
+
37
+ fmin: float = Field(default=1e9, gt=0, description="Min frequency in Hz")
38
+ fmax: float = Field(default=100e9, gt=0, description="Max frequency in Hz")
39
+ num_points: int = Field(default=40, ge=1, description="Number of frequency points")
40
+ scale: Literal["linear", "log"] = "linear"
41
+
42
+ # Adaptive options
43
+ adaptive_tol: float = Field(
44
+ default=0.02, ge=0, description="Adaptive tolerance (0 disables adaptive)"
45
+ )
46
+ adaptive_max_samples: int = Field(default=20, ge=1)
47
+
48
+ # S-parameter options
49
+ compute_s_params: bool = True
50
+ reference_impedance: float = Field(default=50.0, gt=0)
51
+
52
+ # Excitation
53
+ excitation_port: str | None = Field(
54
+ default=None, description="Port to excite (None = first port)"
55
+ )
56
+
57
+ @model_validator(mode="after")
58
+ def validate_frequency_range(self) -> Self:
59
+ """Validate that fmin < fmax."""
60
+ if self.fmin >= self.fmax:
61
+ raise ValueError(f"fmin ({self.fmin}) must be less than fmax ({self.fmax})")
62
+ return self
63
+
64
+ def to_palace_config(self) -> dict:
65
+ """Convert to Palace JSON config format."""
66
+ freq_step = (self.fmax - self.fmin) / max(1, self.num_points - 1) / 1e9
67
+ return {
68
+ "Samples": [
69
+ {
70
+ "Type": "Linear" if self.scale == "linear" else "Log",
71
+ "MinFreq": self.fmin / 1e9,
72
+ "MaxFreq": self.fmax / 1e9,
73
+ "FreqStep": freq_step,
74
+ "SaveStep": 0,
75
+ }
76
+ ],
77
+ "AdaptiveTol": max(0, self.adaptive_tol),
78
+ }
79
+
80
+
81
+ class EigenmodeConfig(BaseModel):
82
+ """Configuration for eigenmode (resonance) simulation.
83
+
84
+ This is used for finding resonant frequencies and mode shapes.
85
+
86
+ Attributes:
87
+ num_modes: Number of modes to find
88
+ target: Target frequency in Hz for mode search
89
+ tolerance: Eigenvalue solver tolerance
90
+ """
91
+
92
+ model_config = ConfigDict(validate_assignment=True)
93
+
94
+ num_modes: int = Field(
95
+ default=10, ge=1, alias="N", description="Number of modes to find"
96
+ )
97
+ target: float | None = Field(default=None, description="Target frequency in Hz")
98
+ tolerance: float = Field(
99
+ default=1e-6, gt=0, description="Eigenvalue solver tolerance"
100
+ )
101
+
102
+ def to_palace_config(self) -> dict:
103
+ """Convert to Palace JSON config format."""
104
+ config: dict = {
105
+ "N": self.num_modes,
106
+ "Tol": self.tolerance,
107
+ }
108
+ if self.target is not None:
109
+ config["Target"] = self.target / 1e9 # Convert to GHz
110
+ return config
111
+
112
+
113
+ class ElectrostaticConfig(BaseModel):
114
+ """Configuration for electrostatic (capacitance matrix) simulation.
115
+
116
+ Attributes:
117
+ save_fields: Number of field solutions to save
118
+ """
119
+
120
+ model_config = ConfigDict(validate_assignment=True)
121
+
122
+ save_fields: int = Field(default=0, ge=0, description="Number of fields to save")
123
+
124
+ def to_palace_config(self) -> dict:
125
+ """Convert to Palace JSON config format."""
126
+ return {
127
+ "Save": self.save_fields,
128
+ }
129
+
130
+
131
+ class MagnetostaticConfig(BaseModel):
132
+ """Configuration for magnetostatic (inductance matrix) simulation.
133
+
134
+ Attributes:
135
+ save_fields: Number of field solutions to save
136
+ """
137
+
138
+ model_config = ConfigDict(validate_assignment=True)
139
+
140
+ save_fields: int = Field(default=0, ge=0, description="Number of fields to save")
141
+
142
+ def to_palace_config(self) -> dict:
143
+ """Convert to Palace JSON config format."""
144
+ return {
145
+ "Save": self.save_fields,
146
+ }
147
+
148
+
149
+ class TransientConfig(BaseModel):
150
+ """Configuration for transient (time-domain) simulation.
151
+
152
+ Attributes:
153
+ max_time: Maximum simulation time in ns
154
+ excitation: Excitation waveform type
155
+ excitation_freq: Excitation frequency in Hz (for sinusoidal)
156
+ excitation_width: Pulse width in ns (for gaussian)
157
+ time_step: Time step in ns (None = adaptive)
158
+ """
159
+
160
+ model_config = ConfigDict(validate_assignment=True)
161
+
162
+ excitation: Literal["sinusoidal", "gaussian", "ramp", "smoothstep"] = "sinusoidal"
163
+ excitation_freq: float | None = Field(
164
+ default=None, description="Excitation frequency in Hz"
165
+ )
166
+ excitation_width: float | None = Field(
167
+ default=None, description="Pulse width in ns (for gaussian)"
168
+ )
169
+ max_time: float = Field(description="Maximum simulation time in ns")
170
+ time_step: float | None = Field(
171
+ default=None, description="Time step in ns (None = adaptive)"
172
+ )
173
+
174
+ def to_palace_config(self) -> dict:
175
+ """Convert to Palace JSON config format."""
176
+ config: dict = {
177
+ "Type": self.excitation.capitalize(),
178
+ "MaxTime": self.max_time,
179
+ }
180
+ if self.excitation_freq is not None:
181
+ config["ExcitationFreq"] = self.excitation_freq / 1e9 # Convert to GHz
182
+ if self.excitation_width is not None:
183
+ config["ExcitationWidth"] = self.excitation_width
184
+ if self.time_step is not None:
185
+ config["TimeStep"] = self.time_step
186
+ return config
187
+
188
+
189
+ __all__ = [
190
+ "DrivenConfig",
191
+ "EigenmodeConfig",
192
+ "ElectrostaticConfig",
193
+ "MagnetostaticConfig",
194
+ "TransientConfig",
195
+ ]