|
| 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