From e67d77faf8303be665c60b9d759cad63c27faf23 Mon Sep 17 00:00:00 2001 From: Jackson Ferguson Date: Thu, 29 Jan 2026 16:18:07 -0800 Subject: [PATCH 1/9] refactor: move generate_svg to scripts/visualization/joyplot.py and add CLI args - Moves src/generate_svg.py to scripts/visualization/joyplot.py to separate imperative scripts from library code. - Replaces hardcoded file paths and constants with argparse CLI arguments. - Adds standard boilerplate for path handling. --- .../scripts/visualization/joyplot.py | 130 +++++++++++++++++ oscilloscope-rp2040/src/generate_svg.py | 138 ------------------ 2 files changed, 130 insertions(+), 138 deletions(-) create mode 100644 oscilloscope-rp2040/scripts/visualization/joyplot.py delete mode 100644 oscilloscope-rp2040/src/generate_svg.py diff --git a/oscilloscope-rp2040/scripts/visualization/joyplot.py b/oscilloscope-rp2040/scripts/visualization/joyplot.py new file mode 100644 index 0000000..79cf799 --- /dev/null +++ b/oscilloscope-rp2040/scripts/visualization/joyplot.py @@ -0,0 +1,130 @@ +import argparse +import sys +import os +import matplotlib.pyplot as plt +import numpy as np +from matplotlib.colors import to_rgba + +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../../"))) + + +def lerp(a, b, t): + return a + (b - a) * t + + +def lerp_rgba(c0, c1, t): + r0, g0, b0, a0 = to_rgba(c0) + r1, g1, b1, a1 = to_rgba(c1) + return (lerp(r0, r1, t), lerp(g0, g1, t), lerp(b0, b1, t), lerp(a0, a1, t)) + + +def generate_joyplot(input_file, output_file, lines, decimate, x_zoom, wave_scale): + # 1. Load Data + try: + data = np.load(input_file) + signal = data["data"] if "data" in data else data[data.files[0]] + except FileNotFoundError: + print(f"Error: Could not find {input_file}") + return + + # 2. Pre-process + signal = signal.flatten() + signal = signal[::decimate] + + signal = signal.astype(float) + smin, smax = np.min(signal), np.max(signal) + if smax == smin: + print("Error: Signal is constant; cannot normalize.") + return + signal = (signal - smin) / (smax - smin) + signal = signal - 0.5 # center around 0 + + # 3. Slice into segments + total_samples = len(signal) + samples_per_line = total_samples // lines + line_spacing = 15 + + # Gradient Configuration + TOP_COLOR = "#3D3229" + BOTTOM_COLOR = "#3D3229" + TOP_ALPHA = 1.0 + BOTTOM_ALPHA = 1.0 + FILL_COLOR = "#FFFFFF" + FILL_FOLLOWS_GRADIENT = False + + # 4. Setup Plot + fig, ax = plt.subplots(figsize=(10, 6)) + + # 5. Render Loop + for i in range(lines): + start = i * samples_per_line + end = start + samples_per_line + if end > total_samples: + break + + segment = signal[start:end] + x = np.arange(len(segment)) + + y_base = (lines - i) * line_spacing + y_curve = (segment * wave_scale) + y_base + + if lines <= 1: + t = 0.0 + else: + t = i / (lines - 1) + + alpha = lerp(TOP_ALPHA, BOTTOM_ALPHA, t) + stroke_rgb = lerp_rgba(TOP_COLOR, BOTTOM_COLOR, t) + stroke_color = (stroke_rgb[0], stroke_rgb[1], stroke_rgb[2], 1.0) + + if FILL_FOLLOWS_GRADIENT: + fill_rgb = lerp_rgba(TOP_COLOR, BOTTOM_COLOR, t) + fill_color = (fill_rgb[0], fill_rgb[1], fill_rgb[2], 1.0) + fill_alpha = alpha + else: + fill_color = FILL_COLOR + fill_alpha = 1.0 + + ax.fill_between( + x, y_base, y_curve, color=fill_color, zorder=i, alpha=fill_alpha + ) + ax.plot(x, y_curve, color=stroke_color, lw=0.8, zorder=i + 1, alpha=alpha) + + # Horizontal zoom + visible = int(samples_per_line / x_zoom) + ax.set_xlim(0, max(visible, 2)) + + # Minimal styling + ax.axis("off") + for spine in ax.spines.values(): + spine.set_visible(False) + + # Save + print(f"Rendering vector graphic to {output_file}...") + plt.tight_layout() + plt.savefig( + output_file, format="pdf", transparent=True, bbox_inches=None, pad_inches=0 + ) + print("Done.") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Generate a Joyplot/Ridgeline plot from audio data." + ) + parser.add_argument("input", help="Path to input .npz file") + parser.add_argument( + "-o", "--output", default="joyplot.pdf", help="Path to output PDF/SVG" + ) + parser.add_argument("--lines", type=int, default=8, help="Number of lines to stack") + parser.add_argument( + "--decimate", type=int, default=7, help="Decimation factor for signal" + ) + parser.add_argument("--zoom", type=float, default=35, help="Horizontal zoom factor") + parser.add_argument("--scale", type=float, default=20, help="Vertical wave scaling") + + args = parser.parse_args() + + generate_joyplot( + args.input, args.output, args.lines, args.decimate, args.zoom, args.scale + ) diff --git a/oscilloscope-rp2040/src/generate_svg.py b/oscilloscope-rp2040/src/generate_svg.py deleted file mode 100644 index 0b4c455..0000000 --- a/oscilloscope-rp2040/src/generate_svg.py +++ /dev/null @@ -1,138 +0,0 @@ -import matplotlib.pyplot as plt -import numpy as np -from matplotlib.colors import to_rgba - -# --- CONFIGURATION --- -INPUT_FILE = "oscilloscope-rp2040/data/continuous/song.npz" -OUTPUT_FILE = "docs/figures/title_graphic.pdf" - -LINES = 8 -DECIMATE = 7 - -WAVE_SCALE = 20 -LINE_SPACING = 15 - -# Horizontal zoom: >1 = zoom in (show less of each segment, looks stretched) -X_ZOOM = 35 - -LINE_WIDTH = 0.8 - -# --- GLOBAL GRADIENT CONTROLS --- -# Set the top/bottom colors here (any matplotlib color: hex, 'black', etc.) -TOP_COLOR = "#3D3229" -BOTTOM_COLOR = "#3D3229" - -# Opacity gradient: -TOP_ALPHA = 1.0 -BOTTOM_ALPHA = 1.0 - -# Fill color that masks stuff behind (leave white unless you want a tinted fill) -FILL_COLOR = "#FFFFFF" -FILL_FOLLOWS_GRADIENT = False # If True, fill will also color-gradient + alpha-gradient - - -def lerp(a, b, t): - return a + (b - a) * t - - -def lerp_rgba(c0, c1, t): - r0, g0, b0, a0 = to_rgba(c0) - r1, g1, b1, a1 = to_rgba(c1) - return (lerp(r0, r1, t), lerp(g0, g1, t), lerp(b0, b1, t), lerp(a0, a1, t)) - - -def generate_joyplot(): - # 1. Load Data - try: - data = np.load(INPUT_FILE) - signal = data["data"] if "data" in data else data[data.files[0]] - except FileNotFoundError: - print(f"Error: Could not find {INPUT_FILE}") - return - - # 2. Pre-process - signal = signal.flatten() - signal = signal[::DECIMATE] - - signal = signal.astype(float) - smin, smax = np.min(signal), np.max(signal) - if smax == smin: - print("Error: Signal is constant; cannot normalize.") - return - signal = (signal - smin) / (smax - smin) - signal = signal - 0.5 # center around 0 - - # 3. Slice into segments - total_samples = len(signal) - samples_per_line = total_samples // LINES - - # 4. Setup Plot - fig, ax = plt.subplots(figsize=(10, 6)) - - # 5. Render Loop - for i in range(LINES): - start = i * samples_per_line - end = start + samples_per_line - if end > total_samples: - break - - segment = signal[start:end] - x = np.arange(len(segment)) - - y_base = (LINES - i) * LINE_SPACING - y_curve = (segment * WAVE_SCALE) + y_base - - # t = 0 at top line, 1 at bottom line - if LINES <= 1: - t = 0.0 - else: - t = i / (LINES - 1) - - # Alpha gradient - alpha = lerp(TOP_ALPHA, BOTTOM_ALPHA, t) - - # Color gradient for the stroke - stroke_rgb = lerp_rgba(TOP_COLOR, BOTTOM_COLOR, t) - stroke_color = ( - stroke_rgb[0], - stroke_rgb[1], - stroke_rgb[2], - 1.0, - ) # keep color opaque; alpha handled separately - - # Fill: either constant masking white, or follow gradient - if FILL_FOLLOWS_GRADIENT: - fill_rgb = lerp_rgba(TOP_COLOR, BOTTOM_COLOR, t) - fill_color = (fill_rgb[0], fill_rgb[1], fill_rgb[2], 1.0) - fill_alpha = alpha - else: - fill_color = FILL_COLOR - fill_alpha = 1.0 # keep masking consistent - - ax.fill_between( - x, y_base, y_curve, color=fill_color, zorder=i, alpha=fill_alpha - ) - ax.plot( - x, y_curve, color=stroke_color, lw=LINE_WIDTH, zorder=i + 1, alpha=alpha - ) - - # Horizontal zoom by cropping x-range - visible = int(samples_per_line / X_ZOOM) - ax.set_xlim(0, max(visible, 2)) - - # Minimal styling - ax.axis("off") - for spine in ax.spines.values(): - spine.set_visible(False) - - # Save - print(f"Rendering vector graphic to {OUTPUT_FILE}...") - plt.tight_layout() - plt.savefig( - OUTPUT_FILE, format="pdf", transparent=True, bbox_inches=None, pad_inches=0 - ) - print("Done.") - - -if __name__ == "__main__": - generate_joyplot() From 174b89d92f7674f2a320c1c1e4920a0f47f7d117 Mon Sep 17 00:00:00 2001 From: Jackson Ferguson Date: Thu, 29 Jan 2026 16:19:54 -0800 Subject: [PATCH 2/9] test: add smoke test to prevent circular imports - Adds tests/test_imports.py to verify that the 'src' package can be imported without circular dependency errors. --- oscilloscope-rp2040/tests/test_imports.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 oscilloscope-rp2040/tests/test_imports.py diff --git a/oscilloscope-rp2040/tests/test_imports.py b/oscilloscope-rp2040/tests/test_imports.py new file mode 100644 index 0000000..9f52a86 --- /dev/null +++ b/oscilloscope-rp2040/tests/test_imports.py @@ -0,0 +1,13 @@ +import sys +import os + +# Ensure src is in path for tests +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../"))) + + +def test_can_import_package(): + """Ensures the dependency graph is acyclic and src can be imported.""" + import src + + # If we got here, __init__.py ran successfully + assert hasattr(src, "__file__") or hasattr(src, "__path__") From bf89cf941a48b0927ac73c61d2a9d13c2b6578c5 Mon Sep 17 00:00:00 2001 From: Jackson Ferguson Date: Thu, 29 Jan 2026 16:22:52 -0800 Subject: [PATCH 3/9] refactor: decouple plotting from diagnostics logic - Moves plot_health_check() from src/diagnostics.py to src/plots.py to keep diagnostics.py pure logic (Layer 3) and plots.py pure UI (Layer 4). - Removes matplotlib dependency from diagnostics.py to allow headless execution. - Updates src/plots.py imports to include config. --- oscilloscope-rp2040/src/diagnostics.py | 20 -------------------- oscilloscope-rp2040/src/plots.py | 21 ++++++++++++++++++++- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/oscilloscope-rp2040/src/diagnostics.py b/oscilloscope-rp2040/src/diagnostics.py index d9907b8..58e99bb 100644 --- a/oscilloscope-rp2040/src/diagnostics.py +++ b/oscilloscope-rp2040/src/diagnostics.py @@ -1,5 +1,4 @@ import numpy as np -import matplotlib.pyplot as plt from scipy.signal import find_peaks from . import config, dsp @@ -67,22 +66,3 @@ def analyze_spectrum_peaks(voltages: np.ndarray, fs: float): print(f" Harmonics: {harmonics} Hz") return dominant_freq, top_freqs - - -def plot_health_check(voltages: np.ndarray, fs: float, title: str, is_healthy: bool): - """Standard verification plot.""" - plt.figure(figsize=(12, 4)) - plt.plot(voltages, color="lime" if is_healthy else "orange", lw=0.7) - - # Safety Rails - plt.axhline(config.V_REF, color="red", linestyle="--", alpha=0.5) - plt.axhline(0.0, color="red", linestyle="--", alpha=0.5) - plt.axhline(config.V_MID, color="cyan", linestyle=":", alpha=0.5, label="V_mid") - - plt.title(f"{title} (FS={fs:.0f}Hz)") - plt.ylabel("Voltage (V)") - plt.xlabel("Samples") - plt.ylim(-0.1, config.V_REF + 0.1) - plt.grid(True, alpha=0.3) - plt.legend(loc="upper right") - plt.show() diff --git a/oscilloscope-rp2040/src/plots.py b/oscilloscope-rp2040/src/plots.py index 6f26c76..290eff0 100644 --- a/oscilloscope-rp2040/src/plots.py +++ b/oscilloscope-rp2040/src/plots.py @@ -4,7 +4,7 @@ import numpy as np from pathlib import Path from typing import Optional, Union, Any -from . import dsp, metrics +from . import dsp, metrics, config # --- Style Configuration --- COLORS = { @@ -406,3 +406,22 @@ def plot_final_report( save_pdf_svg(fig, savepath, bbox_inches="tight") if show: plt.show() + + +def plot_health_check(voltages: np.ndarray, fs: float, title: str, is_healthy: bool): + """Standard verification plot.""" + plt.figure(figsize=(12, 4)) + plt.plot(voltages, color="lime" if is_healthy else "orange", lw=0.7) + + # Safety Rails + plt.axhline(config.V_REF, color="red", linestyle="--", alpha=0.5) + plt.axhline(0.0, color="red", linestyle="--", alpha=0.5) + plt.axhline(config.V_MID, color="cyan", linestyle=":", alpha=0.5, label="V_mid") + + plt.title(f"{title} (FS={fs:.0f}Hz)") + plt.ylabel("Voltage (V)") + plt.xlabel("Samples") + plt.ylim(-0.1, config.V_REF + 0.1) + plt.grid(True, alpha=0.3) + plt.legend(loc="upper right") + plt.show() From df1d1cf0f775d99232218da5c16e3ef1fbc1a2b8 Mon Sep 17 00:00:00 2001 From: Jackson Ferguson Date: Thu, 29 Jan 2026 16:30:25 -0800 Subject: [PATCH 4/9] refactor: move spectrum viz to src/viz.py and flatten script - Moves the visualization logic from scripts/analysis/spectrum.py into src/viz.py as a reusable function `analyze_signal_plot()`. - Flattens scripts/analysis/spectrum.py to remove the `analyze_file` wrapper, making `main()` call the library function directly. --- .../scripts/analysis/spectrum.py | 38 +++---------------- oscilloscope-rp2040/src/viz.py | 33 ++++++++++++++++ 2 files changed, 38 insertions(+), 33 deletions(-) diff --git a/oscilloscope-rp2040/scripts/analysis/spectrum.py b/oscilloscope-rp2040/scripts/analysis/spectrum.py index cdd25b1..6a4d378 100644 --- a/oscilloscope-rp2040/scripts/analysis/spectrum.py +++ b/oscilloscope-rp2040/scripts/analysis/spectrum.py @@ -1,46 +1,18 @@ import sys import os -import matplotlib.pyplot as plt -import numpy as np sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../../"))) -from src import io, dsp, config - - -def analyze_file(filepath): - print(f"Loading: {filepath}") - - signal, fs = io.load_signal(filepath) - - # Prep analysis - ac_signal = dsp.remove_dc(signal) - freqs, mags = dsp.compute_spectrum(ac_signal, fs) - fundamental = dsp.estimate_fundamental(freqs, mags) - - # Plotting - t_axis = (np.arange(signal.size) / fs) * 1000 - - plt.figure(figsize=(12, 8)) - - plt.subplot(2, 1, 1) - plt.plot(t_axis, signal, color="lime") - plt.title(f"File: {os.path.basename(filepath)} | Pitch: {fundamental:.1f} Hz") - plt.grid(True, alpha=0.3) - - plt.subplot(2, 1, 2) - plt.plot(freqs, mags, color="orange") - plt.xlim(0, 2000) - plt.grid(True, alpha=0.3) - - plt.tight_layout() - plt.show() +from src import io, config, viz def main(): filepath = io.select_file_cli(config.DATA_DIR_BURST) if filepath: - analyze_file(filepath) + print(f"Loading: {filepath}") + signal, fs = io.load_signal(filepath) + + viz.analyze_signal_plot(signal, fs, title=f"File: {os.path.basename(filepath)}") if __name__ == "__main__": diff --git a/oscilloscope-rp2040/src/viz.py b/oscilloscope-rp2040/src/viz.py index bdb8fde..0a52708 100644 --- a/oscilloscope-rp2040/src/viz.py +++ b/oscilloscope-rp2040/src/viz.py @@ -101,3 +101,36 @@ def run_live_scope( finally: plt.close(fig) print("Scope Closed.") + + +def analyze_signal_plot(signal: np.ndarray, fs: float, title: str = "Signal Analysis"): + """ + Static analysis plot for a captured signal file. + Shows Time Domain and Frequency Spectrum. + """ + # Prep analysis + ac_signal = dsp.remove_dc(signal) + freqs, mags = dsp.compute_spectrum(ac_signal, fs) + fundamental = dsp.estimate_fundamental(freqs, mags) + + # Plotting + t_axis = (np.arange(signal.size) / fs) * 1000 + + plt.figure(figsize=(12, 8)) + + plt.subplot(2, 1, 1) + plt.plot(t_axis, signal, color="lime") + plt.title(f"{title} | Pitch: {fundamental:.1f} Hz") + plt.grid(True, alpha=0.3) + plt.ylabel("Voltage (V)") + plt.xlabel("Time (ms)") + + plt.subplot(2, 1, 2) + plt.plot(freqs, mags, color="orange") + plt.xlim(0, 2000) + plt.grid(True, alpha=0.3) + plt.ylabel("Magnitude") + plt.xlabel("Frequency (Hz)") + + plt.tight_layout() + plt.show() From 12b6c12996c449e9a4ef335d8042fd18d9ad66fe Mon Sep 17 00:00:00 2001 From: Jackson Ferguson Date: Thu, 29 Jan 2026 16:46:01 -0800 Subject: [PATCH 5/9] config: configure ruff and pytest in pyproject.toml - Adds [tool.ruff] configuration for Python 3.13. - Adds [tool.pytest.ini_options] to locate tests in the subfolder and ensure src/ is importable. --- pyproject.toml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index e30d658..86ab764 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,3 +22,22 @@ dev = [ "pre-commit>=4.5.1", "ruff>=0.14.14", ] + +[tool.ruff] +line-length = 88 +target-version = "py313" + +[tool.ruff.lint] +select = ["E", "F", "I"] # pycodestyle, pyflakes, isort + +[tool.pytest.ini_options] +minversion = "6.0" +addopts = "-ra -q" + +testpaths = [ + "oscilloscope-rp2040/tests", +] + +pythonpath = [ + "oscilloscope-rp2040/src", +] From d80c4f049160f2a1c0095ae5cd933ee93e7b057a Mon Sep 17 00:00:00 2001 From: Jackson Ferguson Date: Thu, 29 Jan 2026 16:46:17 -0800 Subject: [PATCH 6/9] ci: add github actions workflow for tests and linting - Creates .github/workflows/ci.yml to run tests and linting on push/PR. - Configures matrix for macOS/Ubuntu and Python 3.13. - Uses uv for fast dependency management. --- .github/workflows/ci.yml | 42 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1acb53f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,42 @@ +name: CI + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build: + name: Test on ${{ matrix.os }} / Python ${{ matrix.python-version }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [macos-latest, ubuntu-latest] + python-version: ["3.13"] + + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 + + - name: Install system dependencies (Linux) + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y libportaudio2 + + - name: Install uv + uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: uv sync --all-extras --dev + + - name: Lint with Ruff + run: | + uv run ruff check . + uv run ruff format --check . + + - name: Run Tests + run: uv run pytest From f5080a5be0926e1ad0ecb3978d964cc17e0976be Mon Sep 17 00:00:00 2001 From: Jackson Ferguson Date: Thu, 29 Jan 2026 16:50:56 -0800 Subject: [PATCH 7/9] config: add pytest to dev dependencies --- pyproject.toml | 1 + uv.lock | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 86ab764..67430bf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,7 @@ dependencies = [ [dependency-groups] dev = [ "pre-commit>=4.5.1", + "pytest>=9.0.2", "ruff>=0.14.14", ] diff --git a/uv.lock b/uv.lock index aa2e48c..c4799c5 100644 --- a/uv.lock +++ b/uv.lock @@ -259,6 +259,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b8/58/40fbbcefeda82364720eba5cf2270f98496bdfa19ea75b4cccae79c698e6/identify-2.6.16-py2.py3-none-any.whl", hash = "sha256:391ee4d77741d994189522896270b787aed8670389bfd60f326d677d64a6dfb0", size = 99202, upload-time = "2026-01-12T18:58:56.627Z" }, ] +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + [[package]] name = "ipykernel" version = "7.1.0" @@ -709,6 +718,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, ] +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + [[package]] name = "pre-commit" version = "4.5.1" @@ -819,6 +837,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/07/bc/587a445451b253b285629263eb51c2d8e9bcea4fc97826266d186f96f558/pyserial-3.5-py2.py3-none-any.whl", hash = "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0", size = 90585, upload-time = "2020-11-23T03:59:13.41Z" }, ] +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -1069,6 +1103,7 @@ dependencies = [ [package.dev-dependencies] dev = [ { name = "pre-commit" }, + { name = "pytest" }, { name = "ruff" }, ] @@ -1089,6 +1124,7 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ { name = "pre-commit", specifier = ">=4.5.1" }, + { name = "pytest", specifier = ">=9.0.2" }, { name = "ruff", specifier = ">=0.14.14" }, ] From 045a366d0dfe47eb99d251d1614c9d00da7abd48 Mon Sep 17 00:00:00 2001 From: Jackson Ferguson Date: Thu, 29 Jan 2026 17:09:25 -0800 Subject: [PATCH 8/9] style: fix linting issues and update plotting references - scripts/capture/master_transfer.py: - Replace unused 'osc' variable with '_' in context managers. - Update deprecated diagnostics.plot_health_check() call to plots.plot_health_check(). - scripts/visualization/render_scope_video.py: - Rename ambiguous variable 'l' to 'glow_line'. - Wrap long comment for ffmpeg presets. - power-regulator/schematic.py: - Shorten long comment to satisfy line length limit. --- oscilloscope-rp2040/firmware/main.py | 7 ++++--- .../notebooks/02_instrument_analysis.ipynb | 14 +++++++------- .../notebooks/03_transfer_acquisition.ipynb | 2 +- .../notebooks/04_transfer_analysis.ipynb | 6 +++--- oscilloscope-rp2040/scripts/analysis/spectrum.py | 4 ++-- .../scripts/capture/master_transfer.py | 7 ++++--- oscilloscope-rp2040/scripts/capture/record.py | 4 ++-- oscilloscope-rp2040/scripts/capture/stream.py | 5 +++-- oscilloscope-rp2040/scripts/signal/play_sweep.py | 3 ++- oscilloscope-rp2040/scripts/signal/play_wave.py | 8 +++----- .../scripts/visualization/joyplot.py | 3 ++- .../scripts/visualization/live_scope.py | 3 ++- .../scripts/visualization/render_scope_video.py | 5 +++-- oscilloscope-rp2040/src/__init__.py | 8 ++++---- oscilloscope-rp2040/src/audio.py | 3 ++- oscilloscope-rp2040/src/calibration.py | 8 +++++--- oscilloscope-rp2040/src/daq.py | 4 +++- oscilloscope-rp2040/src/diagnostics.py | 1 + oscilloscope-rp2040/src/dsp.py | 1 + oscilloscope-rp2040/src/experiments.py | 8 +++++--- oscilloscope-rp2040/src/io.py | 3 ++- oscilloscope-rp2040/src/metrics.py | 5 +++-- oscilloscope-rp2040/src/plots.py | 12 +++++++----- oscilloscope-rp2040/src/render.py | 11 +++++++---- oscilloscope-rp2040/src/viz.py | 4 +++- oscilloscope-rp2040/tests/test_imports.py | 2 +- power-regulator-12v-to-9v/schematic/schematic.py | 3 ++- 27 files changed, 84 insertions(+), 60 deletions(-) diff --git a/oscilloscope-rp2040/firmware/main.py b/oscilloscope-rp2040/firmware/main.py index 9c2e907..be55d00 100644 --- a/oscilloscope-rp2040/firmware/main.py +++ b/oscilloscope-rp2040/firmware/main.py @@ -1,8 +1,9 @@ -import machine import array -import micropython -import sys import gc +import sys + +import machine +import micropython import uselect # ------------------------------------------------------------------------- diff --git a/oscilloscope-rp2040/notebooks/02_instrument_analysis.ipynb b/oscilloscope-rp2040/notebooks/02_instrument_analysis.ipynb index da3e2a8..f7020e1 100644 --- a/oscilloscope-rp2040/notebooks/02_instrument_analysis.ipynb +++ b/oscilloscope-rp2040/notebooks/02_instrument_analysis.ipynb @@ -42,11 +42,11 @@ ], "source": [ "# ruff: noqa: E402\n", - "import sys\n", "import os\n", + "import sys\n", + "\n", "import numpy as np\n", "import seaborn as sns\n", - "import matplotlib.pyplot as plt\n", "\n", "# 1. Define Paths\n", "# Current: .../oscilloscope-rp2040/notebooks\n", @@ -64,7 +64,7 @@ "sys.path.append(project_root)\n", "\n", "# 3. Import Libraries\n", - "from src import config, io, dsp, plots\n", + "from src import config, dsp, io, plots\n", "\n", "# 4. Set Styling\n", "sns.set_style(\"darkgrid\")\n", @@ -189,7 +189,7 @@ "clean_ac = dsp.remove_dc(volts_clean)\n", "llama_ac = dsp.remove_dc(volts_llama)\n", "\n", - "print(f\"--- LOAD COMPLETE ---\")\n", + "print(\"--- LOAD COMPLETE ---\")\n", "print(f\"Clean Signal Amplitude: {(np.max(clean_ac) - np.min(clean_ac)):.3f} Vpp\")\n", "print(f\"Llama Signal Amplitude: {(np.max(llama_ac) - np.min(llama_ac)):.3f} Vpp\")" ] @@ -308,7 +308,7 @@ "noise_mean = np.mean(volts_noise)\n", "noise_std = np.std(volts_noise)\n", "\n", - "print(f\"--- DETECTOR CHARACTERIZATION ---\")\n", + "print(\"--- DETECTOR CHARACTERIZATION ---\")\n", "print(f\"Bias Level (DC): {noise_mean:.6f} V\")\n", "print(f\"Read Noise (RMS): {noise_std * 1000:.3f} mV\")\n", "\n", @@ -459,7 +459,7 @@ "thd_clean = dsp.calculate_selective_thd(clean_ac, fs, 82.4)\n", "thd_llama = dsp.calculate_selective_thd(llama_ac, fs, 82.4)\n", "\n", - "print(f\"--- SELECTIVE THD ---\")\n", + "print(\"--- SELECTIVE THD ---\")\n", "print(f\"Clean DI Signal: {thd_clean:.2f}%\")\n", "print(f\"Red Llama Signal: {thd_llama:.2f}%\")" ] @@ -529,7 +529,7 @@ ], "metadata": { "kernelspec": { - "display_name": "hardware-builds (3.13.11)", + "display_name": "systems-audio-lab (3.13.11)", "language": "python", "name": "python3" }, diff --git a/oscilloscope-rp2040/notebooks/03_transfer_acquisition.ipynb b/oscilloscope-rp2040/notebooks/03_transfer_acquisition.ipynb index 16fb49b..a8d5659 100644 --- a/oscilloscope-rp2040/notebooks/03_transfer_acquisition.ipynb +++ b/oscilloscope-rp2040/notebooks/03_transfer_acquisition.ipynb @@ -17,8 +17,8 @@ ], "source": [ "# Cell 1: Imports & Setup\n", - "import sys\n", "import os\n", + "import sys\n", "\n", "# Add project root to path\n", "sys.path.append(\"..\")\n", diff --git a/oscilloscope-rp2040/notebooks/04_transfer_analysis.ipynb b/oscilloscope-rp2040/notebooks/04_transfer_analysis.ipynb index 2003090..c44e153 100644 --- a/oscilloscope-rp2040/notebooks/04_transfer_analysis.ipynb +++ b/oscilloscope-rp2040/notebooks/04_transfer_analysis.ipynb @@ -8,8 +8,8 @@ "outputs": [], "source": [ "# ruff: noqa: E402\n", - "import sys\n", "import os\n", + "import sys\n", "from pathlib import Path\n", "\n", "# 1. Define Paths\n", @@ -18,7 +18,7 @@ "sys.path.append(project_root)\n", "\n", "# 2. Import Libraries\n", - "from src import plots, io\n", + "from src import io, plots\n", "\n", "# 3. Define File Paths\n", "DATA = Path(\"..\") / \"data\"\n", @@ -124,7 +124,7 @@ ], "metadata": { "kernelspec": { - "display_name": "hardware-builds (3.13.11)", + "display_name": "systems-audio-lab (3.13.11)", "language": "python", "name": "python3" }, diff --git a/oscilloscope-rp2040/scripts/analysis/spectrum.py b/oscilloscope-rp2040/scripts/analysis/spectrum.py index 6a4d378..907cfc7 100644 --- a/oscilloscope-rp2040/scripts/analysis/spectrum.py +++ b/oscilloscope-rp2040/scripts/analysis/spectrum.py @@ -1,9 +1,9 @@ -import sys import os +import sys sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../../"))) -from src import io, config, viz +from src import config, io, viz def main(): diff --git a/oscilloscope-rp2040/scripts/capture/master_transfer.py b/oscilloscope-rp2040/scripts/capture/master_transfer.py index 322e5c4..60a5533 100644 --- a/oscilloscope-rp2040/scripts/capture/master_transfer.py +++ b/oscilloscope-rp2040/scripts/capture/master_transfer.py @@ -1,12 +1,13 @@ -import sys import os +import sys import time + import numpy as np import sounddevice as sd sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../../"))) -from src import audio, daq, io, config, dsp +from src import audio, config, daq, dsp, io # --- USER CONFIGURATION --- # Uncomment ONE mode below to select it: @@ -67,7 +68,7 @@ def run_sweep_capture(): def run_steady_capture(): print(f"🔹 Starting Oscillator ({STEADY_SHAPE} @ {STEADY_FREQ}Hz)...") - with audio.ContinuousOscillator(STEADY_SHAPE, STEADY_FREQ, STEADY_AMP) as osc: + with audio.ContinuousOscillator(STEADY_SHAPE, STEADY_FREQ, STEADY_AMP) as _: # Allow signal to settle time.sleep(0.5) diff --git a/oscilloscope-rp2040/scripts/capture/record.py b/oscilloscope-rp2040/scripts/capture/record.py index 35cab18..1c4fd37 100644 --- a/oscilloscope-rp2040/scripts/capture/record.py +++ b/oscilloscope-rp2040/scripts/capture/record.py @@ -1,10 +1,10 @@ -import sys import os +import sys # Add project root to path sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../../"))) -from src import daq, dsp, io, config +from src import config, daq, dsp, io def main(): diff --git a/oscilloscope-rp2040/scripts/capture/stream.py b/oscilloscope-rp2040/scripts/capture/stream.py index 9755dbf..a025daf 100644 --- a/oscilloscope-rp2040/scripts/capture/stream.py +++ b/oscilloscope-rp2040/scripts/capture/stream.py @@ -1,12 +1,13 @@ -import sys import os +import sys import time + import numpy as np # Add project root to path sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../../"))) -from src import daq, io, config +from src import config, daq, io def record_stream(): diff --git a/oscilloscope-rp2040/scripts/signal/play_sweep.py b/oscilloscope-rp2040/scripts/signal/play_sweep.py index 9b6323c..e710cc9 100644 --- a/oscilloscope-rp2040/scripts/signal/play_sweep.py +++ b/oscilloscope-rp2040/scripts/signal/play_sweep.py @@ -1,5 +1,6 @@ -import sys import os +import sys + import sounddevice as sd # Add project root to path diff --git a/oscilloscope-rp2040/scripts/signal/play_wave.py b/oscilloscope-rp2040/scripts/signal/play_wave.py index a62fb05..19c78aa 100644 --- a/oscilloscope-rp2040/scripts/signal/play_wave.py +++ b/oscilloscope-rp2040/scripts/signal/play_wave.py @@ -1,16 +1,15 @@ -import sys import os +import sys # Add project root to path sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../../"))) from src import audio, daq, viz -# --- CONFIGURATION (EDIT ME) --- +# Configuration SHAPE = "sine" # "sine", "triangle", "square", "saw" FREQ_HZ = 440.0 # Frequency AMPLITUDE = 0.5 # 0.0 to 1.0 (Watch your volume!) -# ------------------------------- def main(): @@ -19,12 +18,11 @@ def main(): # 1. Init Audio (auto_start=False means it waits) with audio.ContinuousOscillator(SHAPE, FREQ_HZ, AMPLITUDE, auto_start=False) as osc: # 2. Start Scope - # We pass osc.play as the callback. Viz will call this ONLY when the window is ready. with daq.DAQInterface() as device: viz.run_live_scope( device.stream_generator(), title=f"Generator: {SHAPE.title()} @ {FREQ_HZ}Hz", - on_launch=osc.play, # <--- The magic happens here + on_launch=osc.play, ) diff --git a/oscilloscope-rp2040/scripts/visualization/joyplot.py b/oscilloscope-rp2040/scripts/visualization/joyplot.py index 79cf799..b5abc3f 100644 --- a/oscilloscope-rp2040/scripts/visualization/joyplot.py +++ b/oscilloscope-rp2040/scripts/visualization/joyplot.py @@ -1,6 +1,7 @@ import argparse -import sys import os +import sys + import matplotlib.pyplot as plt import numpy as np from matplotlib.colors import to_rgba diff --git a/oscilloscope-rp2040/scripts/visualization/live_scope.py b/oscilloscope-rp2040/scripts/visualization/live_scope.py index 39b3c61..3a03d69 100644 --- a/oscilloscope-rp2040/scripts/visualization/live_scope.py +++ b/oscilloscope-rp2040/scripts/visualization/live_scope.py @@ -1,6 +1,7 @@ -import sys import os +import sys import time + import matplotlib.pyplot as plt # Add project root to path diff --git a/oscilloscope-rp2040/scripts/visualization/render_scope_video.py b/oscilloscope-rp2040/scripts/visualization/render_scope_video.py index 76c14c3..ba1448a 100644 --- a/oscilloscope-rp2040/scripts/visualization/render_scope_video.py +++ b/oscilloscope-rp2040/scripts/visualization/render_scope_video.py @@ -15,7 +15,8 @@ "dpi": 100, "bitrate": 8000, # kbps "crf": 18, # 0-51 (lower is better quality, 18 is visually lossless) - "preset": "slow", # ultrafast, superfast, veryfast, faster, fast, medium, slow, slower, veryslow + # ultrafast, superfast, veryfast, faster, fast, medium, slow, slower, veryslow + "preset": "slow", } @@ -34,7 +35,7 @@ def main(): for key, (name, _) in render.EFFECTS.items(): print(f"[{key}] {name}") - effect_choice = input(f"Select Effect [Default: 2]: ").strip() + effect_choice = input("Select Effect [Default: 2]: ").strip() if not effect_choice: effect_choice = "2" diff --git a/oscilloscope-rp2040/src/__init__.py b/oscilloscope-rp2040/src/__init__.py index 9f123d8..1b9361c 100644 --- a/oscilloscope-rp2040/src/__init__.py +++ b/oscilloscope-rp2040/src/__init__.py @@ -1,11 +1,11 @@ +from . import audio as audio +from . import calibration as calibration from . import config as config from . import daq as daq +from . import diagnostics as diagnostics from . import dsp as dsp +from . import experiments as experiments from . import io as io from . import viz as viz -from . import calibration as calibration -from . import diagnostics as diagnostics -from . import audio as audio -from . import experiments as experiments __version__ = "1.0.0" diff --git a/oscilloscope-rp2040/src/audio.py b/oscilloscope-rp2040/src/audio.py index 1132f88..990538d 100644 --- a/oscilloscope-rp2040/src/audio.py +++ b/oscilloscope-rp2040/src/audio.py @@ -1,6 +1,7 @@ -import numpy as np import sys +import numpy as np + try: import sounddevice as sd except ImportError: diff --git a/oscilloscope-rp2040/src/calibration.py b/oscilloscope-rp2040/src/calibration.py index 849b530..4fece18 100644 --- a/oscilloscope-rp2040/src/calibration.py +++ b/oscilloscope-rp2040/src/calibration.py @@ -1,10 +1,12 @@ -import time import json import os -import numpy as np +import time + import matplotlib.pyplot as plt +import numpy as np from scipy.signal import windows -from . import daq, dsp, config + +from . import config, daq, dsp def save_calibration(fs: float): diff --git a/oscilloscope-rp2040/src/daq.py b/oscilloscope-rp2040/src/daq.py index ef81181..a7f726c 100644 --- a/oscilloscope-rp2040/src/daq.py +++ b/oscilloscope-rp2040/src/daq.py @@ -1,6 +1,8 @@ -import serial import time + import numpy as np +import serial + from . import config diff --git a/oscilloscope-rp2040/src/diagnostics.py b/oscilloscope-rp2040/src/diagnostics.py index 58e99bb..9ab2c43 100644 --- a/oscilloscope-rp2040/src/diagnostics.py +++ b/oscilloscope-rp2040/src/diagnostics.py @@ -1,5 +1,6 @@ import numpy as np from scipy.signal import find_peaks + from . import config, dsp diff --git a/oscilloscope-rp2040/src/dsp.py b/oscilloscope-rp2040/src/dsp.py index e1ce4cc..f72cc8c 100644 --- a/oscilloscope-rp2040/src/dsp.py +++ b/oscilloscope-rp2040/src/dsp.py @@ -1,5 +1,6 @@ import numpy as np import scipy.signal as spsig + from . import config diff --git a/oscilloscope-rp2040/src/experiments.py b/oscilloscope-rp2040/src/experiments.py index 041cdd7..923e8c9 100644 --- a/oscilloscope-rp2040/src/experiments.py +++ b/oscilloscope-rp2040/src/experiments.py @@ -1,7 +1,9 @@ import time + import numpy as np import sounddevice as sd -from . import audio, daq, io, config, dsp, diagnostics + +from . import audio, config, daq, diagnostics, dsp, io, plots def capture_sweep_transfer( @@ -79,7 +81,7 @@ def capture_steady_transfer( """ print(f"🔹 Starting Oscillator ({shape} @ {freq}Hz)...") - with audio.ContinuousOscillator(shape, freq, amp) as osc: + with audio.ContinuousOscillator(shape, freq, amp) as _: time.sleep(duration_buffer) print("📸 Capturing Burst...") @@ -121,7 +123,7 @@ def capture_steady_transfer( # Plot title = f"{prefix} (Meas: {dom_freq:.1f}Hz)" - diagnostics.plot_health_check(volts, config.FS_DEFAULT, title, is_healthy) + plots.plot_health_check(volts, config.FS_DEFAULT, title, is_healthy) return path diff --git a/oscilloscope-rp2040/src/io.py b/oscilloscope-rp2040/src/io.py index e3d59d9..4497905 100644 --- a/oscilloscope-rp2040/src/io.py +++ b/oscilloscope-rp2040/src/io.py @@ -1,7 +1,8 @@ import os import sys -import numpy as np from datetime import datetime + +import numpy as np import pandas as pd diff --git a/oscilloscope-rp2040/src/metrics.py b/oscilloscope-rp2040/src/metrics.py index d91b652..bd1905a 100644 --- a/oscilloscope-rp2040/src/metrics.py +++ b/oscilloscope-rp2040/src/metrics.py @@ -1,8 +1,9 @@ +from typing import Any, Dict, Optional, Tuple + import numpy as np import pandas as pd import scipy.signal as spsig -from typing import Dict, Tuple, Optional, Any -import scipy.signal as spsig + from . import dsp diff --git a/oscilloscope-rp2040/src/plots.py b/oscilloscope-rp2040/src/plots.py index 290eff0..52cfad4 100644 --- a/oscilloscope-rp2040/src/plots.py +++ b/oscilloscope-rp2040/src/plots.py @@ -1,10 +1,12 @@ -import matplotlib.pyplot as plt +from pathlib import Path +from typing import Any, Optional, Union + import matplotlib.gridspec as gridspec -import seaborn as sns +import matplotlib.pyplot as plt import numpy as np -from pathlib import Path -from typing import Optional, Union, Any -from . import dsp, metrics, config +import seaborn as sns + +from . import config, dsp, metrics # --- Style Configuration --- COLORS = { diff --git a/oscilloscope-rp2040/src/render.py b/oscilloscope-rp2040/src/render.py index c81246d..a741792 100644 --- a/oscilloscope-rp2040/src/render.py +++ b/oscilloscope-rp2040/src/render.py @@ -1,7 +1,8 @@ -import sys import shutil -import numpy as np +import sys + import matplotlib.pyplot as plt +import numpy as np from matplotlib.animation import FFMpegWriter from . import config, dsp, io @@ -53,8 +54,10 @@ def setup_crt_bloom(samples, fs, v_conf): for i in range(3): lw = 4 + (i * 4) alpha = 0.1 / (i + 1) - (l,) = ax.plot(x, np.zeros(samples), color="#32CD32", lw=lw, alpha=alpha) - lines.append(l) + (glow_line,) = ax.plot( + x, np.zeros(samples), color="#32CD32", lw=lw, alpha=alpha + ) + lines.append(glow_line) (core,) = ax.plot(x, np.zeros(samples), color="#ccffcc", lw=1.2, alpha=1.0) lines.append(core) diff --git a/oscilloscope-rp2040/src/viz.py b/oscilloscope-rp2040/src/viz.py index 0a52708..1f41bde 100644 --- a/oscilloscope-rp2040/src/viz.py +++ b/oscilloscope-rp2040/src/viz.py @@ -1,6 +1,8 @@ +import time + import matplotlib.pyplot as plt import numpy as np -import time + from . import config, dsp diff --git a/oscilloscope-rp2040/tests/test_imports.py b/oscilloscope-rp2040/tests/test_imports.py index 9f52a86..2a1e5cc 100644 --- a/oscilloscope-rp2040/tests/test_imports.py +++ b/oscilloscope-rp2040/tests/test_imports.py @@ -1,5 +1,5 @@ -import sys import os +import sys # Ensure src is in path for tests sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../"))) diff --git a/power-regulator-12v-to-9v/schematic/schematic.py b/power-regulator-12v-to-9v/schematic/schematic.py index 9a906a0..ba45d8f 100644 --- a/power-regulator-12v-to-9v/schematic/schematic.py +++ b/power-regulator-12v-to-9v/schematic/schematic.py @@ -1,4 +1,5 @@ from pathlib import Path + import schemdraw import schemdraw.elements as elm @@ -153,7 +154,7 @@ def draw_schematic(): ) d.pop() - # Explicitly save the PDF version before the context manager closes (and saves SVG) + # Explicitly save PDF before context manager closes (which saves SVG) d.save(FILE_PDF) From d370af8e5e208016ca694a23b4f5ae50426ee934 Mon Sep 17 00:00:00 2001 From: Jackson Ferguson Date: Thu, 29 Jan 2026 17:12:05 -0800 Subject: [PATCH 9/9] docs: add README for tests directory --- oscilloscope-rp2040/tests/README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 oscilloscope-rp2040/tests/README.md diff --git a/oscilloscope-rp2040/tests/README.md b/oscilloscope-rp2040/tests/README.md new file mode 100644 index 0000000..d11cd72 --- /dev/null +++ b/oscilloscope-rp2040/tests/README.md @@ -0,0 +1,17 @@ +## Contents + +* **`test_imports.py`**: A "smoke test" that attempts to import the `src` package. If this fails, it usually indicates a circular dependency or a missing `__init__.py` file in the module graph. + +## Running Tests + +We use `pytest` for test discovery and execution. + +### Using `uv` (Recommended) + +```bash +# Run all tests +uv run pytest + +# Run with verbose output +uv run pytest -v +```