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.
This commit is contained in:
parent
1118ac2be1
commit
90cd07d9bc
52
tests/test_output_format.py
Normal file
52
tests/test_output_format.py
Normal file
@ -0,0 +1,52 @@
|
||||
"""Tests for the JSON output-formatting helpers."""
|
||||
|
||||
import math
|
||||
|
||||
import numpy as np
|
||||
|
||||
from mcltspice.output_format import compact_list, downsample_indices, round_sig
|
||||
|
||||
|
||||
class TestRoundSig:
|
||||
def test_significant_figures(self):
|
||||
assert round_sig(123456.789, 3) == 123000.0
|
||||
assert round_sig(0.00123456, 3) == 0.00123
|
||||
|
||||
def test_zero_and_nonfinite_pass_through(self):
|
||||
assert round_sig(0.0) == 0.0
|
||||
assert math.isinf(round_sig(float("inf")))
|
||||
assert math.isnan(round_sig(float("nan")))
|
||||
|
||||
|
||||
class TestCompactList:
|
||||
def test_decimal_places(self):
|
||||
assert compact_list([1.23456, 2.98765], decimal_places=2) == [1.23, 2.99]
|
||||
|
||||
def test_sig_figs(self):
|
||||
assert compact_list([123456.0], sig_figs=3) == [123000.0]
|
||||
|
||||
def test_no_rounding_returns_floats(self):
|
||||
out = compact_list([1, 2, 3])
|
||||
assert out == [1.0, 2.0, 3.0]
|
||||
assert all(isinstance(v, float) for v in out)
|
||||
|
||||
|
||||
class TestDownsampleIndices:
|
||||
def test_no_downsample_when_under_limit(self):
|
||||
idx = downsample_indices(50, 100)
|
||||
assert np.array_equal(idx, np.arange(50))
|
||||
|
||||
def test_stride_for_transient(self):
|
||||
idx = downsample_indices(1000, 100)
|
||||
assert len(idx) <= 100
|
||||
# plain stride: evenly spaced
|
||||
assert idx[0] == 0
|
||||
assert idx[1] - idx[0] == idx[2] - idx[1]
|
||||
|
||||
def test_peak_preserving_keeps_a_narrow_spike(self):
|
||||
# A flat magnitude with one sharp spike: peak-preserving must keep it.
|
||||
n = 1000
|
||||
mag = np.ones(n, dtype=complex)
|
||||
mag[497] = 1000.0 # narrow resonance peak
|
||||
idx = downsample_indices(n, 50, signals=[mag])
|
||||
assert 497 in idx # spike survived downsampling
|
||||
77
tests/test_waveform_query.py
Normal file
77
tests/test_waveform_query.py
Normal file
@ -0,0 +1,77 @@
|
||||
"""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
|
||||
Loading…
x
Reference in New Issue
Block a user