Skip to content

Commit 1928618

Browse files
Merge pull request #180 from SpiceSharp/claude_skills
Claude SKILL.md + fix for AC voltage exports
2 parents c3b56c6 + b16978f commit 1928618

7 files changed

Lines changed: 1119 additions & 9 deletions

File tree

.claude/skills/spicesharp-circuit-design/SKILL.md

Lines changed: 859 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Key Discoveries
2+
3+
Lessons learned during circuit design that apply across projects. Read this before starting any new design.
4+
5+
---
6+
7+
## 1. `.TRAN` tmax is required for large-cap diode circuits
8+
9+
**Problem:** Simulation produces wildly wrong output voltages (e.g., 0.8V instead of 22V) and completes suspiciously fast.
10+
11+
**Root Cause:** SpiceSharp's adaptive timestep takes ~18ms steps with large capacitors (millifarad range), completely skipping over the sub-millisecond diode conduction windows. The diodes never conduct, so the capacitors never charge.
12+
13+
**Solution:** Always specify the `tmax` parameter (4th argument) in `.TRAN`:
14+
```spice
15+
.TRAN 100u 2 0 100u UIC
16+
* ^step ^stop ^start ^tmax
17+
```
18+
Rule of thumb: tmax ≤ 1/10 of the AC period (for 50Hz: tmax ≤ 2ms; 100µs works well).
19+
20+
---
21+
22+
## 2. Prefer `.MEAS` over `.SAVE` for automated spec verification
23+
24+
**Problem:** Using `.SAVE` to collect raw waveform data requires complex post-processing in test code to extract specs (e.g., finding threshold crossings, computing averages over windows).
25+
26+
**Root Cause:** `.SAVE` gives raw data points; all analysis logic must be written in C#. This is error-prone and verbose.
27+
28+
**Solution:** Use `.MEAS` directives in the netlist to extract specs directly:
29+
```spice
30+
.MEAS TRAN v_out_avg AVG V(out) FROM=1.5 TO=2.0
31+
.MEAS TRAN ripple_pp PARAM='v_out_max - v_out_min'
32+
.MEAS TRAN settle_95 WHEN V(out) = 21.47 RISE=1
33+
```
34+
Then in tests: `var meas = CircuitTestHelper.GetMeasurements(netlist);` and assert on named values.
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
using System;
2+
using System.Linq;
3+
using Xunit;
4+
5+
namespace SpiceSharpParser.IntegrationTests
6+
{
7+
public class VoltageExportTests : BaseTests
8+
{
9+
/// <summary>
10+
/// RC low-pass filter: R=1k, C=159nF => fc ≈ 1 kHz.
11+
/// At DC (1 Hz), gain ≈ 1 so VDB ≈ 0 dB.
12+
/// At high frequency (1 MHz), gain << 1 so VDB << 0 dB.
13+
/// Verifies VDB uses 20*log10 (not bare log10).
14+
/// </summary>
15+
[Fact]
16+
public void VDB_ReturnsCorrect20Log10_ForRCFilter()
17+
{
18+
var model = GetSpiceSharpModel(
19+
"VDB test - RC low-pass",
20+
"V1 IN 0 AC 1",
21+
"R1 IN OUT 1e3",
22+
"C1 OUT 0 159e-9",
23+
".AC DEC 10 1 1e6",
24+
".MEAS AC vdb_at_dc FIND VDB(OUT) AT=1",
25+
".MEAS AC vdb_at_fc FIND VDB(OUT) AT=1e3",
26+
".END");
27+
28+
RunSimulations(model);
29+
30+
// At 1 Hz (essentially DC), magnitude ≈ 1, so VDB ≈ 0 dB
31+
AssertMeasurementSuccess(model, "vdb_at_dc");
32+
double vdbDc = model.Measurements["vdb_at_dc"][0].Value;
33+
Assert.True(Math.Abs(vdbDc) < 0.1, $"VDB at DC should be ~0 dB, got {vdbDc}");
34+
35+
// At cutoff (1 kHz), magnitude ≈ 1/sqrt(2), so VDB ≈ -3.01 dB
36+
AssertMeasurementSuccess(model, "vdb_at_fc");
37+
double vdbFc = model.Measurements["vdb_at_fc"][0].Value;
38+
Assert.True(Math.Abs(vdbFc - (-3.01)) < 0.5,
39+
$"VDB at cutoff should be ~-3 dB, got {vdbFc}");
40+
}
41+
42+
/// <summary>
43+
/// Without the 20x multiplier, log10(1) = 0 would still pass,
44+
/// but log10(0.707) = -0.15 which is NOT -3 dB.
45+
/// This test catches the missing multiplier by checking magnitude well below unity.
46+
/// </summary>
47+
[Fact]
48+
public void VDB_HighFrequency_NotBareLog10()
49+
{
50+
var model = GetSpiceSharpModel(
51+
"VDB multiplier test",
52+
"V1 IN 0 AC 1",
53+
"R1 IN OUT 1e3",
54+
"C1 OUT 0 159e-9",
55+
".AC DEC 10 1 1e6",
56+
".MEAS AC vdb_high FIND VDB(OUT) AT=100e3",
57+
".END");
58+
59+
RunSimulations(model);
60+
61+
AssertMeasurementSuccess(model, "vdb_high");
62+
double vdbHigh = model.Measurements["vdb_high"][0].Value;
63+
64+
// At 100 kHz (100x fc), gain ≈ 1/100, VDB ≈ -40 dB
65+
// Without 20x multiplier, bare log10(0.01) = -2, which is > -10
66+
Assert.True(vdbHigh < -30,
67+
$"VDB at 100kHz should be << -30 dB (around -40), got {vdbHigh}. " +
68+
"If ~-2, the 20*log10 multiplier is missing.");
69+
}
70+
71+
/// <summary>
72+
/// At DC, phase should be ~0.
73+
/// At frequencies well above cutoff, phase should approach -pi/2 (-90°).
74+
/// Verifies VP returns phase (radians), not magnitude.
75+
/// </summary>
76+
[Fact]
77+
public void VP_ReturnsPhase_NotMagnitude()
78+
{
79+
var model = GetSpiceSharpModel(
80+
"VP test - RC low-pass",
81+
"V1 IN 0 AC 1",
82+
"R1 IN OUT 1e3",
83+
"C1 OUT 0 159e-9",
84+
".AC DEC 10 1 1e6",
85+
".MEAS AC vp_at_dc FIND VP(OUT) AT=1",
86+
".MEAS AC vp_at_fc FIND VP(OUT) AT=1e3",
87+
".MEAS AC vp_at_high FIND VP(OUT) AT=100e3",
88+
".END");
89+
90+
RunSimulations(model);
91+
92+
// At DC: phase ≈ 0
93+
AssertMeasurementSuccess(model, "vp_at_dc");
94+
double vpDc = model.Measurements["vp_at_dc"][0].Value;
95+
Assert.True(Math.Abs(vpDc) < 0.01,
96+
$"VP at DC should be ~0 radians, got {vpDc}");
97+
98+
// At cutoff: phase ≈ -pi/4 (-0.785 rad)
99+
AssertMeasurementSuccess(model, "vp_at_fc");
100+
double vpFc = model.Measurements["vp_at_fc"][0].Value;
101+
Assert.True(Math.Abs(vpFc - (-Math.PI / 4)) < 0.1,
102+
$"VP at cutoff should be ~-0.785 rad, got {vpFc}");
103+
104+
// At high freq: phase ≈ -pi/2 (-1.571 rad)
105+
// If VP returned magnitude instead, it would be a small positive number (~0.01)
106+
AssertMeasurementSuccess(model, "vp_at_high");
107+
double vpHigh = model.Measurements["vp_at_high"][0].Value;
108+
Assert.True(vpHigh < -1.0,
109+
$"VP at high freq should be near -pi/2 (~-1.57), got {vpHigh}. " +
110+
"If positive, VP is returning magnitude instead of phase.");
111+
}
112+
113+
/// <summary>
114+
/// VR should return the real part of complex voltage.
115+
/// At DC, VR ≈ 1 (full voltage, no imaginary component).
116+
/// At cutoff, VR ≈ 0.5 (real part of 1/(1+j) = 0.5 - 0.5j).
117+
/// </summary>
118+
[Fact]
119+
public void VR_ReturnsRealPart_NotMagnitude()
120+
{
121+
var model = GetSpiceSharpModel(
122+
"VR test - RC low-pass",
123+
"V1 IN 0 AC 1",
124+
"R1 IN OUT 1e3",
125+
"C1 OUT 0 159e-9",
126+
".AC DEC 10 1 1e6",
127+
".MEAS AC vr_at_dc FIND VR(OUT) AT=1",
128+
".MEAS AC vr_at_fc FIND VR(OUT) AT=1e3",
129+
".MEAS AC vm_at_fc FIND VM(OUT) AT=1e3",
130+
".END");
131+
132+
RunSimulations(model);
133+
134+
// At DC: VR ≈ 1
135+
AssertMeasurementSuccess(model, "vr_at_dc");
136+
double vrDc = model.Measurements["vr_at_dc"][0].Value;
137+
Assert.True(Math.Abs(vrDc - 1.0) < 0.01,
138+
$"VR at DC should be ~1.0, got {vrDc}");
139+
140+
// At cutoff: VR ≈ 0.5, VM ≈ 0.707
141+
// VR != VM proves we're getting the real part, not magnitude
142+
AssertMeasurementSuccess(model, "vr_at_fc");
143+
AssertMeasurementSuccess(model, "vm_at_fc");
144+
double vrFc = model.Measurements["vr_at_fc"][0].Value;
145+
double vmFc = model.Measurements["vm_at_fc"][0].Value;
146+
147+
Assert.True(Math.Abs(vrFc - 0.5) < 0.05,
148+
$"VR at cutoff should be ~0.5, got {vrFc}");
149+
Assert.True(Math.Abs(vmFc - 0.707) < 0.05,
150+
$"VM at cutoff should be ~0.707, got {vmFc}");
151+
Assert.True(Math.Abs(vrFc - vmFc) > 0.1,
152+
$"VR ({vrFc}) should differ from VM ({vmFc}) at cutoff");
153+
}
154+
155+
/// <summary>
156+
/// Cross-check: VR² + VI² should equal VM² (Pythagorean identity).
157+
/// This validates that VR and VI are the true real/imaginary components.
158+
/// </summary>
159+
[Fact]
160+
public void VR_And_VI_Satisfy_PythagoreanIdentity()
161+
{
162+
var model = GetSpiceSharpModel(
163+
"VR/VI/VM identity test",
164+
"V1 IN 0 AC 1",
165+
"R1 IN OUT 1e3",
166+
"C1 OUT 0 159e-9",
167+
".AC DEC 10 1 1e6",
168+
".MEAS AC vr_fc FIND VR(OUT) AT=1e3",
169+
".MEAS AC vi_fc FIND VI(OUT) AT=1e3",
170+
".MEAS AC vm_fc FIND VM(OUT) AT=1e3",
171+
".END");
172+
173+
RunSimulations(model);
174+
175+
AssertMeasurementSuccess(model, "vr_fc");
176+
AssertMeasurementSuccess(model, "vi_fc");
177+
AssertMeasurementSuccess(model, "vm_fc");
178+
179+
double vr = model.Measurements["vr_fc"][0].Value;
180+
double vi = model.Measurements["vi_fc"][0].Value;
181+
double vm = model.Measurements["vm_fc"][0].Value;
182+
183+
double computedMag = Math.Sqrt(vr * vr + vi * vi);
184+
Assert.True(Math.Abs(computedMag - vm) < 1e-6,
185+
$"sqrt(VR²+VI²) = {computedMag} should equal VM = {vm}");
186+
}
187+
188+
/// <summary>
189+
/// VDB should equal 20*log10(VM) — cross-check between two export types.
190+
/// </summary>
191+
[Fact]
192+
public void VDB_Equals_20Log10_VM()
193+
{
194+
var model = GetSpiceSharpModel(
195+
"VDB vs VM cross-check",
196+
"V1 IN 0 AC 1",
197+
"R1 IN OUT 1e3",
198+
"C1 OUT 0 159e-9",
199+
".AC DEC 10 1 1e6",
200+
".MEAS AC vdb_val FIND VDB(OUT) AT=10e3",
201+
".MEAS AC vm_val FIND VM(OUT) AT=10e3",
202+
".END");
203+
204+
RunSimulations(model);
205+
206+
AssertMeasurementSuccess(model, "vdb_val");
207+
AssertMeasurementSuccess(model, "vm_val");
208+
209+
double vdb = model.Measurements["vdb_val"][0].Value;
210+
double vm = model.Measurements["vm_val"][0].Value;
211+
double expected = 20.0 * Math.Log10(vm);
212+
213+
Assert.True(Math.Abs(vdb - expected) < 1e-6,
214+
$"VDB ({vdb}) should equal 20*log10(VM) ({expected})");
215+
}
216+
}
217+
}

src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/Controls/Exporters/VoltageExports/VoltageDecibelExport.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ public override double Extract()
6363
return double.NaN;
6464
}
6565

66-
return Math.Log10(ExportImpl.Value.Magnitude);
66+
return 20.0 * Math.Log10(ExportImpl.Value.Magnitude);
6767
}
6868
}
6969
}

src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/Controls/Exporters/VoltageExports/VoltagePhaseExport.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public override double Extract()
6161

6262
return double.NaN;
6363
}
64-
return ExportImpl.Value.Magnitude;
64+
return ExportImpl.Value.Phase;
6565
}
6666
}
6767
}

src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/Controls/Exporters/VoltageExports/VoltageRealExport.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public VoltageRealExport(string name, ISimulationWithEvents simulation, string n
2121
Name = name ?? throw new System.ArgumentNullException(nameof(name));
2222
Node = node ?? throw new System.ArgumentNullException(nameof(node));
2323
Reference = reference;
24-
ExportImpl = new RealVoltageExport((IBiasingSimulation)simulation, node, reference);
24+
ExportImpl = new ComplexVoltageExport((FrequencySimulation)simulation, node, reference);
2525
}
2626

2727
/// <summary>
@@ -40,15 +40,15 @@ public VoltageRealExport(string name, ISimulationWithEvents simulation, string n
4040
public override string QuantityUnit => "Voltage (V)";
4141

4242
/// <summary>
43-
/// Gets the real voltage export that provides voltage
43+
/// Gets the complex voltage export that provides voltage real part
4444
/// </summary>
45-
protected RealVoltageExport ExportImpl { get; }
45+
protected ComplexVoltageExport ExportImpl { get; }
4646

4747
/// <summary>
48-
/// Extracts the voltage at the main node
48+
/// Extracts the real part of the voltage at the main node
4949
/// </summary>
5050
/// <returns>
51-
/// A voltage (real) at the main node
51+
/// The real part of the complex voltage at the main node
5252
/// </returns>
5353
public override double Extract()
5454
{
@@ -62,7 +62,7 @@ public override double Extract()
6262
return double.NaN;
6363
}
6464

65-
return ExportImpl.Value;
65+
return ExportImpl.Value.Real;
6666
}
6767
}
6868
}

src/SpiceSharpParser/SpiceSharpParser.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
<StartupObject />
2323
<PackageLicenseExpression>MIT</PackageLicenseExpression>
2424
<LangVersion>latest</LangVersion>
25-
<Version>3.2.12</Version>
25+
<Version>3.3.0</Version>
2626
</PropertyGroup>
2727

2828
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|AnyCPU'">

0 commit comments

Comments
 (0)