mcltspice/tests/test_waveform_query.py
Ryan Malloy 90cd07d9bc Test output_format and waveform_query without a simulator
17 tests: round_sig/compact_list rounding, downsample_indices stride vs
peak-preserving (a narrow spike must survive), and extract_waveform/
analyze_signal across AC/transient/x-range/error/all-analysis paths.
2026-06-20 22:25:54 -06:00

78 lines
2.9 KiB
Python

"""Tests for extract_waveform / analyze_signal -- RawFile queries sans simulator."""
import numpy as np
from mcltspice.raw_parser import RawFile, Variable
from mcltspice.waveform_query import analyze_signal, extract_waveform
def _ac_raw() -> RawFile:
freq = np.logspace(0, 6, 300)
hout = 1.0 / (1.0 + 1j * (freq / 1000.0))
data = np.array([freq.astype(complex), hout])
return RawFile(
title="", date="", plotname="AC Analysis", flags=["complex"],
variables=[Variable(0, "frequency", "frequency"), Variable(1, "V(out)", "voltage")],
points=len(freq), data=data,
)
def _tran_raw() -> RawFile:
t = np.linspace(0, 0.01, 800)
v = 2.0 * np.sin(2 * np.pi * 1000 * t)
return RawFile(
title="", date="", plotname="Transient Analysis", flags=["real"],
variables=[Variable(0, "time", "time"), Variable(1, "V(out)", "voltage")],
points=len(t), data=np.array([t, v]),
)
class TestExtractWaveform:
def test_transient_values(self):
r = extract_waveform(_tran_raw(), ["V(out)"], max_points=100)
assert r["x_axis_name"] == "time"
assert r["returned_points"] <= 100
assert "values" in r["signals"]["V(out)"]
def test_ac_gives_magnitude_and_phase(self):
r = extract_waveform(_ac_raw(), ["V(out)"])
assert r["x_axis_name"] == "frequency"
sig = r["signals"]["V(out)"]
assert "magnitude_db" in sig and "phase_degrees" in sig
def test_x_range_filter(self):
full = extract_waveform(_tran_raw(), ["V(out)"], max_points=10000)["returned_points"]
clipped = extract_waveform(
_tran_raw(), ["V(out)"], max_points=10000, x_min=0.002, x_max=0.004
)["returned_points"]
assert clipped < full
def test_empty_range_error(self):
r = extract_waveform(_tran_raw(), ["V(out)"], x_min=1e12)
assert "error" in r and "range" in r["error"]
def test_run_on_non_stepped_error(self):
r = extract_waveform(_tran_raw(), ["V(out)"], run=2)
assert "error" in r and "stepped" in r["error"]
class TestAnalyzeSignal:
def test_rms_and_peak_to_peak(self):
r = analyze_signal(_tran_raw(), "V(out)", ["rms", "peak_to_peak"])
assert r["signal"] == "V(out)"
assert abs(r["rms"] - 2.0 / np.sqrt(2)) < 0.05 # RMS of a 2 V amplitude sine
assert abs(r["peak_to_peak"]["peak_to_peak"] - 4.0) < 0.05
def test_fft_and_thd_present(self):
r = analyze_signal(_tran_raw(), "V(out)", ["fft", "thd"])
assert "fft" in r and "thd" in r
def test_missing_signal_error(self):
r = analyze_signal(_tran_raw(), "V(ghost)", ["rms"])
assert "error" in r and "V(ghost)" in r["error"]
def test_complex_signal_uses_magnitude(self):
# AC (complex) signal: analysis should run on |signal| without raising
r = analyze_signal(_ac_raw(), "V(out)", ["rms"])
assert "rms" in r and r["rms"] >= 0