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

"""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