Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,435 changes: 378 additions & 1,057 deletions nbs/palace_2d_cpw.ipynb

Large diffs are not rendered by default.

134 changes: 111 additions & 23 deletions nbs/palace_2d_cpw.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
# extension: .py
# format_name: percent
# format_version: '1.3'
# jupytext_version: 1.19.2
# jupytext_version: 1.19.3
# kernelspec:
# display_name: .venv
# language: python
Expand All @@ -25,6 +25,10 @@
# %% [markdown] papermill={"duration": 0.003686, "end_time": "2026-06-12T07:04:20.157977", "exception": false, "start_time": "2026-06-12T07:04:20.154291", "status": "completed"}
# ### Define GSG electrode

# %%
# %load_ext autoreload
# %autoreload 2

# %% papermill={"duration": 2.54829, "end_time": "2026-06-12T07:04:22.708430", "exception": false, "start_time": "2026-06-12T07:04:20.160140", "status": "completed"}
from pathlib import Path

Expand All @@ -48,28 +52,41 @@ def gsg_electrode(
length: float = 800,
s_width: float = 20,
g_width: float = 40,
gap_width: float = 15,
g_width_top: float | None = None,
g_width_bottom: float | None = None,
gap_width_top: float = 15,
gap_width_bottom: float = 15,
layer=LAYER.TopMetal2drawing,
) -> gf.Component:
"""
Create a GSG (Ground-Signal-Ground) electrode.

Supports different gap widths on the top and bottom sides
of the signal electrode for asymmetric CPW modelling.
Also supports different ground electrode widths.

Args:
length: horizontal length of the electrodes
s_width: width of the signal (center) electrode
g_width: width of the ground electrodes
gap_width: gap between signal and ground electrodes
g_width: width of the ground electrodes (default, used if g_width_top/bottom are None)
g_width_top: width of the top ground electrode (overrides g_width)
g_width_bottom: width of the bottom ground electrode (overrides g_width)
gap_width_top: gap between signal and top ground electrode
gap_width_bottom: gap between signal and bottom ground electrode
layer: layer for the metal
"""
g_width_top = g_width if g_width_top is None else g_width_top
g_width_bottom = g_width if g_width_bottom is None else g_width_bottom

c = gf.Component()

r1 = c << gf.c.rectangle((length, g_width), centered=True, layer=layer)
r1.move((0, (g_width + s_width) / 2 + gap_width))
r1 = c << gf.c.rectangle((length, g_width_top), centered=True, layer=layer)
r1.move((0, (g_width_top + s_width) / 2 + gap_width_top))

_r2 = c << gf.c.rectangle((length, s_width), centered=True, layer=layer)

r3 = c << gf.c.rectangle((length, g_width), centered=True, layer=layer)
r3.move((0, -(g_width + s_width) / 2 - gap_width))
r3 = c << gf.c.rectangle((length, g_width_bottom), centered=True, layer=layer)
r3.move((0, -(g_width_bottom + s_width) / 2 - gap_width_bottom))

c.add_port(
name="o1",
Expand All @@ -92,7 +109,15 @@ def gsg_electrode(
return c


c = gsg_electrode()
c = gsg_electrode(
length=800,
s_width=20,
g_width=40,
g_width_top=40,
g_width_bottom=40,
gap_width_top=15,
gap_width_bottom=15,
)
cc = c.copy()
cc.draw_ports()
cc
Expand Down Expand Up @@ -121,8 +146,10 @@ def gsg_electrode(
mode_sim.set_airbox(margin_x=50.0, margin_y=50, z_above=100.0, z_below=100.0)
mode_sim.set_geometry(c)

freq = 30e9

mode_sim.set_cross_section("x=0")
mode_sim.set_boundary_mode(freq=50e9, num_modes=2, save=2)
mode_sim.set_boundary_mode(freq=freq, num_modes=2, save=2)

# Inspect the geometric cross section before meshing.
section = cross_section.extract_plane_section(c.copy(), stack, axis="x", value=0.0)
Expand Down Expand Up @@ -165,21 +192,82 @@ def gsg_electrode(
importlib.reload(field_viz)
importlib.reload(palace_results)

# If this kernel still holds an older PalaceTextResults object, rebuild it from disk.
if not hasattr(mode_results, "modes"):
mode_results = palace_results.load_text_results("./palace-sim-cpw-waveport-2d")
# If this kernel still holds an older PalaceTextResults object (e.g. from a
# previous run_local that didn't set freq_hz), patch in the correct frequency.
if not hasattr(mode_results, "freq_hz") or mode_results.freq_hz is None:
mode_results.freq_hz = freq

# Pretty-print mode summaries (k_n, n_eff, eta_eff).
# Pretty-print mode summaries with phase velocity (Im/Re k_n, v_p, n_eff).
mode_results.print()

# Dictionary-like access to parsed mode values.
m1 = mode_results["mode_1"]
print("mode_1 dict:", m1)
print(f"mode_1 n_eff = {m1['n_eff']}, mode_1 k_n = {m1['k_n']}")

# Centralized plotting utility from gsim source with tuned defaults.
fig, ax, stream_inputs = plot_fields_2d("./palace-sim-cpw-waveport-2d")
print(
f"streamplot grid={stream_inputs.u.shape}, "
f"seeds={stream_inputs.start_points.shape[0]}"
# For boundary mode, cycles=[1, 2] selects the ParaView cycles corresponding.
# Use chip_bounds to crop the airbox — here (y_min, y_max, z_min, z_max)
# covers the electrode stack without the surrounding air region.
# chip_ymax = 90 # g_width_top/2 + max_gap + s_width/2 + margin
# chip_ymin = -60
# chip_zmax = 30.0 # top of metal + small margin
# chip_zmin = -10.0 # bottom of stack + small margin

fig, axes, stream_inputs_list = plot_fields_2d(
"./palace-sim-cpw-waveport-2d",
field="E_real",
scalar="real",
cycles=[1, 2],
# chip_bounds=(chip_ymin, chip_ymax, chip_zmin, chip_zmax),
show=True,
)

# %% [markdown]
# ### Phase analysis of the electric field in the two gaps
#
# For a quasi-TEM CPW mode, the electric field in the two gaps (signal-to-ground) should be $180^\circ$ out of phase. We probe the complex $\vec{E}$ field at points inside each gap and compare their phases.

# %%
import numpy as np
import pyvista as pv

from gsim.palace.results import load_fields

# Geometry parameters matching the gsg_electrode() call above.
_s_width = 20
_gap_width_top = 15
_gap_width_bottom = 15

# Probe points: middle of each gap, at the metal layer height (z ~ 0).
# top gap center: y = +s_width/2 + gap_width_top/2
# bottom gap center: y = -s_width/2 - gap_width_bottom/2
gap_top_y = _s_width / 2 + _gap_width_top / 2
gap_bot_y = -(_s_width / 2 + _gap_width_bottom / 2)
probe_pts = np.array([[0.0, gap_top_y, 0.0], [0.0, gap_bot_y, 0.0]])

print("Probe points:")
print(f" top gap center: y = {gap_top_y:+.2f}")
print(f" bottom gap center: y = {gap_bot_y:+.2f}\n")

for mode_idx in [1, 2]:
ds = load_fields("./palace-sim-cpw-waveport-2d", cycle=mode_idx)

# Build a pyvista PolyData probe set and sample the dataset.
probe = pv.PolyData(probe_pts)
sampled = probe.sample(ds, snap_to_closest_point=True)

# Complex E = E_real + 1j * E_imag (3-component vectors).
er = np.asarray(sampled.point_data["E_real"]).reshape(-1, 3)
ei = np.asarray(sampled.point_data["E_imag"]).reshape(-1, 3)
ec = er + 1j * ei

# Use the vector sum's phase as a robust scalar.
phase_top = np.angle(np.sum(ec[0]))
phase_bot = np.angle(np.sum(ec[1]))
delta = np.degrees(phase_top - phase_bot)
delta = (delta + 180.0) % 360.0 - 180.0 # wrap to (-180, 180]

print(f"Mode {mode_idx}:")
print(
f" top gap |E|={np.abs(ec[0]).mean():.4e}, phase={np.degrees(phase_top):+7.1f} deg"
)
print(
f" bottom gap |E|={np.abs(ec[1]).mean():.4e}, phase={np.degrees(phase_bot):+7.1f} deg"
)
print(f" phase difference = {delta:+7.1f} deg (expect ~180 for quasi-TEM)\n")
533 changes: 533 additions & 0 deletions nbs/palace_cpw_asymmetric.ipynb

Large diffs are not rendered by default.

Loading
Loading