|
8 | 8 | "# Aliasing and anti-aliasing" |
9 | 9 | ] |
10 | 10 | }, |
| 11 | + { |
| 12 | + "cell_type": "markdown", |
| 13 | + "id": "aa-interactive-note", |
| 14 | + "metadata": {}, |
| 15 | + "source": [ |
| 16 | + "```{admonition} Interactive page\n", |
| 17 | + ":class: interactive\n", |
| 18 | + "This page contains interactive elements. Press the launch button at the top right side in order to use them.\n", |
| 19 | + "```" |
| 20 | + ] |
| 21 | + }, |
11 | 22 | { |
12 | 23 | "cell_type": "markdown", |
13 | 24 | "id": "497bf687", |
|
248 | 259 | }, |
249 | 260 | { |
250 | 261 | "cell_type": "markdown", |
251 | | - "id": "d49c7d32", |
| 262 | + "id": "aa-simulate", |
| 263 | + "metadata": {}, |
| 264 | + "source": [ |
| 265 | + "### Simulate\n", |
| 266 | + "\n", |
| 267 | + "The demos below let you test aliasing and anti-aliasing before the practicum. Launch the page first (button at the top right), run the helper cell once, then run each demo. Change the marked variables and watch how the sampled spectrum reacts." |
| 268 | + ] |
| 269 | + }, |
| 270 | + { |
| 271 | + "cell_type": "markdown", |
| 272 | + "id": "aa-helpers-md", |
| 273 | + "metadata": {}, |
| 274 | + "source": [ |
| 275 | + "#### FFT and sampling helper functions\n", |
| 276 | + "\n", |
| 277 | + "Run this cell before the demos. These functions are provided as measurement tools, so you do not need to write the FFT from scratch." |
| 278 | + ] |
| 279 | + }, |
| 280 | + { |
| 281 | + "cell_type": "code", |
| 282 | + "execution_count": null, |
| 283 | + "id": "08247426", |
| 284 | + "metadata": {}, |
| 285 | + "outputs": [], |
| 286 | + "source": [ |
| 287 | + "import numpy as np\n", |
| 288 | + "import matplotlib.pyplot as plt\n", |
| 289 | + "\n", |
| 290 | + "\n", |
| 291 | + "def dominant_fft_peak(signal, rate, ignore_dc=True):\n", |
| 292 | + " \"\"\"Return the dominant FFT peak frequency and amplitude.\"\"\"\n", |
| 293 | + " frequency_axis, spectrum = calculate_fft(signal, rate)\n", |
| 294 | + " start = 1 if ignore_dc else 0\n", |
| 295 | + " peak_index = start + np.argmax(spectrum[start:])\n", |
| 296 | + " return frequency_axis[peak_index], spectrum[peak_index]\n", |
| 297 | + "\n", |
| 298 | + "\n", |
| 299 | + "def alias_frequency(signal_frequency, sampling_rate):\n", |
| 300 | + " \"\"\"Fold a frequency into the range from 0 to the Nyquist frequency.\"\"\"\n", |
| 301 | + " remainder = signal_frequency % sampling_rate\n", |
| 302 | + " if np.isclose(remainder, sampling_rate / 2):\n", |
| 303 | + " return 0.0\n", |
| 304 | + " if remainder < sampling_rate / 2:\n", |
| 305 | + " return remainder\n", |
| 306 | + " return sampling_rate - remainder\n", |
| 307 | + "\n", |
| 308 | + "\n", |
| 309 | + "def alias_phase_sign(signal_frequency, sampling_rate):\n", |
| 310 | + " \"\"\"Return +1 for same initial slope, -1 for opposite initial slope, and 0 for Nyquist ambiguity.\"\"\"\n", |
| 311 | + " remainder = signal_frequency % sampling_rate\n", |
| 312 | + " if np.isclose(remainder, 0) or np.isclose(remainder, sampling_rate / 2):\n", |
| 313 | + " return 0\n", |
| 314 | + " return 1 if remainder < sampling_rate / 2 else -1\n", |
| 315 | + "\n", |
| 316 | + "\n", |
| 317 | + "def rc_lowpass_gain(frequency, cutoff_frequency):\n", |
| 318 | + " \"\"\"Magnitude response of a first-order RC low-pass filter.\"\"\"\n", |
| 319 | + " frequency = np.asarray(frequency)\n", |
| 320 | + " return 1 / np.sqrt(1 + (frequency / cutoff_frequency) ** 2)\n", |
| 321 | + "\n", |
| 322 | + "\n", |
| 323 | + "def fold_periodic_samples(time, voltage, period):\n", |
| 324 | + " \"\"\"Fold time-stamped samples onto one known period and sort by phase.\"\"\"\n", |
| 325 | + " phase = (np.asarray(time) % period) / period\n", |
| 326 | + " order = np.argsort(phase)\n", |
| 327 | + " return phase[order], np.asarray(voltage)[order]\n" |
| 328 | + ] |
| 329 | + }, |
| 330 | + { |
| 331 | + "cell_type": "markdown", |
| 332 | + "id": "aa-demo1-md", |
| 333 | + "metadata": {}, |
| 334 | + "source": [ |
| 335 | + "#### Demo 1: aliasing of a single sine\n", |
| 336 | + "\n", |
| 337 | + "This demo builds one analog sine, samples it at `sampling_rate`, and finds the FFT peak of the sampled record. A sine above the Nyquist frequency folds back to a lower peak. Change `signal_frequency` and watch the measured peak and the phase sign. Try 100, 450, 500, 650, and 1100 Hz at a sampling rate of 1000 Hz." |
| 338 | + ] |
| 339 | + }, |
| 340 | + { |
| 341 | + "cell_type": "code", |
| 342 | + "execution_count": null, |
| 343 | + "id": "c9e566c3", |
| 344 | + "metadata": {}, |
| 345 | + "outputs": [], |
| 346 | + "source": [ |
| 347 | + "signal_frequency = 650 # Hz\n", |
| 348 | + "sampling_rate = 1000 # samples per second\n", |
| 349 | + "duration = 0.08 # seconds\n", |
| 350 | + "\n", |
| 351 | + "continuous_time = np.linspace(0, duration, 5000, endpoint=False)\n", |
| 352 | + "continuous_voltage = np.sin(2 * np.pi * signal_frequency * continuous_time)\n", |
| 353 | + "\n", |
| 354 | + "sample_time = np.arange(0, duration, 1 / sampling_rate)\n", |
| 355 | + "sample_voltage = np.sin(2 * np.pi * signal_frequency * sample_time)\n", |
| 356 | + "\n", |
| 357 | + "peak_frequency, peak_amplitude = dominant_fft_peak(sample_voltage, sampling_rate)\n", |
| 358 | + "print(f\"Predicted alias: {alias_frequency(signal_frequency, sampling_rate):.1f} Hz\")\n", |
| 359 | + "print(f\"Measured FFT peak: {peak_frequency:.1f} Hz\")\n", |
| 360 | + "print(f\"Phase sign: {alias_phase_sign(signal_frequency, sampling_rate)}\")\n", |
| 361 | + "\n", |
| 362 | + "frequency_axis, spectrum = calculate_fft(sample_voltage, sampling_rate)\n", |
| 363 | + "\n", |
| 364 | + "fig, axes = plt.subplots(1, 2, figsize=(12, 4))\n", |
| 365 | + "axes[0].plot(continuous_time * 1000, continuous_voltage, label=\"analog sine\")\n", |
| 366 | + "axes[0].plot(sample_time * 1000, sample_voltage, \"o\", label=\"ADC samples\")\n", |
| 367 | + "axes[0].set_xlabel(\"time [ms]\")\n", |
| 368 | + "axes[0].set_ylabel(\"voltage [a.u.]\")\n", |
| 369 | + "axes[0].legend()\n", |
| 370 | + "axes[0].grid(True)\n", |
| 371 | + "\n", |
| 372 | + "axes[1].stem(frequency_axis, spectrum, basefmt=\" \")\n", |
| 373 | + "axes[1].axvline(sampling_rate / 2, color=\"C1\", label=\"Nyquist\")\n", |
| 374 | + "axes[1].set_xlabel(\"frequency [Hz]\")\n", |
| 375 | + "axes[1].set_ylabel(\"FFT amplitude [a.u.]\")\n", |
| 376 | + "axes[1].set_xlim(0, sampling_rate / 2)\n", |
| 377 | + "axes[1].legend()\n", |
| 378 | + "axes[1].grid(True)\n", |
| 379 | + "plt.tight_layout()\n" |
| 380 | + ] |
| 381 | + }, |
| 382 | + { |
| 383 | + "cell_type": "markdown", |
| 384 | + "id": "aa-demo2-md", |
| 385 | + "metadata": {}, |
| 386 | + "source": [ |
| 387 | + "#### Demo 2: the anti-aliasing RC filter\n", |
| 388 | + "\n", |
| 389 | + "A first-order RC low-pass filter placed before the ADC reduces the high harmonics that would otherwise alias. The magnitude response is $|H(f)| = 1 / \\sqrt{1 + (f/f_c)^2}$. Change `C` (try 47 nF and 100 nF with R = 10 kΩ) and see which square-wave harmonics are reduced before sampling." |
| 390 | + ] |
| 391 | + }, |
| 392 | + { |
| 393 | + "cell_type": "code", |
| 394 | + "execution_count": null, |
| 395 | + "id": "bb694068", |
| 396 | + "metadata": {}, |
| 397 | + "outputs": [], |
| 398 | + "source": [ |
| 399 | + "fundamental = 100 # Hz\n", |
| 400 | + "harmonic_numbers = np.array([1, 3, 5, 7, 9])\n", |
| 401 | + "harmonic_frequencies = fundamental * harmonic_numbers\n", |
| 402 | + "harmonic_amplitudes = 1 / harmonic_numbers\n", |
| 403 | + "\n", |
| 404 | + "R = 10_000\n", |
| 405 | + "C = 100e-9\n", |
| 406 | + "cutoff_frequency = 1 / (2 * np.pi * R * C)\n", |
| 407 | + "filtered_amplitudes = harmonic_amplitudes * rc_lowpass_gain(harmonic_frequencies, cutoff_frequency)\n", |
| 408 | + "\n", |
| 409 | + "print(f\"cutoff frequency: {cutoff_frequency:.1f} Hz\")\n", |
| 410 | + "for k, f, raw, filtered in zip(harmonic_numbers, harmonic_frequencies, harmonic_amplitudes, filtered_amplitudes):\n", |
| 411 | + " print(f\"k={k:2d}, f={f:5.0f} Hz, before={raw:.3f}, after={filtered:.3f}\")\n", |
| 412 | + "\n", |
| 413 | + "plt.figure(figsize=(8, 4))\n", |
| 414 | + "plt.stem(harmonic_frequencies, harmonic_amplitudes, linefmt=\"C0-\", markerfmt=\"C0o\", basefmt=\" \", label=\"before RC\")\n", |
| 415 | + "plt.stem(harmonic_frequencies, filtered_amplitudes, linefmt=\"C1-\", markerfmt=\"C1s\", basefmt=\" \", label=\"after RC\")\n", |
| 416 | + "plt.axvline(cutoff_frequency, color=\"C2\", label=\"cutoff\")\n", |
| 417 | + "plt.xlabel(\"frequency [Hz]\")\n", |
| 418 | + "plt.ylabel(\"relative amplitude [a.u.]\")\n", |
| 419 | + "plt.legend()\n", |
| 420 | + "plt.grid(True)\n", |
| 421 | + "plt.tight_layout()\n" |
| 422 | + ] |
| 423 | + }, |
| 424 | + { |
| 425 | + "cell_type": "markdown", |
| 426 | + "id": "aa-demo3-md", |
| 427 | + "metadata": {}, |
| 428 | + "source": [ |
| 429 | + "#### Demo 3: deliberate undersampling (equivalent-time sampling)\n", |
| 430 | + "\n", |
| 431 | + "Undersampling is normally a problem, but for a stable periodic signal it becomes a measurement strategy. You sample at many phases of the same repeating waveform, then fold all samples onto one known period. The cell below folds an example sparse acquisition. Replace the example arrays with your measured time and voltage arrays during the practicum." |
| 432 | + ] |
| 433 | + }, |
| 434 | + { |
| 435 | + "cell_type": "code", |
| 436 | + "execution_count": null, |
| 437 | + "id": "abc6ff10", |
252 | 438 | "metadata": {}, |
| 439 | + "outputs": [], |
253 | 440 | "source": [ |
254 | | - "### Simulate" |
| 441 | + "# Replace these arrays with your measured time and voltage arrays from Task I3.\n", |
| 442 | + "signal_frequency = 73 # Hz, example value\n", |
| 443 | + "period = 1 / signal_frequency\n", |
| 444 | + "\n", |
| 445 | + "# Example sparse measurement. Delete this block when using real ALPACA data.\n", |
| 446 | + "example_time = np.arange(0, 0.8, 1 / 120)\n", |
| 447 | + "example_voltage = np.sin(2 * np.pi * signal_frequency * example_time)\n", |
| 448 | + "\n", |
| 449 | + "phase, folded_voltage = fold_periodic_samples(example_time, example_voltage, period)\n", |
| 450 | + "\n", |
| 451 | + "plt.figure(figsize=(8, 4))\n", |
| 452 | + "plt.plot(phase, folded_voltage, \"o\", label=\"folded samples\")\n", |
| 453 | + "plt.xlabel(\"phase within one period\")\n", |
| 454 | + "plt.ylabel(\"voltage [a.u.]\")\n", |
| 455 | + "plt.grid(True)\n", |
| 456 | + "plt.legend()\n", |
| 457 | + "plt.tight_layout()\n" |
255 | 458 | ] |
256 | 459 | }, |
257 | 460 | { |
258 | 461 | "cell_type": "markdown", |
259 | | - "id": "685e192f", |
| 462 | + "id": "a1776758", |
260 | 463 | "metadata": {}, |
261 | 464 | "source": [ |
262 | 465 | "### Implement and Investigate\n", |
|
273 | 476 | "Tell them to set the sampling duration so they record 5 full cycles of the sine wave\n", |
274 | 477 | "\n" |
275 | 478 | ] |
276 | | - }, |
277 | | - { |
278 | | - "cell_type": "markdown", |
279 | | - "id": "82f4f7b4", |
280 | | - "metadata": {}, |
281 | | - "source": [] |
282 | 479 | } |
283 | 480 | ], |
284 | 481 | "metadata": { |
|
0 commit comments