Skip to content

Commit 40db34f

Browse files
feat(plotly): implement smith-chart-basic (#3869)
## Implementation: `smith-chart-basic` - plotly Implements the **plotly** version of `smith-chart-basic`. **File:** `plots/smith-chart-basic/implementations/plotly.py` **Parent Issue:** #3792 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/21047488889)* --------- 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 de2ca56 commit 40db34f

File tree

2 files changed

+439
-0
lines changed

2 files changed

+439
-0
lines changed
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
""" pyplots.ai
2+
smith-chart-basic: Smith Chart for RF/Impedance
3+
Library: plotly 6.5.2 | Python 3.13.11
4+
Quality: 91/100 | Created: 2026-01-15
5+
"""
6+
7+
import numpy as np
8+
import plotly.graph_objects as go
9+
10+
11+
# Reference impedance
12+
Z0 = 50 # ohms
13+
14+
# Generate sample impedance data (antenna-like frequency sweep)
15+
np.random.seed(42)
16+
freq = np.linspace(1e9, 6e9, 50) # 1-6 GHz
17+
18+
# Simulate realistic antenna impedance trajectory
19+
# Starting inductive, moving through resonance to capacitive
20+
t = np.linspace(0, 2 * np.pi, 50)
21+
z_real = 25 + 40 * np.sin(t / 2) ** 2 + 5 * np.random.randn(50)
22+
z_imag = 60 * np.cos(t) + 10 * np.sin(2 * t)
23+
24+
# Normalize impedance
25+
z_norm = (z_real + 1j * z_imag) / Z0
26+
27+
# Calculate reflection coefficient (gamma)
28+
gamma = (z_norm - 1) / (z_norm + 1)
29+
gamma_real = gamma.real
30+
gamma_imag = gamma.imag
31+
32+
# Create figure
33+
fig = go.Figure()
34+
35+
# Draw Smith chart grid - constant resistance circles
36+
r_values = [0, 0.2, 0.5, 1, 2, 5]
37+
theta_grid = np.linspace(0, 2 * np.pi, 200)
38+
39+
for r in r_values:
40+
# Constant resistance circle: center at (r/(r+1), 0), radius 1/(r+1)
41+
center_x = r / (r + 1)
42+
radius = 1 / (r + 1)
43+
circle_x = center_x + radius * np.cos(theta_grid)
44+
circle_y = radius * np.sin(theta_grid)
45+
# Clip to unit circle
46+
mask = circle_x**2 + circle_y**2 <= 1.01
47+
circle_x_clipped = np.where(mask, circle_x, np.nan)
48+
circle_y_clipped = np.where(mask, circle_y, np.nan)
49+
fig.add_trace(
50+
go.Scatter(
51+
x=circle_x_clipped,
52+
y=circle_y_clipped,
53+
mode="lines",
54+
line=dict(color="rgba(100,100,100,0.4)", width=1),
55+
hoverinfo="skip",
56+
showlegend=False,
57+
)
58+
)
59+
60+
# Draw constant reactance arcs
61+
x_values = [0.2, 0.5, 1, 2, 5]
62+
63+
for x in x_values:
64+
# Constant reactance arc: center at (1, 1/x), radius 1/x
65+
center_y = 1 / x
66+
radius = 1 / x
67+
arc_theta = np.linspace(-np.pi, np.pi, 400)
68+
arc_x = 1 + radius * np.cos(arc_theta)
69+
arc_y = center_y + radius * np.sin(arc_theta)
70+
# Clip to unit circle
71+
mask = (arc_x**2 + arc_y**2 <= 1.01) & (arc_x >= -1)
72+
arc_x_clipped = np.where(mask, arc_x, np.nan)
73+
arc_y_clipped = np.where(mask, arc_y, np.nan)
74+
fig.add_trace(
75+
go.Scatter(
76+
x=arc_x_clipped,
77+
y=arc_y_clipped,
78+
mode="lines",
79+
line=dict(color="rgba(100,100,100,0.4)", width=1),
80+
hoverinfo="skip",
81+
showlegend=False,
82+
)
83+
)
84+
# Negative reactance (mirror)
85+
fig.add_trace(
86+
go.Scatter(
87+
x=arc_x_clipped,
88+
y=-arc_y_clipped,
89+
mode="lines",
90+
line=dict(color="rgba(100,100,100,0.4)", width=1),
91+
hoverinfo="skip",
92+
showlegend=False,
93+
)
94+
)
95+
96+
# Draw horizontal axis (real axis)
97+
fig.add_trace(
98+
go.Scatter(
99+
x=[-1, 1],
100+
y=[0, 0],
101+
mode="lines",
102+
line=dict(color="rgba(100,100,100,0.5)", width=1),
103+
hoverinfo="skip",
104+
showlegend=False,
105+
)
106+
)
107+
108+
# Draw unit circle (boundary)
109+
boundary_theta = np.linspace(0, 2 * np.pi, 200)
110+
fig.add_trace(
111+
go.Scatter(
112+
x=np.cos(boundary_theta),
113+
y=np.sin(boundary_theta),
114+
mode="lines",
115+
line=dict(color="#306998", width=2),
116+
hoverinfo="skip",
117+
showlegend=False,
118+
)
119+
)
120+
121+
# Plot impedance locus
122+
freq_ghz = freq / 1e9
123+
hover_text = [f"{f:.2f} GHz<br>Z = {z_real[i]:.1f} + j{z_imag[i]:.1f} Ω" for i, f in enumerate(freq_ghz)]
124+
125+
fig.add_trace(
126+
go.Scatter(
127+
x=gamma_real,
128+
y=gamma_imag,
129+
mode="lines+markers",
130+
line=dict(color="#306998", width=4),
131+
marker=dict(size=10, color="#FFD43B", line=dict(color="#306998", width=2)),
132+
name="Impedance Locus",
133+
text=hover_text,
134+
hoverinfo="text",
135+
)
136+
)
137+
138+
# Add frequency labels at key points with varied positions to avoid overlap
139+
label_configs = [
140+
(0, 40, -40), # 1.0 GHz - upper right
141+
(16, -50, -30), # 2.6 GHz - left
142+
(32, 50, 30), # 4.3 GHz - right
143+
(49, 40, -50), # 6.0 GHz - upper right
144+
]
145+
for idx, ax_offset, ay_offset in label_configs:
146+
fig.add_annotation(
147+
x=gamma_real[idx],
148+
y=gamma_imag[idx],
149+
text=f"{freq_ghz[idx]:.1f} GHz",
150+
showarrow=True,
151+
arrowhead=2,
152+
arrowsize=1,
153+
arrowwidth=2,
154+
arrowcolor="#306998",
155+
ax=ax_offset,
156+
ay=ay_offset,
157+
font=dict(size=16, color="#306998"),
158+
bgcolor="white",
159+
bordercolor="#306998",
160+
borderwidth=1,
161+
borderpad=4,
162+
)
163+
164+
# Mark center (matched condition)
165+
fig.add_trace(
166+
go.Scatter(
167+
x=[0],
168+
y=[0],
169+
mode="markers",
170+
marker=dict(size=15, color="#FFD43B", symbol="x", line=dict(color="#306998", width=3)),
171+
name="Matched (Z = Z₀)",
172+
hoverinfo="name",
173+
)
174+
)
175+
176+
# Update layout
177+
fig.update_layout(
178+
title=dict(text="smith-chart-basic · plotly · pyplots.ai", font=dict(size=28), x=0.5, xanchor="center"),
179+
xaxis=dict(
180+
title=dict(text="Real(Γ)", font=dict(size=22)),
181+
tickfont=dict(size=18),
182+
range=[-1.15, 1.15],
183+
scaleanchor="y",
184+
scaleratio=1,
185+
showgrid=False,
186+
zeroline=False,
187+
),
188+
yaxis=dict(
189+
title=dict(text="Imag(Γ)", font=dict(size=22)),
190+
tickfont=dict(size=18),
191+
range=[-1.15, 1.15],
192+
showgrid=False,
193+
zeroline=False,
194+
),
195+
template="plotly_white",
196+
legend=dict(
197+
x=0.02, y=0.98, font=dict(size=18), bgcolor="rgba(255,255,255,0.9)", bordercolor="#306998", borderwidth=1
198+
),
199+
margin=dict(l=80, r=80, t=100, b=80),
200+
)
201+
202+
# Add resistance labels on the right
203+
r_labels = [(0, "0"), (0.2, "0.2"), (0.5, "0.5"), (1, "1"), (2, "2"), (5, "5")]
204+
for r, label in r_labels:
205+
x_pos = r / (r + 1) + 1 / (r + 1)
206+
if x_pos <= 1.0:
207+
fig.add_annotation(x=x_pos, y=0, text=label, showarrow=False, font=dict(size=14, color="gray"), yshift=-20)
208+
209+
# Add reactance labels at chart boundary
210+
reactance_label_positions = [
211+
(1, 0.85, 0.52, "+j1"), # +j1 label
212+
(1, 0.85, -0.52, "-j1"), # -j1 label
213+
(0.5, 0.6, 0.8, "+j0.5"), # +j0.5 label
214+
(0.5, 0.6, -0.8, "-j0.5"), # -j0.5 label
215+
]
216+
for _x, lx, ly, label in reactance_label_positions:
217+
fig.add_annotation(x=lx, y=ly, text=label, showarrow=False, font=dict(size=14, color="gray"))
218+
219+
# Save as PNG and HTML
220+
fig.write_image("plot.png", width=1600, height=900, scale=3)
221+
fig.write_html("plot.html")

0 commit comments

Comments
 (0)