|
35 | 35 | "plt.style.use('../pathsim_docs.mplstyle')\n", |
36 | 36 | "\n", |
37 | 37 | "from pathsim import Simulation, Connection\n", |
38 | | - "from pathsim.blocks import Source, Scope, Spectrum\n", |
| 38 | + "from pathsim.blocks import Source, Scope\n", |
39 | 39 | "from pathsim.solvers import RKCK54\n", |
40 | 40 | "\n", |
41 | 41 | "from pathsim_rf import RFAmplifier" |
|
45 | 45 | "cell_type": "markdown", |
46 | 46 | "metadata": {}, |
47 | 47 | "source": [ |
48 | | - "## System Setup\n", |
| 48 | + "## Time-Domain Waveforms\n", |
49 | 49 | "\n", |
50 | | - "We create an amplifier with 20 dB gain and an IIP3 of +10 dBm, and drive it with a single-tone sinusoidal source at 1 GHz. We sweep the input amplitude to trace out the compression curve." |
| 50 | + "First, let's look at the amplifier output in the linear and compressed regimes. We drive the amplifier with a sinusoidal input and observe the output using `Scope.plot()`." |
51 | 51 | ] |
52 | 52 | }, |
53 | 53 | { |
|
62 | 62 | "Z0 = 50.0 # Reference impedance [Ohm]\n", |
63 | 63 | "f0 = 100.0 # Signal frequency [Hz] (scaled for simulation)\n", |
64 | 64 | "\n", |
| 65 | + "# Linear regime: -20 dBm input\n", |
| 66 | + "p_watts = 10.0 ** (-20.0 / 10.0) * 1e-3\n", |
| 67 | + "v_peak = np.sqrt(2.0 * Z0 * p_watts)\n", |
| 68 | + "\n", |
| 69 | + "src = Source(func=lambda t: v_peak * np.sin(2 * np.pi * f0 * t))\n", |
| 70 | + "amp = RFAmplifier(gain=gain_dB, IIP3=IIP3_dBm, Z0=Z0)\n", |
| 71 | + "sco = Scope(labels=['input', 'output'])\n", |
| 72 | + "\n", |
| 73 | + "sim = Simulation(\n", |
| 74 | + " [src, amp, sco],\n", |
| 75 | + " [Connection(src, amp, sco[0]), Connection(amp, sco[1])],\n", |
| 76 | + " dt=1 / (40 * f0),\n", |
| 77 | + " Solver=RKCK54\n", |
| 78 | + ")\n", |
| 79 | + "\n", |
| 80 | + "sim.run(3 / f0)\n", |
| 81 | + "\n", |
| 82 | + "fig, ax = sco.plot()\n", |
| 83 | + "ax.set_title('Linear Regime (-20 dBm Input)')\n", |
| 84 | + "plt.show()" |
| 85 | + ] |
| 86 | + }, |
| 87 | + { |
| 88 | + "cell_type": "code", |
| 89 | + "execution_count": null, |
| 90 | + "metadata": {}, |
| 91 | + "outputs": [], |
| 92 | + "source": [ |
| 93 | + "# Compressed regime: +5 dBm input\n", |
| 94 | + "p_watts = 10.0 ** (5.0 / 10.0) * 1e-3\n", |
| 95 | + "v_peak = np.sqrt(2.0 * Z0 * p_watts)\n", |
| 96 | + "\n", |
| 97 | + "src = Source(func=lambda t: v_peak * np.sin(2 * np.pi * f0 * t))\n", |
| 98 | + "amp = RFAmplifier(gain=gain_dB, IIP3=IIP3_dBm, Z0=Z0)\n", |
| 99 | + "sco = Scope(labels=['input', 'output'])\n", |
| 100 | + "\n", |
| 101 | + "sim = Simulation(\n", |
| 102 | + " [src, amp, sco],\n", |
| 103 | + " [Connection(src, amp, sco[0]), Connection(amp, sco[1])],\n", |
| 104 | + " dt=1 / (40 * f0),\n", |
| 105 | + " Solver=RKCK54\n", |
| 106 | + ")\n", |
| 107 | + "\n", |
| 108 | + "sim.run(3 / f0)\n", |
| 109 | + "\n", |
| 110 | + "fig, ax = sco.plot()\n", |
| 111 | + "ax.set_title('Compressed Regime (+5 dBm Input)')\n", |
| 112 | + "plt.show()" |
| 113 | + ] |
| 114 | + }, |
| 115 | + { |
| 116 | + "cell_type": "markdown", |
| 117 | + "metadata": {}, |
| 118 | + "source": [ |
| 119 | + "## Compression Curve\n", |
| 120 | + "\n", |
| 121 | + "Now we sweep the input power and measure the output power at each level to trace the compression curve. This requires custom plotting since we aggregate results from many simulations." |
| 122 | + ] |
| 123 | + }, |
| 124 | + { |
| 125 | + "cell_type": "code", |
| 126 | + "execution_count": null, |
| 127 | + "metadata": {}, |
| 128 | + "outputs": [], |
| 129 | + "source": [ |
65 | 130 | "# Sweep input power levels\n", |
66 | 131 | "pin_dbm = np.arange(-30, 15, 1.0)\n", |
67 | 132 | "pout_dbm = np.zeros_like(pin_dbm)\n", |
|
84 | 149 | " Solver=RKCK54\n", |
85 | 150 | " )\n", |
86 | 151 | "\n", |
87 | | - " # Run for several cycles to reach steady state\n", |
88 | 152 | " sim.run(10 / f0)\n", |
89 | 153 | "\n", |
90 | | - " # Read output and measure peak amplitude (last 2 cycles)\n", |
| 154 | + " # Read output and measure peak amplitude (last half)\n", |
91 | 155 | " t, [y] = sco.read()\n", |
92 | 156 | " n_last = int(len(t) * 0.5)\n", |
93 | 157 | " v_out_peak = np.max(np.abs(y[n_last:]))\n", |
|
97 | 161 | " pout_dbm[i] = 10 * np.log10(p_out / 1e-3) if p_out > 0 else -100" |
98 | 162 | ] |
99 | 163 | }, |
100 | | - { |
101 | | - "cell_type": "markdown", |
102 | | - "metadata": {}, |
103 | | - "source": [ |
104 | | - "## Compression Curve\n", |
105 | | - "\n", |
106 | | - "The plot shows output power vs. input power. The dashed line represents ideal linear gain. The deviation from linearity shows the gain compression characteristic." |
107 | | - ] |
108 | | - }, |
109 | 164 | { |
110 | 165 | "cell_type": "code", |
111 | 166 | "execution_count": null, |
|
114 | 169 | "source": [ |
115 | 170 | "fig, ax = plt.subplots(dpi=120)\n", |
116 | 171 | "\n", |
117 | | - "# Ideal linear response\n", |
118 | 172 | "ax.plot(pin_dbm, pin_dbm + gain_dB, '--', label='Ideal (linear)', alpha=0.7)\n", |
119 | | - "\n", |
120 | | - "# Simulated response\n", |
121 | 173 | "ax.plot(pin_dbm, pout_dbm, linewidth=2, label='Simulated')\n", |
122 | 174 | "\n", |
123 | | - "# Mark P1dB point\n", |
124 | 175 | "p1db_in = IIP3_dBm - 9.6\n", |
125 | 176 | "ax.axvline(x=p1db_in, color='grey', linestyle=':', alpha=0.5, label=f'P1dB = {p1db_in:.1f} dBm')\n", |
126 | 177 | "\n", |
|
132 | 183 | "plt.tight_layout()\n", |
133 | 184 | "plt.show()" |
134 | 185 | ] |
135 | | - }, |
136 | | - { |
137 | | - "cell_type": "markdown", |
138 | | - "metadata": {}, |
139 | | - "source": [ |
140 | | - "## Time-Domain Waveforms\n", |
141 | | - "\n", |
142 | | - "Let's compare the output waveform in the linear and compressed regimes to visualize the clipping behavior." |
143 | | - ] |
144 | | - }, |
145 | | - { |
146 | | - "cell_type": "code", |
147 | | - "execution_count": null, |
148 | | - "metadata": {}, |
149 | | - "outputs": [], |
150 | | - "source": [ |
151 | | - "fig, axes = plt.subplots(1, 2, figsize=(10, 4), dpi=120)\n", |
152 | | - "\n", |
153 | | - "for ax, p_in, title in zip(axes, [-20, 5], ['Linear regime (-20 dBm)', 'Compressed regime (+5 dBm)']):\n", |
154 | | - " p_watts = 10.0 ** (p_in / 10.0) * 1e-3\n", |
155 | | - " v_peak = np.sqrt(2.0 * Z0 * p_watts)\n", |
156 | | - "\n", |
157 | | - " src = Source(func=lambda t, vp=v_peak: vp * np.sin(2 * np.pi * f0 * t))\n", |
158 | | - " amp = RFAmplifier(gain=gain_dB, IIP3=IIP3_dBm, Z0=Z0)\n", |
159 | | - " sco_in = Scope(labels=['input'])\n", |
160 | | - " sco_out = Scope(labels=['output'])\n", |
161 | | - "\n", |
162 | | - " sim = Simulation(\n", |
163 | | - " [src, amp, sco_in, sco_out],\n", |
164 | | - " [Connection(src, amp, sco_in), Connection(amp, sco_out)],\n", |
165 | | - " dt=1 / (40 * f0),\n", |
166 | | - " Solver=RKCK54\n", |
167 | | - " )\n", |
168 | | - "\n", |
169 | | - " sim.run(3 / f0)\n", |
170 | | - "\n", |
171 | | - " t_in, [y_in] = sco_in.read()\n", |
172 | | - " t_out, [y_out] = sco_out.read()\n", |
173 | | - "\n", |
174 | | - " ax.plot(np.array(t_in) * f0, y_in * 1000, label='Input')\n", |
175 | | - " ax.plot(np.array(t_out) * f0, y_out * 1000, label='Output')\n", |
176 | | - " ax.set_xlabel('Cycles')\n", |
177 | | - " ax.set_ylabel('Voltage [mV]')\n", |
178 | | - " ax.set_title(title)\n", |
179 | | - " ax.legend()\n", |
180 | | - " ax.grid(True, alpha=0.3)\n", |
181 | | - "\n", |
182 | | - "plt.tight_layout()\n", |
183 | | - "plt.show()" |
184 | | - ] |
185 | 186 | } |
186 | 187 | ], |
187 | 188 | "metadata": { |
|
0 commit comments