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.
@@ -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.palace.stack import LayerStack
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
- logger.info("Generating Palace config...")
941
- config_path = _generate_palace_config(
942
- groups, ports, port_info, stack, output_dir, model_name, fmax
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
@@ -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
+ ]