"""Shared fixtures for mcp-ltspice test suite. All fixtures produce synthetic data -- no LTspice or Wine required. """ import tempfile from pathlib import Path import numpy as np import pytest from mcp_ltspice.raw_parser import RawFile, Variable from mcp_ltspice.schematic import Component, Flag, Schematic, Text, Wire # --------------------------------------------------------------------------- # Time-domain fixtures # --------------------------------------------------------------------------- @pytest.fixture def sample_rate() -> float: """Default sample rate: 100 kHz.""" return 100_000.0 @pytest.fixture def duration() -> float: """Default signal duration: 10 ms (enough for 1 kHz signals).""" return 0.01 @pytest.fixture def time_array(sample_rate, duration) -> np.ndarray: """Uniformly spaced time array.""" n = int(sample_rate * duration) return np.linspace(0, duration, n, endpoint=False) @pytest.fixture def sine_1khz(time_array) -> np.ndarray: """1 kHz sine wave, 1 V peak.""" return np.sin(2 * np.pi * 1000 * time_array) @pytest.fixture def dc_signal() -> np.ndarray: """Constant 3.3 V DC signal (1000 samples).""" return np.full(1000, 3.3) @pytest.fixture def step_signal(time_array) -> np.ndarray: """Unit step at t = duration/2 with exponential rise (tau = duration/10).""" t = time_array mid = t[-1] / 2 tau = t[-1] / 10 sig = np.where(t >= mid, 1.0 - np.exp(-(t - mid) / tau), 0.0) return sig # --------------------------------------------------------------------------- # Frequency-domain / AC fixtures # --------------------------------------------------------------------------- @pytest.fixture def ac_frequency() -> np.ndarray: """Log-spaced frequency array: 1 Hz to 10 MHz, 500 points.""" return np.logspace(0, 7, 500) @pytest.fixture def lowpass_response(ac_frequency) -> np.ndarray: """First-order lowpass magnitude in dB (fc ~ 1 kHz).""" fc = 1000.0 mag = 1.0 / np.sqrt(1.0 + (ac_frequency / fc) ** 2) return 20.0 * np.log10(mag) @pytest.fixture def lowpass_complex(ac_frequency) -> np.ndarray: """First-order lowpass as complex transfer function (fc ~ 1 kHz).""" fc = 1000.0 s = 1j * ac_frequency / fc return 1.0 / (1.0 + s) # --------------------------------------------------------------------------- # Stepped / multi-run fixtures # --------------------------------------------------------------------------- @pytest.fixture def stepped_time() -> np.ndarray: """Time axis for 3 runs, each 0..1 ms with 100 points per run.""" runs = [] for _ in range(3): runs.append(np.linspace(0, 1e-3, 100, endpoint=False)) return np.concatenate(runs) @pytest.fixture def stepped_data(stepped_time) -> np.ndarray: """Two variables (time + V(out)) across 3 runs.""" n = len(stepped_time) data = np.zeros((2, n)) data[0] = stepped_time # Each run has a different amplitude sine wave for run_idx in range(3): start = run_idx * 100 end = start + 100 t_run = data[0, start:end] data[1, start:end] = (run_idx + 1) * np.sin(2 * np.pi * 1000 * t_run) return data # --------------------------------------------------------------------------- # Mock RawFile fixtures # --------------------------------------------------------------------------- @pytest.fixture def mock_rawfile(time_array, sine_1khz) -> RawFile: """A simple transient-analysis RawFile with time and V(out).""" n = len(time_array) data = np.zeros((2, n)) data[0] = time_array data[1] = sine_1khz return RawFile( title="Test Circuit", date="2026-01-01", plotname="Transient Analysis", flags=["real"], variables=[ Variable(0, "time", "time"), Variable(1, "V(out)", "voltage"), ], points=n, data=data, ) @pytest.fixture def mock_rawfile_stepped(stepped_data) -> RawFile: """A stepped RawFile with 3 runs.""" n = stepped_data.shape[1] return RawFile( title="Stepped Sim", date="2026-01-01", plotname="Transient Analysis", flags=["real", "stepped"], variables=[ Variable(0, "time", "time"), Variable(1, "V(out)", "voltage"), ], points=n, data=stepped_data, n_runs=3, run_boundaries=[0, 100, 200], ) @pytest.fixture def mock_rawfile_ac(ac_frequency, lowpass_complex) -> RawFile: """An AC-analysis RawFile with complex frequency-domain data.""" n = len(ac_frequency) data = np.zeros((2, n), dtype=np.complex128) data[0] = ac_frequency.astype(np.complex128) data[1] = lowpass_complex return RawFile( title="AC Sim", date="2026-01-01", plotname="AC Analysis", flags=["complex"], variables=[ Variable(0, "frequency", "frequency"), Variable(1, "V(out)", "voltage"), ], points=n, data=data, ) # --------------------------------------------------------------------------- # Netlist / Schematic string fixtures # --------------------------------------------------------------------------- @pytest.fixture def sample_netlist_str() -> str: """A basic SPICE netlist string for an RC lowpass.""" return ( "* RC Lowpass Filter\n" "V1 in 0 AC 1\n" "R1 in out 1k\n" "C1 out 0 100n\n" ".ac dec 100 1 1meg\n" ".backanno\n" ".end\n" ) @pytest.fixture def sample_asc_str() -> str: """A minimal .asc schematic string for an RC lowpass.""" return ( "Version 4\n" "SHEET 1 880 680\n" "WIRE 80 96 176 96\n" "WIRE 176 176 272 176\n" "FLAG 80 176 0\n" "FLAG 272 240 0\n" "FLAG 176 176 out\n" "SYMBOL voltage 80 80 R0\n" "SYMATTR InstName V1\n" "SYMATTR Value AC 1\n" "SYMBOL res 160 80 R0\n" "SYMATTR InstName R1\n" "SYMATTR Value 1k\n" "SYMBOL cap 256 176 R0\n" "SYMATTR InstName C1\n" "SYMATTR Value 100n\n" "TEXT 80 296 Left 2 !.ac dec 100 1 1meg\n" ) # --------------------------------------------------------------------------- # Touchstone fixtures # --------------------------------------------------------------------------- @pytest.fixture def sample_s2p_content() -> str: """Synthetic .s2p file content (MA format, GHz).""" lines = [ "! Two-port S-parameter data", "! Freq S11(mag) S11(ang) S21(mag) S21(ang) S12(mag) S12(ang) S22(mag) S22(ang)", "# GHZ S MA R 50", "1.0 0.5 -30 0.9 -10 0.1 170 0.4 -40", "2.0 0.6 -50 0.8 -20 0.12 160 0.5 -60", "3.0 0.7 -70 0.7 -30 0.15 150 0.55 -80", ] return "\n".join(lines) + "\n" @pytest.fixture def tmp_s2p_file(sample_s2p_content, tmp_path) -> Path: """Write synthetic .s2p content to a temp file and return path.""" p = tmp_path / "test.s2p" p.write_text(sample_s2p_content) return p # --------------------------------------------------------------------------- # Schematic object fixtures (for DRC and diff tests) # --------------------------------------------------------------------------- @pytest.fixture def valid_schematic() -> Schematic: """A schematic with ground, components, wires, and sim directive.""" sch = Schematic() sch.flags = [ Flag(80, 176, "0"), Flag(272, 240, "0"), Flag(176, 176, "out"), ] sch.wires = [ Wire(80, 96, 176, 96), Wire(176, 176, 272, 176), ] sch.components = [ Component(name="V1", symbol="voltage", x=80, y=80, rotation=0, mirror=False, attributes={"Value": "AC 1"}), Component(name="R1", symbol="res", x=160, y=80, rotation=0, mirror=False, attributes={"Value": "1k"}), Component(name="C1", symbol="cap", x=256, y=176, rotation=0, mirror=False, attributes={"Value": "100n"}), ] sch.texts = [ Text(80, 296, ".ac dec 100 1 1meg", type="spice"), ] return sch @pytest.fixture def schematic_no_ground() -> Schematic: """A schematic missing a ground node.""" sch = Schematic() sch.flags = [Flag(176, 176, "out")] sch.wires = [Wire(80, 96, 176, 96)] sch.components = [ Component(name="R1", symbol="res", x=160, y=80, rotation=0, mirror=False, attributes={"Value": "1k"}), ] sch.texts = [Text(80, 296, ".tran 10m", type="spice")] return sch @pytest.fixture def schematic_no_sim() -> Schematic: """A schematic missing a simulation directive.""" sch = Schematic() sch.flags = [Flag(80, 176, "0")] sch.wires = [Wire(80, 96, 176, 96)] sch.components = [ Component(name="R1", symbol="res", x=160, y=80, rotation=0, mirror=False, attributes={"Value": "1k"}), ] sch.texts = [] return sch @pytest.fixture def schematic_duplicate_names() -> Schematic: """A schematic with duplicate component names.""" sch = Schematic() sch.flags = [Flag(80, 176, "0")] sch.wires = [Wire(80, 96, 176, 96)] sch.components = [ Component(name="R1", symbol="res", x=160, y=80, rotation=0, mirror=False, attributes={"Value": "1k"}), Component(name="R1", symbol="res", x=320, y=80, rotation=0, mirror=False, attributes={"Value": "2.2k"}), ] sch.texts = [Text(80, 296, ".tran 10m", type="spice")] return sch