diff --git a/tests/test_plotting.py b/tests/test_plotting.py new file mode 100644 index 0000000..774739f --- /dev/null +++ b/tests/test_plotting.py @@ -0,0 +1,103 @@ +"""Tests for build_waveform_svg -- the plot_waveform core, sans file I/O. + +Hand-built RawFiles drive every branch (auto plot-type detection, clipping, +multi-signal, errors) without LTspice or writing an SVG to disk. +""" + +import numpy as np + +from mcltspice.plotting import build_waveform_svg +from mcltspice.raw_parser import RawFile, Variable + + +def _ac_raw() -> RawFile: + """Frequency axis + complex V(out), V(in) -> AC analysis.""" + freq = np.logspace(0, 6, 400) + hout = 1.0 / (1.0 + 1j * (freq / 1000.0)) + data = np.array([freq.astype(complex), hout, np.ones_like(freq, dtype=complex)]) + return RawFile( + title="ac", date="", plotname="AC Analysis", flags=["complex"], + variables=[ + Variable(0, "frequency", "frequency"), + Variable(1, "V(out)", "voltage"), + Variable(2, "V(in)", "voltage"), + ], + points=len(freq), data=data, + ) + + +def _tran_raw() -> RawFile: + """Time axis + two real signals -> transient.""" + t = np.linspace(0, 0.01, 500) + data = np.array([t, np.sin(2 * np.pi * 1000 * t), np.cos(2 * np.pi * 1000 * t)]) + return RawFile( + title="tran", date="", plotname="Transient Analysis", flags=["real"], + variables=[ + Variable(0, "time", "time"), + Variable(1, "V(out)", "voltage"), + Variable(2, "V(ref)", "voltage"), + ], + points=len(t), data=data, + ) + + +class TestAutoPlotType: + def test_complex_frequency_is_bode(self): + r = build_waveform_svg(_ac_raw(), ["V(out)"]) + assert r["plot_type"] == "bode" + assert r["svg"].lstrip().startswith("<") # real SVG markup + + def test_real_frequency_is_spectrum(self): + # frequency axis but real signal data -> spectrum + freq = np.logspace(0, 6, 200) + data = np.array([freq, np.abs(1.0 / (1.0 + 1j * freq / 1000.0))]) + raw = RawFile( + title="", date="", plotname="AC Analysis", flags=["real"], + variables=[Variable(0, "frequency", "frequency"), Variable(1, "V(out)", "voltage")], + points=len(freq), data=data, + ) + assert build_waveform_svg(raw, ["V(out)"])["plot_type"] == "spectrum" + + def test_time_axis_is_time(self): + assert build_waveform_svg(_tran_raw(), ["V(out)"])["plot_type"] == "time" + + def test_explicit_overrides_auto(self): + assert build_waveform_svg(_tran_raw(), ["V(out)"], plot_type="time")["plot_type"] == "time" + + +class TestErrorsAndMetadata: + def test_missing_signal(self): + r = build_waveform_svg(_ac_raw(), ["V(ghost)"]) + assert "error" in r and "V(ghost)" in r["error"] + assert r["available_signals"] == ["frequency", "V(out)", "V(in)"] + + def test_multi_signal_non_time_is_error(self): + r = build_waveform_svg(_ac_raw(), ["V(out)", "V(in)"], plot_type="bode") + assert "error" in r and "time-domain" in r["error"] + + def test_dimensions_and_signal_metadata(self): + r = build_waveform_svg(_tran_raw(), ["V(out)"], width=1000, height=600) + assert r["dimensions"] == "1000x600" + assert r["signal"] == "V(out)" + + def test_default_height_depends_on_plot_type(self): + assert build_waveform_svg(_ac_raw(), ["V(out)"])["dimensions"] == "800x500" # bode + assert build_waveform_svg(_tran_raw(), ["V(out)"])["dimensions"] == "800x400" # time + + +class TestSamplingAndMulti: + def test_max_points_downsamples(self): + r = build_waveform_svg(_tran_raw(), ["V(out)"], max_points=100) + assert r["points"] <= 100 + + def test_x_range_clip_reduces_points(self): + full = build_waveform_svg(_tran_raw(), ["V(out)"], max_points=10000)["points"] + clipped = build_waveform_svg( + _tran_raw(), ["V(out)"], x_min=0.002, x_max=0.004, max_points=10000 + )["points"] + assert clipped < full + + def test_multi_signal_time_overlay(self): + r = build_waveform_svg(_tran_raw(), ["V(out)", "V(ref)"]) + assert r["plot_type"] == "time" + assert r["signal"] == "V(out), V(ref)"