Skip to content

Commit db77ad4

Browse files
Add CircuitBuilder, StandardValues, and NetlistLinter for enhanced SPICE netlist handling
- Implemented CircuitBuilder for programmatic construction of SPICE netlists, allowing for easy addition and modification of circuit components, sources, and analyses. - Introduced StandardValues utility to provide standard electronic component value series (E12, E24, E96) and methods for snapping calculated values to the nearest standard value. - Developed NetlistLinter for pre-simulation structural validation of SpiceSharp circuit models, detecting common errors such as missing models, duplicate components, and ensuring DC paths to ground.
1 parent 56b3382 commit db77ad4

11 files changed

Lines changed: 3666 additions & 0 deletions

File tree

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using SpiceSharpParser.Analysis;
5+
using Xunit;
6+
7+
namespace SpiceSharpParser.Tests.Analysis
8+
{
9+
public class WaveformAnalyzerTests
10+
{
11+
[Fact]
12+
public void Average_SineWave_ReturnsOffset()
13+
{
14+
// Full cycle of sine with DC offset of 2
15+
var data = new List<(double Time, double Value)>();
16+
for (int i = 0; i <= 1000; i++)
17+
{
18+
double t = i / 1000.0;
19+
data.Add((t, 2.0 + Math.Sin(2 * Math.PI * t)));
20+
}
21+
22+
double avg = WaveformAnalyzer.Average(data);
23+
Assert.InRange(avg, 1.99, 2.01);
24+
}
25+
26+
[Fact]
27+
public void RMS_SineWave_ReturnsExpected()
28+
{
29+
// RMS of sin(t) = amplitude / sqrt(2)
30+
double amplitude = 3.0;
31+
var data = new List<(double Time, double Value)>();
32+
for (int i = 0; i <= 10000; i++)
33+
{
34+
double t = i / 10000.0;
35+
data.Add((t, amplitude * Math.Sin(2 * Math.PI * t)));
36+
}
37+
38+
double rms = WaveformAnalyzer.RMS(data);
39+
double expected = amplitude / Math.Sqrt(2);
40+
Assert.InRange(rms, expected * 0.99, expected * 1.01);
41+
}
42+
43+
[Fact]
44+
public void PeakToPeak_ReturnsCorrectRange()
45+
{
46+
var data = new List<(double Time, double Value)>
47+
{
48+
(0, 1), (1, 5), (2, -3), (3, 2),
49+
};
50+
51+
double pp = WaveformAnalyzer.PeakToPeak(data);
52+
Assert.Equal(8, pp, 6);
53+
}
54+
55+
[Fact]
56+
public void PeakToPeak_WithWindow_FiltersCorrectly()
57+
{
58+
var data = new List<(double Time, double Value)>
59+
{
60+
(0, 100), (1, 1), (2, 5), (3, 2), (4, -100),
61+
};
62+
63+
double pp = WaveformAnalyzer.PeakToPeak(data, fromTime: 0.5, toTime: 3.5);
64+
Assert.Equal(4, pp, 6); // 5 - 1
65+
}
66+
67+
[Fact]
68+
public void RiseTime_StepResponse_ReturnsCorrectTime()
69+
{
70+
// Simulate a step response: 0 to 1 with a linear ramp from t=1 to t=3
71+
var data = new List<(double Time, double Value)>();
72+
for (int i = 0; i <= 100; i++)
73+
{
74+
double t = i * 0.05; // 0 to 5
75+
double v = t < 1.0 ? 0.0 : (t > 3.0 ? 1.0 : (t - 1.0) / 2.0);
76+
data.Add((t, v));
77+
}
78+
79+
double rt = WaveformAnalyzer.RiseTime(data);
80+
// 10% of range = 0.1, 90% = 0.9
81+
// Time at 0.1 = 1.0 + 0.1*2 = 1.2
82+
// Time at 0.9 = 1.0 + 0.9*2 = 2.8
83+
// Rise time = 2.8 - 1.2 = 1.6
84+
Assert.InRange(rt, 1.5, 1.7);
85+
}
86+
87+
[Fact]
88+
public void SettlingTime_ReturnsCorrectTime()
89+
{
90+
// Step response that overshoots then settles at 1.0
91+
var data = new List<(double Time, double Value)>
92+
{
93+
(0, 0), (1, 1.3), (2, 0.95), (3, 1.02), (4, 0.99), (5, 1.0),
94+
};
95+
96+
double st = WaveformAnalyzer.SettlingTime(data, 1.0, 0.05); // ±5%
97+
Assert.InRange(st, 2, 4); // Should settle around t=2-3
98+
}
99+
100+
[Fact]
101+
public void Overshoot_ReturnsCorrectPercentage()
102+
{
103+
var data = new List<(double Time, double Value)>
104+
{
105+
(0, 0), (1, 1.2), (2, 1.0),
106+
};
107+
108+
double os = WaveformAnalyzer.Overshoot(data, 1.0);
109+
Assert.Equal(20.0, os, 6);
110+
}
111+
112+
[Fact]
113+
public void InterpolateAt_LinearData_ReturnsCorrectValue()
114+
{
115+
var data = new List<(double X, double Y)>
116+
{
117+
(0, 0), (1, 10), (2, 20),
118+
};
119+
120+
Assert.Equal(5, WaveformAnalyzer.InterpolateAt(data, 0.5), 6);
121+
Assert.Equal(15, WaveformAnalyzer.InterpolateAt(data, 1.5), 6);
122+
}
123+
124+
[Fact]
125+
public void FindCrossing_ReturnsCorrectX()
126+
{
127+
var data = new List<(double X, double Y)>
128+
{
129+
(0, 0), (1, 2), (2, 4), (3, 2), (4, 0),
130+
};
131+
132+
double crossing = WaveformAnalyzer.FindCrossing(data, 3.0, occurrence: 1);
133+
Assert.InRange(crossing, 1.4, 1.6); // 3.0 is between (1,2) and (2,4)
134+
}
135+
136+
[Fact]
137+
public void BandwidthFrom3dBPoints_LowPassFilter_ReturnsCorrectBW()
138+
{
139+
// Simulate a low-pass filter response: flat at 0dB, rolling off at 1kHz
140+
var data = new List<(double Freq, double GainDb)>();
141+
for (double f = 1; f <= 100000; f *= 1.1)
142+
{
143+
double gainDb = -10 * Math.Log10(1 + Math.Pow(f / 1000, 2));
144+
data.Add((f, gainDb));
145+
}
146+
147+
double bw = WaveformAnalyzer.BandwidthFrom3dBPoints(data);
148+
Assert.InRange(bw, 900, 1100); // Should be ~1kHz
149+
}
150+
151+
[Fact]
152+
public void FFT_PureSine_HasSinglePeak()
153+
{
154+
// Generate a 100Hz sine sampled at 1kHz for 1 second
155+
var data = new List<(double Time, double Value)>();
156+
int samples = 1024;
157+
double sampleRate = 1000.0;
158+
for (int i = 0; i < samples; i++)
159+
{
160+
double t = i / sampleRate;
161+
data.Add((t, Math.Sin(2 * Math.PI * 100 * t)));
162+
}
163+
164+
var spectrum = WaveformAnalyzer.FFT(data);
165+
Assert.True(spectrum.Count > 0);
166+
167+
// Find peak (excluding DC)
168+
var peak = spectrum.Skip(1).OrderByDescending(s => s.Magnitude).First();
169+
Assert.InRange(peak.Frequency, 95, 105); // Should be near 100Hz
170+
}
171+
172+
[Fact]
173+
public void THD_PureSine_NearZero()
174+
{
175+
var data = new List<(double Time, double Value)>();
176+
int samples = 4096;
177+
double sampleRate = 10000.0;
178+
for (int i = 0; i < samples; i++)
179+
{
180+
double t = i / sampleRate;
181+
data.Add((t, Math.Sin(2 * Math.PI * 100 * t)));
182+
}
183+
184+
double thd = WaveformAnalyzer.THD(data, 100);
185+
Assert.InRange(thd, 0, 1.0); // Should be near 0% for pure sine
186+
}
187+
}
188+
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
using SpiceSharpParser.Builder;
2+
using Xunit;
3+
4+
namespace SpiceSharpParser.Tests.Builder
5+
{
6+
public class CircuitBuilderTests
7+
{
8+
[Fact]
9+
public void ToNetlist_SimpleRC_GeneratesValidNetlist()
10+
{
11+
var netlist = CircuitBuilder.Create("RC Filter")
12+
.VoltageSource("V1", "in", "0", dc: 5)
13+
.Resistor("R1", "in", "out", 1000)
14+
.Capacitor("C1", "out", "0", 1e-6)
15+
.OP()
16+
.Save("V(out)")
17+
.ToNetlist();
18+
19+
Assert.Contains("RC Filter", netlist);
20+
Assert.Contains("V1", netlist);
21+
Assert.Contains("R1 in out 1k", netlist);
22+
Assert.Contains("C1 out 0 1u", netlist);
23+
Assert.Contains(".OP", netlist);
24+
Assert.Contains(".SAVE V(out)", netlist);
25+
Assert.Contains(".END", netlist);
26+
}
27+
28+
[Fact]
29+
public void Build_SimpleRC_ParsesSuccessfully()
30+
{
31+
var model = CircuitBuilder.Create("RC OP Test")
32+
.VoltageSource("V1", "in", "0", dc: 10)
33+
.Resistor("R1", "in", "out", 1000)
34+
.Resistor("R2", "out", "0", 1000)
35+
.OP()
36+
.Save("V(out)")
37+
.Build();
38+
39+
Assert.NotNull(model);
40+
Assert.False(model.ValidationResult.HasError);
41+
Assert.NotNull(model.Circuit);
42+
Assert.True(model.Simulations.Count > 0);
43+
}
44+
45+
[Fact]
46+
public void SetValue_ModifiesComponent()
47+
{
48+
var builder = CircuitBuilder.Create("Test")
49+
.VoltageSource("V1", "in", "0", dc: 5)
50+
.Resistor("R1", "in", "out", 1000)
51+
.Resistor("R2", "out", "0", 2000);
52+
53+
var netlistBefore = builder.ToNetlist();
54+
Assert.Contains("R1 in out 1k", netlistBefore);
55+
56+
builder.SetValue("R1", 2200);
57+
var netlistAfter = builder.ToNetlist();
58+
Assert.Contains("R1 in out 2.2k", netlistAfter);
59+
}
60+
61+
[Fact]
62+
public void RemoveComponent_RemovesFromNetlist()
63+
{
64+
var builder = CircuitBuilder.Create("Test")
65+
.VoltageSource("V1", "in", "0", dc: 5)
66+
.Resistor("R1", "in", "out", 1000)
67+
.Resistor("R2", "out", "0", 2000);
68+
69+
builder.RemoveComponent("R1");
70+
var netlist = builder.ToNetlist();
71+
Assert.DoesNotContain("R1", netlist);
72+
Assert.Contains("R2", netlist);
73+
}
74+
75+
[Fact]
76+
public void AC_GeneratesCorrectStatement()
77+
{
78+
var netlist = CircuitBuilder.Create("AC Test")
79+
.VoltageSource("V1", "in", "0", ac: 1)
80+
.Resistor("R1", "in", "out", 1000)
81+
.Capacitor("C1", "out", "0", 1e-9)
82+
.AC("DEC", 10, 1, 1e6)
83+
.Save("VDB(out)")
84+
.ToNetlist();
85+
86+
Assert.Contains(".AC DEC 10 1 1MEG", netlist);
87+
}
88+
89+
[Fact]
90+
public void Tran_WithMaxStep_GeneratesCorrectStatement()
91+
{
92+
var netlist = CircuitBuilder.Create("Tran Test")
93+
.VoltageSourceSine("V1", "in", "0", 0, 1, 1000)
94+
.Resistor("R1", "in", "out", 1000)
95+
.Capacitor("C1", "out", "0", 1e-6)
96+
.Tran(1e-6, 1e-3, maxStep: 1e-5)
97+
.Save("V(out)")
98+
.ToNetlist();
99+
100+
Assert.Contains(".TRAN 1u 1m 0 10u", netlist);
101+
}
102+
103+
[Fact]
104+
public void BJT_GeneratesCorrectLine()
105+
{
106+
var netlist = CircuitBuilder.Create("BJT Test")
107+
.VoltageSource("VCC", "vcc", "0", dc: 12)
108+
.Resistor("RC", "vcc", "out", 4700)
109+
.Resistor("RB", "vcc", "base", 100000)
110+
.BJT("Q1", "out", "base", "0", "2N3904")
111+
.ModelRaw(".MODEL 2N3904 NPN(BF=200 IS=1e-14)")
112+
.OP()
113+
.Save("V(out)")
114+
.ToNetlist();
115+
116+
Assert.Contains("Q1 out base 0 2N3904", netlist);
117+
Assert.Contains(".MODEL 2N3904 NPN(BF=200 IS=1e-14)", netlist);
118+
}
119+
120+
[Fact]
121+
public void Meas_GeneratesCorrectStatement()
122+
{
123+
var netlist = CircuitBuilder.Create("Meas Test")
124+
.VoltageSource("V1", "in", "0", ac: 1)
125+
.Resistor("R1", "in", "out", 1000)
126+
.Capacitor("C1", "out", "0", 1e-9)
127+
.AC("DEC", 100, 1, 1e9)
128+
.Save("VDB(out)")
129+
.Meas("AC", "f3dB", "WHEN VDB(out) = -3")
130+
.ToNetlist();
131+
132+
Assert.Contains(".MEAS AC f3dB WHEN VDB(out) = -3", netlist);
133+
}
134+
}
135+
}

0 commit comments

Comments
 (0)