mcltspice/tests/test_raw_parser.py
Ryan Malloy 9b418a06c5 Add SVG plotting, circuit tuning, 5 new templates, fix prompts
- SVG waveform plots (svg_plot.py): pure-SVG timeseries, Bode, spectrum
  generation with plot_waveform MCP tool — no matplotlib dependency
- Circuit tuning tool (tune_circuit): single-shot simulate → measure →
  compare targets → suggest adjustments workflow for iterative design
- 5 new circuit templates: Sallen-Key lowpass, boost converter,
  instrumentation amplifier, current mirror, transimpedance amplifier
  (both netlist and .asc schematic generators, 15 total templates)
- Fix all 6 prompts to return list[Message] per FastMCP 2.x spec
- Add ltspice://templates and ltspice://template/{name} resources
- Add troubleshoot_simulation prompt
- Integration tests for RC lowpass and non-inverting amp (2/4 pass;
  CE amp and Colpitts oscillator have pre-existing schematic bugs)
- 360 unit tests passing, ruff clean
2026-02-11 05:13:50 -07:00

102 lines
3.8 KiB
Python

"""Tests for raw_parser module: run boundaries, variable lookup, run slicing."""
import numpy as np
import pytest
from mcp_ltspice.raw_parser import _detect_run_boundaries
class TestDetectRunBoundaries:
def test_single_run(self):
"""Monotonically increasing time -> single run starting at 0."""
x = np.linspace(0, 1e-3, 500)
boundaries = _detect_run_boundaries(x)
assert boundaries == [0]
def test_multi_run(self):
"""Three runs: time resets to near-zero at each boundary."""
run1 = np.linspace(0, 1e-3, 100)
run2 = np.linspace(0, 1e-3, 100)
run3 = np.linspace(0, 1e-3, 100)
x = np.concatenate([run1, run2, run3])
boundaries = _detect_run_boundaries(x)
assert len(boundaries) == 3
assert boundaries[0] == 0
assert boundaries[1] == 100
assert boundaries[2] == 200
def test_complex_ac(self):
"""AC analysis with complex frequency axis that resets."""
run1 = np.logspace(0, 6, 50).astype(np.complex128)
run2 = np.logspace(0, 6, 50).astype(np.complex128)
x = np.concatenate([run1, run2])
boundaries = _detect_run_boundaries(x)
assert len(boundaries) == 2
assert boundaries[0] == 0
assert boundaries[1] == 50
def test_single_point(self):
"""Single data point -> one run."""
boundaries = _detect_run_boundaries(np.array([0.0]))
assert boundaries == [0]
class TestRawFileGetVariable:
def test_exact_match(self, mock_rawfile):
"""Exact name match returns correct data."""
result = mock_rawfile.get_variable("V(out)")
assert result is not None
assert len(result) == mock_rawfile.points
def test_case_insensitive(self, mock_rawfile):
"""Variable lookup is case-insensitive (partial match)."""
result = mock_rawfile.get_variable("v(out)")
assert result is not None
def test_partial_match(self, mock_rawfile):
"""Substring match should work: 'out' matches 'V(out)'."""
result = mock_rawfile.get_variable("out")
assert result is not None
def test_missing_variable(self, mock_rawfile):
"""Non-existent variable returns None."""
result = mock_rawfile.get_variable("V(nonexistent)")
assert result is None
def test_get_time(self, mock_rawfile):
result = mock_rawfile.get_time()
assert result is not None
assert len(result) == mock_rawfile.points
class TestRawFileRunData:
def test_get_run_data_slicing(self, mock_rawfile_stepped):
"""Extracting a single run produces correct point count."""
run0 = mock_rawfile_stepped.get_run_data(0)
assert run0.points == 100
assert run0.n_runs == 1
assert run0.is_stepped is False
def test_get_run_data_values(self, mock_rawfile_stepped):
"""Each run has the expected amplitude scaling."""
for i in range(3):
run = mock_rawfile_stepped.get_run_data(i)
sig = run.get_variable("V(out)")
# Peak amplitude should be approximately (i+1)
assert float(np.max(np.abs(sig))) == pytest.approx(i + 1, rel=0.1)
def test_is_stepped(self, mock_rawfile_stepped, mock_rawfile):
assert mock_rawfile_stepped.is_stepped is True
assert mock_rawfile.is_stepped is False
def test_get_variable_with_run(self, mock_rawfile_stepped):
"""get_variable with run= parameter slices correctly."""
v_run1 = mock_rawfile_stepped.get_variable("V(out)", run=1)
assert v_run1 is not None
assert len(v_run1) == 100
def test_non_stepped_get_run_data(self, mock_rawfile):
"""Getting run data from non-stepped file returns self."""
run = mock_rawfile.get_run_data(0)
assert run.points == mock_rawfile.points