|
| 1 | +""" pyplots.ai |
| 2 | +smith-chart-basic: Smith Chart for RF/Impedance |
| 3 | +Library: matplotlib 3.10.8 | Python 3.13.11 |
| 4 | +Quality: 91/100 | Created: 2026-01-15 |
| 5 | +""" |
| 6 | + |
| 7 | +import matplotlib.pyplot as plt |
| 8 | +import numpy as np |
| 9 | + |
| 10 | + |
| 11 | +# Reference impedance |
| 12 | +Z0 = 50 # ohms |
| 13 | + |
| 14 | +# Generate example data: antenna impedance sweep from 1 GHz to 6 GHz |
| 15 | +np.random.seed(42) |
| 16 | +frequency = np.linspace(1e9, 6e9, 50) # 50 frequency points |
| 17 | + |
| 18 | +# Simulate realistic antenna impedance variation with frequency |
| 19 | +# This creates a spiral-like pattern typical of antenna impedance measurements |
| 20 | +z_real = 50 + 30 * np.sin(2 * np.pi * (frequency - 1e9) / 2e9) + 10 * np.cos(4 * np.pi * (frequency - 1e9) / 5e9) |
| 21 | +z_imag = 20 * np.cos(2 * np.pi * (frequency - 1e9) / 1.5e9) + 15 * np.sin(3 * np.pi * (frequency - 1e9) / 5e9) |
| 22 | + |
| 23 | +# Normalize impedance |
| 24 | +z_norm = (z_real + 1j * z_imag) / Z0 |
| 25 | + |
| 26 | +# Convert to reflection coefficient (gamma) |
| 27 | +gamma = (z_norm - 1) / (z_norm + 1) |
| 28 | + |
| 29 | +# Create square figure for Smith chart |
| 30 | +fig, ax = plt.subplots(figsize=(12, 12)) |
| 31 | +ax.set_aspect("equal") |
| 32 | + |
| 33 | +# Colors |
| 34 | +grid_color = "#888888" |
| 35 | +circle_color = "#306998" |
| 36 | +data_color = "#FFD43B" |
| 37 | + |
| 38 | +# Draw constant resistance circles |
| 39 | +r_values = [0, 0.2, 0.5, 1, 2, 5] |
| 40 | +theta = np.linspace(0, 2 * np.pi, 500) |
| 41 | + |
| 42 | +for r in r_values: |
| 43 | + # Center and radius of constant resistance circle |
| 44 | + center = r / (r + 1) |
| 45 | + radius = 1 / (r + 1) |
| 46 | + x_circle = center + radius * np.cos(theta) |
| 47 | + y_circle = radius * np.sin(theta) |
| 48 | + # Clip to unit circle |
| 49 | + mask = x_circle**2 + y_circle**2 <= 1.001 |
| 50 | + x_clipped = np.where(mask, x_circle, np.nan) |
| 51 | + y_clipped = np.where(mask, y_circle, np.nan) |
| 52 | + ax.plot(x_clipped, y_clipped, color=grid_color, linewidth=1, alpha=0.6) |
| 53 | + # Label resistance circles |
| 54 | + if r > 0: |
| 55 | + label_x = center + radius |
| 56 | + if label_x <= 1: |
| 57 | + ax.text(label_x + 0.02, 0.02, f"{r}", fontsize=12, color=grid_color, ha="left", va="bottom") |
| 58 | + |
| 59 | +# Draw constant reactance arcs |
| 60 | +x_values = [0.2, 0.5, 1, 2, 5] |
| 61 | + |
| 62 | +for x in x_values: |
| 63 | + # Positive reactance (inductive - upper half) |
| 64 | + center_y = 1 / x |
| 65 | + radius = 1 / x |
| 66 | + arc_theta = np.linspace(0, np.pi, 500) |
| 67 | + x_arc = 1 + radius * np.cos(arc_theta + np.pi) |
| 68 | + y_arc = center_y + radius * np.sin(arc_theta + np.pi) |
| 69 | + # Clip to unit circle |
| 70 | + mask = x_arc**2 + y_arc**2 <= 1.001 |
| 71 | + x_clipped = np.where(mask, x_arc, np.nan) |
| 72 | + y_clipped = np.where(mask, y_arc, np.nan) |
| 73 | + ax.plot(x_clipped, y_clipped, color=grid_color, linewidth=1, alpha=0.6) |
| 74 | + |
| 75 | + # Negative reactance (capacitive - lower half) |
| 76 | + y_arc_neg = -center_y + radius * np.sin(arc_theta) |
| 77 | + mask = x_arc**2 + y_arc_neg**2 <= 1.001 |
| 78 | + x_clipped = np.where(mask, x_arc, np.nan) |
| 79 | + y_clipped = np.where(mask, y_arc_neg, np.nan) |
| 80 | + ax.plot(x_clipped, y_clipped, color=grid_color, linewidth=1, alpha=0.6) |
| 81 | + |
| 82 | + # Label reactance arcs |
| 83 | + # Find intersection with unit circle |
| 84 | + if x <= 1: |
| 85 | + angle = 2 * np.arctan(1 / x) |
| 86 | + label_x = np.cos(angle) |
| 87 | + label_y = np.sin(angle) |
| 88 | + ax.text(label_x, label_y + 0.05, f"+j{x}", fontsize=11, color=grid_color, ha="center", va="bottom") |
| 89 | + ax.text(label_x, -label_y - 0.05, f"-j{x}", fontsize=11, color=grid_color, ha="center", va="top") |
| 90 | + |
| 91 | +# Draw horizontal axis (real axis, x=0) |
| 92 | +ax.axhline(y=0, color=grid_color, linewidth=1, alpha=0.6) |
| 93 | + |
| 94 | +# Draw unit circle (boundary) |
| 95 | +unit_theta = np.linspace(0, 2 * np.pi, 500) |
| 96 | +ax.plot(np.cos(unit_theta), np.sin(unit_theta), color=circle_color, linewidth=2.5) |
| 97 | + |
| 98 | +# Draw center point (matched condition) |
| 99 | +ax.plot(0, 0, "o", color=circle_color, markersize=10, label="Matched (Z=Z₀)") |
| 100 | + |
| 101 | +# Plot impedance locus |
| 102 | +ax.plot(gamma.real, gamma.imag, color=data_color, linewidth=3, label="Impedance Locus", zorder=5) |
| 103 | +ax.scatter(gamma.real, gamma.imag, c=data_color, s=60, edgecolors="black", linewidths=0.5, zorder=6) |
| 104 | + |
| 105 | +# Add frequency labels at key points |
| 106 | +freq_label_indices = [0, 12, 24, 36, 49] # Start, middle points, end |
| 107 | +for idx in freq_label_indices: |
| 108 | + freq_ghz = frequency[idx] / 1e9 |
| 109 | + x_pos = gamma.real[idx] |
| 110 | + y_pos = gamma.imag[idx] |
| 111 | + # Offset labels to avoid overlap with data points |
| 112 | + offset_x = 0.08 if x_pos < 0.5 else -0.08 |
| 113 | + offset_y = 0.08 if y_pos >= 0 else -0.08 |
| 114 | + ax.annotate( |
| 115 | + f"{freq_ghz:.1f} GHz", |
| 116 | + (x_pos, y_pos), |
| 117 | + xytext=(x_pos + offset_x, y_pos + offset_y), |
| 118 | + fontsize=14, |
| 119 | + fontweight="bold", |
| 120 | + color="black", |
| 121 | + arrowprops={"arrowstyle": "->", "color": "black", "lw": 1.5}, |
| 122 | + zorder=10, |
| 123 | + ) |
| 124 | + |
| 125 | +# Draw VSWR circles (optional - constant reflection coefficient magnitude) |
| 126 | +vswr_values = [1.5, 2, 3] |
| 127 | +vswr_angles = [np.pi / 4, np.pi / 3, 5 * np.pi / 12] # Different angles for each label |
| 128 | +for vswr, label_angle in zip(vswr_values, vswr_angles, strict=True): |
| 129 | + gamma_mag = (vswr - 1) / (vswr + 1) |
| 130 | + vswr_theta = np.linspace(0, 2 * np.pi, 200) |
| 131 | + ax.plot( |
| 132 | + gamma_mag * np.cos(vswr_theta), |
| 133 | + gamma_mag * np.sin(vswr_theta), |
| 134 | + "--", |
| 135 | + color="#CC4444", |
| 136 | + linewidth=1.5, |
| 137 | + alpha=0.7, |
| 138 | + label=f"VSWR={vswr}" if vswr == 1.5 else "", |
| 139 | + ) |
| 140 | + # Label VSWR circles at different angles to avoid overlap |
| 141 | + label_x = gamma_mag * np.cos(label_angle) |
| 142 | + label_y = gamma_mag * np.sin(label_angle) |
| 143 | + ax.text(label_x, label_y + 0.06, f"VSWR={vswr}", fontsize=11, color="#CC4444", alpha=0.9, ha="center") |
| 144 | + |
| 145 | +# Styling |
| 146 | +ax.set_xlim(-1.25, 1.25) |
| 147 | +ax.set_ylim(-1.25, 1.25) |
| 148 | +ax.set_xlabel("Real(Γ)", fontsize=20) |
| 149 | +ax.set_ylabel("Imag(Γ)", fontsize=20) |
| 150 | +ax.set_title("smith-chart-basic · matplotlib · pyplots.ai", fontsize=24) |
| 151 | +ax.tick_params(axis="both", labelsize=16) |
| 152 | +ax.legend(fontsize=14, loc="upper left") |
| 153 | + |
| 154 | +# Remove axis ticks for cleaner Smith chart appearance |
| 155 | +ax.set_xticks([]) |
| 156 | +ax.set_yticks([]) |
| 157 | + |
| 158 | +# Add axis labels on chart edge |
| 159 | +ax.text(1.15, 0, "Open\n(Γ=1)", fontsize=12, ha="center", va="center", color=circle_color) |
| 160 | +ax.text(-1.15, 0, "Short\n(Γ=-1)", fontsize=12, ha="center", va="center", color=circle_color) |
| 161 | +ax.text(0, 1.15, "+jX\n(Inductive)", fontsize=12, ha="center", va="center", color=grid_color) |
| 162 | +ax.text(0, -1.15, "-jX\n(Capacitive)", fontsize=12, ha="center", va="center", color=grid_color) |
| 163 | + |
| 164 | +plt.tight_layout() |
| 165 | +plt.savefig("plot.png", dpi=300, bbox_inches="tight") |
0 commit comments