"""Tests for the pure-SVG waveform plot generation module.""" import numpy as np import pytest from mcltspice.svg_plot import ( _format_freq, _nice_ticks, plot_bode, plot_spectrum, plot_timeseries, ) # --------------------------------------------------------------------------- # Fixtures # --------------------------------------------------------------------------- @pytest.fixture() def sine_wave(): """A 1 kHz sine wave sampled at 100 kHz for 10 ms.""" t = np.linspace(0, 0.01, 1000, endpoint=False) v = np.sin(2 * np.pi * 1000 * t) return t, v @pytest.fixture() def bode_data(): """Simple first-order lowpass Bode response (fc = 1 kHz).""" freq = np.logspace(1, 6, 500) fc = 1e3 mag_db = -10 * np.log10(1 + (freq / fc) ** 2) phase_deg = -np.degrees(np.arctan(freq / fc)) return freq, mag_db, phase_deg @pytest.fixture() def spectrum_data(): """Synthetic FFT spectrum with a peak at 1 kHz.""" freq = np.logspace(1, 5, 300) mag_db = -60 * np.ones_like(freq) peak_idx = np.argmin(np.abs(freq - 1e3)) mag_db[peak_idx] = 0.0 return freq, mag_db # --------------------------------------------------------------------------- # Timeseries # --------------------------------------------------------------------------- class TestPlotTimeseries: def test_basic(self, sine_wave): """A simple sine wave produces a valid SVG with expected elements.""" t, v = sine_wave svg = plot_timeseries(t, v) assert svg.startswith("" in svg def test_custom_title_and_labels(self, sine_wave): """Custom title and ylabel should appear in the SVG output.""" t, v = sine_wave svg = plot_timeseries(t, v, title="My Signal", ylabel="Current (A)") assert "My Signal" in svg assert "Current (A)" in svg def test_svg_dimensions(self, sine_wave): """The width/height attributes should match the requested size.""" t, v = sine_wave svg = plot_timeseries(t, v, width=1024, height=768) assert 'width="1024"' in svg assert 'height="768"' in svg # --------------------------------------------------------------------------- # Bode # --------------------------------------------------------------------------- class TestPlotBode: def test_magnitude_only(self, bode_data): """Bode plot without phase produces a valid SVG with one trace.""" freq, mag_db, _ = bode_data svg = plot_bode(freq, mag_db) assert svg.startswith(" elements (mag + phase).""" freq, mag_db, phase_deg = bode_data svg = plot_bode(freq, mag_db, phase_deg) assert svg.startswith("= 2 # Phase subplot label assert "Phase (deg)" in svg def test_log_axis_ticks(self, bode_data): """Log frequency axis should contain tick labels at powers of 10.""" freq, mag_db, _ = bode_data svg = plot_bode(freq, mag_db) # Expect at least some frequency labels like "100", "1k", "10k", "100k" found = sum(1 for lbl in ("100", "1k", "10k", "100k") if lbl in svg) assert found >= 2, f"Expected log tick labels in SVG; found {found}" # --------------------------------------------------------------------------- # Spectrum # --------------------------------------------------------------------------- class TestPlotSpectrum: def test_basic(self, spectrum_data): """A simple spectrum produces a valid SVG.""" freq, mag_db = spectrum_data svg = plot_spectrum(freq, mag_db) assert svg.startswith("= 3 assert ticks[0] <= 0 assert ticks[-1] >= 10 def test_equal_values(self): """When vmin == vmax, return a single-element list.""" ticks = _nice_ticks(5, 5) assert ticks == [5] def test_negative_range(self): ticks = _nice_ticks(-100, -20, n_ticks=5) assert ticks[0] <= -100 assert ticks[-1] >= -20 def test_small_range(self): ticks = _nice_ticks(0.001, 0.005, n_ticks=5) assert all(0 <= t <= 0.01 for t in ticks) class TestFormatFreq: def test_hz(self): assert _format_freq(1) == "1" assert _format_freq(10) == "10" assert _format_freq(100) == "100" def test_khz(self): assert _format_freq(1000) == "1k" assert _format_freq(10000) == "10k" assert _format_freq(100000) == "100k" def test_mhz(self): assert _format_freq(1e6) == "1M" assert _format_freq(10e6) == "10M" def test_ghz(self): assert _format_freq(1e9) == "1G" def test_zero(self): assert _format_freq(0) == "0" class TestSvgDimensions: def test_timeseries_dimensions(self): t = np.linspace(0, 1, 100) v = np.sin(t) svg = plot_timeseries(t, v, width=640, height=480) assert 'width="640"' in svg assert 'height="480"' in svg def test_bode_dimensions(self): freq = np.logspace(1, 5, 50) mag = np.zeros(50) svg = plot_bode(freq, mag, width=900, height=600) assert 'width="900"' in svg assert 'height="600"' in svg def test_spectrum_dimensions(self): freq = np.logspace(1, 5, 50) mag = np.zeros(50) svg = plot_spectrum(freq, mag, width=1000, height=500) assert 'width="1000"' in svg assert 'height="500"' in svg