Skip to content

Commit cd8958a

Browse files
feat(letsplot): implement smith-chart-basic (#3871)
## Implementation: `smith-chart-basic` - letsplot Implements the **letsplot** version of `smith-chart-basic`. **File:** `plots/smith-chart-basic/implementations/letsplot.py` **Parent Issue:** #3792 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/21047489117)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent ef6cee2 commit cd8958a

File tree

2 files changed

+435
-0
lines changed

2 files changed

+435
-0
lines changed
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
""" pyplots.ai
2+
smith-chart-basic: Smith Chart for RF/Impedance
3+
Library: letsplot 4.8.2 | Python 3.13.11
4+
Quality: 91/100 | Created: 2026-01-15
5+
"""
6+
7+
import numpy as np
8+
import pandas as pd
9+
from lets_plot import (
10+
LetsPlot,
11+
aes,
12+
coord_fixed,
13+
element_rect,
14+
element_text,
15+
geom_path,
16+
geom_point,
17+
geom_text,
18+
ggplot,
19+
ggsize,
20+
labs,
21+
layer_tooltips,
22+
theme,
23+
theme_void,
24+
)
25+
from lets_plot.export import ggsave
26+
27+
28+
LetsPlot.setup_html()
29+
30+
# Reference impedance
31+
Z0 = 50 # ohms
32+
33+
# Generate example impedance data: simulated antenna impedance sweep
34+
np.random.seed(42)
35+
n_points = 60
36+
freq = np.linspace(1e9, 6e9, n_points) # 1-6 GHz
37+
38+
# Simulate realistic antenna impedance that varies with frequency
39+
# Creates a trajectory that traverses both upper (inductive) and lower (capacitive) regions
40+
t = np.linspace(0, 2.5 * np.pi, n_points)
41+
# Resistance transitions from low to high across frequency sweep
42+
r_base = 20 + 80 * (1 - np.exp(-t / 2.5)) # Resistance varies 20-100 ohms
43+
# Reactance oscillates between inductive (+) and capacitive (-) regions
44+
x_base = 50 * np.sin(t) * np.exp(-t / 6) # Stronger reactance swing
45+
z_real = r_base + 3 * np.random.randn(n_points)
46+
z_imag = x_base + 2 * np.random.randn(n_points)
47+
48+
# Normalize impedance
49+
z_norm_real = z_real / Z0
50+
z_norm_imag = z_imag / Z0
51+
52+
# Convert normalized impedance to reflection coefficient (gamma)
53+
# gamma = (Z - Z0) / (Z + Z0) = (z_norm - 1) / (z_norm + 1)
54+
z_norm = z_norm_real + 1j * z_norm_imag
55+
gamma = (z_norm - 1) / (z_norm + 1)
56+
gamma_real = np.real(gamma)
57+
gamma_imag = np.imag(gamma)
58+
59+
# Create dataframe for impedance locus with full data for tooltips
60+
df_locus = pd.DataFrame(
61+
{
62+
"gamma_real": gamma_real,
63+
"gamma_imag": gamma_imag,
64+
"freq_ghz": freq / 1e9,
65+
"z_real": z_real,
66+
"z_imag": z_imag,
67+
"vswr": (1 + np.abs(gamma)) / (1 - np.abs(gamma)),
68+
}
69+
)
70+
71+
# Build Smith chart grid data - constant resistance circles
72+
# Formula: circle centered at (r/(r+1), 0) with radius 1/(r+1)
73+
grid_data = []
74+
r_values = [0, 0.2, 0.5, 1, 2, 5]
75+
for r in r_values:
76+
center_x = r / (r + 1)
77+
radius = 1 / (r + 1)
78+
theta = np.linspace(0, 2 * np.pi, 100)
79+
x = center_x + radius * np.cos(theta)
80+
y = radius * np.sin(theta)
81+
# Clip to unit circle
82+
mask = (x**2 + y**2) <= 1.001
83+
for i in np.where(mask)[0]:
84+
grid_data.append({"x": x[i], "y": y[i], "type": "resistance", "group": f"r_{r}"})
85+
86+
# Build Smith chart grid data - constant reactance arcs
87+
# Formula: circle centered at (1, 1/x) with radius |1/x|
88+
x_values = [0.2, 0.5, 1, 2, 5]
89+
for xv in x_values:
90+
for sign in [1, -1]:
91+
x_val = sign * xv
92+
center_y = 1 / x_val
93+
radius = abs(1 / x_val)
94+
theta = np.linspace(0, 2 * np.pi, 100)
95+
x = 1 + radius * np.cos(theta)
96+
y = center_y + radius * np.sin(theta)
97+
# Keep only points inside unit circle
98+
mask = (x**2 + y**2) <= 1.001
99+
for i in np.where(mask)[0]:
100+
grid_data.append({"x": x[i], "y": y[i], "type": "reactance", "group": f"x_{x_val}"})
101+
102+
df_grid = pd.DataFrame(grid_data)
103+
104+
# Unit circle (outer boundary)
105+
theta_circle = np.linspace(0, 2 * np.pi, 200)
106+
df_boundary = pd.DataFrame({"x": np.cos(theta_circle), "y": np.sin(theta_circle)})
107+
108+
# Real axis (horizontal line through center)
109+
df_axis = pd.DataFrame({"x": [-1, 1], "y": [0, 0]})
110+
111+
# Key frequency labels at start, middle, and end (reduced to avoid overlap)
112+
label_indices = [0, n_points // 2, n_points - 1]
113+
df_labels = pd.DataFrame(
114+
{
115+
"x": [gamma_real[i] for i in label_indices],
116+
"y": [gamma_imag[i] for i in label_indices],
117+
"label": [f"{freq[i] / 1e9:.1f} GHz" for i in label_indices],
118+
}
119+
)
120+
121+
# Start/end marker dataframes with labels for legend
122+
df_start = df_locus.head(1).copy()
123+
df_start["marker"] = "Start (1.0 GHz)"
124+
df_end = df_locus.tail(1).copy()
125+
df_end["marker"] = "End (6.0 GHz)"
126+
127+
# Center point (matched condition Z = Z0)
128+
df_center = pd.DataFrame({"x": [0], "y": [0]})
129+
130+
# Legend data for markers (positioned outside chart area)
131+
df_legend = pd.DataFrame(
132+
{
133+
"x": [1.15, 1.15],
134+
"y": [0.15, -0.15],
135+
"color": ["#22C55E", "#DC2626"],
136+
"label": ["Start (1.0 GHz)", "End (6.0 GHz)"],
137+
}
138+
)
139+
140+
# Create the Smith chart plot
141+
plot = (
142+
ggplot()
143+
# Outer boundary circle
144+
+ geom_path(aes(x="x", y="y"), data=df_boundary, color="#333333", size=1.5)
145+
# Real axis
146+
+ geom_path(aes(x="x", y="y"), data=df_axis, color="#333333", size=0.8)
147+
# Grid lines - resistance circles
148+
+ geom_path(
149+
aes(x="x", y="y", group="group"),
150+
data=df_grid[df_grid["type"] == "resistance"],
151+
color="#306998",
152+
size=0.6,
153+
alpha=0.5,
154+
)
155+
# Grid lines - reactance arcs
156+
+ geom_path(
157+
aes(x="x", y="y", group="group"),
158+
data=df_grid[df_grid["type"] == "reactance"],
159+
color="#306998",
160+
size=0.6,
161+
alpha=0.5,
162+
)
163+
# Impedance locus curve
164+
+ geom_path(aes(x="gamma_real", y="gamma_imag"), data=df_locus, color="#FFD43B", size=2.5)
165+
# Interactive points with hover tooltips showing impedance data
166+
+ geom_point(
167+
aes(x="gamma_real", y="gamma_imag"),
168+
data=df_locus,
169+
color="#FFD43B",
170+
size=4,
171+
alpha=0.8,
172+
tooltips=layer_tooltips()
173+
.line("Freq: @freq_ghz GHz")
174+
.line("Z: @z_real + j@z_imag Ω")
175+
.line("VSWR: @vswr")
176+
.format("z_real", ".1f")
177+
.format("z_imag", ".1f")
178+
.format("vswr", ".2f"),
179+
)
180+
# Start marker (green)
181+
+ geom_point(aes(x="gamma_real", y="gamma_imag"), data=df_start, color="#22C55E", size=10)
182+
# End marker (red)
183+
+ geom_point(aes(x="gamma_real", y="gamma_imag"), data=df_end, color="#DC2626", size=10)
184+
# Center point (matched condition)
185+
+ geom_point(aes(x="x", y="y"), data=df_center, color="#333333", size=6, shape=3)
186+
# Frequency labels along trajectory
187+
+ geom_text(aes(x="x", y="y", label="label"), data=df_labels, size=14, nudge_x=0.08, nudge_y=0.08, color="#333333")
188+
# Legend markers (outside chart)
189+
+ geom_point(aes(x="x", y="y"), data=df_legend[df_legend["label"].str.contains("Start")], color="#22C55E", size=8)
190+
+ geom_point(aes(x="x", y="y"), data=df_legend[df_legend["label"].str.contains("End")], color="#DC2626", size=8)
191+
+ geom_text(aes(x="x", y="y", label="label"), data=df_legend, size=12, hjust=0, nudge_x=0.05, color="#333333")
192+
# Styling
193+
+ labs(title="smith-chart-basic · letsplot · pyplots.ai")
194+
+ theme_void()
195+
+ theme(
196+
plot_title=element_text(size=24, hjust=0.5),
197+
plot_background=element_rect(fill="white"),
198+
panel_background=element_rect(fill="white"),
199+
)
200+
+ coord_fixed(ratio=1, xlim=(-1.3, 1.8), ylim=(-1.3, 1.3)) # Extend x to accommodate legend
201+
+ ggsize(1200, 1200) # Square aspect for Smith chart
202+
)
203+
204+
# Save outputs
205+
ggsave(plot, "plot.png", path=".", scale=3)
206+
ggsave(plot, "plot.html", path=".")

0 commit comments

Comments
 (0)