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
gsim/palace/mesh/generator.py
CHANGED
|
@@ -18,8 +18,9 @@ import gmsh
|
|
|
18
18
|
from . import gmsh_utils
|
|
19
19
|
|
|
20
20
|
if TYPE_CHECKING:
|
|
21
|
+
from gsim.palace.models import DrivenConfig
|
|
21
22
|
from gsim.palace.ports.config import PalacePort
|
|
22
|
-
from gsim.
|
|
23
|
+
from gsim.common.stack import LayerStack
|
|
23
24
|
|
|
24
25
|
from gsim.palace.ports.config import PortGeometry
|
|
25
26
|
|
|
@@ -42,6 +43,12 @@ class MeshResult:
|
|
|
42
43
|
mesh_path: Path
|
|
43
44
|
config_path: Path | None = None
|
|
44
45
|
port_info: list = field(default_factory=list)
|
|
46
|
+
mesh_stats: dict = field(default_factory=dict)
|
|
47
|
+
# Data needed for deferred config generation
|
|
48
|
+
groups: dict = field(default_factory=dict)
|
|
49
|
+
output_dir: Path | None = None
|
|
50
|
+
model_name: str = "palace"
|
|
51
|
+
fmax: float = 100e9
|
|
45
52
|
|
|
46
53
|
|
|
47
54
|
def extract_geometry(component, stack: LayerStack) -> GeometryData:
|
|
@@ -680,8 +687,39 @@ def _generate_palace_config(
|
|
|
680
687
|
output_path: Path,
|
|
681
688
|
model_name: str,
|
|
682
689
|
fmax: float,
|
|
690
|
+
driven_config: DrivenConfig | None = None,
|
|
683
691
|
) -> Path:
|
|
684
|
-
"""Generate Palace config.json file.
|
|
692
|
+
"""Generate Palace config.json file.
|
|
693
|
+
|
|
694
|
+
Args:
|
|
695
|
+
groups: Physical group information from mesh generation
|
|
696
|
+
ports: List of PalacePort objects
|
|
697
|
+
port_info: Port metadata list
|
|
698
|
+
stack: Layer stack for material properties
|
|
699
|
+
output_path: Output directory path
|
|
700
|
+
model_name: Base name for output files
|
|
701
|
+
fmax: Maximum frequency (Hz) - used as fallback if driven_config not provided
|
|
702
|
+
driven_config: Optional DrivenConfig for frequency sweep settings
|
|
703
|
+
"""
|
|
704
|
+
# Use driven_config if provided, otherwise fall back to legacy parameters
|
|
705
|
+
if driven_config is not None:
|
|
706
|
+
solver_driven = driven_config.to_palace_config()
|
|
707
|
+
else:
|
|
708
|
+
# Legacy behavior - compute from fmax
|
|
709
|
+
freq_step = fmax / 40e9
|
|
710
|
+
solver_driven = {
|
|
711
|
+
"Samples": [
|
|
712
|
+
{
|
|
713
|
+
"Type": "Linear",
|
|
714
|
+
"MinFreq": 1.0, # 1 GHz
|
|
715
|
+
"MaxFreq": fmax / 1e9,
|
|
716
|
+
"FreqStep": freq_step,
|
|
717
|
+
"SaveStep": 0,
|
|
718
|
+
}
|
|
719
|
+
],
|
|
720
|
+
"AdaptiveTol": 0.02,
|
|
721
|
+
}
|
|
722
|
+
|
|
685
723
|
config: dict[str, object] = {
|
|
686
724
|
"Problem": {
|
|
687
725
|
"Type": "Driven",
|
|
@@ -706,18 +744,7 @@ def _generate_palace_config(
|
|
|
706
744
|
},
|
|
707
745
|
"Order": 2,
|
|
708
746
|
"Device": "CPU",
|
|
709
|
-
"Driven":
|
|
710
|
-
"Samples": [
|
|
711
|
-
{
|
|
712
|
-
"Type": "Linear",
|
|
713
|
-
"MinFreq": 1e9 / 1e9,
|
|
714
|
-
"MaxFreq": fmax / 1e9,
|
|
715
|
-
"FreqStep": fmax / 40e9,
|
|
716
|
-
"SaveStep": 0,
|
|
717
|
-
}
|
|
718
|
-
],
|
|
719
|
-
"AdaptiveTol": 2e-2,
|
|
720
|
-
},
|
|
747
|
+
"Driven": solver_driven,
|
|
721
748
|
},
|
|
722
749
|
}
|
|
723
750
|
|
|
@@ -831,6 +858,103 @@ def _generate_palace_config(
|
|
|
831
858
|
return config_path
|
|
832
859
|
|
|
833
860
|
|
|
861
|
+
def _collect_mesh_stats() -> dict:
|
|
862
|
+
"""Collect mesh statistics from gmsh after mesh generation."""
|
|
863
|
+
stats = {}
|
|
864
|
+
|
|
865
|
+
# Get bounding box
|
|
866
|
+
try:
|
|
867
|
+
xmin, ymin, zmin, xmax, ymax, zmax = gmsh.model.getBoundingBox(-1, -1)
|
|
868
|
+
stats["bbox"] = {
|
|
869
|
+
"xmin": xmin,
|
|
870
|
+
"ymin": ymin,
|
|
871
|
+
"zmin": zmin,
|
|
872
|
+
"xmax": xmax,
|
|
873
|
+
"ymax": ymax,
|
|
874
|
+
"zmax": zmax,
|
|
875
|
+
}
|
|
876
|
+
except Exception:
|
|
877
|
+
pass
|
|
878
|
+
|
|
879
|
+
# Get node count
|
|
880
|
+
try:
|
|
881
|
+
node_tags, _, _ = gmsh.model.mesh.getNodes()
|
|
882
|
+
stats["nodes"] = len(node_tags)
|
|
883
|
+
except Exception:
|
|
884
|
+
pass
|
|
885
|
+
|
|
886
|
+
# Get element counts and collect tet tags for quality
|
|
887
|
+
tet_tags = []
|
|
888
|
+
try:
|
|
889
|
+
element_types, element_tags, _ = gmsh.model.mesh.getElements()
|
|
890
|
+
total_elements = sum(len(tags) for tags in element_tags)
|
|
891
|
+
stats["elements"] = total_elements
|
|
892
|
+
|
|
893
|
+
# Count tetrahedra (type 4) and save tags
|
|
894
|
+
for etype, tags in zip(element_types, element_tags):
|
|
895
|
+
if etype == 4: # 4-node tetrahedron
|
|
896
|
+
stats["tetrahedra"] = len(tags)
|
|
897
|
+
tet_tags = list(tags)
|
|
898
|
+
except Exception:
|
|
899
|
+
pass
|
|
900
|
+
|
|
901
|
+
# Get mesh quality for tetrahedra
|
|
902
|
+
if tet_tags:
|
|
903
|
+
# Gamma: inscribed/circumscribed radius ratio (shape quality)
|
|
904
|
+
try:
|
|
905
|
+
qualities = gmsh.model.mesh.getElementQualities(tet_tags, "gamma")
|
|
906
|
+
if len(qualities) > 0:
|
|
907
|
+
stats["quality"] = {
|
|
908
|
+
"min": round(min(qualities), 3),
|
|
909
|
+
"max": round(max(qualities), 3),
|
|
910
|
+
"mean": round(sum(qualities) / len(qualities), 3),
|
|
911
|
+
}
|
|
912
|
+
except Exception:
|
|
913
|
+
pass
|
|
914
|
+
|
|
915
|
+
# SICN: Signed Inverse Condition Number (negative = invalid element)
|
|
916
|
+
try:
|
|
917
|
+
sicn = gmsh.model.mesh.getElementQualities(tet_tags, "minSICN")
|
|
918
|
+
if len(sicn) > 0:
|
|
919
|
+
sicn_min = min(sicn)
|
|
920
|
+
invalid_count = sum(1 for s in sicn if s < 0)
|
|
921
|
+
stats["sicn"] = {
|
|
922
|
+
"min": round(sicn_min, 3),
|
|
923
|
+
"mean": round(sum(sicn) / len(sicn), 3),
|
|
924
|
+
"invalid": invalid_count,
|
|
925
|
+
}
|
|
926
|
+
except Exception:
|
|
927
|
+
pass
|
|
928
|
+
|
|
929
|
+
# Edge lengths
|
|
930
|
+
try:
|
|
931
|
+
min_edges = gmsh.model.mesh.getElementQualities(tet_tags, "minEdge")
|
|
932
|
+
max_edges = gmsh.model.mesh.getElementQualities(tet_tags, "maxEdge")
|
|
933
|
+
if len(min_edges) > 0 and len(max_edges) > 0:
|
|
934
|
+
stats["edge_length"] = {
|
|
935
|
+
"min": round(min(min_edges), 3),
|
|
936
|
+
"max": round(max(max_edges), 3),
|
|
937
|
+
}
|
|
938
|
+
except Exception:
|
|
939
|
+
pass
|
|
940
|
+
|
|
941
|
+
# Get physical groups with tags
|
|
942
|
+
try:
|
|
943
|
+
groups = {"volumes": [], "surfaces": []}
|
|
944
|
+
for dim, tag in gmsh.model.getPhysicalGroups():
|
|
945
|
+
name = gmsh.model.getPhysicalName(dim, tag)
|
|
946
|
+
entry = {"name": name, "tag": tag}
|
|
947
|
+
if dim == 3:
|
|
948
|
+
groups["volumes"].append(entry)
|
|
949
|
+
elif dim == 2:
|
|
950
|
+
groups["surfaces"].append(entry)
|
|
951
|
+
stats["groups"] = groups
|
|
952
|
+
except Exception:
|
|
953
|
+
pass
|
|
954
|
+
|
|
955
|
+
return stats
|
|
956
|
+
|
|
957
|
+
|
|
834
958
|
def generate_mesh(
|
|
835
959
|
component,
|
|
836
960
|
stack: LayerStack,
|
|
@@ -843,6 +967,8 @@ def generate_mesh(
|
|
|
843
967
|
air_margin: float = 50.0,
|
|
844
968
|
fmax: float = 100e9,
|
|
845
969
|
show_gui: bool = False,
|
|
970
|
+
driven_config: DrivenConfig | None = None,
|
|
971
|
+
write_config: bool = True,
|
|
846
972
|
) -> MeshResult:
|
|
847
973
|
"""Generate mesh for Palace EM simulation.
|
|
848
974
|
|
|
@@ -858,6 +984,8 @@ def generate_mesh(
|
|
|
858
984
|
air_margin: Air box margin (um)
|
|
859
985
|
fmax: Max frequency for config (Hz)
|
|
860
986
|
show_gui: Show gmsh GUI during meshing
|
|
987
|
+
driven_config: Optional DrivenConfig for frequency sweep settings
|
|
988
|
+
write_config: Whether to write config.json (default True)
|
|
861
989
|
|
|
862
990
|
Returns:
|
|
863
991
|
MeshResult with paths and metadata
|
|
@@ -928,6 +1056,9 @@ def generate_mesh(
|
|
|
928
1056
|
logger.info("Generating mesh...")
|
|
929
1057
|
gmsh.model.mesh.generate(3)
|
|
930
1058
|
|
|
1059
|
+
# Collect mesh statistics
|
|
1060
|
+
mesh_stats = _collect_mesh_stats()
|
|
1061
|
+
|
|
931
1062
|
# Save mesh
|
|
932
1063
|
gmsh.option.setNumber("Mesh.Binary", 0)
|
|
933
1064
|
gmsh.option.setNumber("Mesh.SaveAll", 0)
|
|
@@ -936,21 +1067,71 @@ def generate_mesh(
|
|
|
936
1067
|
|
|
937
1068
|
logger.info("Mesh saved: %s", msh_path)
|
|
938
1069
|
|
|
939
|
-
# Generate config
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
1070
|
+
# Generate config if requested
|
|
1071
|
+
config_path = None
|
|
1072
|
+
if write_config:
|
|
1073
|
+
logger.info("Generating Palace config...")
|
|
1074
|
+
config_path = _generate_palace_config(
|
|
1075
|
+
groups, ports, port_info, stack, output_dir, model_name, fmax, driven_config
|
|
1076
|
+
)
|
|
944
1077
|
|
|
945
1078
|
finally:
|
|
946
1079
|
gmsh.clear()
|
|
947
1080
|
gmsh.finalize()
|
|
948
1081
|
|
|
949
|
-
# Build result
|
|
1082
|
+
# Build result (store groups for deferred config generation)
|
|
950
1083
|
result = MeshResult(
|
|
951
1084
|
mesh_path=msh_path,
|
|
952
1085
|
config_path=config_path,
|
|
953
1086
|
port_info=port_info,
|
|
1087
|
+
mesh_stats=mesh_stats,
|
|
1088
|
+
groups=groups,
|
|
1089
|
+
output_dir=output_dir,
|
|
1090
|
+
model_name=model_name,
|
|
1091
|
+
fmax=fmax,
|
|
954
1092
|
)
|
|
955
1093
|
|
|
956
1094
|
return result
|
|
1095
|
+
|
|
1096
|
+
|
|
1097
|
+
def write_config(
|
|
1098
|
+
mesh_result: MeshResult,
|
|
1099
|
+
stack: LayerStack,
|
|
1100
|
+
ports: list[PalacePort],
|
|
1101
|
+
driven_config: DrivenConfig | None = None,
|
|
1102
|
+
) -> Path:
|
|
1103
|
+
"""Write Palace config.json from a MeshResult.
|
|
1104
|
+
|
|
1105
|
+
Use this to generate config separately after mesh().
|
|
1106
|
+
|
|
1107
|
+
Args:
|
|
1108
|
+
mesh_result: Result from generate_mesh(write_config=False)
|
|
1109
|
+
stack: LayerStack for material properties
|
|
1110
|
+
ports: List of PalacePort objects
|
|
1111
|
+
driven_config: Optional DrivenConfig for frequency sweep settings
|
|
1112
|
+
|
|
1113
|
+
Returns:
|
|
1114
|
+
Path to the generated config.json
|
|
1115
|
+
|
|
1116
|
+
Example:
|
|
1117
|
+
>>> result = sim.mesh(output_dir, write_config=False)
|
|
1118
|
+
>>> config_path = write_config(result, stack, ports, driven_config)
|
|
1119
|
+
"""
|
|
1120
|
+
if not mesh_result.groups:
|
|
1121
|
+
raise ValueError("MeshResult has no groups data. Was it generated with write_config=False?")
|
|
1122
|
+
|
|
1123
|
+
config_path = _generate_palace_config(
|
|
1124
|
+
groups=mesh_result.groups,
|
|
1125
|
+
ports=ports,
|
|
1126
|
+
port_info=mesh_result.port_info,
|
|
1127
|
+
stack=stack,
|
|
1128
|
+
output_path=mesh_result.output_dir,
|
|
1129
|
+
model_name=mesh_result.model_name,
|
|
1130
|
+
fmax=mesh_result.fmax,
|
|
1131
|
+
driven_config=driven_config,
|
|
1132
|
+
)
|
|
1133
|
+
|
|
1134
|
+
# Update the mesh_result with the config path
|
|
1135
|
+
mesh_result.config_path = config_path
|
|
1136
|
+
|
|
1137
|
+
return config_path
|
gsim/palace/mesh/pipeline.py
CHANGED
|
@@ -12,8 +12,9 @@ from pathlib import Path
|
|
|
12
12
|
from typing import TYPE_CHECKING
|
|
13
13
|
|
|
14
14
|
if TYPE_CHECKING:
|
|
15
|
+
from gsim.common.stack import LayerStack
|
|
16
|
+
from gsim.palace.models import DrivenConfig
|
|
15
17
|
from gsim.palace.ports.config import PalacePort
|
|
16
|
-
from gsim.palace.stack.extractor import LayerStack
|
|
17
18
|
|
|
18
19
|
from gsim.palace.mesh.generator import generate_mesh as gen_mesh
|
|
19
20
|
|
|
@@ -138,6 +139,15 @@ class MeshResult:
|
|
|
138
139
|
# Port metadata
|
|
139
140
|
port_info: list = field(default_factory=list)
|
|
140
141
|
|
|
142
|
+
# Mesh statistics
|
|
143
|
+
mesh_stats: dict = field(default_factory=dict)
|
|
144
|
+
|
|
145
|
+
# Data needed for deferred config generation
|
|
146
|
+
groups: dict = field(default_factory=dict)
|
|
147
|
+
output_dir: Path | None = None
|
|
148
|
+
model_name: str = "palace"
|
|
149
|
+
fmax: float = 100e9
|
|
150
|
+
|
|
141
151
|
|
|
142
152
|
def generate_mesh(
|
|
143
153
|
component,
|
|
@@ -146,6 +156,8 @@ def generate_mesh(
|
|
|
146
156
|
output_dir: str | Path,
|
|
147
157
|
config: MeshConfig | None = None,
|
|
148
158
|
model_name: str = "palace",
|
|
159
|
+
driven_config: DrivenConfig | None = None,
|
|
160
|
+
write_config: bool = True,
|
|
149
161
|
) -> MeshResult:
|
|
150
162
|
"""Generate mesh for Palace EM simulation.
|
|
151
163
|
|
|
@@ -156,6 +168,8 @@ def generate_mesh(
|
|
|
156
168
|
output_dir: Directory for output files
|
|
157
169
|
config: MeshConfig with mesh parameters
|
|
158
170
|
model_name: Base name for output files (default: "mesh" -> mesh.msh)
|
|
171
|
+
driven_config: Optional DrivenConfig for frequency sweep settings
|
|
172
|
+
write_config: Whether to write config.json (default True)
|
|
159
173
|
|
|
160
174
|
Returns:
|
|
161
175
|
MeshResult with mesh path and metadata
|
|
@@ -178,6 +192,8 @@ def generate_mesh(
|
|
|
178
192
|
air_margin=config.margin,
|
|
179
193
|
fmax=config.fmax,
|
|
180
194
|
show_gui=config.show_gui,
|
|
195
|
+
driven_config=driven_config,
|
|
196
|
+
write_config=write_config,
|
|
181
197
|
)
|
|
182
198
|
|
|
183
199
|
# Convert to pipeline's MeshResult format
|
|
@@ -185,4 +201,9 @@ def generate_mesh(
|
|
|
185
201
|
mesh_path=result.mesh_path,
|
|
186
202
|
config_path=result.config_path,
|
|
187
203
|
port_info=result.port_info,
|
|
204
|
+
mesh_stats=result.mesh_stats,
|
|
205
|
+
groups=result.groups,
|
|
206
|
+
output_dir=result.output_dir,
|
|
207
|
+
model_name=result.model_name,
|
|
208
|
+
fmax=result.fmax,
|
|
188
209
|
)
|
|
@@ -0,0 +1,60 @@
|
|
|
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
|
+
# Geometry
|
|
39
|
+
"GeometryConfig",
|
|
40
|
+
# Stack
|
|
41
|
+
"MaterialConfig",
|
|
42
|
+
# Ports
|
|
43
|
+
"CPWPortConfig",
|
|
44
|
+
"PortConfig",
|
|
45
|
+
"TerminalConfig",
|
|
46
|
+
"WavePortConfig",
|
|
47
|
+
# Mesh
|
|
48
|
+
"MeshConfig",
|
|
49
|
+
# Numerical
|
|
50
|
+
"NumericalConfig",
|
|
51
|
+
# Problems
|
|
52
|
+
"DrivenConfig",
|
|
53
|
+
"EigenmodeConfig",
|
|
54
|
+
"ElectrostaticConfig",
|
|
55
|
+
"MagnetostaticConfig",
|
|
56
|
+
"TransientConfig",
|
|
57
|
+
# Results
|
|
58
|
+
"SimulationResult",
|
|
59
|
+
"ValidationResult",
|
|
60
|
+
]
|
|
@@ -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) -> 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 = {
|
|
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)
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def default(cls, **kwargs) -> 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 = {
|
|
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)
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def fine(cls, **kwargs) -> 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 = {
|
|
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)
|
|
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
|
+
config = {
|
|
48
|
+
"Order": self.order,
|
|
49
|
+
"Solver": {
|
|
50
|
+
"Tolerance": self.tolerance,
|
|
51
|
+
"MaxIterations": self.max_iterations,
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if self.solver_type != "Default":
|
|
56
|
+
config["Solver"]["Type"] = self.solver_type
|
|
57
|
+
|
|
58
|
+
if self.preconditioner != "Default":
|
|
59
|
+
config["Solver"]["Preconditioner"] = self.preconditioner
|
|
60
|
+
|
|
61
|
+
return config
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
__all__ = [
|
|
65
|
+
"NumericalConfig",
|
|
66
|
+
]
|