"""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)"