gsim 0.0.0__py3-none-any.whl → 0.0.2__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/__init__.py +1 -1
- gsim/common/__init__.py +61 -0
- gsim/common/geometry.py +76 -0
- gsim/{palace → common}/stack/__init__.py +8 -5
- gsim/{palace → common}/stack/extractor.py +29 -103
- gsim/{palace → common}/stack/materials.py +27 -11
- gsim/palace/__init__.py +94 -43
- gsim/palace/base.py +68 -0
- gsim/palace/driven.py +1004 -0
- gsim/palace/eigenmode.py +777 -0
- gsim/palace/electrostatic.py +622 -0
- gsim/palace/mesh/generator.py +201 -20
- gsim/palace/mesh/pipeline.py +22 -1
- gsim/palace/models/__init__.py +60 -0
- gsim/palace/models/geometry.py +34 -0
- gsim/palace/models/mesh.py +95 -0
- gsim/palace/models/numerical.py +66 -0
- gsim/palace/models/ports.py +138 -0
- gsim/palace/models/problems.py +195 -0
- gsim/palace/models/results.py +159 -0
- gsim/palace/models/stack.py +59 -0
- gsim/palace/ports/config.py +1 -1
- {gsim-0.0.0.dist-info → gsim-0.0.2.dist-info}/METADATA +5 -3
- gsim-0.0.2.dist-info/RECORD +32 -0
- gsim-0.0.0.dist-info/RECORD +0 -18
- /gsim/{palace → common}/stack/visualization.py +0 -0
- {gsim-0.0.0.dist-info → gsim-0.0.2.dist-info}/WHEEL +0 -0
- {gsim-0.0.0.dist-info → gsim-0.0.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,138 @@
|
|
|
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":
|
|
51
|
+
if self.from_layer is None or self.to_layer is None:
|
|
52
|
+
raise ValueError(
|
|
53
|
+
"Via ports require both 'from_layer' and 'to_layer'"
|
|
54
|
+
)
|
|
55
|
+
return self
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class CPWPortConfig(BaseModel):
|
|
59
|
+
"""Configuration for a coplanar waveguide (CPW) port.
|
|
60
|
+
|
|
61
|
+
CPW ports consist of two elements (upper and lower gaps) that are
|
|
62
|
+
excited with opposite E-field directions to create the CPW mode.
|
|
63
|
+
|
|
64
|
+
Attributes:
|
|
65
|
+
upper: Name of the upper gap port on the component
|
|
66
|
+
lower: Name of the lower gap port on the component
|
|
67
|
+
layer: Target conductor layer
|
|
68
|
+
length: Port extent along direction (um)
|
|
69
|
+
impedance: Port impedance (Ohms)
|
|
70
|
+
excited: Whether this port is excited
|
|
71
|
+
name: Optional name for the CPW port (default: "cpw_{lower}")
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
model_config = ConfigDict(validate_assignment=True)
|
|
75
|
+
|
|
76
|
+
upper: str = Field(description="Name of upper gap port")
|
|
77
|
+
lower: str = Field(description="Name of lower gap port")
|
|
78
|
+
layer: str = Field(description="Target conductor layer")
|
|
79
|
+
length: float = Field(gt=0, description="Port extent in um")
|
|
80
|
+
impedance: float = Field(default=50.0, gt=0)
|
|
81
|
+
excited: bool = True
|
|
82
|
+
name: str | None = Field(
|
|
83
|
+
default=None, description="CPW port name (default: cpw_{lower})"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def effective_name(self) -> str:
|
|
88
|
+
"""Get the effective port name."""
|
|
89
|
+
return self.name if self.name else f"cpw_{self.lower}"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class TerminalConfig(BaseModel):
|
|
93
|
+
"""Configuration for a terminal (for electrostatic capacitance extraction).
|
|
94
|
+
|
|
95
|
+
Terminals define conductor surfaces for capacitance matrix extraction
|
|
96
|
+
in electrostatic simulations.
|
|
97
|
+
|
|
98
|
+
Attributes:
|
|
99
|
+
name: Terminal name
|
|
100
|
+
layer: Target conductor layer
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
model_config = ConfigDict(validate_assignment=True)
|
|
104
|
+
|
|
105
|
+
name: str
|
|
106
|
+
layer: str
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class WavePortConfig(BaseModel):
|
|
110
|
+
"""Configuration for a wave port (domain boundary with mode solving).
|
|
111
|
+
|
|
112
|
+
Wave ports are used for domain-boundary ports where mode solving
|
|
113
|
+
is needed. This is an alternative to lumped ports for more accurate
|
|
114
|
+
S-parameter extraction.
|
|
115
|
+
|
|
116
|
+
Attributes:
|
|
117
|
+
name: Port name (must match component port name)
|
|
118
|
+
layer: Target conductor layer
|
|
119
|
+
mode: Mode number to excite
|
|
120
|
+
excited: Whether this port is excited
|
|
121
|
+
offset: De-embedding distance in um
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
model_config = ConfigDict(validate_assignment=True)
|
|
125
|
+
|
|
126
|
+
name: str
|
|
127
|
+
layer: str
|
|
128
|
+
mode: int = Field(default=1, ge=1, description="Mode number to excite")
|
|
129
|
+
excited: bool = True
|
|
130
|
+
offset: float = Field(default=0.0, ge=0, description="De-embedding distance in um")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
__all__ = [
|
|
134
|
+
"CPWPortConfig",
|
|
135
|
+
"PortConfig",
|
|
136
|
+
"TerminalConfig",
|
|
137
|
+
"WavePortConfig",
|
|
138
|
+
]
|
|
@@ -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": self.adaptive_tol if self.adaptive_tol > 0 else 0,
|
|
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
|
+
]
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Result models for Palace simulations.
|
|
2
|
+
|
|
3
|
+
This module contains Pydantic models for simulation results and validation.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ValidationResult(BaseModel):
|
|
14
|
+
"""Result of simulation configuration validation.
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
valid: Whether the configuration is valid
|
|
18
|
+
errors: List of error messages
|
|
19
|
+
warnings: List of warning messages
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
model_config = ConfigDict(validate_assignment=True)
|
|
23
|
+
|
|
24
|
+
valid: bool
|
|
25
|
+
errors: list[str] = Field(default_factory=list)
|
|
26
|
+
warnings: list[str] = Field(default_factory=list)
|
|
27
|
+
|
|
28
|
+
def __bool__(self) -> bool:
|
|
29
|
+
return self.valid
|
|
30
|
+
|
|
31
|
+
def __str__(self) -> str:
|
|
32
|
+
lines = []
|
|
33
|
+
if self.valid:
|
|
34
|
+
lines.append("Validation: PASSED")
|
|
35
|
+
else:
|
|
36
|
+
lines.append("Validation: FAILED")
|
|
37
|
+
if self.errors:
|
|
38
|
+
lines.append("Errors:")
|
|
39
|
+
lines.extend([f" - {e}" for e in self.errors])
|
|
40
|
+
if self.warnings:
|
|
41
|
+
lines.append("Warnings:")
|
|
42
|
+
lines.extend([f" - {w}" for w in self.warnings])
|
|
43
|
+
return "\n".join(lines)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class SimulationResult(BaseModel):
|
|
47
|
+
"""Result from running a Palace simulation.
|
|
48
|
+
|
|
49
|
+
Attributes:
|
|
50
|
+
mesh_path: Path to the generated mesh file
|
|
51
|
+
output_dir: Output directory path
|
|
52
|
+
config_path: Path to the Palace config file
|
|
53
|
+
results: Dictionary mapping result filenames to paths
|
|
54
|
+
conductor_groups: Physical group info for conductors
|
|
55
|
+
dielectric_groups: Physical group info for dielectrics
|
|
56
|
+
port_groups: Physical group info for ports
|
|
57
|
+
boundary_groups: Physical group info for boundaries
|
|
58
|
+
port_info: Port metadata
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
model_config = ConfigDict(validate_assignment=True, arbitrary_types_allowed=True)
|
|
62
|
+
|
|
63
|
+
mesh_path: Path
|
|
64
|
+
output_dir: Path
|
|
65
|
+
config_path: Path | None = None
|
|
66
|
+
results: dict[str, Path] = Field(default_factory=dict)
|
|
67
|
+
|
|
68
|
+
# Physical group info for Palace
|
|
69
|
+
conductor_groups: dict = Field(default_factory=dict)
|
|
70
|
+
dielectric_groups: dict = Field(default_factory=dict)
|
|
71
|
+
port_groups: dict = Field(default_factory=dict)
|
|
72
|
+
boundary_groups: dict = Field(default_factory=dict)
|
|
73
|
+
|
|
74
|
+
# Port metadata
|
|
75
|
+
port_info: list = Field(default_factory=list)
|
|
76
|
+
|
|
77
|
+
# Mesh statistics
|
|
78
|
+
mesh_stats: dict = Field(default_factory=dict)
|
|
79
|
+
|
|
80
|
+
def __str__(self) -> str:
|
|
81
|
+
lines = ["Mesh Summary"]
|
|
82
|
+
lines.append("=" * 40)
|
|
83
|
+
|
|
84
|
+
# Dimensions
|
|
85
|
+
if self.mesh_stats:
|
|
86
|
+
bbox = self.mesh_stats.get("bbox", {})
|
|
87
|
+
if bbox:
|
|
88
|
+
dx = bbox.get("xmax", 0) - bbox.get("xmin", 0)
|
|
89
|
+
dy = bbox.get("ymax", 0) - bbox.get("ymin", 0)
|
|
90
|
+
dz = bbox.get("zmax", 0) - bbox.get("zmin", 0)
|
|
91
|
+
lines.append(f"Dimensions: {dx:.1f} x {dy:.1f} x {dz:.1f} µm")
|
|
92
|
+
|
|
93
|
+
# Mesh info
|
|
94
|
+
nodes = self.mesh_stats.get("nodes", 0)
|
|
95
|
+
elements = self.mesh_stats.get("elements", 0)
|
|
96
|
+
tets = self.mesh_stats.get("tetrahedra", 0)
|
|
97
|
+
if nodes or elements:
|
|
98
|
+
lines.append(f"Nodes: {nodes:,}")
|
|
99
|
+
lines.append(f"Elements: {elements:,}")
|
|
100
|
+
if tets:
|
|
101
|
+
lines.append(f"Tetrahedra: {tets:,}")
|
|
102
|
+
|
|
103
|
+
# Edge lengths
|
|
104
|
+
edge = self.mesh_stats.get("edge_length", {})
|
|
105
|
+
if edge:
|
|
106
|
+
lines.append(
|
|
107
|
+
f"Edge length: {edge.get('min', 0):.2f} - {edge.get('max', 0):.2f} µm"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Mesh quality (gamma)
|
|
111
|
+
quality = self.mesh_stats.get("quality", {})
|
|
112
|
+
if quality:
|
|
113
|
+
q_min = quality.get("min", 0)
|
|
114
|
+
q_mean = quality.get("mean", 0)
|
|
115
|
+
lines.append(f"Quality: {q_mean:.3f} (min: {q_min:.3f})")
|
|
116
|
+
|
|
117
|
+
# SICN quality (shows invalid elements)
|
|
118
|
+
sicn = self.mesh_stats.get("sicn", {})
|
|
119
|
+
if sicn:
|
|
120
|
+
invalid = sicn.get("invalid", 0)
|
|
121
|
+
if invalid > 0:
|
|
122
|
+
lines.append(f"SICN: {sicn.get('mean', 0):.3f} ({invalid} invalid!)")
|
|
123
|
+
else:
|
|
124
|
+
lines.append(f"SICN: {sicn.get('mean', 0):.3f} (all valid)")
|
|
125
|
+
|
|
126
|
+
# Physical groups
|
|
127
|
+
groups = self.mesh_stats.get("groups", {})
|
|
128
|
+
if groups:
|
|
129
|
+
volumes = groups.get("volumes", [])
|
|
130
|
+
surfaces = groups.get("surfaces", [])
|
|
131
|
+
lines.append("-" * 40)
|
|
132
|
+
if volumes:
|
|
133
|
+
lines.append(f"Volumes ({len(volumes)}):")
|
|
134
|
+
for v in volumes:
|
|
135
|
+
name = v["name"] if isinstance(v, dict) else v
|
|
136
|
+
tag = v.get("tag", "") if isinstance(v, dict) else ""
|
|
137
|
+
lines.append(f" - {name}" + (f" [{tag}]" if tag else ""))
|
|
138
|
+
if surfaces:
|
|
139
|
+
lines.append(f"Surfaces ({len(surfaces)}):")
|
|
140
|
+
for s in surfaces:
|
|
141
|
+
name = s["name"] if isinstance(s, dict) else s
|
|
142
|
+
tag = s.get("tag", "") if isinstance(s, dict) else ""
|
|
143
|
+
lines.append(f" - {name}" + (f" [{tag}]" if tag else ""))
|
|
144
|
+
|
|
145
|
+
lines.append("-" * 40)
|
|
146
|
+
lines.append(f"Mesh: {self.mesh_path}")
|
|
147
|
+
if self.config_path:
|
|
148
|
+
lines.append(f"Config: {self.config_path}")
|
|
149
|
+
|
|
150
|
+
return "\n".join(lines)
|
|
151
|
+
|
|
152
|
+
def __repr__(self) -> str:
|
|
153
|
+
return self.__str__()
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
__all__ = [
|
|
157
|
+
"SimulationResult",
|
|
158
|
+
"ValidationResult",
|
|
159
|
+
]
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Material configuration model for Palace simulations.
|
|
2
|
+
|
|
3
|
+
This module contains the MaterialConfig Pydantic model for material property overrides
|
|
4
|
+
in simulations. Layer and stack configuration is handled by gsim.common.stack.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Literal, Self
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MaterialConfig(BaseModel):
|
|
15
|
+
"""EM properties for a material.
|
|
16
|
+
|
|
17
|
+
Used for material property overrides in simulation classes.
|
|
18
|
+
|
|
19
|
+
Attributes:
|
|
20
|
+
type: Material type (conductor, dielectric, or semiconductor)
|
|
21
|
+
conductivity: Conductivity in S/m (for conductors)
|
|
22
|
+
permittivity: Relative permittivity (for dielectrics)
|
|
23
|
+
loss_tangent: Dielectric loss tangent
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
model_config = ConfigDict(validate_assignment=True)
|
|
27
|
+
|
|
28
|
+
type: Literal["conductor", "dielectric", "semiconductor"]
|
|
29
|
+
conductivity: float | None = Field(default=None, ge=0)
|
|
30
|
+
permittivity: float | None = Field(default=None, ge=1.0)
|
|
31
|
+
loss_tangent: float | None = Field(default=None, ge=0, le=1)
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def conductor(cls, conductivity: float = 5.8e7) -> Self:
|
|
35
|
+
"""Create a conductor material."""
|
|
36
|
+
return cls(type="conductor", conductivity=conductivity)
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def dielectric(cls, permittivity: float, loss_tangent: float = 0.0) -> Self:
|
|
40
|
+
"""Create a dielectric material."""
|
|
41
|
+
return cls(
|
|
42
|
+
type="dielectric", permittivity=permittivity, loss_tangent=loss_tangent
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def to_dict(self) -> dict[str, object]:
|
|
46
|
+
"""Convert to dictionary for YAML output."""
|
|
47
|
+
d: dict[str, object] = {"type": self.type}
|
|
48
|
+
if self.conductivity is not None:
|
|
49
|
+
d["conductivity"] = self.conductivity
|
|
50
|
+
if self.permittivity is not None:
|
|
51
|
+
d["permittivity"] = self.permittivity
|
|
52
|
+
if self.loss_tangent is not None:
|
|
53
|
+
d["loss_tangent"] = self.loss_tangent
|
|
54
|
+
return d
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
__all__ = [
|
|
58
|
+
"MaterialConfig",
|
|
59
|
+
]
|
gsim/palace/ports/config.py
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gsim
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.2
|
|
4
4
|
Author-email: flaport <floris.laporte@gmail.com>
|
|
5
5
|
Classifier: Programming Language :: Python :: 3.12
|
|
6
6
|
Classifier: Programming Language :: Python :: 3.13
|
|
7
7
|
Classifier: Operating System :: OS Independent
|
|
8
8
|
Requires-Python: <3.14,>=3.12
|
|
9
9
|
Description-Content-Type: text/markdown
|
|
10
|
-
Requires-Dist: gdsfactory>=
|
|
10
|
+
Requires-Dist: gdsfactory>=9.32.0
|
|
11
11
|
Requires-Dist: gdsfactoryplus>=1.3.7
|
|
12
12
|
Requires-Dist: gmsh
|
|
13
13
|
Requires-Dist: meshio>=5.0.0
|
|
@@ -43,8 +43,10 @@ Requires-Dist: towncrier>=24.0.0; extra == "dev"
|
|
|
43
43
|
Requires-Dist: vega-datasets>=0.9.0; extra == "dev"
|
|
44
44
|
Requires-Dist: nbstripout>=0.8.1; extra == "dev"
|
|
45
45
|
Requires-Dist: ty>=0.0.13; extra == "dev"
|
|
46
|
+
Provides-Extra: docs
|
|
47
|
+
Requires-Dist: ihp-gdsfactory>=0.1.4; extra == "docs"
|
|
46
48
|
|
|
47
|
-
# Gsim 0.0.
|
|
49
|
+
# Gsim 0.0.2
|
|
48
50
|
|
|
49
51
|
> a GDSFactory Simulation Plugin
|
|
50
52
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
gsim/__init__.py,sha256=ltF9Dg54VN5FFluo_Ixh2hEEsCv603Cqs9omInOjyyc,288
|
|
2
|
+
gsim/gcloud.py,sha256=ulYaNyGXCRoTbud0im1qHyuwcvIpNpk1RRvnRxekCe4,5155
|
|
3
|
+
gsim/viz.py,sha256=cakE-oH_O0wuL51gURl5YjLeCZQz-FFUzfr2jn2-MI4,2645
|
|
4
|
+
gsim/common/__init__.py,sha256=76hHcbSSJw45_Xz-WNTPzAhxJXqacs9Vx5BOKaMk3xw,1415
|
|
5
|
+
gsim/common/geometry.py,sha256=WdFhbHFZjKM7HL35UDmNgmgwWJHPf3YFs-sxMd4w6f4,2170
|
|
6
|
+
gsim/common/stack/__init__.py,sha256=0v44u-BQSnW7Gb9t7a9tZ0HFxBVaCFKIAbXf40C3sFU,4023
|
|
7
|
+
gsim/common/stack/extractor.py,sha256=u9ZAf8PjBig_OJefSocONE2NIAEDzE_ys1QQa2ALlYQ,17745
|
|
8
|
+
gsim/common/stack/materials.py,sha256=Eq8u1LwrY03Ewc5BSRTILofp_sqNXP-DAhUmORf3cM8,5168
|
|
9
|
+
gsim/common/stack/visualization.py,sha256=JwUDmdWqKYjSfeBX_EA1vp3ggxYAX8lI5soNIKHpwkg,19216
|
|
10
|
+
gsim/palace/__init__.py,sha256=XOA8ZmwFu3hJm7wlm7HNGqIgPxMhIpOdyEslfmwhxao,3997
|
|
11
|
+
gsim/palace/base.py,sha256=GHbiVhxeXJ7IL_3_OqZIL_Kpi0dpUFOUv_4XzqrjNE0,2002
|
|
12
|
+
gsim/palace/driven.py,sha256=kgF-jncBD0ffXZkUfyiV5zX6E88PpGYtDZJj6UQ8ggw,34190
|
|
13
|
+
gsim/palace/eigenmode.py,sha256=NIEEW7U8haD_JZyC3gSADV627jdzuae-19_NJ3TP5Pw,26341
|
|
14
|
+
gsim/palace/electrostatic.py,sha256=oeHg4B3RS3oPgh0Aa2yJX3ttKxkpQDN9CajZrkiLEis,20928
|
|
15
|
+
gsim/palace/mesh/__init__.py,sha256=vif960c46QbH1fkr4J9ydFh5ODcmxRS_Nx3D6JaVlMw,1207
|
|
16
|
+
gsim/palace/mesh/generator.py,sha256=uWIK4c5VkehYPUFjDb5rqQKyziaDR1SUm_1OT2pfkO8,36845
|
|
17
|
+
gsim/palace/mesh/gmsh_utils.py,sha256=G8JJUSlUL2Rr1tcM9G0M6F9UjxePFpWSUGfmJ-LWh8E,13801
|
|
18
|
+
gsim/palace/mesh/pipeline.py,sha256=cMGyuHuY6ZJYoT3cjf1Jneck2UZQTgGQQhG-D-0oz9Q,6695
|
|
19
|
+
gsim/palace/models/__init__.py,sha256=15of4sl3Q3qJ88BWWF0bldey5_Mijdkq-TyTuKghpPI,1592
|
|
20
|
+
gsim/palace/models/geometry.py,sha256=9jD2HKeJKYhyaBLLCNc4LyuW-OpIouX32WYiE2XP9wg,987
|
|
21
|
+
gsim/palace/models/mesh.py,sha256=vPFR5toiWLIpyYd0ZW2hN8oIOvBohJqDE5alFMpgiBc,3119
|
|
22
|
+
gsim/palace/models/numerical.py,sha256=fH57yFemH2myHWjj9Si5F7Op7jauleqaK2wbdkKf_Uw,1792
|
|
23
|
+
gsim/palace/models/ports.py,sha256=NpL3w1uwsMGOX8NsnmOj1mV87xUtDWXGn2VUOyODJVw,4550
|
|
24
|
+
gsim/palace/models/problems.py,sha256=mmWuXg-gU0BxXPB5oyOPTcV459gsyD4JK0FZu8sKLGY,6592
|
|
25
|
+
gsim/palace/models/results.py,sha256=clyHwLAGvKWWtfwm9yzv-OhaFLXhID130gShBDidwO4,5555
|
|
26
|
+
gsim/palace/models/stack.py,sha256=g3cw-YliXAoMqEoynMXYUY2Ys39llpN-9nPhRBQYfYU,1975
|
|
27
|
+
gsim/palace/ports/__init__.py,sha256=IoP11atw-Uf447AkZC5ixNcgqlNrC3GCnTGugjR_wgw,803
|
|
28
|
+
gsim/palace/ports/config.py,sha256=DK8TwspCsghQFl1uqKzu-YYEhjfu37rMDuHErOLPPgU,12289
|
|
29
|
+
gsim-0.0.2.dist-info/METADATA,sha256=1siJwZ2ue0Ipqk1dcI3fgsWG_5lHfqwtd1Dl8Ae20Ho,4300
|
|
30
|
+
gsim-0.0.2.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
|
|
31
|
+
gsim-0.0.2.dist-info/top_level.txt,sha256=USwf5QaCLsXq3JCR6ejGvHANZmIifvuQAAl7w-cUE0Y,5
|
|
32
|
+
gsim-0.0.2.dist-info/RECORD,,
|
gsim-0.0.0.dist-info/RECORD
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
gsim/__init__.py,sha256=FmMjn3al4lB-2cGAlMcYoDpfEgb1CotlaBTKbfCCbfA,288
|
|
2
|
-
gsim/gcloud.py,sha256=ulYaNyGXCRoTbud0im1qHyuwcvIpNpk1RRvnRxekCe4,5155
|
|
3
|
-
gsim/viz.py,sha256=cakE-oH_O0wuL51gURl5YjLeCZQz-FFUzfr2jn2-MI4,2645
|
|
4
|
-
gsim/palace/__init__.py,sha256=FHY3L9NRGx5xFCU-mLHKIDjJZc_G0kcIMoq6pX9RHyo,2938
|
|
5
|
-
gsim/palace/mesh/__init__.py,sha256=vif960c46QbH1fkr4J9ydFh5ODcmxRS_Nx3D6JaVlMw,1207
|
|
6
|
-
gsim/palace/mesh/generator.py,sha256=wQR64aqRMuHwA6klLHIASr03Z6tIUqHvvuwvovtwOBY,30735
|
|
7
|
-
gsim/palace/mesh/gmsh_utils.py,sha256=G8JJUSlUL2Rr1tcM9G0M6F9UjxePFpWSUGfmJ-LWh8E,13801
|
|
8
|
-
gsim/palace/mesh/pipeline.py,sha256=K5G1mb5kk9_y_lVHQH7fOOUquarlGEBtw3NTcNpni60,5936
|
|
9
|
-
gsim/palace/ports/__init__.py,sha256=IoP11atw-Uf447AkZC5ixNcgqlNrC3GCnTGugjR_wgw,803
|
|
10
|
-
gsim/palace/ports/config.py,sha256=LHgKqpk4w7rutUBEf-368bRu2l4N02m-Y8qPdKOuQmk,12289
|
|
11
|
-
gsim/palace/stack/__init__.py,sha256=xcofg-9HuAiymv_77_AuEtTyVnpAjBbxOqRC3-kjQXs,3910
|
|
12
|
-
gsim/palace/stack/extractor.py,sha256=elQn4aEux9_gslyPB1zDTdcOpdiBbCADF7B9vVPd6hg,20440
|
|
13
|
-
gsim/palace/stack/materials.py,sha256=IzUGuPufWu7tx4uTuERodygsh8mA-BZh7NQfSBYLLBU,4637
|
|
14
|
-
gsim/palace/stack/visualization.py,sha256=JwUDmdWqKYjSfeBX_EA1vp3ggxYAX8lI5soNIKHpwkg,19216
|
|
15
|
-
gsim-0.0.0.dist-info/METADATA,sha256=ZNJ5JOBN4ruqnQszxFq9jP-Z1lxDpRP3lEI_RHvaxt4,4225
|
|
16
|
-
gsim-0.0.0.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
|
|
17
|
-
gsim-0.0.0.dist-info/top_level.txt,sha256=USwf5QaCLsXq3JCR6ejGvHANZmIifvuQAAl7w-cUE0Y,5
|
|
18
|
-
gsim-0.0.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|