gsim 0.0.0__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,363 @@
1
+ """Port configuration for Palace EM simulation.
2
+
3
+ Ports define where excitation and measurement occur in the simulation.
4
+ This module provides helpers to configure gdsfactory ports with Palace metadata.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass
10
+ from enum import Enum
11
+ from typing import TYPE_CHECKING
12
+
13
+ if TYPE_CHECKING:
14
+ from gsim.palace.stack import LayerStack
15
+
16
+
17
+ class PortType(Enum):
18
+ """Palace port types (maps to Palace config)."""
19
+
20
+ LUMPED = "lumped" # LumpedPort: internal boundary with circuit impedance
21
+ WAVEPORT = "waveport" # WavePort: domain boundary, modal port
22
+ # SURFACE_CURRENT = "surface_current" # Future: inductance matrix extraction
23
+ # TERMINAL = "terminal" # Future: capacitance matrix extraction (electrostatics)
24
+
25
+
26
+ class PortGeometry(Enum):
27
+ """Internal geometry type for mesh generation."""
28
+
29
+ INPLANE = "inplane" # Horizontal surface on single metal layer (Direction: +X, +Y)
30
+ VIA = "via" # Vertical surface between two metal layers (Direction: +Z)
31
+
32
+
33
+ @dataclass
34
+ class PalacePort:
35
+ """Port definition for Palace simulation."""
36
+
37
+ name: str
38
+ port_type: PortType = PortType.LUMPED # Palace port type
39
+ geometry: PortGeometry = PortGeometry.INPLANE # Mesh geometry type
40
+ center: tuple[float, float] = (0.0, 0.0) # (x, y) in um
41
+ width: float = 0.0 # um
42
+ orientation: float = 0.0 # degrees (0=east, 90=north, 180=west, 270=south)
43
+
44
+ # Z coordinates (filled from stack)
45
+ zmin: float = 0.0
46
+ zmax: float = 0.0
47
+
48
+ # Layer info
49
+ layer: str | None = None # For inplane: target layer
50
+ from_layer: str | None = None # For via: bottom layer
51
+ to_layer: str | None = None # For via: top layer
52
+
53
+ # Port geometry
54
+ length: float | None = None # Port extent along direction (um)
55
+
56
+ # Multi-element support (for CPW)
57
+ multi_element: bool = False
58
+ centers: list[tuple[float, float]] | None = None # Multiple centers for CPW
59
+ directions: list[str] | None = (
60
+ None # Direction per element for CPW (e.g., ["+Y", "-Y"])
61
+ )
62
+
63
+ # Electrical properties
64
+ impedance: float = 50.0 # Ohms
65
+ excited: bool = True # Whether this port is excited (vs just measured)
66
+
67
+ @property
68
+ def direction(self) -> str:
69
+ """Get direction from orientation."""
70
+ # Normalize orientation to 0-360
71
+ angle = self.orientation % 360
72
+ if angle < 45 or angle >= 315:
73
+ return "x" # East
74
+ if 45 <= angle < 135:
75
+ return "y" # North
76
+ if 135 <= angle < 225:
77
+ return "-x" # West
78
+ return "-y" # South
79
+
80
+
81
+ def configure_inplane_port(
82
+ ports,
83
+ layer: str,
84
+ length: float,
85
+ impedance: float = 50.0,
86
+ excited: bool = True,
87
+ ):
88
+ """Configure gdsfactory port(s) as inplane (lumped) ports for Palace simulation.
89
+
90
+ Inplane ports are horizontal ports on a single metal layer, used for CPW gaps
91
+ or similar structures where excitation occurs in the XY plane.
92
+
93
+ Args:
94
+ ports: Single gdsfactory Port or iterable of Ports (e.g., c.ports)
95
+ layer: Target conductor layer name (e.g., 'topmetal2')
96
+ length: Port extent along direction in um (perpendicular to port width)
97
+ impedance: Port impedance in Ohms (default: 50)
98
+ excited: Whether port is excited vs just measured (default: True)
99
+
100
+ Examples:
101
+ ```python
102
+ configure_inplane_port(c.ports["o1"], layer="topmetal2", length=5.0)
103
+ configure_inplane_port(c.ports, layer="topmetal2", length=5.0) # all ports
104
+ ```
105
+ """
106
+ # Handle single port or iterable
107
+ port_list = [ports] if hasattr(ports, "info") else ports
108
+
109
+ for port in port_list:
110
+ port.info["palace_type"] = "lumped"
111
+ port.info["layer"] = layer
112
+ port.info["length"] = length
113
+ port.info["impedance"] = impedance
114
+ port.info["excited"] = excited
115
+
116
+
117
+ def configure_via_port(
118
+ ports,
119
+ from_layer: str,
120
+ to_layer: str,
121
+ impedance: float = 50.0,
122
+ excited: bool = True,
123
+ ):
124
+ """Configure gdsfactory port(s) as via (vertical) lumped ports.
125
+
126
+ Via ports are vertical lumped ports between two metal layers, used for microstrip
127
+ feed structures where excitation occurs in the Z direction.
128
+
129
+ Args:
130
+ ports: Single gdsfactory Port or iterable of Ports (e.g., c.ports)
131
+ from_layer: Bottom conductor layer name (e.g., 'metal1')
132
+ to_layer: Top conductor layer name (e.g., 'topmetal2')
133
+ impedance: Port impedance in Ohms (default: 50)
134
+ excited: Whether port is excited vs just measured (default: True)
135
+
136
+ Examples:
137
+ ```python
138
+ configure_via_port(c.ports["o1"], from_layer="metal1", to_layer="topmetal2")
139
+ configure_via_port(
140
+ c.ports, from_layer="metal1", to_layer="topmetal2"
141
+ ) # all ports
142
+ ```
143
+ """
144
+ # Handle single port or iterable
145
+ port_list = [ports] if hasattr(ports, "info") else ports
146
+
147
+ for port in port_list:
148
+ port.info["palace_type"] = "lumped"
149
+ port.info["from_layer"] = from_layer
150
+ port.info["to_layer"] = to_layer
151
+ port.info["impedance"] = impedance
152
+ port.info["excited"] = excited
153
+
154
+
155
+ def configure_cpw_port(
156
+ port_upper,
157
+ port_lower,
158
+ layer: str,
159
+ length: float,
160
+ impedance: float = 50.0,
161
+ excited: bool = True,
162
+ cpw_name: str | None = None,
163
+ ):
164
+ """Configure two gdsfactory ports as a CPW (multi-element) lumped port.
165
+
166
+ In CPW (Ground-Signal-Ground), E-fields are opposite in the two gaps.
167
+ This function links two ports to form one multi-element lumped port
168
+ that Palace will excite with proper CPW mode.
169
+
170
+ Args:
171
+ port_upper: gdsfactory Port for upper gap (signal-to-ground2)
172
+ port_lower: gdsfactory Port for lower gap (ground1-to-signal)
173
+ layer: Target conductor layer name (e.g., 'topmetal2')
174
+ length: Port extent along direction (um)
175
+ impedance: Port impedance in Ohms (default: 50)
176
+ excited: Whether port is excited (default: True)
177
+ cpw_name: Optional name for the CPW port (default: uses port_lower.name)
178
+
179
+ Examples:
180
+ ```python
181
+ configure_cpw_port(
182
+ port_upper=c.ports["gap_upper"],
183
+ port_lower=c.ports["gap_lower"],
184
+ layer="topmetal2",
185
+ length=5.0,
186
+ )
187
+ ```
188
+ """
189
+ # Generate unique CPW group ID
190
+ cpw_group_id = cpw_name or f"cpw_{port_lower.name}"
191
+
192
+ # Auto-detect directions based on positions
193
+ upper_y = float(port_upper.center[1])
194
+ lower_y = float(port_lower.center[1])
195
+
196
+ # The port farther from origin in Y gets negative direction (E toward signal)
197
+ # The port closer to origin gets positive direction (E toward signal)
198
+ if upper_y > lower_y:
199
+ upper_direction = "-Y"
200
+ lower_direction = "+Y"
201
+ else:
202
+ upper_direction = "+Y"
203
+ lower_direction = "-Y"
204
+
205
+ # Store metadata on BOTH ports, marking them as CPW elements
206
+ for port, direction in [
207
+ (port_upper, upper_direction),
208
+ (port_lower, lower_direction),
209
+ ]:
210
+ port.info["palace_type"] = "cpw_element"
211
+ port.info["cpw_group"] = cpw_group_id
212
+ port.info["cpw_direction"] = direction
213
+ port.info["layer"] = layer
214
+ port.info["length"] = length
215
+ port.info["impedance"] = impedance
216
+ port.info["excited"] = excited
217
+
218
+
219
+ def extract_ports(component, stack: LayerStack) -> list[PalacePort]:
220
+ """Extract Palace ports from a gdsfactory component.
221
+
222
+ Handles all port types: inplane, via, and CPW (multi-element).
223
+ CPW ports are automatically grouped by their cpw_group ID.
224
+
225
+ Args:
226
+ component: gdsfactory Component with configured ports
227
+ stack: LayerStack from stack module
228
+
229
+ Returns:
230
+ List of PalacePort objects ready for simulation
231
+ """
232
+ palace_ports = []
233
+
234
+ # First, collect CPW elements grouped by cpw_group
235
+ cpw_groups: dict[str, list] = {}
236
+
237
+ for port in component.ports:
238
+ info = port.info
239
+ palace_type = info.get("palace_type")
240
+
241
+ if palace_type is None:
242
+ continue
243
+
244
+ if palace_type == "cpw_element":
245
+ group_id = info.get("cpw_group")
246
+ if group_id:
247
+ if group_id not in cpw_groups:
248
+ cpw_groups[group_id] = []
249
+ cpw_groups[group_id].append(port)
250
+ continue
251
+
252
+ # Handle single-element ports (lumped, waveport)
253
+ center = (float(port.center[0]), float(port.center[1]))
254
+ width = float(port.width)
255
+ orientation = float(port.orientation) if port.orientation is not None else 0.0
256
+
257
+ zmin, zmax = 0.0, 0.0
258
+ from_layer = info.get("from_layer")
259
+ to_layer = info.get("to_layer")
260
+ layer_name = info.get("layer")
261
+
262
+ if palace_type == "lumped":
263
+ port_type = PortType.LUMPED
264
+ if from_layer and to_layer:
265
+ geometry = PortGeometry.VIA
266
+ if from_layer in stack.layers:
267
+ zmin = stack.layers[from_layer].zmin
268
+ if to_layer in stack.layers:
269
+ zmax = stack.layers[to_layer].zmax
270
+ elif layer_name:
271
+ geometry = PortGeometry.INPLANE
272
+ if layer_name in stack.layers:
273
+ layer = stack.layers[layer_name]
274
+ zmin = layer.zmin
275
+ zmax = layer.zmax
276
+ else:
277
+ raise ValueError(f"Lumped port '{port.name}' missing layer info")
278
+
279
+ elif palace_type == "waveport":
280
+ port_type = PortType.WAVEPORT
281
+ geometry = PortGeometry.INPLANE # Waveport geometry TBD
282
+ zmin, zmax = stack.get_z_range()
283
+
284
+ else:
285
+ raise ValueError(f"Unknown port type: {palace_type}")
286
+
287
+ palace_port = PalacePort(
288
+ name=port.name,
289
+ port_type=port_type,
290
+ geometry=geometry,
291
+ center=center,
292
+ width=width,
293
+ orientation=orientation,
294
+ zmin=zmin,
295
+ zmax=zmax,
296
+ layer=layer_name,
297
+ from_layer=from_layer,
298
+ to_layer=to_layer,
299
+ length=info.get("length"),
300
+ impedance=info.get("impedance", 50.0),
301
+ excited=info.get("excited", True),
302
+ )
303
+ palace_ports.append(palace_port)
304
+
305
+ # Now process CPW groups into multi-element PalacePort objects
306
+ for group_id, ports in cpw_groups.items():
307
+ if len(ports) != 2:
308
+ raise ValueError(
309
+ f"CPW group '{group_id}' must have exactly 2 ports, got {len(ports)}"
310
+ )
311
+
312
+ # Sort by Y position to get consistent ordering
313
+ ports_sorted = sorted(ports, key=lambda p: p.center[1], reverse=True)
314
+ port_upper, port_lower = ports_sorted[0], ports_sorted[1]
315
+
316
+ info = port_lower.info
317
+ layer_name = info.get("layer")
318
+
319
+ # Get z coordinates from stack
320
+ zmin, zmax = 0.0, 0.0
321
+ if layer_name and layer_name in stack.layers:
322
+ layer = stack.layers[layer_name]
323
+ zmin = layer.zmin
324
+ zmax = layer.zmax
325
+
326
+ # Get centers and directions
327
+ centers = [
328
+ (float(port_upper.center[0]), float(port_upper.center[1])),
329
+ (float(port_lower.center[0]), float(port_lower.center[1])),
330
+ ]
331
+ directions = [
332
+ port_upper.info.get("cpw_direction", "-Y"),
333
+ port_lower.info.get("cpw_direction", "+Y"),
334
+ ]
335
+
336
+ # Use average center for the main center field
337
+ avg_center = (
338
+ (centers[0][0] + centers[1][0]) / 2,
339
+ (centers[0][1] + centers[1][1]) / 2,
340
+ )
341
+
342
+ cpw_port = PalacePort(
343
+ name=group_id,
344
+ port_type=PortType.LUMPED,
345
+ geometry=PortGeometry.INPLANE,
346
+ center=avg_center,
347
+ width=float(port_lower.width),
348
+ orientation=float(port_lower.orientation)
349
+ if port_lower.orientation
350
+ else 0.0,
351
+ zmin=zmin,
352
+ zmax=zmax,
353
+ layer=layer_name,
354
+ length=info.get("length"),
355
+ multi_element=True,
356
+ centers=centers,
357
+ directions=directions,
358
+ impedance=info.get("impedance", 50.0),
359
+ excited=info.get("excited", True),
360
+ )
361
+ palace_ports.append(cpw_port)
362
+
363
+ return palace_ports
@@ -0,0 +1,149 @@
1
+ """Layer stack extraction and parsing for Palace EM simulation.
2
+
3
+ Usage:
4
+ # From PDK module (preferred, no file needed)
5
+ from gsim.palace.stack import get_stack
6
+ stack = get_stack(pdk=ihp)
7
+
8
+ # From YAML file (for custom/tweaked stacks)
9
+ stack = get_stack(yaml_path="my_stack.yaml")
10
+
11
+ # Export to YAML for manual editing
12
+ stack.to_yaml("my_stack.yaml")
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from pathlib import Path
18
+
19
+ import gdsfactory as gf
20
+ import yaml
21
+
22
+ from gsim.palace.stack.extractor import (
23
+ Layer,
24
+ LayerStack,
25
+ ValidationResult,
26
+ extract_from_pdk,
27
+ extract_layer_stack,
28
+ )
29
+ from gsim.palace.stack.materials import (
30
+ MATERIALS_DB,
31
+ MaterialProperties,
32
+ get_material_properties,
33
+ material_is_conductor,
34
+ material_is_dielectric,
35
+ )
36
+ from gsim.palace.stack.visualization import (
37
+ StackLayer,
38
+ parse_layer_stack,
39
+ plot_stack,
40
+ print_stack,
41
+ print_stack_table,
42
+ )
43
+
44
+
45
+ def get_stack(
46
+ yaml_path: str | Path | None = None,
47
+ **kwargs,
48
+ ) -> LayerStack:
49
+ """Get layer stack from active PDK or YAML file.
50
+
51
+ Args:
52
+ yaml_path: Path to custom YAML stack file. If None, uses active PDK.
53
+ **kwargs: Additional args passed to extract_layer_stack:
54
+ - substrate_thickness: Thickness below z=0 in um (default: 2.0)
55
+ - air_above: Air box height above top metal in um (default: 200)
56
+ - include_substrate: Include lossy silicon substrate (default: False).
57
+ When False, matches gds2palace "nosub" behavior for RF simulation.
58
+
59
+ Returns:
60
+ LayerStack object
61
+
62
+ Examples:
63
+ # From active PDK (after PDK.activate()) - no substrate (recommended for RF)
64
+ stack = get_stack()
65
+
66
+ # With lossy substrate (for substrate coupling studies)
67
+ stack = get_stack(include_substrate=True)
68
+
69
+ # From YAML file
70
+ stack = get_stack(yaml_path="custom_stack.yaml")
71
+
72
+ # With custom settings
73
+ stack = get_stack(air_above=300, substrate_thickness=5.0)
74
+ """
75
+ if yaml_path is not None:
76
+ return load_stack_yaml(yaml_path)
77
+
78
+ pdk = gf.get_active_pdk()
79
+ if pdk is None:
80
+ raise ValueError("No active PDK found. Call PDK.activate() first.")
81
+
82
+ return extract_from_pdk(pdk, **kwargs)
83
+
84
+
85
+ def load_stack_yaml(yaml_path: str | Path) -> LayerStack:
86
+ """Load layer stack from YAML file.
87
+
88
+ Args:
89
+ yaml_path: Path to YAML file
90
+
91
+ Returns:
92
+ LayerStack object
93
+ """
94
+ yaml_path = Path(yaml_path)
95
+ with open(yaml_path) as f:
96
+ data = yaml.safe_load(f)
97
+
98
+ # Reconstruct LayerStack from dict
99
+ stack = LayerStack(
100
+ pdk_name=data.get("pdk", "unknown"),
101
+ units=data.get("units", "um"),
102
+ )
103
+
104
+ # Load materials
105
+ stack.materials = data.get("materials", {})
106
+
107
+ # Load layers
108
+ for name, layer_data in data.get("layers", {}).items():
109
+ stack.layers[name] = Layer(
110
+ name=name,
111
+ gds_layer=tuple(layer_data["gds_layer"]),
112
+ zmin=layer_data["zmin"],
113
+ zmax=layer_data["zmax"],
114
+ thickness=layer_data.get(
115
+ "thickness", layer_data["zmax"] - layer_data["zmin"]
116
+ ),
117
+ material=layer_data["material"],
118
+ layer_type=layer_data["type"],
119
+ mesh_resolution=layer_data.get("mesh_resolution", "medium"),
120
+ )
121
+
122
+ # Load dielectrics
123
+ stack.dielectrics = data.get("dielectrics", [])
124
+
125
+ # Load simulation settings
126
+ stack.simulation = data.get("simulation", {})
127
+
128
+ return stack
129
+
130
+
131
+ __all__ = [
132
+ "MATERIALS_DB",
133
+ "Layer",
134
+ "LayerStack",
135
+ "MaterialProperties",
136
+ "StackLayer",
137
+ "ValidationResult",
138
+ "extract_from_pdk",
139
+ "extract_layer_stack",
140
+ "get_material_properties",
141
+ "get_stack",
142
+ "load_stack_yaml",
143
+ "material_is_conductor",
144
+ "material_is_dielectric",
145
+ "parse_layer_stack",
146
+ "plot_stack",
147
+ "print_stack",
148
+ "print_stack_table",
149
+ ]