Skip to content

Commit de2ca56

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

File tree

2 files changed

+410
-0
lines changed

2 files changed

+410
-0
lines changed
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
""" pyplots.ai
2+
smith-chart-basic: Smith Chart for RF/Impedance
3+
Library: bokeh 3.8.2 | Python 3.13.11
4+
Quality: 91/100 | Created: 2026-01-15
5+
"""
6+
7+
import numpy as np
8+
from bokeh.io import export_png, output_file, save
9+
from bokeh.models import ColumnDataSource, Label
10+
from bokeh.plotting import figure
11+
12+
13+
# Reference impedance
14+
Z0 = 50 # ohms
15+
16+
# Create figure (square for Smith chart)
17+
p = figure(
18+
width=3600,
19+
height=3600,
20+
title="smith-chart-basic · bokeh · pyplots.ai",
21+
x_axis_label="Real(Γ)",
22+
y_axis_label="Imag(Γ)",
23+
x_range=(-1.35, 1.35),
24+
y_range=(-1.35, 1.35),
25+
match_aspect=True,
26+
)
27+
28+
# Style settings
29+
p.title.text_font_size = "32pt"
30+
p.xaxis.axis_label_text_font_size = "24pt"
31+
p.yaxis.axis_label_text_font_size = "24pt"
32+
p.xaxis.major_label_text_font_size = "18pt"
33+
p.yaxis.major_label_text_font_size = "18pt"
34+
35+
# Smith chart grid colors
36+
GRID_COLOR = "#808080"
37+
GRID_ALPHA = 0.4
38+
BOUNDARY_COLOR = "#306998"
39+
40+
# Draw unit circle (outer boundary - |Γ| = 1)
41+
theta_circle = np.linspace(0, 2 * np.pi, 500)
42+
unit_x = np.cos(theta_circle)
43+
unit_y = np.sin(theta_circle)
44+
p.line(unit_x, unit_y, line_width=3, line_color=BOUNDARY_COLOR, alpha=0.9)
45+
46+
# Constant resistance circles: r = 0, 0.2, 0.5, 1, 2, 5
47+
r_values = [0, 0.2, 0.5, 1, 2, 5]
48+
for r in r_values:
49+
if r == 0:
50+
# r=0 is the unit circle (already drawn)
51+
continue
52+
# Circle center at (r/(1+r), 0) with radius 1/(1+r)
53+
center = r / (1 + r)
54+
radius = 1 / (1 + r)
55+
theta = np.linspace(0, 2 * np.pi, 300)
56+
cx = center + radius * np.cos(theta)
57+
cy = radius * np.sin(theta)
58+
# Only keep points inside unit circle
59+
mask = cx**2 + cy**2 <= 1.001
60+
cx, cy = cx[mask], cy[mask]
61+
if len(cx) > 0:
62+
p.line(cx, cy, line_width=1.5, line_color=GRID_COLOR, alpha=GRID_ALPHA)
63+
64+
# Constant reactance arcs: x = ±0.2, ±0.5, ±1, ±2, ±5
65+
x_values = [0.2, 0.5, 1, 2, 5]
66+
for x in x_values:
67+
# Positive reactance arc: center at (1, 1/x) with radius |1/x|
68+
center_y = 1.0 / x
69+
radius = 1.0 / x
70+
# Generate arc points
71+
theta = np.linspace(-np.pi, np.pi, 500)
72+
ax = 1.0 + radius * np.cos(theta)
73+
ay = center_y + radius * np.sin(theta)
74+
# Keep only points inside unit circle
75+
mask = (ax**2 + ay**2 <= 1.001) & (ax >= -0.001)
76+
ax, ay = ax[mask], ay[mask]
77+
if len(ax) > 1:
78+
# Sort by angle from center for smooth arc
79+
angles = np.arctan2(ay - center_y, ax - 1.0)
80+
order = np.argsort(angles)
81+
p.line(ax[order], ay[order], line_width=1.5, line_color=GRID_COLOR, alpha=GRID_ALPHA)
82+
83+
# Negative reactance arc: center at (1, -1/x)
84+
center_y = -1.0 / x
85+
radius = 1.0 / x
86+
theta = np.linspace(-np.pi, np.pi, 500)
87+
ax = 1.0 + radius * np.cos(theta)
88+
ay = center_y + radius * np.sin(theta)
89+
mask = (ax**2 + ay**2 <= 1.001) & (ax >= -0.001)
90+
ax, ay = ax[mask], ay[mask]
91+
if len(ax) > 1:
92+
angles = np.arctan2(ay - center_y, ax - 1.0)
93+
order = np.argsort(angles)
94+
p.line(ax[order], ay[order], line_width=1.5, line_color=GRID_COLOR, alpha=GRID_ALPHA)
95+
96+
# Draw horizontal axis (pure resistance line, x = 0)
97+
p.line([-1, 1], [0, 0], line_width=2, line_color="#444444", alpha=0.6)
98+
99+
# Generate example impedance data (antenna S11 sweep from 1-6 GHz)
100+
np.random.seed(42)
101+
n_points = 50
102+
freq = np.linspace(1e9, 6e9, n_points) # 1-6 GHz
103+
104+
# Simulate realistic antenna impedance: resonance around 3.5 GHz
105+
f_res = 3.5e9
106+
Q = 5
107+
108+
# Series RLC model: Z = R + jX
109+
# Resistance peaks near resonance, reactance crosses zero at resonance
110+
R = 45 + 10 * np.exp(-((freq - f_res) ** 2) / (0.5e9) ** 2)
111+
X = Z0 * Q * (freq / f_res - f_res / freq) + 5 * np.sin(2 * np.pi * freq / 2e9)
112+
113+
# Normalize impedance and convert to reflection coefficient Γ
114+
z_norm = (R + 1j * X) / Z0
115+
gamma = (z_norm - 1) / (z_norm + 1)
116+
gamma_real = np.real(gamma)
117+
gamma_imag = np.imag(gamma)
118+
119+
# Create data source for impedance locus
120+
source = ColumnDataSource(data={"gamma_real": gamma_real, "gamma_imag": gamma_imag, "freq_ghz": freq / 1e9})
121+
122+
# Plot impedance locus curve
123+
p.line("gamma_real", "gamma_imag", source=source, line_width=5, line_color="#FFD43B", alpha=0.9)
124+
p.scatter(
125+
"gamma_real",
126+
"gamma_imag",
127+
source=source,
128+
size=14,
129+
fill_color="#FFD43B",
130+
line_color="#306998",
131+
line_width=2,
132+
alpha=0.85,
133+
)
134+
135+
# Add frequency labels at key points along the locus
136+
label_indices = [0, n_points // 4, n_points // 2, 3 * n_points // 4, n_points - 1]
137+
for idx in label_indices:
138+
# Offset label position to avoid overlap with data points
139+
offset_y = 0.08 if gamma_imag[idx] >= 0 else -0.12
140+
freq_label = Label(
141+
x=gamma_real[idx],
142+
y=gamma_imag[idx] + offset_y,
143+
text=f"{freq[idx] / 1e9:.1f} GHz",
144+
text_font_size="18pt",
145+
text_color="#306998",
146+
text_font_style="bold",
147+
)
148+
p.add_layout(freq_label)
149+
150+
# Mark the matched condition (center point: Z = Z0, Γ = 0)
151+
p.scatter([0], [0], size=22, fill_color="#306998", line_color="white", line_width=3, alpha=0.95)
152+
center_label = Label(x=0.06, y=0.06, text="Z=Z₀", text_font_size="20pt", text_color="#306998", text_font_style="bold")
153+
p.add_layout(center_label)
154+
155+
# Add resistance value labels on horizontal axis
156+
for r in [0.2, 0.5, 1, 2]:
157+
gamma_r = r / (1 + r)
158+
r_label = Label(x=gamma_r, y=-0.1, text=f"r={r}", text_font_size="16pt", text_color="#666666", text_align="center")
159+
p.add_layout(r_label)
160+
161+
# Add reactance labels at chart boundary
162+
for x in [0.5, 1, 2]:
163+
# Calculate position on unit circle for positive x
164+
# Intersection of x-arc with unit circle
165+
angle = 2 * np.arctan(1 / x)
166+
lx = np.cos(angle)
167+
ly = np.sin(angle)
168+
x_label = Label(x=lx + 0.05, y=ly + 0.02, text=f"x={x}", text_font_size="14pt", text_color="#666666")
169+
p.add_layout(x_label)
170+
# Negative x
171+
x_label_neg = Label(x=lx + 0.05, y=-ly - 0.06, text=f"x=-{x}", text_font_size="14pt", text_color="#666666")
172+
p.add_layout(x_label_neg)
173+
174+
# Grid and background styling
175+
p.grid.visible = False
176+
p.background_fill_color = "#fafafa"
177+
p.border_fill_color = "#fafafa"
178+
179+
# Save PNG
180+
export_png(p, filename="plot.png")
181+
182+
# Save HTML for interactive viewing
183+
output_file("plot.html", title="Smith Chart - bokeh - pyplots.ai")
184+
save(p)

0 commit comments

Comments
 (0)