You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
817 lines
23 KiB
817 lines
23 KiB
"""Tests for the netlist pre-processor (LTspice -> ngspice translation)."""
|
|
|
|
import pytest
|
|
from pyngspice.netlist import (
|
|
preprocess_netlist, _process_inductor_rser,
|
|
_collect_component_names, _is_savecurrents_option,
|
|
_process_capacitor_probe, _parse_save_cap_currents,
|
|
)
|
|
|
|
|
|
class TestInductorRser:
|
|
"""Test Rser= parameter extraction from inductor lines."""
|
|
|
|
def test_basic_rser(self):
|
|
netlist = """Tesla Coil
|
|
L1 p1 0 8.5u Rser=0.012
|
|
C1 p1 0 100n
|
|
.tran 1u 100u
|
|
.end
|
|
"""
|
|
result = preprocess_netlist(netlist)
|
|
assert "L1 p1 _rser_L1 8.5u" in result
|
|
assert "R_L1_ser _rser_L1 0 0.012" in result
|
|
assert "Rser" not in result
|
|
|
|
def test_rser_with_suffix(self):
|
|
"""Rser value with engineering suffix (12m = 12 milliohms)."""
|
|
netlist = """Test
|
|
L1 a b 10u Rser=12m
|
|
.end
|
|
"""
|
|
result = preprocess_netlist(netlist)
|
|
assert "L1 a _rser_L1 10u" in result
|
|
assert "R_L1_ser _rser_L1 b 12m" in result
|
|
|
|
def test_rser_scientific_notation(self):
|
|
"""Rser value in scientific notation."""
|
|
netlist = """Test
|
|
L1 a b 1.5e-6 Rser=1.2e-3
|
|
.end
|
|
"""
|
|
result = preprocess_netlist(netlist)
|
|
assert "R_L1_ser" in result
|
|
assert "1.2e-3" in result
|
|
|
|
def test_multiple_inductors(self):
|
|
"""Multiple inductors with Rser= in same netlist."""
|
|
netlist = """Multi Inductor
|
|
L1 a 0 10u Rser=0.01
|
|
L2 b 0 20u Rser=0.02
|
|
L3 c 0 30u Rser=0.03
|
|
.end
|
|
"""
|
|
result = preprocess_netlist(netlist)
|
|
assert "R_L1_ser" in result
|
|
assert "R_L2_ser" in result
|
|
assert "R_L3_ser" in result
|
|
assert "_rser_L1" in result
|
|
assert "_rser_L2" in result
|
|
assert "_rser_L3" in result
|
|
|
|
def test_named_inductor(self):
|
|
"""Inductor with alphanumeric name (Lprimary)."""
|
|
netlist = """Test
|
|
Lprimary drive tank 8.5u Rser=0.012
|
|
.end
|
|
"""
|
|
result = preprocess_netlist(netlist)
|
|
assert "Lprimary drive _rser_Lprimary 8.5u" in result
|
|
assert "R_Lprimary_ser _rser_Lprimary tank 0.012" in result
|
|
|
|
def test_inductor_without_rser_unchanged(self):
|
|
"""Inductors without Rser= should pass through unchanged."""
|
|
netlist = """Test
|
|
L1 a b 10u
|
|
.end
|
|
"""
|
|
result = preprocess_netlist(netlist)
|
|
assert "L1 a b 10u" in result
|
|
assert "R_L1" not in result
|
|
|
|
def test_rser_with_spaces(self):
|
|
"""Rser with spaces around equals sign."""
|
|
netlist = """Test
|
|
L1 a b 10u Rser = 0.05
|
|
.end
|
|
"""
|
|
result = preprocess_netlist(netlist)
|
|
assert "R_L1_ser" in result
|
|
assert "0.05" in result
|
|
|
|
def test_resistors_before_end(self):
|
|
"""Extra resistor lines should be inserted before .end."""
|
|
netlist = """Test
|
|
L1 a 0 10u Rser=0.01
|
|
.tran 1u 100u
|
|
.end
|
|
"""
|
|
result = preprocess_netlist(netlist)
|
|
lines = result.splitlines()
|
|
# Find positions
|
|
rser_idx = next(i for i, l in enumerate(lines) if "R_L1_ser" in l)
|
|
end_idx = next(i for i, l in enumerate(lines) if l.strip().lower() == '.end')
|
|
assert rser_idx < end_idx
|
|
|
|
|
|
class TestBackanno:
|
|
"""Test .backanno directive stripping."""
|
|
|
|
def test_backanno_removed(self):
|
|
netlist = """Test
|
|
V1 in 0 1
|
|
.backanno
|
|
.end
|
|
"""
|
|
result = preprocess_netlist(netlist)
|
|
assert ".backanno" not in result
|
|
assert ".end" in result
|
|
assert "V1 in 0 1" in result
|
|
|
|
def test_backanno_case_insensitive(self):
|
|
netlist = """Test
|
|
V1 in 0 1
|
|
.BACKANNO
|
|
.end
|
|
"""
|
|
result = preprocess_netlist(netlist)
|
|
assert ".BACKANNO" not in result
|
|
|
|
|
|
class TestPassthrough:
|
|
"""Test that standard SPICE constructs pass through unchanged."""
|
|
|
|
def test_simple_netlist_unchanged(self):
|
|
netlist = """Simple RC
|
|
V1 in 0 DC 1
|
|
R1 in out 1k
|
|
C1 out 0 1u
|
|
.tran 0.1m 10m
|
|
.end"""
|
|
result = preprocess_netlist(netlist)
|
|
assert result == netlist
|
|
|
|
def test_behavioral_source_unchanged(self):
|
|
"""Behavioral sources should pass through."""
|
|
netlist = """Test
|
|
B2 N015 0 V=I(L1)*{iscale}
|
|
.end
|
|
"""
|
|
result = preprocess_netlist(netlist)
|
|
assert "B2 N015 0 V=I(L1)*{iscale}" in result
|
|
|
|
def test_subcircuit_unchanged(self):
|
|
"""Subcircuit definitions should pass through."""
|
|
netlist = """Test
|
|
.subckt mycomp in out
|
|
R1 in out 1k
|
|
.ends mycomp
|
|
.end
|
|
"""
|
|
result = preprocess_netlist(netlist)
|
|
assert ".subckt mycomp in out" in result
|
|
assert ".ends mycomp" in result
|
|
|
|
def test_continuation_lines(self):
|
|
"""Continuation lines (starting with +) should pass through."""
|
|
netlist = """Test
|
|
V1 in 0 PULSE(0 5 0 1n 1n
|
|
+ 500n 1u)
|
|
.end
|
|
"""
|
|
result = preprocess_netlist(netlist)
|
|
assert "+ 500n 1u)" in result
|
|
|
|
def test_empty_lines_preserved(self):
|
|
"""Empty lines should be preserved."""
|
|
netlist = """Test
|
|
|
|
V1 in 0 1
|
|
|
|
R1 in out 1k
|
|
|
|
.end"""
|
|
result = preprocess_netlist(netlist)
|
|
assert "\n\n" in result
|
|
|
|
|
|
class TestProcessInductorRser:
|
|
"""Test the internal _process_inductor_rser function directly."""
|
|
|
|
def test_basic(self):
|
|
line, extras = _process_inductor_rser("L1 p1 0 8.5u Rser=0.012")
|
|
assert line == "L1 p1 _rser_L1 8.5u"
|
|
assert len(extras) == 1
|
|
assert extras[0] == "R_L1_ser _rser_L1 0 0.012"
|
|
|
|
def test_no_rser_returns_original(self):
|
|
line, extras = _process_inductor_rser("L1 p1 0 8.5u")
|
|
assert line == "L1 p1 0 8.5u"
|
|
assert extras == []
|
|
|
|
def test_preserves_remaining_params(self):
|
|
"""Parameters after Rser= that are not LTspice-specific should be kept."""
|
|
line, extras = _process_inductor_rser("L1 a b 10u Rser=0.01 IC=0.5")
|
|
assert "IC=0.5" in line
|
|
assert len(extras) == 1
|
|
|
|
|
|
class TestSaveCurrents:
|
|
"""Test .options savecurrents expansion into explicit .save directives."""
|
|
|
|
def test_savecurrents_expanded(self):
|
|
"""`.options savecurrents` should be removed and .save directives added."""
|
|
netlist = """Tesla Coil
|
|
V1 vin 0 AC 1
|
|
C1 vin out 0.03u
|
|
R1 out 0 50
|
|
.options savecurrents
|
|
.ac dec 100 1k 3meg
|
|
.end
|
|
"""
|
|
result = preprocess_netlist(netlist)
|
|
assert ".options savecurrents" not in result.lower()
|
|
assert ".save all" in result
|
|
assert "i(V1)" in result
|
|
assert "i(C1)" in result
|
|
assert "i(R1)" in result
|
|
|
|
def test_savecurrents_includes_all_components(self):
|
|
"""All R, C, L, V components should get i(name) saves."""
|
|
netlist = """Test
|
|
V1 in 0 DC 1
|
|
R1 in mid 1k
|
|
L1 mid out 10u
|
|
C1 out 0 1u
|
|
.options savecurrents
|
|
.tran 1u 100u
|
|
.end
|
|
"""
|
|
result = preprocess_netlist(netlist)
|
|
assert "i(V1)" in result
|
|
assert "i(R1)" in result
|
|
assert "i(L1)" in result
|
|
assert "i(C1)" in result
|
|
|
|
def test_savecurrents_skips_K_elements(self):
|
|
"""K (coupling) elements should NOT get i(K1) saves."""
|
|
netlist = """Test
|
|
V1 in 0 AC 1
|
|
L1 in 0 10u
|
|
L2 0 out 20u
|
|
K1 L1 L2 0.5
|
|
.options savecurrents
|
|
.ac dec 10 1k 1meg
|
|
.end
|
|
"""
|
|
result = preprocess_netlist(netlist)
|
|
assert "i(K1)" not in result
|
|
assert "i(V1)" in result
|
|
assert "i(L1)" in result
|
|
assert "i(L2)" in result
|
|
|
|
def test_savecurrents_includes_rser_resistors(self):
|
|
"""Expanded Rser resistors should be included in .save directives."""
|
|
netlist = """Test
|
|
V1 in 0 AC 1
|
|
L1 in 0 10u Rser=0.01
|
|
.options savecurrents
|
|
.ac dec 10 1k 1meg
|
|
.end
|
|
"""
|
|
result = preprocess_netlist(netlist)
|
|
assert "i(R_L1_ser)" in result
|
|
assert "i(L1)" in result
|
|
assert "i(V1)" in result
|
|
|
|
def test_savecurrents_case_insensitive(self):
|
|
""".OPTIONS SAVECURRENTS should be handled."""
|
|
netlist = """Test
|
|
V1 in 0 1
|
|
R1 in 0 1k
|
|
.OPTIONS SAVECURRENTS
|
|
.op
|
|
.end
|
|
"""
|
|
result = preprocess_netlist(netlist)
|
|
assert ".OPTIONS SAVECURRENTS" not in result
|
|
assert ".save all" in result
|
|
assert "i(V1)" in result
|
|
|
|
def test_no_savecurrents_unchanged(self):
|
|
"""Netlists without .options savecurrents should not get .save directives."""
|
|
netlist = """Test
|
|
V1 in 0 DC 1
|
|
R1 in out 1k
|
|
C1 out 0 1u
|
|
.tran 0.1m 10m
|
|
.end"""
|
|
result = preprocess_netlist(netlist)
|
|
assert ".save" not in result.lower()
|
|
assert result == netlist
|
|
|
|
def test_savecurrents_with_rser_full_tesla_coil(self):
|
|
"""Full Tesla coil netlist: Rser + savecurrents + K coupling + probes."""
|
|
netlist = """Tesla Coil AC Analysis
|
|
V1 vin 0 AC 1
|
|
C_mmc vin p1 0.03u
|
|
L1 p1 0 10.927u Rser=0.001
|
|
L2 0 top 15.987m Rser=0.001
|
|
C_topload top 0 13.822p
|
|
K1 L1 L2 0.3204
|
|
.options savecurrents
|
|
.ac dec 100 1k 3meg
|
|
.end
|
|
"""
|
|
result = preprocess_netlist(netlist)
|
|
# .options savecurrents removed
|
|
assert ".options savecurrents" not in result.lower()
|
|
# Rser expanded
|
|
assert "R_L1_ser" in result
|
|
assert "R_L2_ser" in result
|
|
# Capacitor probes inserted
|
|
assert "V_probe_C_mmc" in result
|
|
assert "V_probe_C_topload" in result
|
|
# .save directives present
|
|
assert ".save all" in result
|
|
assert "i(V1)" in result
|
|
assert "i(L1)" in result
|
|
assert "i(L2)" in result
|
|
assert "i(R_L1_ser)" in result
|
|
assert "i(R_L2_ser)" in result
|
|
assert "i(V_probe_C_mmc)" in result
|
|
assert "i(V_probe_C_topload)" in result
|
|
# K element NOT in saves
|
|
assert "i(K1)" not in result
|
|
# .end still present
|
|
assert ".end" in result
|
|
|
|
def test_save_directives_before_end(self):
|
|
""".save directives should appear before .end."""
|
|
netlist = """Test
|
|
V1 in 0 1
|
|
R1 in 0 1k
|
|
.options savecurrents
|
|
.op
|
|
.end
|
|
"""
|
|
result = preprocess_netlist(netlist)
|
|
lines = result.splitlines()
|
|
save_idx = next(i for i, l in enumerate(lines) if '.save all' in l)
|
|
end_idx = next(i for i, l in enumerate(lines) if l.strip().lower() == '.end')
|
|
assert save_idx < end_idx
|
|
|
|
|
|
class TestIsSaveCurrentsOption:
|
|
"""Test the _is_savecurrents_option helper."""
|
|
|
|
def test_standard(self):
|
|
assert _is_savecurrents_option(".options savecurrents")
|
|
|
|
def test_uppercase(self):
|
|
assert _is_savecurrents_option(".OPTIONS SAVECURRENTS")
|
|
|
|
def test_mixed_case(self):
|
|
assert _is_savecurrents_option(".Options SaveCurrents")
|
|
|
|
def test_singular_option(self):
|
|
assert _is_savecurrents_option(".option savecurrents")
|
|
|
|
def test_extra_spaces(self):
|
|
assert _is_savecurrents_option(".options savecurrents")
|
|
|
|
def test_not_savecurrents(self):
|
|
assert not _is_savecurrents_option(".options reltol=1e-4")
|
|
|
|
def test_not_options(self):
|
|
assert not _is_savecurrents_option("V1 in 0 1")
|
|
|
|
|
|
class TestCollectComponentNames:
|
|
"""Test the _collect_component_names helper."""
|
|
|
|
def test_basic(self):
|
|
lines = ["Title", "V1 in 0 1", "R1 in out 1k", ".tran 1u 10u", ".end"]
|
|
names = _collect_component_names(lines)
|
|
assert names == ["V1", "R1"]
|
|
|
|
def test_skips_comments(self):
|
|
lines = ["* Comment", "V1 in 0 1", "* Another comment", ".end"]
|
|
names = _collect_component_names(lines)
|
|
assert names == ["V1"]
|
|
|
|
def test_skips_K_elements(self):
|
|
lines = ["V1 in 0 1", "L1 in 0 10u", "K1 L1 L2 0.5", ".end"]
|
|
names = _collect_component_names(lines)
|
|
assert "K1" not in names
|
|
assert "V1" in names
|
|
assert "L1" in names
|
|
|
|
def test_skips_directives(self):
|
|
lines = [".tran 1u 10u", "V1 in 0 1", ".end"]
|
|
names = _collect_component_names(lines)
|
|
assert names == ["V1"]
|
|
|
|
|
|
class TestCapacitorProbes:
|
|
"""Test capacitor current probe insertion (0V voltage sources)."""
|
|
|
|
def test_probe_inserted(self):
|
|
"""Basic capacitor should get a 0V probe voltage source."""
|
|
netlist = """Test
|
|
V1 in 0 AC 1
|
|
C1 in out 1u
|
|
.options savecurrents
|
|
.ac dec 10 1k 1meg
|
|
.end
|
|
"""
|
|
result = preprocess_netlist(netlist)
|
|
assert 'C1 _probe_C1 out 1u' in result
|
|
assert 'V_probe_C1 in _probe_C1 0' in result
|
|
|
|
def test_probe_multiple_caps(self):
|
|
"""Multiple capacitors should each get separate probes."""
|
|
netlist = """Test
|
|
V1 in 0 AC 1
|
|
C1 in mid 1u
|
|
C2 mid out 2u
|
|
.options savecurrents
|
|
.ac dec 10 1k 1meg
|
|
.end
|
|
"""
|
|
result = preprocess_netlist(netlist)
|
|
assert 'V_probe_C1' in result
|
|
assert 'V_probe_C2' in result
|
|
assert '_probe_C1' in result
|
|
assert '_probe_C2' in result
|
|
|
|
def test_probe_preserves_params(self):
|
|
"""Capacitor parameters (IC=, etc.) should be preserved."""
|
|
netlist = """Test
|
|
V1 in 0 1
|
|
C1 in out 1u IC=0.5
|
|
.options savecurrents
|
|
.tran 1u 100u
|
|
.end
|
|
"""
|
|
result = preprocess_netlist(netlist)
|
|
assert 'C1 _probe_C1 out 1u IC=0.5' in result
|
|
|
|
def test_probe_only_with_savecurrents(self):
|
|
"""No probes should be inserted without .options savecurrents."""
|
|
netlist = """Test
|
|
V1 in 0 AC 1
|
|
C1 in out 1u
|
|
.ac dec 10 1k 1meg
|
|
.end
|
|
"""
|
|
result = preprocess_netlist(netlist)
|
|
assert '_probe_' not in result
|
|
assert 'V_probe_' not in result
|
|
|
|
def test_probe_with_rser_combined(self):
|
|
"""Full Tesla coil: Rser + probes + savecurrents."""
|
|
netlist = """Tesla Coil AC Analysis
|
|
V1 vin 0 AC 1
|
|
C_mmc vin p1 0.03u
|
|
L1 p1 0 10.927u Rser=0.001
|
|
L2 0 top 15.987m Rser=0.001
|
|
C_topload top 0 13.822p
|
|
K1 L1 L2 0.3204
|
|
.options savecurrents
|
|
.ac dec 100 1k 3meg
|
|
.end
|
|
"""
|
|
result = preprocess_netlist(netlist)
|
|
# Rser expanded
|
|
assert 'R_L1_ser' in result
|
|
assert 'R_L2_ser' in result
|
|
# Capacitor probes inserted
|
|
assert 'V_probe_C_mmc' in result
|
|
assert 'V_probe_C_topload' in result
|
|
assert 'C_mmc _probe_C_mmc p1 0.03u' in result
|
|
assert 'C_topload _probe_C_topload 0 13.822p' in result
|
|
# Probe V sources in .save
|
|
assert 'i(V_probe_C_mmc)' in result
|
|
assert 'i(V_probe_C_topload)' in result
|
|
# K element NOT in saves
|
|
assert 'i(K1)' not in result
|
|
|
|
def test_probe_naming_convention(self):
|
|
"""Probe names should follow V_probe_Cname convention."""
|
|
netlist = """Test
|
|
V1 in 0 1
|
|
C_mmc in 0 1u
|
|
.options savecurrents
|
|
.op
|
|
.end
|
|
"""
|
|
result = preprocess_netlist(netlist)
|
|
assert '_probe_C_mmc' in result
|
|
assert 'V_probe_C_mmc' in result
|
|
assert 'V_probe_C_mmc in _probe_C_mmc 0' in result
|
|
|
|
def test_probe_not_inside_subckt(self):
|
|
"""Capacitors inside .subckt should NOT get probes."""
|
|
netlist = """Test
|
|
V1 in 0 AC 1
|
|
C1 in out 1u
|
|
.subckt mycomp a b
|
|
C2 a b 10u
|
|
.ends mycomp
|
|
.options savecurrents
|
|
.ac dec 10 1k 1meg
|
|
.end
|
|
"""
|
|
result = preprocess_netlist(netlist)
|
|
assert 'V_probe_C1' in result # top-level probed
|
|
assert 'V_probe_C2' not in result # subcircuit NOT probed
|
|
|
|
def test_probe_before_end(self):
|
|
"""Probe V sources should be inserted before .end."""
|
|
netlist = """Test
|
|
V1 in 0 1
|
|
C1 in 0 1u
|
|
.options savecurrents
|
|
.op
|
|
.end
|
|
"""
|
|
result = preprocess_netlist(netlist)
|
|
lines = result.splitlines()
|
|
probe_idx = next(i for i, l in enumerate(lines) if 'V_probe_C1' in l)
|
|
end_idx = next(i for i, l in enumerate(lines) if l.strip().lower() == '.end')
|
|
assert probe_idx < end_idx
|
|
|
|
|
|
class TestProcessCapacitorProbe:
|
|
"""Test the _process_capacitor_probe helper."""
|
|
|
|
def test_basic(self):
|
|
mod, probe = _process_capacitor_probe("C1 in out 1u")
|
|
assert mod == "C1 _probe_C1 out 1u"
|
|
assert probe == "V_probe_C1 in _probe_C1 0"
|
|
|
|
def test_named_cap(self):
|
|
mod, probe = _process_capacitor_probe("C_mmc vin p1 0.03u")
|
|
assert mod == "C_mmc _probe_C_mmc p1 0.03u"
|
|
assert probe == "V_probe_C_mmc vin _probe_C_mmc 0"
|
|
|
|
def test_preserves_params(self):
|
|
mod, probe = _process_capacitor_probe("C1 a b 10u IC=0.5")
|
|
assert "IC=0.5" in mod
|
|
assert "_probe_C1" in mod
|
|
|
|
def test_too_few_tokens(self):
|
|
"""Lines with fewer than 4 tokens should be returned unchanged."""
|
|
mod, probe = _process_capacitor_probe("C1 a")
|
|
assert mod == "C1 a"
|
|
assert probe == ""
|
|
|
|
|
|
class TestParseSaveCapCurrents:
|
|
"""Test the _parse_save_cap_currents helper."""
|
|
|
|
def test_single_cap(self):
|
|
result = _parse_save_cap_currents(".save i(C_mmc)")
|
|
assert result == ["C_mmc"]
|
|
|
|
def test_multiple_caps(self):
|
|
result = _parse_save_cap_currents(".save i(C_mmc) i(C_topload)")
|
|
assert result == ["C_mmc", "C_topload"]
|
|
|
|
def test_non_cap_ignored(self):
|
|
result = _parse_save_cap_currents(".save i(V1) i(R1) i(L1)")
|
|
assert result == []
|
|
|
|
def test_mixed(self):
|
|
result = _parse_save_cap_currents(".save i(C_mmc) i(V1) i(C_topload)")
|
|
assert result == ["C_mmc", "C_topload"]
|
|
|
|
def test_voltage_saves_ignored(self):
|
|
result = _parse_save_cap_currents(".save v(out) v(in)")
|
|
assert result == []
|
|
|
|
def test_not_save_directive(self):
|
|
result = _parse_save_cap_currents("V1 in 0 1")
|
|
assert result == []
|
|
|
|
def test_case_insensitive(self):
|
|
result = _parse_save_cap_currents(".SAVE I(C_mmc)")
|
|
assert result == ["C_mmc"]
|
|
|
|
def test_save_all(self):
|
|
result = _parse_save_cap_currents(".save all")
|
|
assert result == []
|
|
|
|
|
|
class TestTargetedCapacitorProbes:
|
|
"""Test targeted capacitor probing via .save i(C_name) directives."""
|
|
|
|
def test_single_targeted_probe(self):
|
|
"""Only the named capacitor should get a probe."""
|
|
netlist = """Tesla Coil
|
|
V1 vin 0 AC 1
|
|
C_mmc vin p1 0.03u
|
|
C_topload p1 0 13.822p
|
|
.save i(C_mmc)
|
|
.ac dec 100 1k 3meg
|
|
.end
|
|
"""
|
|
result = preprocess_netlist(netlist)
|
|
# C_mmc should be probed
|
|
assert 'V_probe_C_mmc' in result
|
|
assert 'C_mmc _probe_C_mmc p1 0.03u' in result
|
|
# C_topload should NOT be probed
|
|
assert 'V_probe_C_topload' not in result
|
|
assert '_probe_C_topload' not in result
|
|
# .save should reference probe, not original cap
|
|
assert 'i(V_probe_C_mmc)' in result
|
|
# .save all should be present
|
|
assert '.save all' in result
|
|
|
|
def test_multiple_targeted_probes_same_line(self):
|
|
"""Multiple caps on one .save line should all get probes."""
|
|
netlist = """Test
|
|
V1 vin 0 AC 1
|
|
C_mmc vin p1 0.03u
|
|
C_topload p1 0 13.822p
|
|
C_other p1 0 10p
|
|
.save i(C_mmc) i(C_topload)
|
|
.ac dec 100 1k 3meg
|
|
.end
|
|
"""
|
|
result = preprocess_netlist(netlist)
|
|
assert 'V_probe_C_mmc' in result
|
|
assert 'V_probe_C_topload' in result
|
|
assert 'V_probe_C_other' not in result
|
|
assert 'i(V_probe_C_mmc)' in result
|
|
assert 'i(V_probe_C_topload)' in result
|
|
|
|
def test_multiple_save_lines(self):
|
|
"""Caps on separate .save lines should all get probes."""
|
|
netlist = """Test
|
|
V1 vin 0 AC 1
|
|
C_mmc vin p1 0.03u
|
|
C_topload p1 0 13.822p
|
|
.save i(C_mmc)
|
|
.save i(C_topload)
|
|
.ac dec 100 1k 3meg
|
|
.end
|
|
"""
|
|
result = preprocess_netlist(netlist)
|
|
assert 'V_probe_C_mmc' in result
|
|
assert 'V_probe_C_topload' in result
|
|
|
|
def test_non_capacitor_save_unchanged(self):
|
|
""".save i(V1) and .save i(L1) should pass through without probes."""
|
|
netlist = """Test
|
|
V1 vin 0 AC 1
|
|
L1 vin 0 10u
|
|
C1 vin 0 1u
|
|
.save i(V1) i(L1)
|
|
.ac dec 100 1k 3meg
|
|
.end
|
|
"""
|
|
result = preprocess_netlist(netlist)
|
|
# No probes inserted (no capacitor current saves)
|
|
assert 'V_probe_' not in result
|
|
assert '_probe_' not in result
|
|
# Original saves preserved
|
|
assert 'i(V1)' in result
|
|
assert 'i(L1)' in result
|
|
|
|
def test_mixed_save_cap_and_non_cap(self):
|
|
""".save with both cap and non-cap currents."""
|
|
netlist = """Test
|
|
V1 vin 0 AC 1
|
|
C_mmc vin p1 0.03u
|
|
L1 p1 0 10u
|
|
.save i(C_mmc) i(V1) i(L1)
|
|
.ac dec 100 1k 3meg
|
|
.end
|
|
"""
|
|
result = preprocess_netlist(netlist)
|
|
# Cap probed
|
|
assert 'V_probe_C_mmc' in result
|
|
# .save rewritten for cap, others preserved
|
|
assert 'i(V_probe_C_mmc)' in result
|
|
assert 'i(V1)' in result
|
|
assert 'i(L1)' in result
|
|
|
|
def test_savecurrents_overrides_targeted(self):
|
|
"""When both .options savecurrents and .save i(C_name) exist,
|
|
savecurrents takes precedence (probes everything)."""
|
|
netlist = """Test
|
|
V1 vin 0 AC 1
|
|
C_mmc vin p1 0.03u
|
|
C_topload p1 0 13.822p
|
|
.save i(C_mmc)
|
|
.options savecurrents
|
|
.ac dec 100 1k 3meg
|
|
.end
|
|
"""
|
|
result = preprocess_netlist(netlist)
|
|
# Both caps probed (savecurrents probes everything)
|
|
assert 'V_probe_C_mmc' in result
|
|
assert 'V_probe_C_topload' in result
|
|
|
|
def test_no_save_no_probes(self):
|
|
"""Without .save or .options savecurrents, no probes should be inserted."""
|
|
netlist = """Test
|
|
V1 vin 0 AC 1
|
|
C1 vin 0 1u
|
|
.ac dec 100 1k 3meg
|
|
.end
|
|
"""
|
|
result = preprocess_netlist(netlist)
|
|
assert 'V_probe_' not in result
|
|
assert '_probe_' not in result
|
|
|
|
def test_targeted_probe_not_inside_subckt(self):
|
|
"""Capacitors inside .subckt should NOT get probes even if targeted."""
|
|
netlist = """Test
|
|
V1 in 0 AC 1
|
|
C1 in out 1u
|
|
.subckt mycomp a b
|
|
C1 a b 10u
|
|
.ends mycomp
|
|
.save i(C1)
|
|
.ac dec 10 1k 1meg
|
|
.end
|
|
"""
|
|
result = preprocess_netlist(netlist)
|
|
# Top-level C1 probed
|
|
assert 'V_probe_C1' in result
|
|
# Only one probe V-source line (not the subckt one)
|
|
probe_vsource_lines = [l for l in result.splitlines()
|
|
if l.strip().startswith('V_probe_C1')]
|
|
assert len(probe_vsource_lines) == 1
|
|
|
|
def test_save_voltage_with_cap_current(self):
|
|
""".save v(out) alongside .save i(C1) should both work."""
|
|
netlist = """Test
|
|
V1 in 0 AC 1
|
|
C1 in out 1u
|
|
R1 out 0 1k
|
|
.save v(out) i(C1)
|
|
.ac dec 10 1k 1meg
|
|
.end
|
|
"""
|
|
result = preprocess_netlist(netlist)
|
|
assert 'V_probe_C1' in result
|
|
assert 'v(out)' in result
|
|
assert 'i(V_probe_C1)' in result
|
|
|
|
def test_targeted_probe_case_insensitive(self):
|
|
""".save I(c_mmc) should match C_mmc component (case-insensitive)."""
|
|
netlist = """Test
|
|
V1 vin 0 AC 1
|
|
C_mmc vin p1 0.03u
|
|
.save I(c_mmc)
|
|
.ac dec 100 1k 3meg
|
|
.end
|
|
"""
|
|
result = preprocess_netlist(netlist)
|
|
# Should probe C_mmc (case-insensitive match)
|
|
assert 'V_probe_C_mmc' in result
|
|
|
|
def test_save_all_not_duplicated(self):
|
|
"""If user already has .save all, don't add another."""
|
|
netlist = """Test
|
|
V1 in 0 AC 1
|
|
C1 in out 1u
|
|
.save all
|
|
.save i(C1)
|
|
.ac dec 10 1k 1meg
|
|
.end
|
|
"""
|
|
result = preprocess_netlist(netlist)
|
|
# Should have exactly one .save all
|
|
save_all_count = sum(1 for line in result.splitlines()
|
|
if line.strip().lower().split() == ['.save', 'all'])
|
|
assert save_all_count == 1
|
|
|
|
def test_targeted_with_rser_combined(self):
|
|
"""Targeted probing combined with inductor Rser expansion."""
|
|
netlist = """Tesla Coil AC Analysis
|
|
V1 vin 0 AC 1
|
|
C_mmc vin p1 0.03u
|
|
L1 p1 0 10.927u Rser=0.001
|
|
C_topload top 0 13.822p
|
|
.save i(C_mmc)
|
|
.ac dec 100 1k 3meg
|
|
.end
|
|
"""
|
|
result = preprocess_netlist(netlist)
|
|
# Rser expanded
|
|
assert 'R_L1_ser' in result
|
|
# Only C_mmc probed, not C_topload
|
|
assert 'V_probe_C_mmc' in result
|
|
assert 'V_probe_C_topload' not in result
|
|
# .save rewritten
|
|
assert 'i(V_probe_C_mmc)' in result
|
|
|
|
def test_probes_before_end(self):
|
|
"""Targeted probe V-sources should appear before .end."""
|
|
netlist = """Test
|
|
V1 in 0 AC 1
|
|
C1 in out 1u
|
|
.save i(C1)
|
|
.ac dec 10 1k 1meg
|
|
.end
|
|
"""
|
|
result = preprocess_netlist(netlist)
|
|
lines = result.splitlines()
|
|
end_idx = next(i for i, l in enumerate(lines) if l.strip().lower() == '.end')
|
|
probe_idx = next(i for i, l in enumerate(lines) if 'V_probe_C1' in l and l.strip().startswith('V_probe'))
|
|
assert probe_idx < end_idx
|