|
| 1 | +""" pyplots.ai |
| 2 | +smith-chart-basic: Smith Chart for RF/Impedance |
| 3 | +Library: seaborn 0.13.2 | Python 3.13.11 |
| 4 | +Quality: 90/100 | Created: 2026-01-15 |
| 5 | +""" |
| 6 | + |
| 7 | +import matplotlib.pyplot as plt |
| 8 | +import numpy as np |
| 9 | +import pandas as pd |
| 10 | +import seaborn as sns |
| 11 | + |
| 12 | + |
| 13 | +# Set seaborn style for better aesthetics |
| 14 | +sns.set_style("whitegrid") |
| 15 | +sns.set_context("talk", font_scale=1.2) |
| 16 | + |
| 17 | +# Reference impedance (ohms) |
| 18 | +z0 = 50 |
| 19 | + |
| 20 | +# Generate frequency sweep data (1 GHz to 6 GHz, 50 points) |
| 21 | +np.random.seed(42) |
| 22 | +n_points = 50 |
| 23 | +freq_ghz = np.linspace(1, 6, n_points) |
| 24 | + |
| 25 | +# Simulate a realistic impedance locus (antenna-like behavior) |
| 26 | +# Create a spiral pattern that doesn't close on itself (avoids label overlap) |
| 27 | +t = np.linspace(0, 1.8 * np.pi, n_points) |
| 28 | +# Spiral-like pattern typical of antenna impedance vs frequency with drift |
| 29 | +z_real = 50 + 30 * np.sin(t) + 15 * np.cos(2 * t) + 10 * (t / (2 * np.pi)) |
| 30 | +z_imag = 40 * np.sin(1.5 * t) + 20 * np.cos(t) - 15 * (t / (2 * np.pi)) |
| 31 | + |
| 32 | +# Normalize impedance to reference |
| 33 | +z_norm = (z_real + 1j * z_imag) / z0 |
| 34 | + |
| 35 | +# Calculate reflection coefficient (Gamma) |
| 36 | +gamma = (z_norm - 1) / (z_norm + 1) |
| 37 | +gamma_real = gamma.real |
| 38 | +gamma_imag = gamma.imag |
| 39 | + |
| 40 | +# Create figure (square for Smith chart) |
| 41 | +fig, ax = plt.subplots(figsize=(12, 12)) |
| 42 | + |
| 43 | +# Draw Smith chart grid |
| 44 | +# Constant resistance circles |
| 45 | +r_values = [0, 0.2, 0.5, 1, 2, 5] |
| 46 | +theta = np.linspace(0, 2 * np.pi, 200) |
| 47 | + |
| 48 | +for r in r_values: |
| 49 | + # Circle center and radius in Gamma plane |
| 50 | + center = r / (r + 1) |
| 51 | + radius = 1 / (r + 1) |
| 52 | + # Parametric circle |
| 53 | + circle_x = center + radius * np.cos(theta) |
| 54 | + circle_y = radius * np.sin(theta) |
| 55 | + # Clip to unit circle |
| 56 | + mask = circle_x**2 + circle_y**2 <= 1.001 |
| 57 | + ax.plot(circle_x[mask], circle_y[mask], color="#306998", linewidth=1) |
| 58 | + # Add r value labels on the real axis (inside unit circle) |
| 59 | + if r > 0: |
| 60 | + label_x = r / (r + 1) - 1 / (r + 1) + 0.02 # Left edge of circle |
| 61 | + if label_x > -0.95: |
| 62 | + ax.text(label_x, 0.03, f"r={r}", fontsize=10, color="#306998", va="bottom") |
| 63 | + |
| 64 | +# Constant reactance arcs |
| 65 | +x_values = [0.2, 0.5, 1, 2, 5] |
| 66 | + |
| 67 | +for x in x_values: |
| 68 | + # Positive reactance arc (inductive) |
| 69 | + center_y = 1 / x |
| 70 | + radius = 1 / x |
| 71 | + arc_theta = np.linspace(-np.pi / 2, np.pi / 2, 200) |
| 72 | + arc_x = 1 + radius * np.cos(arc_theta) |
| 73 | + arc_y = center_y + radius * np.sin(arc_theta) |
| 74 | + mask = (arc_x**2 + arc_y**2 <= 1.001) & (arc_x >= -0.001) |
| 75 | + ax.plot(arc_x[mask], arc_y[mask], color="#D4A017", linewidth=1.5) |
| 76 | + |
| 77 | + # Negative reactance arc (capacitive) |
| 78 | + arc_y_neg = -center_y + radius * np.sin(arc_theta) |
| 79 | + mask_neg = (arc_x**2 + arc_y_neg**2 <= 1.001) & (arc_x >= -0.001) |
| 80 | + ax.plot(arc_x[mask_neg], arc_y_neg[mask_neg], color="#D4A017", linewidth=1.5) |
| 81 | + |
| 82 | + # Add x value labels inside the unit circle boundary |
| 83 | + if x <= 2: |
| 84 | + label_angle = np.arctan(1 / x) |
| 85 | + label_x_pos = 0.85 * np.cos(label_angle) |
| 86 | + label_y_pos = 0.85 * np.sin(label_angle) |
| 87 | + ax.text(label_x_pos, label_y_pos + 0.03, f"x={x}", fontsize=10, color="#D4A017", va="bottom", ha="center") |
| 88 | + ax.text(label_x_pos, -label_y_pos - 0.03, f"x=-{x}", fontsize=10, color="#D4A017", va="top", ha="center") |
| 89 | + |
| 90 | +# Draw unit circle (|Gamma| = 1 boundary) |
| 91 | +unit_theta = np.linspace(0, 2 * np.pi, 200) |
| 92 | +ax.plot(np.cos(unit_theta), np.sin(unit_theta), color="#306998", linewidth=2.5) |
| 93 | + |
| 94 | +# Draw horizontal axis (real axis) |
| 95 | +ax.axhline(0, color="#306998", linewidth=1.5, alpha=0.6) |
| 96 | + |
| 97 | +# Create DataFrame for seaborn plotting |
| 98 | +df_locus = pd.DataFrame({"gamma_real": gamma_real, "gamma_imag": gamma_imag, "freq_ghz": freq_ghz}) |
| 99 | + |
| 100 | +# Plot impedance locus using seaborn lineplot for the trajectory |
| 101 | +sns.lineplot( |
| 102 | + data=df_locus, x="gamma_real", y="gamma_imag", color="#E74C3C", linewidth=3, ax=ax, legend=False, sort=False |
| 103 | +) |
| 104 | + |
| 105 | +# Add markers at key frequency points using seaborn scatterplot |
| 106 | +key_indices = [0, n_points // 4, n_points // 2, 3 * n_points // 4, n_points - 1] |
| 107 | +df_markers = df_locus.iloc[key_indices].copy() |
| 108 | +sns.scatterplot( |
| 109 | + data=df_markers, |
| 110 | + x="gamma_real", |
| 111 | + y="gamma_imag", |
| 112 | + s=200, |
| 113 | + color="#E74C3C", |
| 114 | + edgecolor="white", |
| 115 | + linewidth=2, |
| 116 | + ax=ax, |
| 117 | + zorder=10, |
| 118 | + legend=False, |
| 119 | +) |
| 120 | + |
| 121 | +# Label key frequency points with smart positioning to avoid overlap |
| 122 | +label_offsets = { |
| 123 | + 0: (12, -18), |
| 124 | + n_points // 4: (15, 12), |
| 125 | + n_points // 2: (12, -18), |
| 126 | + 3 * n_points // 4: (-60, 10), |
| 127 | + n_points - 1: (-60, -15), |
| 128 | +} |
| 129 | + |
| 130 | +for idx in key_indices: |
| 131 | + offset = label_offsets.get(idx, (10, 10)) |
| 132 | + ax.annotate( |
| 133 | + f"{freq_ghz[idx]:.1f} GHz", |
| 134 | + (gamma_real[idx], gamma_imag[idx]), |
| 135 | + textcoords="offset points", |
| 136 | + xytext=offset, |
| 137 | + fontsize=14, |
| 138 | + fontweight="bold", |
| 139 | + color="#2C3E50", |
| 140 | + ) |
| 141 | + |
| 142 | +# Mark the center (matched condition) |
| 143 | +ax.scatter([0], [0], s=150, color="#27AE60", marker="+", linewidths=3, zorder=10) |
| 144 | +ax.annotate( |
| 145 | + "Z₀ (50Ω)", (0, 0), textcoords="offset points", xytext=(-40, -20), fontsize=14, color="#27AE60", fontweight="bold" |
| 146 | +) |
| 147 | + |
| 148 | +# Add VSWR circle (|Gamma| = 0.5, VSWR = 3:1) |
| 149 | +vswr_radius = 0.5 |
| 150 | +vswr_circle_x = vswr_radius * np.cos(unit_theta) |
| 151 | +vswr_circle_y = vswr_radius * np.sin(unit_theta) |
| 152 | +ax.plot(vswr_circle_x, vswr_circle_y, "--", color="#9B59B6", linewidth=2) |
| 153 | +ax.annotate("VSWR 3:1", (0.35, 0.35), fontsize=12, color="#9B59B6", fontweight="bold") |
| 154 | + |
| 155 | +# Styling |
| 156 | +ax.set_xlim(-1.15, 1.15) |
| 157 | +ax.set_ylim(-1.15, 1.15) |
| 158 | +ax.set_aspect("equal") |
| 159 | +ax.set_xlabel("Real(Γ)", fontsize=20) |
| 160 | +ax.set_ylabel("Imag(Γ)", fontsize=20) |
| 161 | +ax.set_title("smith-chart-basic · seaborn · pyplots.ai", fontsize=24, fontweight="bold", pad=20) |
| 162 | +ax.tick_params(axis="both", labelsize=16) |
| 163 | +ax.grid(False) # Turn off default grid, we drew our own Smith grid |
| 164 | + |
| 165 | +# Add legend with matching line styles |
| 166 | +ax.plot([], [], color="#306998", linewidth=1, label="Constant R circles") |
| 167 | +ax.plot([], [], color="#D4A017", linewidth=1.5, label="Constant X arcs") |
| 168 | +ax.plot([], [], color="#E74C3C", linewidth=3, label="Impedance locus") |
| 169 | +ax.plot([], [], color="#9B59B6", linewidth=2, linestyle="--", label="VSWR 3:1 circle") |
| 170 | +ax.legend(loc="upper left", fontsize=14, framealpha=0.9) |
| 171 | + |
| 172 | +plt.tight_layout() |
| 173 | +plt.savefig("plot.png", dpi=300, bbox_inches="tight") |
0 commit comments