From 10e726c2ee6b0f6f3c2f3ce0638f9de430dce860 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Go=C5=82=C4=99biowski?= Date: Sat, 7 Mar 2026 09:32:49 +0100 Subject: [PATCH 1/9] Add documentation for SPICE statements and components - Introduced articles for various SPICE statements including: - .NOISE: Noise analysis syntax and examples - .OP: DC operating point analysis details - .OPTIONS: Simulator options for accuracy and behavior - .PARAM: Definition of named parameters for expressions - .PLOT: Capturing signals for XY plots during simulation - .PRINT: Specifying signals to print as tabular data - .SAVE: Saving signals during simulation - .SPARAM: Defining scalar parameters with immediate evaluation - .ST: Parameter sweep statement for PSpice compatibility - .STEP: Parameter sweep statement with various sweep types - .SUBCKT: Definition of reusable subcircuit blocks - .TEMP: Specifying simulation temperatures - .TRAN: Transient analysis statement - .V: Independent voltage source with various waveform types - .S: Voltage switch with model parameters - .G: Voltage-controlled current source (VCCS) - .E: Voltage-controlled voltage source (VCVS) - .T: Lossless transmission line model - Updated table of contents to include new articles - Enhanced index page with links to articles and categories --- README.md | 98 +- .../DotStatements/MeasTests.cs | 1314 +++++++++++++++++ .../Netlist/Spice/ISpiceSharpModel.cs | 9 +- .../Spice/Readers/Controls/MeasControl.cs | 728 +++++++++ .../Measurements/MeasurementDefinition.cs | 153 ++ .../Measurements/MeasurementEvaluator.cs | 466 ++++++ .../Measurements/MeasurementResult.cs | 51 + .../Netlist/Spice/SpiceObjectMappings.cs | 2 + .../Netlist/Spice/SpiceSharpModel.cs | 8 + .../Netlist/Spice/SpiceStatementsOrderer.cs | 2 +- src/docs/articles/ac.md | 70 + src/docs/articles/appendmodel.md | 24 + src/docs/articles/behavioral-source.md | 55 + src/docs/articles/bjt.md | 74 + src/docs/articles/capacitor.md | 46 + src/docs/articles/cccs.md | 39 + src/docs/articles/ccvs.md | 38 + src/docs/articles/current-source.md | 54 + src/docs/articles/current-switch.md | 39 + src/docs/articles/dc.md | 62 + src/docs/articles/diode.md | 58 + src/docs/articles/distribution.md | 41 + src/docs/articles/func.md | 61 + src/docs/articles/global.md | 43 + src/docs/articles/ic.md | 42 + src/docs/articles/if.md | 65 + src/docs/articles/include.md | 45 + src/docs/articles/inductor.md | 40 + src/docs/articles/intro.md | 109 +- src/docs/articles/jfet.md | 45 + src/docs/articles/let.md | 26 + src/docs/articles/lib.md | 53 + src/docs/articles/mc.md | 40 + src/docs/articles/meas.md | 180 +++ src/docs/articles/mosfet.md | 82 + src/docs/articles/mutual-inductance.md | 43 + src/docs/articles/nodeset.md | 47 + src/docs/articles/noise.md | 41 + src/docs/articles/op.md | 44 + src/docs/articles/options.md | 85 ++ src/docs/articles/param.md | 61 + src/docs/articles/plot.md | 60 + src/docs/articles/print.md | 57 + src/docs/articles/resistor.md | 51 + src/docs/articles/save.md | 68 + src/docs/articles/sparam.md | 26 + src/docs/articles/st.md | 21 + src/docs/articles/step.md | 80 + src/docs/articles/subcircuit-instance.md | 57 + src/docs/articles/subckt.md | 73 + src/docs/articles/temp.md | 44 + src/docs/articles/toc.yml | 119 ++ src/docs/articles/tran.md | 67 + src/docs/articles/transmission-line.md | 44 + src/docs/articles/vccs.md | 41 + src/docs/articles/vcvs.md | 46 + src/docs/articles/voltage-source.md | 104 ++ src/docs/articles/voltage-switch.md | 39 + src/docs/index.md | 22 +- 59 files changed, 5550 insertions(+), 52 deletions(-) create mode 100644 src/SpiceSharpParser.IntegrationTests/DotStatements/MeasTests.cs create mode 100644 src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/Controls/MeasControl.cs create mode 100644 src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/Controls/Measurements/MeasurementDefinition.cs create mode 100644 src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/Controls/Measurements/MeasurementEvaluator.cs create mode 100644 src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/Controls/Measurements/MeasurementResult.cs create mode 100644 src/docs/articles/ac.md create mode 100644 src/docs/articles/appendmodel.md create mode 100644 src/docs/articles/behavioral-source.md create mode 100644 src/docs/articles/bjt.md create mode 100644 src/docs/articles/capacitor.md create mode 100644 src/docs/articles/cccs.md create mode 100644 src/docs/articles/ccvs.md create mode 100644 src/docs/articles/current-source.md create mode 100644 src/docs/articles/current-switch.md create mode 100644 src/docs/articles/dc.md create mode 100644 src/docs/articles/diode.md create mode 100644 src/docs/articles/distribution.md create mode 100644 src/docs/articles/func.md create mode 100644 src/docs/articles/global.md create mode 100644 src/docs/articles/ic.md create mode 100644 src/docs/articles/if.md create mode 100644 src/docs/articles/include.md create mode 100644 src/docs/articles/inductor.md create mode 100644 src/docs/articles/jfet.md create mode 100644 src/docs/articles/let.md create mode 100644 src/docs/articles/lib.md create mode 100644 src/docs/articles/mc.md create mode 100644 src/docs/articles/meas.md create mode 100644 src/docs/articles/mosfet.md create mode 100644 src/docs/articles/mutual-inductance.md create mode 100644 src/docs/articles/nodeset.md create mode 100644 src/docs/articles/noise.md create mode 100644 src/docs/articles/op.md create mode 100644 src/docs/articles/options.md create mode 100644 src/docs/articles/param.md create mode 100644 src/docs/articles/plot.md create mode 100644 src/docs/articles/print.md create mode 100644 src/docs/articles/resistor.md create mode 100644 src/docs/articles/save.md create mode 100644 src/docs/articles/sparam.md create mode 100644 src/docs/articles/st.md create mode 100644 src/docs/articles/step.md create mode 100644 src/docs/articles/subcircuit-instance.md create mode 100644 src/docs/articles/subckt.md create mode 100644 src/docs/articles/temp.md create mode 100644 src/docs/articles/tran.md create mode 100644 src/docs/articles/transmission-line.md create mode 100644 src/docs/articles/vccs.md create mode 100644 src/docs/articles/vcvs.md create mode 100644 src/docs/articles/voltage-source.md create mode 100644 src/docs/articles/voltage-switch.md diff --git a/README.md b/README.md index 8cb97a3d..e895d7f4 100644 --- a/README.md +++ b/README.md @@ -67,61 +67,63 @@ At the moment due to lack of implementation of LAPLACE and FREQ (part of analog ### Dot statements supported: | Statement | Documentation | |:------------|-----------------------:| -|.AC |[Wiki](https://github.com/SpiceSharp/SpiceSharpParser/wiki/.AC)| -|.APPENDMODEL |[Wiki](https://github.com/SpiceSharp/SpiceSharpParser/wiki/.APPENDMODEL)| -|.DC |[Wiki](https://github.com/SpiceSharp/SpiceSharpParser/wiki/.DC)| -|.DISTRIBUTION|[Wiki](https://github.com/SpiceSharp/SpiceSharpParser/wiki/.DISTRIBUTION)| -|.ELSE |[Wiki](https://github.com/SpiceSharp/SpiceSharpParser/wiki/.ELSE)| -|.ENDIF |[Wiki](https://github.com/SpiceSharp/SpiceSharpParser/wiki/.ENDIF)| -|.FUNC |[Wiki](https://github.com/SpiceSharp/SpiceSharpParser/wiki/.FUNC)| -|.GLOBAL |[Wiki](https://github.com/SpiceSharp/SpiceSharpParser/wiki/.GLOBAL)| -|.IC |[Wiki](https://github.com/SpiceSharp/SpiceSharpParser/wiki/.IC)| -|.IF |[Wiki](https://github.com/SpiceSharp/SpiceSharpParser/wiki/.IF)| -|.INCLUDE |[Wiki](https://github.com/SpiceSharp/SpiceSharpParser/wiki/.INCLUDE)| -|.LET |[Wiki](https://github.com/SpiceSharp/SpiceSharpParser/wiki/.LET)| -|.LIB |[Wiki](https://github.com/SpiceSharp/SpiceSharpParser/wiki/.LIB)| -|.MC |[Wiki](https://github.com/SpiceSharp/SpiceSharpParser/wiki/.MC)| -|.NODESET |[Wiki](https://github.com/SpiceSharp/SpiceSharpParser/wiki/.NODESET)| -|.NOISE |[Wiki](https://github.com/SpiceSharp/SpiceSharpParser/wiki/.NOISE)| -|.OP |[Wiki](https://github.com/SpiceSharp/SpiceSharpParser/wiki/.OP)| -|.OPTIONS |[Wiki](https://github.com/SpiceSharp/SpiceSharpParser/wiki/.OPTIONS)| -|.PARAM |[Wiki](https://github.com/SpiceSharp/SpiceSharpParser/wiki/.PARAM)| -|.PLOT |[Wiki](https://github.com/SpiceSharp/SpiceSharpParser/wiki/.PLOT)| -|.PRINT |[Wiki](https://github.com/SpiceSharp/SpiceSharpParser/wiki/.PRINT)| -|.TRAN |[Wiki](https://github.com/SpiceSharp/SpiceSharpParser/wiki/.TRAN)| -|.SAVE |[Wiki](https://github.com/SpiceSharp/SpiceSharpParser/wiki/.SAVE)| -|.SPARAM |[Wiki](https://github.com/SpiceSharp/SpiceSharpParser/wiki/.SPARAM)| -|.ST |[Wiki](https://github.com/SpiceSharp/SpiceSharpParser/wiki/.ST)|| -|.STEP |[Wiki](https://github.com/SpiceSharp/SpiceSharpParser/wiki/.STEP)| -|.SUBCKT |[Wiki](https://github.com/SpiceSharp/SpiceSharpParser/wiki/.SUBCKT)| -|.TEMP |[Wiki](https://github.com/SpiceSharp/SpiceSharpParser/wiki/.TEMP)| +|.AC |[Docs](src/docs/articles/ac.md)| +|.APPENDMODEL |[Docs](src/docs/articles/appendmodel.md)| +|.DC |[Docs](src/docs/articles/dc.md)| +|.DISTRIBUTION|[Docs](src/docs/articles/distribution.md)| +|.ELSE |[Docs](src/docs/articles/if.md)| +|.ENDIF |[Docs](src/docs/articles/if.md)| +|.FUNC |[Docs](src/docs/articles/func.md)| +|.GLOBAL |[Docs](src/docs/articles/global.md)| +|.IC |[Docs](src/docs/articles/ic.md)| +|.IF |[Docs](src/docs/articles/if.md)| +|.INCLUDE |[Docs](src/docs/articles/include.md)| +|.LET |[Docs](src/docs/articles/let.md)| +|.LIB |[Docs](src/docs/articles/lib.md)| +|.MC |[Docs](src/docs/articles/mc.md)| +|.MEAS |[Docs](src/docs/articles/meas.md)| +|.MEASURE |[Docs](src/docs/articles/meas.md)| +|.NODESET |[Docs](src/docs/articles/nodeset.md)| +|.NOISE |[Docs](src/docs/articles/noise.md)| +|.OP |[Docs](src/docs/articles/op.md)| +|.OPTIONS |[Docs](src/docs/articles/options.md)| +|.PARAM |[Docs](src/docs/articles/param.md)| +|.PLOT |[Docs](src/docs/articles/plot.md)| +|.PRINT |[Docs](src/docs/articles/print.md)| +|.TRAN |[Docs](src/docs/articles/tran.md)| +|.SAVE |[Docs](src/docs/articles/save.md)| +|.SPARAM |[Docs](src/docs/articles/sparam.md)| +|.ST |[Docs](src/docs/articles/st.md)| +|.STEP |[Docs](src/docs/articles/step.md)| +|.SUBCKT |[Docs](src/docs/articles/subckt.md)| +|.TEMP |[Docs](src/docs/articles/temp.md)| ### Device statements supported: | Device Statement | Documentation | |:------------|-----------------------:| -|B (Arbitrary Behavioral Voltage or Current Source)|[Wiki](https://github.com/SpiceSharp/SpiceSharpParser/wiki/B)| -|C (Capacitor)|[Wiki](https://github.com/SpiceSharp/SpiceSharpParser/wiki/C)| -|D (Diode)|[Wiki](https://github.com/SpiceSharp/SpiceSharpParser/wiki/D)| -|E (Voltage-Controlled Voltage Source)|[Wiki](https://github.com/SpiceSharp/SpiceSharpParser/wiki/E)| -|F (Current-Controlled Current Source)|[Wiki](https://github.com/SpiceSharp/SpiceSharpParser/wiki/F)| -|G (Voltage-Controlled Current Source)|[Wiki](https://github.com/SpiceSharp/SpiceSharpParser/wiki/G)| -|H (Current-Controlled Voltage Source)|[Wiki](https://github.com/SpiceSharp/SpiceSharpParser/wiki/H)| -|I (Independent Current Source)|[Wiki](https://github.com/SpiceSharp/SpiceSharpParser/wiki/I)| -|J (JFET)|[Wiki](https://github.com/SpiceSharp/SpiceSharpParser/wiki/J)| -|K (Mutual Inductance)|[Wiki](https://github.com/SpiceSharp/SpiceSharpParser/wiki/K)| -|L (Inductor)|[Wiki](https://github.com/SpiceSharp/SpiceSharpParser/wiki/L)| -|M (Mosfet)|[Wiki](https://github.com/SpiceSharp/SpiceSharpParser/wiki/M)| -|Q (Bipolar Junction Transistor)|[Wiki](https://github.com/SpiceSharp/SpiceSharpParser/wiki/Q)| -|R (Resistor)|[Wiki](https://github.com/SpiceSharp/SpiceSharpParser/wiki/R)| -|S (Voltage Switch)|[Wiki](https://github.com/SpiceSharp/SpiceSharpParser/wiki/S)| -|T (Lossless Transmission Line)|[Wiki](https://github.com/SpiceSharp/SpiceSharpParser/wiki/T)| -|V (Independent Voltage Source)|[Wiki](https://github.com/SpiceSharp/SpiceSharpParser/wiki/V)| -|W (Current Switch)|[Wiki](https://github.com/SpiceSharp/SpiceSharpParser/wiki/W)| -|X (Subcircuit)|[Wiki](https://github.com/SpiceSharp/SpiceSharpParser/wiki/X)| +|B (Arbitrary Behavioral Voltage or Current Source)|[Docs](src/docs/articles/behavioral-source.md)| +|C (Capacitor)|[Docs](src/docs/articles/capacitor.md)| +|D (Diode)|[Docs](src/docs/articles/diode.md)| +|E (Voltage-Controlled Voltage Source)|[Docs](src/docs/articles/vcvs.md)| +|F (Current-Controlled Current Source)|[Docs](src/docs/articles/cccs.md)| +|G (Voltage-Controlled Current Source)|[Docs](src/docs/articles/vccs.md)| +|H (Current-Controlled Voltage Source)|[Docs](src/docs/articles/ccvs.md)| +|I (Independent Current Source)|[Docs](src/docs/articles/current-source.md)| +|J (JFET)|[Docs](src/docs/articles/jfet.md)| +|K (Mutual Inductance)|[Docs](src/docs/articles/mutual-inductance.md)| +|L (Inductor)|[Docs](src/docs/articles/inductor.md)| +|M (Mosfet)|[Docs](src/docs/articles/mosfet.md)| +|Q (Bipolar Junction Transistor)|[Docs](src/docs/articles/bjt.md)| +|R (Resistor)|[Docs](src/docs/articles/resistor.md)| +|S (Voltage Switch)|[Docs](src/docs/articles/voltage-switch.md)| +|T (Lossless Transmission Line)|[Docs](src/docs/articles/transmission-line.md)| +|V (Independent Voltage Source)|[Docs](src/docs/articles/voltage-source.md)| +|W (Current Switch)|[Docs](src/docs/articles/current-switch.md)| +|X (Subcircuit)|[Docs](src/docs/articles/subcircuit-instance.md)| ## Documentation +* Documentation articles are available in [src/docs/articles](src/docs/articles). * API documentation is available at . -* Wiki is available at ## License SpiceSharpParser is under MIT License diff --git a/src/SpiceSharpParser.IntegrationTests/DotStatements/MeasTests.cs b/src/SpiceSharpParser.IntegrationTests/DotStatements/MeasTests.cs new file mode 100644 index 00000000..835ae13b --- /dev/null +++ b/src/SpiceSharpParser.IntegrationTests/DotStatements/MeasTests.cs @@ -0,0 +1,1314 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace SpiceSharpParser.IntegrationTests.DotStatements +{ + public class MeasTests : BaseTests + { + // ===================================================================== + // Group A: TRIG/TARG — Delay & Timing Measurements (TRAN) + // ===================================================================== + + [Fact] + public void TrigTargRiseTimeRC() + { + // RC circuit: R=10k, C=1µF, step input 10V + // Rise time from 10% (1V) to 90% (9V) = RC * ln(9) ≈ 21.97ms + double rc = 10e3 * 1e-6; // 10ms + double expectedRiseTime = rc * Math.Log(9.0 / 1.0); // ln(0.9/0.1) = ln(9) + + var model = GetSpiceSharpModel( + "MEAS TRIG/TARG Rise Time of RC Circuit", + "V1 IN 0 10.0", + "R1 IN OUT 10e3", + "C1 OUT 0 1e-6", + ".IC V(OUT)=0.0", + ".TRAN 1e-5 50e-3", + ".MEAS TRAN rise_time TRIG V(OUT) VAL=1.0 RISE=1 TARG V(OUT) VAL=9.0 RISE=1", + ".END"); + + RunSimulations(model); + + Assert.True(model.Measurements.ContainsKey("rise_time")); + var results = model.Measurements["rise_time"]; + Assert.Single(results); + Assert.True(results[0].Success); + Assert.True(EqualsWithTol(expectedRiseTime, results[0].Value)); + } + + [Fact] + public void TrigTargPropagationDelayRC() + { + // Two-stage RC: R1=1k, C1=10nF. Measure delay from input crossing 2.5V to output crossing 2.5V. + // With a step input, the mid-node voltage is filtered through the RC. + var model = GetSpiceSharpModel( + "MEAS TRIG/TARG Propagation Delay", + "V1 IN 0 PULSE(0 5 0 1n 1n 100u 200u)", + "R1 IN MID 1e3", + "C1 MID 0 10e-9", + ".IC V(MID)=0.0", + ".TRAN 1e-8 50e-6", + ".MEAS TRAN tpd TRIG V(IN) VAL=2.5 RISE=1 TARG V(MID) VAL=2.5 RISE=1", + ".END"); + + RunSimulations(model); + + Assert.True(model.Measurements.ContainsKey("tpd")); + var results = model.Measurements["tpd"]; + Assert.Single(results); + Assert.True(results[0].Success); + // Delay should be positive and proportional to RC = 1k * 10nF = 10µs + Assert.True(results[0].Value > 0); + } + + [Fact] + public void TrigTargWithTD() + { + // RC circuit with time delay offset on trigger search + var model = GetSpiceSharpModel( + "MEAS TRIG/TARG With TD", + "V1 IN 0 10.0", + "R1 IN OUT 10e3", + "C1 OUT 0 1e-6", + ".IC V(OUT)=0.0", + ".TRAN 1e-5 50e-3", + ".MEAS TRAN delayed TRIG V(OUT) VAL=1.0 RISE=1 TD=5e-3 TARG V(OUT) VAL=9.0 RISE=1", + ".END"); + + RunSimulations(model); + + Assert.True(model.Measurements.ContainsKey("delayed")); + var results = model.Measurements["delayed"]; + Assert.Single(results); + // TD delays the trigger search, so result should differ from no-TD case + } + + [Fact] + public void TrigTargFallTime() + { + // RC discharge: pulse goes high then falls + double rc = 10e3 * 1e-6; // 10ms + + var model = GetSpiceSharpModel( + "MEAS TRIG/TARG Fall Time", + "V1 IN 0 PULSE(10 0 10e-3 1n 1n 50e-3 100e-3)", + "R1 IN OUT 10e3", + "C1 OUT 0 1e-6", + ".IC V(OUT)=10.0", + ".TRAN 1e-5 70e-3", + ".MEAS TRAN fall_time TRIG V(OUT) VAL=9.0 FALL=1 TARG V(OUT) VAL=1.0 FALL=1", + ".END"); + + RunSimulations(model); + + Assert.True(model.Measurements.ContainsKey("fall_time")); + var results = model.Measurements["fall_time"]; + Assert.Single(results); + Assert.True(results[0].Success); + Assert.True(results[0].Value > 0); + } + + [Fact] + public void TrigTargCrossEdge() + { + // Pulse waveform with multiple crossings, use CROSS=2 and CROSS=3 + var model = GetSpiceSharpModel( + "MEAS TRIG/TARG Cross Edge", + "V1 OUT 0 PULSE(0 5 0 1n 1n 10e-6 20e-6)", + "R1 OUT 0 1e3", + ".TRAN 1e-8 60e-6", + ".MEAS TRAN t_between TRIG V(OUT) VAL=2.5 CROSS=2 TARG V(OUT) VAL=2.5 CROSS=3", + ".END"); + + RunSimulations(model); + + Assert.True(model.Measurements.ContainsKey("t_between")); + var results = model.Measurements["t_between"]; + Assert.Single(results); + Assert.True(results[0].Success); + // Time between CROSS=2 and CROSS=3 should be ~half the period (10µs) + Assert.True(EqualsWithTol(10e-6, results[0].Value)); + } + + // ===================================================================== + // Group B: WHEN — Threshold Crossing Time (TRAN) + // ===================================================================== + + [Fact] + public void WhenCombinedSyntax() + { + // RC circuit: find time when V(out) = 5V (50% of 10V step) + double rc = 10e3 * 1e-6; // 10ms + double expected = rc * Math.Log(2); // t = -RC*ln(1 - 0.5) = RC*ln(2) + + var model = GetSpiceSharpModel( + "MEAS WHEN Combined Syntax", + "V1 IN 0 10.0", + "R1 IN OUT 10e3", + "C1 OUT 0 1e-6", + ".IC V(OUT)=0.0", + ".TRAN 1e-5 50e-3", + ".MEAS TRAN t50 WHEN V(OUT)=5.0", + ".END"); + + RunSimulations(model); + + Assert.True(model.Measurements.ContainsKey("t50")); + var results = model.Measurements["t50"]; + Assert.Single(results); + Assert.True(results[0].Success); + Assert.True(EqualsWithTol(expected, results[0].Value)); + } + + [Fact] + public void WhenSeparateSyntax() + { + // Same circuit as WhenCombinedSyntax but with separate WHEN syntax + double rc = 10e3 * 1e-6; + double expected = rc * Math.Log(2); + + var model = GetSpiceSharpModel( + "MEAS WHEN Separate Syntax", + "V1 IN 0 10.0", + "R1 IN OUT 10e3", + "C1 OUT 0 1e-6", + ".IC V(OUT)=0.0", + ".TRAN 1e-5 50e-3", + ".MEAS TRAN t50_sep WHEN V(OUT) VAL=5.0 CROSS=1", + ".END"); + + RunSimulations(model); + + Assert.True(model.Measurements.ContainsKey("t50_sep")); + var results = model.Measurements["t50_sep"]; + Assert.Single(results); + Assert.True(results[0].Success); + Assert.True(EqualsWithTol(expected, results[0].Value)); + } + + [Fact] + public void WhenRiseN() + { + // Pulse waveform, find time of 2nd rising crossing at 2.5V + var model = GetSpiceSharpModel( + "MEAS WHEN RISE=2", + "V1 OUT 0 PULSE(0 5 0 1n 1n 10e-6 20e-6)", + "R1 OUT 0 1e3", + ".TRAN 1e-8 60e-6", + ".MEAS TRAN t_rise2 WHEN V(OUT)=2.5 RISE=2", + ".END"); + + RunSimulations(model); + + Assert.True(model.Measurements.ContainsKey("t_rise2")); + var results = model.Measurements["t_rise2"]; + Assert.Single(results); + Assert.True(results[0].Success); + // 2nd rising edge at 2.5V should be approximately at t = 20µs (start of 2nd period) + Assert.True(results[0].Value > 19e-6 && results[0].Value < 21e-6); + } + + [Fact] + public void WhenFallN() + { + // Pulse waveform, find time of 1st falling crossing at 2.5V + var model = GetSpiceSharpModel( + "MEAS WHEN FALL=1", + "V1 OUT 0 PULSE(0 5 0 1n 1n 10e-6 20e-6)", + "R1 OUT 0 1e3", + ".TRAN 1e-8 60e-6", + ".MEAS TRAN t_fall1 WHEN V(OUT)=2.5 FALL=1", + ".END"); + + RunSimulations(model); + + Assert.True(model.Measurements.ContainsKey("t_fall1")); + var results = model.Measurements["t_fall1"]; + Assert.Single(results); + Assert.True(results[0].Success); + // 1st falling edge at 2.5V should be around t ≈ 10µs (after first pulse) + Assert.True(results[0].Value > 9e-6 && results[0].Value < 11e-6); + } + + [Fact] + public void WhenNoCrossing() + { + // DC source at 5V, try to find crossing at 10V — impossible + var model = GetSpiceSharpModel( + "MEAS WHEN No Crossing", + "V1 OUT 0 5.0", + "R1 OUT 0 1e3", + ".TRAN 1e-7 10e-6", + ".MEAS TRAN impossible WHEN V(OUT)=10.0", + ".END"); + + RunSimulations(model); + + Assert.True(model.Measurements.ContainsKey("impossible")); + var results = model.Measurements["impossible"]; + Assert.Single(results); + Assert.False(results[0].Success); + } + + // ===================================================================== + // Group C: FIND/WHEN — Value at Threshold (TRAN) + // ===================================================================== + + [Fact] + public void FindWhenBasic() + { + // Pulse input through RC. Find V(OUT) when V(IN) crosses 2.5V + var model = GetSpiceSharpModel( + "MEAS FIND/WHEN Basic", + "V1 IN 0 PULSE(0 5 0 1e-6 1e-6 10e-6 20e-6)", + "R1 IN OUT 1e3", + "C1 OUT 0 10e-9", + ".IC V(OUT)=0.0", + ".TRAN 1e-8 30e-6", + ".MEAS TRAN vout_at_cross FIND V(OUT) WHEN V(IN)=2.5", + ".END"); + + RunSimulations(model); + + Assert.True(model.Measurements.ContainsKey("vout_at_cross")); + var results = model.Measurements["vout_at_cross"]; + Assert.Single(results); + Assert.True(results[0].Success); + // V(OUT) should be some value between 0 and 5V at the crossing time + Assert.True(results[0].Value >= 0 && results[0].Value <= 5.0); + } + + [Fact] + public void FindCurrentWhenVoltage() + { + // RC circuit, find current when V(OUT) = 5V + var model = GetSpiceSharpModel( + "MEAS FIND Current WHEN Voltage", + "V1 IN 0 10.0", + "R1 IN OUT 10e3", + "C1 OUT 0 1e-6", + ".IC V(OUT)=0.0", + ".TRAN 1e-5 50e-3", + ".MEAS TRAN i_at_5v FIND I(R1) WHEN V(OUT)=5.0", + ".END"); + + RunSimulations(model); + + Assert.True(model.Measurements.ContainsKey("i_at_5v")); + var results = model.Measurements["i_at_5v"]; + Assert.Single(results); + Assert.True(results[0].Success); + } + + [Fact] + public void FindWhenWithRise() + { + // Pulse circuit with multiple crossings, find V(OUT) at 2nd rising crossing of V(IN) + var model = GetSpiceSharpModel( + "MEAS FIND/WHEN With RISE", + "V1 IN 0 PULSE(0 5 0 1n 1n 10e-6 20e-6)", + "R1 IN OUT 1e3", + "C1 OUT 0 10e-9", + ".IC V(OUT)=0.0", + ".TRAN 1e-8 60e-6", + ".MEAS TRAN v_at_rise2 FIND V(OUT) WHEN V(IN)=2.5 RISE=2", + ".END"); + + RunSimulations(model); + + Assert.True(model.Measurements.ContainsKey("v_at_rise2")); + var results = model.Measurements["v_at_rise2"]; + Assert.Single(results); + Assert.True(results[0].Success); + } + + // ===================================================================== + // Group D: MIN/MAX/AVG/PP/RMS — Statistical Measurements (TRAN) + // ===================================================================== + + [Fact] + public void MaxVoltage() + { + // RC charging from step input. Max should approach 10V. + var model = GetSpiceSharpModel( + "MEAS MAX Voltage", + "V1 IN 0 10.0", + "R1 IN OUT 10e3", + "C1 OUT 0 1e-6", + ".IC V(OUT)=0.0", + ".TRAN 1e-5 50e-3", + ".MEAS TRAN vmax MAX V(OUT)", + ".END"); + + RunSimulations(model); + + Assert.True(model.Measurements.ContainsKey("vmax")); + var results = model.Measurements["vmax"]; + Assert.Single(results); + Assert.True(results[0].Success); + // After 5*RC = 50ms, capacitor should be very close to 10V + Assert.True(results[0].Value > 9.9); + } + + [Fact] + public void MinVoltage() + { + // RC charging from 0V. Min should be near 0V. + var model = GetSpiceSharpModel( + "MEAS MIN Voltage", + "V1 IN 0 10.0", + "R1 IN OUT 10e3", + "C1 OUT 0 1e-6", + ".IC V(OUT)=0.0", + ".TRAN 1e-5 50e-3", + ".MEAS TRAN vmin MIN V(OUT)", + ".END"); + + RunSimulations(model); + + Assert.True(model.Measurements.ContainsKey("vmin")); + var results = model.Measurements["vmin"]; + Assert.Single(results); + Assert.True(results[0].Success); + // Min voltage should be 0V (initial condition) + Assert.True(results[0].Value < 0.1); + } + + [Fact] + public void AvgVoltageDC() + { + // DC source 10V through voltage divider (R1=R2=10k) + // Average of constant 5V signal = 5V + var model = GetSpiceSharpModel( + "MEAS AVG DC Voltage", + "V1 IN 0 10.0", + "R1 IN MID 10e3", + "R2 MID 0 10e3", + ".TRAN 1e-7 10e-6", + ".MEAS TRAN vavg_dc AVG V(MID)", + ".END"); + + RunSimulations(model); + + Assert.True(model.Measurements.ContainsKey("vavg_dc")); + var results = model.Measurements["vavg_dc"]; + Assert.Single(results); + Assert.True(results[0].Success); + Assert.True(EqualsWithTol(5.0, results[0].Value)); + } + + [Fact] + public void PeakToPeak() + { + // Pulse source 0 to 5V. PP should be 5V. + var model = GetSpiceSharpModel( + "MEAS PP Peak-to-Peak", + "V1 OUT 0 PULSE(0 5 0 1n 1n 5e-6 10e-6)", + "R1 OUT 0 1e3", + ".TRAN 1e-8 30e-6", + ".MEAS TRAN vpp PP V(OUT)", + ".END"); + + RunSimulations(model); + + Assert.True(model.Measurements.ContainsKey("vpp")); + var results = model.Measurements["vpp"]; + Assert.Single(results); + Assert.True(results[0].Success); + Assert.True(EqualsWithTol(5.0, results[0].Value)); + } + + [Fact] + public void RmsSineWave() + { + // Sine wave: amplitude 5V, freq 1MHz, run for 10 full cycles + // RMS = 5/sqrt(2) ≈ 3.536V + double expectedRms = 5.0 / Math.Sqrt(2); + + var model = GetSpiceSharpModel( + "MEAS RMS Sine Wave", + "V1 OUT 0 SIN(0 5 1MEG)", + "R1 OUT 0 1e3", + ".TRAN 1e-9 10e-6", + ".MEAS TRAN vrms RMS V(OUT) FROM=1e-6 TO=9e-6", + ".END"); + + RunSimulations(model); + + Assert.True(model.Measurements.ContainsKey("vrms")); + var results = model.Measurements["vrms"]; + Assert.Single(results); + Assert.True(results[0].Success, $"RMS measurement failed, value={results[0].Value}"); + // Allow 5% tolerance for numerical RMS on discrete samples + double tol = expectedRms * 0.05; + Assert.True(Math.Abs(results[0].Value - expectedRms) < tol, + $"Expected RMS ≈ {expectedRms}, but got {results[0].Value}"); + } + + [Fact] + public void MaxWithFromTo() + { + // Pulse source, measure MAX only within a time window + var model = GetSpiceSharpModel( + "MEAS MAX With FROM/TO Window", + "V1 OUT 0 PULSE(0 5 5e-6 1n 1n 10e-6 20e-6)", + "R1 OUT 0 1e3", + ".TRAN 1e-8 40e-6", + ".MEAS TRAN vmax_window MAX V(OUT) FROM=5e-6 TO=15e-6", + ".END"); + + RunSimulations(model); + + Assert.True(model.Measurements.ContainsKey("vmax_window")); + var results = model.Measurements["vmax_window"]; + Assert.Single(results); + Assert.True(results[0].Success); + // In the window 5µs-15µs, the pulse is high (5V) + Assert.True(EqualsWithTol(5.0, results[0].Value)); + } + + [Fact] + public void MinWithFromTo() + { + // Pulse source, measure MIN within a window where pulse is high + var model = GetSpiceSharpModel( + "MEAS MIN With FROM/TO Window", + "V1 OUT 0 PULSE(0 5 5e-6 1n 1n 10e-6 20e-6)", + "R1 OUT 0 1e3", + ".TRAN 1e-8 40e-6", + ".MEAS TRAN vmin_window MIN V(OUT) FROM=6e-6 TO=14e-6", + ".END"); + + RunSimulations(model); + + Assert.True(model.Measurements.ContainsKey("vmin_window")); + var results = model.Measurements["vmin_window"]; + Assert.Single(results); + Assert.True(results[0].Success); + // Within 6µs-14µs, pulse should be steady at 5V, so min ≈ 5V + Assert.True(results[0].Value > 4.9); + } + + // ===================================================================== + // Group E: INTEG — Integration (TRAN) + // ===================================================================== + + [Fact] + public void IntegConstantVoltage() + { + // DC voltage 5V. Integral from 0 to 10µs = 5V * 10µs = 50µV·s + double expected = 5.0 * 10e-6; + + var model = GetSpiceSharpModel( + "MEAS INTEG Constant Voltage", + "V1 OUT 0 5.0", + "R1 OUT 0 1e3", + ".TRAN 1e-8 10e-6", + ".MEAS TRAN vt_integral INTEG V(OUT) FROM=0 TO=10e-6", + ".END"); + + RunSimulations(model); + + Assert.True(model.Measurements.ContainsKey("vt_integral")); + var results = model.Measurements["vt_integral"]; + Assert.Single(results); + Assert.True(results[0].Success); + Assert.True(EqualsWithTol(expected, results[0].Value)); + } + + [Fact] + public void IntegFullWindow() + { + // DC voltage 5V, full simulation. Integral = 5V * 10µs + double expected = 5.0 * 10e-6; + + var model = GetSpiceSharpModel( + "MEAS INTEG Full Window", + "V1 OUT 0 5.0", + "R1 OUT 0 1e3", + ".TRAN 1e-8 10e-6", + ".MEAS TRAN vt_full INTEG V(OUT)", + ".END"); + + RunSimulations(model); + + Assert.True(model.Measurements.ContainsKey("vt_full")); + var results = model.Measurements["vt_full"]; + Assert.Single(results); + Assert.True(results[0].Success); + Assert.True(EqualsWithTol(expected, results[0].Value)); + } + + // ===================================================================== + // Group F: DERIV — Derivative (TRAN) + // ===================================================================== + + [Fact] + public void DerivRampVoltage() + { + // Linear ramp from 0 to 10V in 10µs. Slope = 1V/µs = 1e6 V/s + double expectedSlope = 10.0 / 10e-6; + + var model = GetSpiceSharpModel( + "MEAS DERIV Ramp Voltage", + "V1 OUT 0 PWL(0 0 10e-6 10)", + "R1 OUT 0 1e3", + ".TRAN 1e-8 10e-6", + ".MEAS TRAN slope DERIV V(OUT) AT=5e-6", + ".END"); + + RunSimulations(model); + + Assert.True(model.Measurements.ContainsKey("slope")); + var results = model.Measurements["slope"]; + Assert.Single(results); + Assert.True(results[0].Success); + Assert.True(EqualsWithTol(expectedSlope, results[0].Value)); + } + + // ===================================================================== + // Group G: PARAM — Computed Measurements (TRAN) + // ===================================================================== + + [Fact] + public void ParamBasicRatio() + { + // Measure MAX and MIN, then compute ratio via PARAM + var model = GetSpiceSharpModel( + "MEAS PARAM Basic Ratio", + "V1 OUT 0 PULSE(2 8 0 1n 1n 5e-6 10e-6)", + "R1 OUT 0 1e3", + ".TRAN 1e-8 30e-6", + ".MEAS TRAN vmax MAX V(OUT)", + ".MEAS TRAN vmin MIN V(OUT)", + ".MEAS TRAN ratio PARAM='vmax/vmin'", + ".END"); + + RunSimulations(model); + + Assert.True(model.Measurements.ContainsKey("ratio")); + var results = model.Measurements["ratio"]; + Assert.Single(results); + Assert.True(results[0].Success); + // 8/2 = 4.0 + Assert.True(EqualsWithTol(4.0, results[0].Value)); + } + + [Fact] + public void ParamExpressionWithMath() + { + // Compute symmetry metric from rise and fall time proxies + var model = GetSpiceSharpModel( + "MEAS PARAM Expression With Math", + "V1 OUT 0 PULSE(0 10 0 1n 1n 5e-6 10e-6)", + "R1 OUT 0 1e3", + ".TRAN 1e-8 30e-6", + ".MEAS TRAN vmax MAX V(OUT)", + ".MEAS TRAN vmin MIN V(OUT)", + ".MEAS TRAN span PARAM='vmax-vmin'", + ".END"); + + RunSimulations(model); + + Assert.True(model.Measurements.ContainsKey("span")); + var results = model.Measurements["span"]; + Assert.Single(results); + Assert.True(results[0].Success); + // 10 - 0 = 10 + Assert.True(EqualsWithTol(10.0, results[0].Value)); + } + + // ===================================================================== + // Group H: AC Analysis Measurements + // ===================================================================== + + [Fact] + public void AcMaxGain() + { + // Simple RC low-pass filter: R=1k, C=159nF → fc ≈ 1kHz + // At low frequency, gain ≈ 1.0 + var model = GetSpiceSharpModel( + "MEAS AC Max Gain", + "V1 IN 0 AC 1", + "R1 IN OUT 1e3", + "C1 OUT 0 159e-9", + ".AC DEC 10 1 1e6", + ".MEAS AC max_gain MAX VM(OUT)", + ".END"); + + RunSimulations(model); + + Assert.True(model.Measurements.ContainsKey("max_gain")); + var results = model.Measurements["max_gain"]; + Assert.Single(results); + Assert.True(results[0].Success); + // Max gain of a passive RC is 1.0 (at DC/low frequencies) + Assert.True(EqualsWithTol(1.0, results[0].Value)); + } + + // ===================================================================== + // Group I: DC Sweep Measurements + // ===================================================================== + + [Fact] + public void DcMaxCurrent() + { + // Resistor R=1k, sweep V1 from 0 to 10V + // Max current at 10V: I = 10/1000 = 10mA + var model = GetSpiceSharpModel( + "MEAS DC Max Current", + "V1 IN 0 10", + "R1 IN 0 1e3", + ".DC V1 0 10 0.1", + ".MEAS DC imax MAX I(R1)", + ".END"); + + RunSimulations(model); + + Assert.True(model.Measurements.ContainsKey("imax")); + var results = model.Measurements["imax"]; + Assert.Single(results); + Assert.True(results[0].Success); + } + + // ===================================================================== + // Group J: OP Measurements + // ===================================================================== + + [Fact] + public void OpMeasVoltage() + { + // Voltage divider: V1=10V, R1=R2=10k → V(MID) = 5V + var model = GetSpiceSharpModel( + "MEAS OP Voltage", + "V1 IN 0 10.0", + "R1 IN MID 10e3", + "R2 MID 0 10e3", + ".OP", + ".MEAS OP vmid MAX V(MID)", + ".END"); + + RunSimulations(model); + + Assert.True(model.Measurements.ContainsKey("vmid")); + var results = model.Measurements["vmid"]; + Assert.Single(results); + Assert.True(results[0].Success); + Assert.True(EqualsWithTol(5.0, results[0].Value)); + } + + [Fact] + public void OpMeasCurrent() + { + // Simple circuit: V1=10V, R1=10k → I(R1) = 1mA + var model = GetSpiceSharpModel( + "MEAS OP Current", + "V1 IN 0 10.0", + "R1 IN 0 10e3", + ".OP", + ".MEAS OP i_r1 MAX I(R1)", + ".END"); + + RunSimulations(model); + + Assert.True(model.Measurements.ContainsKey("i_r1")); + var results = model.Measurements["i_r1"]; + Assert.Single(results); + Assert.True(results[0].Success); + } + + // ===================================================================== + // Group K: .MEASURE Alias + // ===================================================================== + + [Fact] + public void MeasureAliasTran() + { + // Same as WhenCombinedSyntax but using .MEASURE + double rc = 10e3 * 1e-6; + double expected = rc * Math.Log(2); + + var model = GetSpiceSharpModel( + "MEASURE Alias TRAN", + "V1 IN 0 10.0", + "R1 IN OUT 10e3", + "C1 OUT 0 1e-6", + ".IC V(OUT)=0.0", + ".TRAN 1e-5 50e-3", + ".MEASURE TRAN t50 WHEN V(OUT)=5.0", + ".END"); + + RunSimulations(model); + + Assert.True(model.Measurements.ContainsKey("t50")); + var results = model.Measurements["t50"]; + Assert.Single(results); + Assert.True(results[0].Success); + Assert.True(EqualsWithTol(expected, results[0].Value)); + } + + [Fact] + public void MeasureAliasAc() + { + // AC measurement using .MEASURE alias + var model = GetSpiceSharpModel( + "MEASURE Alias AC", + "V1 IN 0 AC 1", + "R1 IN OUT 1e3", + "C1 OUT 0 159e-9", + ".AC DEC 10 1 1e6", + ".MEASURE AC max_gain MAX VM(OUT)", + ".END"); + + RunSimulations(model); + + Assert.True(model.Measurements.ContainsKey("max_gain")); + var results = model.Measurements["max_gain"]; + Assert.Single(results); + Assert.True(results[0].Success); + } + + // ===================================================================== + // Group L: Multiple Measurements in One Netlist + // ===================================================================== + + [Fact] + public void MultipleMeasSameAnalysis() + { + // Five measurements in one netlist + var model = GetSpiceSharpModel( + "Multiple MEAS Same Analysis", + "V1 IN 0 10.0", + "R1 IN OUT 10e3", + "C1 OUT 0 1e-6", + ".IC V(OUT)=0.0", + ".TRAN 1e-5 50e-3", + ".MEAS TRAN vmax MAX V(OUT)", + ".MEAS TRAN vmin MIN V(OUT)", + ".MEAS TRAN vavg AVG V(OUT)", + ".MEAS TRAN vpp PP V(OUT)", + ".MEAS TRAN vrms RMS V(OUT)", + ".END"); + + RunSimulations(model); + + Assert.True(model.Measurements.ContainsKey("vmax")); + Assert.True(model.Measurements.ContainsKey("vmin")); + Assert.True(model.Measurements.ContainsKey("vavg")); + Assert.True(model.Measurements.ContainsKey("vpp")); + Assert.True(model.Measurements.ContainsKey("vrms")); + + Assert.True(model.Measurements["vmax"][0].Success); + Assert.True(model.Measurements["vmin"][0].Success); + Assert.True(model.Measurements["vavg"][0].Success); + Assert.True(model.Measurements["vpp"][0].Success); + Assert.True(model.Measurements["vrms"][0].Success); + } + + [Fact] + public void MultipleMeasDifferentAnalyses() + { + // Both TRAN and DC measurements in one netlist + var model = GetSpiceSharpModel( + "Multiple MEAS Different Analyses", + "V1 IN 0 10.0", + "R1 IN OUT 10e3", + "C1 OUT 0 1e-6", + ".IC V(OUT)=0.0", + ".TRAN 1e-5 50e-3", + ".DC V1 0 10 1", + ".MEAS TRAN vmax_tran MAX V(OUT)", + ".MEAS DC vmax_dc MAX V(OUT)", + ".END"); + + RunSimulations(model); + + Assert.True(model.Measurements.ContainsKey("vmax_tran")); + Assert.True(model.Measurements.ContainsKey("vmax_dc")); + Assert.True(model.Measurements["vmax_tran"][0].Success); + Assert.True(model.Measurements["vmax_dc"][0].Success); + } + + [Fact] + public void TenMeasurementsStressTest() + { + // Ten measurements in one netlist + var model = GetSpiceSharpModel( + "Ten MEAS Stress Test", + "V1 IN 0 PULSE(0 10 0 1n 1n 5e-6 10e-6)", + "R1 IN OUT 1e3", + "C1 OUT 0 10e-9", + ".IC V(OUT)=0.0", + ".TRAN 1e-8 30e-6", + ".MEAS TRAN m1 MAX V(OUT)", + ".MEAS TRAN m2 MIN V(OUT)", + ".MEAS TRAN m3 AVG V(OUT)", + ".MEAS TRAN m4 PP V(OUT)", + ".MEAS TRAN m5 RMS V(OUT)", + ".MEAS TRAN m6 INTEG V(OUT)", + ".MEAS TRAN m7 MAX V(IN)", + ".MEAS TRAN m8 MIN V(IN)", + ".MEAS TRAN m9 AVG V(IN)", + ".MEAS TRAN m10 PP V(IN)", + ".END"); + + RunSimulations(model); + + for (int i = 1; i <= 10; i++) + { + string key = $"m{i}"; + Assert.True(model.Measurements.ContainsKey(key), $"Missing measurement: {key}"); + Assert.True(model.Measurements[key][0].Success, $"Measurement {key} not successful"); + } + } + + // ===================================================================== + // Group M: .STEP Interaction + // ===================================================================== + + [Fact] + public void MeasWithStepList() + { + // .STEP with 3 voltage values → 3 simulation runs → 3 results + var model = GetSpiceSharpModel( + "MEAS With STEP LIST", + "V1 IN 0 {V_val}", + "R1 IN OUT 10e3", + "C1 OUT 0 1e-6", + ".IC V(OUT)=0.0", + ".TRAN 1e-5 50e-3", + ".STEP PARAM V_val LIST 2 5 10", + ".MEAS TRAN vmax MAX V(OUT)", + ".END"); + + RunSimulations(model); + + Assert.True(model.Measurements.ContainsKey("vmax")); + var results = model.Measurements["vmax"]; + Assert.Equal(3, results.Count); + Assert.All(results, r => Assert.True(r.Success)); + } + + [Fact] + public void MeasWithStepConcurrent() + { + // Thread safety test: run .STEP simulations concurrently + var model = GetSpiceSharpModel( + "MEAS With STEP Concurrent", + "V1 IN 0 {V_val}", + "R1 IN OUT 10e3", + "C1 OUT 0 1e-6", + ".IC V(OUT)=0.0", + ".TRAN 1e-5 50e-3", + ".STEP PARAM V_val LIST 1 2 3 4 5", + ".MEAS TRAN vmax MAX V(OUT)", + ".END"); + + // Run simulations concurrently + Parallel.ForEach(model.Simulations, sim => + { + var codes = sim.Run(model.Circuit, -1); + codes = sim.InvokeEvents(codes); + codes.ToArray(); + }); + + Assert.True(model.Measurements.ContainsKey("vmax")); + var results = model.Measurements["vmax"]; + Assert.Equal(5, results.Count); + Assert.All(results, r => Assert.True(r.Success)); + } + + // ===================================================================== + // Group N: Error Handling & Edge Cases + // ===================================================================== + + [Fact] + public void MeasNoCrossingReturnsFailure() + { + // DC source, threshold above max → should fail + var model = GetSpiceSharpModel( + "MEAS No Crossing Failure", + "V1 OUT 0 5.0", + "R1 OUT 0 1e3", + ".TRAN 1e-7 10e-6", + ".MEAS TRAN cross WHEN V(OUT)=100.0", + ".END"); + + RunSimulations(model); + + Assert.True(model.Measurements.ContainsKey("cross")); + var results = model.Measurements["cross"]; + Assert.Single(results); + Assert.False(results[0].Success); + Assert.True(double.IsNaN(results[0].Value)); + } + + [Fact] + public void MeasEmptyFromToWindow() + { + // Window outside simulation range + var model = GetSpiceSharpModel( + "MEAS Empty FROM/TO Window", + "V1 OUT 0 5.0", + "R1 OUT 0 1e3", + ".TRAN 1e-7 10e-6", + ".MEAS TRAN vmax_far MAX V(OUT) FROM=100 TO=200", + ".END"); + + RunSimulations(model); + + Assert.True(model.Measurements.ContainsKey("vmax_far")); + var results = model.Measurements["vmax_far"]; + Assert.Single(results); + Assert.False(results[0].Success); + } + + [Fact] + public void MeasCaseInsensitive() + { + // All lowercase — should still parse correctly + var model = GetSpiceSharpModel( + "MEAS Case Insensitive", + "V1 IN 0 10.0", + "R1 IN OUT 10e3", + "C1 OUT 0 1e-6", + ".IC V(OUT)=0.0", + ".TRAN 1e-5 50e-3", + ".meas tran vmax max V(OUT)", + ".END"); + + RunSimulations(model); + + Assert.True(model.Measurements.ContainsKey("vmax")); + var results = model.Measurements["vmax"]; + Assert.Single(results); + Assert.True(results[0].Success); + } + + [Fact] + public void MeasWithIC() + { + // Circuit with .IC, verify MEAS works with initial conditions + var model = GetSpiceSharpModel( + "MEAS With IC", + "V1 IN 0 10.0", + "R1 IN OUT 10e3", + "C1 OUT 0 1e-6", + ".IC V(OUT)=0.0", + ".TRAN 1e-5 50e-3", + ".MEAS TRAN vmax MAX V(OUT)", + ".MEAS TRAN vmin MIN V(OUT)", + ".END"); + + RunSimulations(model); + + Assert.True(model.Measurements.ContainsKey("vmax")); + Assert.True(model.Measurements.ContainsKey("vmin")); + Assert.True(model.Measurements["vmax"][0].Success); + Assert.True(model.Measurements["vmin"][0].Success); + Assert.True(model.Measurements["vmin"][0].Value < 0.1); + } + + [Fact] + public void MeasOnPulseSource() + { + // Known PULSE waveform, measure peak-to-peak and max + var model = GetSpiceSharpModel( + "MEAS On Pulse Source", + "V1 OUT 0 PULSE(0 5 1e-6 10e-9 10e-9 5e-6 10e-6)", + "R1 OUT 0 1e3", + ".TRAN 1e-8 30e-6", + ".MEAS TRAN vpp PP V(OUT)", + ".MEAS TRAN vmax MAX V(OUT)", + ".END"); + + RunSimulations(model); + + Assert.True(model.Measurements.ContainsKey("vpp")); + Assert.True(model.Measurements.ContainsKey("vmax")); + Assert.True(EqualsWithTol(5.0, model.Measurements["vpp"][0].Value)); + Assert.True(EqualsWithTol(5.0, model.Measurements["vmax"][0].Value)); + } + + // ===================================================================== + // Group O: Subcircuit Signals + // ===================================================================== + + [Fact] + public void MeasSubcircuitVoltage() + { + // Measure internal node of subcircuit + var model = GetSpiceSharpModel( + "MEAS Subcircuit Voltage", + ".SUBCKT DIVIDER IN OUT", + "R1 IN MID 10e3", + "R2 MID OUT 10e3", + ".ENDS", + "V1 IN 0 10.0", + "X1 IN OUT DIVIDER", + "R3 OUT 0 1e6", + ".TRAN 1e-7 10e-6", + ".MEAS TRAN vx MAX V(X1.MID)", + ".END"); + + RunSimulations(model); + + Assert.True(model.Measurements.ContainsKey("vx")); + var results = model.Measurements["vx"]; + Assert.Single(results); + Assert.True(results[0].Success); + } + + // ===================================================================== + // Group P: Combined with Other Controls + // ===================================================================== + + [Fact] + public void MeasWithPrint() + { + // Both .MEAS and .PRINT in same netlist + var model = GetSpiceSharpModel( + "MEAS With PRINT", + "V1 IN 0 10.0", + "R1 IN OUT 10e3", + "C1 OUT 0 1e-6", + ".IC V(OUT)=0.0", + ".TRAN 1e-5 50e-3", + ".MEAS TRAN vmax MAX V(OUT)", + ".PRINT TRAN V(OUT)", + ".END"); + + RunSimulations(model); + + // Both should produce results + Assert.True(model.Measurements.ContainsKey("vmax")); + Assert.True(model.Measurements["vmax"][0].Success); + Assert.Single(model.Prints); + } + + [Fact] + public void MeasWithSave() + { + // Both .MEAS and .SAVE in same netlist + var model = GetSpiceSharpModel( + "MEAS With SAVE", + "V1 IN 0 10.0", + "R1 IN OUT 10e3", + "C1 OUT 0 1e-6", + ".IC V(OUT)=0.0", + ".TRAN 1e-5 50e-3", + ".MEAS TRAN vmax MAX V(OUT)", + ".SAVE V(OUT)", + ".END"); + + RunSimulations(model); + + Assert.True(model.Measurements.ContainsKey("vmax")); + Assert.True(model.Measurements["vmax"][0].Success); + } + + // ===================================================================== + // Additional tests for completeness + // ===================================================================== + + [Fact] + public void AvgVoltageWithFromTo() + { + // DC 5V, average in a window should still be 5V + var model = GetSpiceSharpModel( + "MEAS AVG With FROM/TO", + "V1 OUT 0 5.0", + "R1 OUT 0 1e3", + ".TRAN 1e-8 10e-6", + ".MEAS TRAN vavg_win AVG V(OUT) FROM=2e-6 TO=8e-6", + ".END"); + + RunSimulations(model); + + Assert.True(model.Measurements.ContainsKey("vavg_win")); + var results = model.Measurements["vavg_win"]; + Assert.Single(results); + Assert.True(results[0].Success); + Assert.True(EqualsWithTol(5.0, results[0].Value)); + } + + [Fact] + public void IntegWithFromTo() + { + // DC 5V, integral from 2µs to 8µs = 5V * 6µs = 30µV·s + double expected = 5.0 * 6e-6; + + var model = GetSpiceSharpModel( + "MEAS INTEG With FROM/TO", + "V1 OUT 0 5.0", + "R1 OUT 0 1e3", + ".TRAN 1e-8 10e-6", + ".MEAS TRAN vt_win INTEG V(OUT) FROM=2e-6 TO=8e-6", + ".END"); + + RunSimulations(model); + + Assert.True(model.Measurements.ContainsKey("vt_win")); + var results = model.Measurements["vt_win"]; + Assert.Single(results); + Assert.True(results[0].Success, $"INTEG measurement failed, value={results[0].Value}"); + // Use a reasonable tolerance for numerical integration with discrete windowing + double tol = Math.Abs(expected) * 0.05; // 5% tolerance for windowed integration + Assert.True(Math.Abs(results[0].Value - expected) < tol, + $"Expected integral ≈ {expected}, but got {results[0].Value}"); + } + + [Fact] + public void MeasurementTypePersists() + { + // Verify that MeasurementType is correctly stored + var model = GetSpiceSharpModel( + "MEAS Type Persistence", + "V1 OUT 0 5.0", + "R1 OUT 0 1e3", + ".TRAN 1e-7 10e-6", + ".MEAS TRAN vmax MAX V(OUT)", + ".END"); + + RunSimulations(model); + + Assert.True(model.Measurements.ContainsKey("vmax")); + var results = model.Measurements["vmax"]; + Assert.Single(results); + Assert.Equal("MAX", results[0].MeasurementType); + } + + [Fact] + public void TrigTargSameSignal() + { + // TRIG and TARG on same signal, different thresholds + var model = GetSpiceSharpModel( + "MEAS TRIG/TARG Same Signal", + "V1 IN 0 10.0", + "R1 IN OUT 10e3", + "C1 OUT 0 1e-6", + ".IC V(OUT)=0.0", + ".TRAN 1e-5 50e-3", + ".MEAS TRAN dt TRIG V(OUT) VAL=2.0 RISE=1 TARG V(OUT) VAL=8.0 RISE=1", + ".END"); + + RunSimulations(model); + + Assert.True(model.Measurements.ContainsKey("dt")); + var results = model.Measurements["dt"]; + Assert.Single(results); + Assert.True(results[0].Success); + Assert.True(results[0].Value > 0); + } + + [Fact] + public void WhenWithFromToWindow() + { + // WHEN with FROM/TO to restrict search window + var model = GetSpiceSharpModel( + "MEAS WHEN With FROM/TO", + "V1 OUT 0 PULSE(0 5 0 1n 1n 10e-6 20e-6)", + "R1 OUT 0 1e3", + ".TRAN 1e-8 60e-6", + ".MEAS TRAN t_in_window WHEN V(OUT)=2.5 RISE=1 FROM=0 TO=30e-6", + ".END"); + + RunSimulations(model); + + Assert.True(model.Measurements.ContainsKey("t_in_window")); + var results = model.Measurements["t_in_window"]; + Assert.Single(results); + Assert.True(results[0].Success); + } + + [Fact] + public void MaxVoltageDirectSource() + { + // Direct voltage source — V(OUT) is exactly 5V + var model = GetSpiceSharpModel( + "MEAS MAX Direct Source", + "V1 OUT 0 5.0", + "R1 OUT 0 1e3", + ".TRAN 1e-7 10e-6", + ".MEAS TRAN vmax_direct MAX V(OUT)", + ".END"); + + RunSimulations(model); + + Assert.True(model.Measurements.ContainsKey("vmax_direct")); + Assert.True(EqualsWithTol(5.0, model.Measurements["vmax_direct"][0].Value)); + } + + [Fact] + public void MinMaxConsistency() + { + // MIN <= AVG <= MAX always + var model = GetSpiceSharpModel( + "MEAS MIN/MAX/AVG Consistency", + "V1 IN 0 10.0", + "R1 IN OUT 10e3", + "C1 OUT 0 1e-6", + ".IC V(OUT)=0.0", + ".TRAN 1e-5 50e-3", + ".MEAS TRAN v_min MIN V(OUT)", + ".MEAS TRAN v_avg AVG V(OUT)", + ".MEAS TRAN v_max MAX V(OUT)", + ".END"); + + RunSimulations(model); + + double min = model.Measurements["v_min"][0].Value; + double avg = model.Measurements["v_avg"][0].Value; + double max = model.Measurements["v_max"][0].Value; + + Assert.True(min <= avg); + Assert.True(avg <= max); + } + + [Fact] + public void PpEqualsMaxMinusMin() + { + // PP should equal MAX - MIN + var model = GetSpiceSharpModel( + "MEAS PP = MAX - MIN", + "V1 IN 0 10.0", + "R1 IN OUT 10e3", + "C1 OUT 0 1e-6", + ".IC V(OUT)=0.0", + ".TRAN 1e-5 50e-3", + ".MEAS TRAN vmin2 MIN V(OUT)", + ".MEAS TRAN vmax2 MAX V(OUT)", + ".MEAS TRAN vpp2 PP V(OUT)", + ".END"); + + RunSimulations(model); + + double min = model.Measurements["vmin2"][0].Value; + double max = model.Measurements["vmax2"][0].Value; + double pp = model.Measurements["vpp2"][0].Value; + + Assert.True(EqualsWithTol(max - min, pp)); + } + + [Fact] + public void MeasSimulationNamePopulated() + { + // Verify that SimulationName is populated + var model = GetSpiceSharpModel( + "MEAS Simulation Name", + "V1 OUT 0 5.0", + "R1 OUT 0 1e3", + ".TRAN 1e-7 10e-6", + ".MEAS TRAN vmax MAX V(OUT)", + ".END"); + + RunSimulations(model); + + var results = model.Measurements["vmax"]; + Assert.Single(results); + Assert.False(string.IsNullOrEmpty(results[0].SimulationName)); + } + } +} diff --git a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/ISpiceSharpModel.cs b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/ISpiceSharpModel.cs index e80e0b1b..bd20faa5 100644 --- a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/ISpiceSharpModel.cs +++ b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/ISpiceSharpModel.cs @@ -1,9 +1,11 @@ -using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Collections.Generic; using SpiceSharp; using SpiceSharp.Simulations; using SpiceSharpParser.Common; using SpiceSharpParser.Common.Validation; using SpiceSharpParser.ModelReaders.Netlist.Spice.Readers.Controls.Exporters; +using SpiceSharpParser.ModelReaders.Netlist.Spice.Readers.Controls.Measurements; using SpiceSharpParser.ModelReaders.Netlist.Spice.Readers.Controls.Plots; using SpiceSharpParser.ModelReaders.Netlist.Spice.Readers.Controls.Prints; using SpiceSharpParser.ModelReaders.Netlist.Spice.Readers.Controls.Simulations; @@ -57,6 +59,11 @@ public interface ISpiceSharpModel /// int? Seed { get; set; } + /// + /// Gets the measurement results from .MEAS/.MEASURE statements. + /// + ConcurrentDictionary> Measurements { get; } + ValidationEntryCollection ValidationResult { get; } } } diff --git a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/Controls/MeasControl.cs b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/Controls/MeasControl.cs new file mode 100644 index 00000000..5e5d0f6c --- /dev/null +++ b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/Controls/MeasControl.cs @@ -0,0 +1,728 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using SpiceSharp.Simulations; +using SpiceSharpParser.Common; +using SpiceSharpParser.Common.Validation; +using SpiceSharpParser.ModelReaders.Netlist.Spice.Context; +using SpiceSharpParser.ModelReaders.Netlist.Spice.Mappings; +using SpiceSharpParser.ModelReaders.Netlist.Spice.Readers.Controls.Common; +using SpiceSharpParser.ModelReaders.Netlist.Spice.Readers.Controls.Exporters; +using SpiceSharpParser.ModelReaders.Netlist.Spice.Readers.Controls.Measurements; +using SpiceSharpParser.Models.Netlist.Spice.Objects; +using SpiceSharpParser.Models.Netlist.Spice.Objects.Parameters; + +namespace SpiceSharpParser.ModelReaders.Netlist.Spice.Readers.Controls +{ + /// + /// Reads .MEAS/.MEASURE from SPICE netlist object model. + /// + public class MeasControl : ExportControl + { + /// + /// Initializes a new instance of the class. + /// + /// The exporter mapper. + /// The export factory. + public MeasControl(IMapper mapper, IExportFactory exportFactory) + : base(mapper, exportFactory) + { + } + + /// + /// Reads statement and modifies the context. + /// + /// A statement to process. + /// A reading context. + public override void Read(Control statement, IReadingContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (statement == null) + { + throw new ArgumentNullException(nameof(statement)); + } + + if (statement.Parameters.Count < 3) + { + context.Result.ValidationResult.AddError( + ValidationEntrySource.Reader, + ".MEAS statement requires at least analysis type, measurement name, and measurement specification"); + return; + } + + var definition = ParseDefinition(statement, context); + if (definition == null) + { + return; + } + + foreach (var simulation in FilterSimulations(context.Result.Simulations, definition.AnalysisType)) + { + SetupMeasurement(definition, simulation, context); + } + } + + private IEnumerable FilterSimulations(IEnumerable simulations, string type) + { + var typeLowered = type.ToLower(); + + foreach (var simulation in simulations) + { + if ((simulation is DC && typeLowered == "dc") + || (simulation is Transient && typeLowered == "tran") + || (simulation is AC && typeLowered == "ac") + || (simulation is OP && typeLowered == "op") + || (simulation is Noise && typeLowered == "noise")) + { + yield return simulation; + } + } + } + + private void SetupMeasurement(MeasurementDefinition definition, ISimulationWithEvents simulation, IReadingContext context) + { + var evaluator = new MeasurementEvaluator(definition); + + // Create exports for the signal(s) being measured + Export primaryExport = null; + Export findExport = null; + + if (definition.Type == MeasType.Param) + { + // PARAM measurements don't need exports - computed from other measurements + simulation.EventAfterExecute += (_, __) => + { + ComputeParamMeasurement(definition, simulation.Name, context); + }; + return; + } + + // Determine which signal parameter to use for the primary data collection + Parameter signalParam = GetPrimarySignalParameter(definition); + + if (signalParam != null) + { + primaryExport = GenerateExport(signalParam, context, simulation); + } + + if (definition.Type == MeasType.FindWhen && definition.FindSignal != null) + { + findExport = GenerateExport(definition.FindSignal, context, simulation); + } + + // For TRIG/TARG, we might need separate exports for trigger and target signals + Export trigExport = null; + Export targExport = null; + + if (definition.Type == MeasType.TrigTarg) + { + if (definition.TrigSignal != null) + { + trigExport = GenerateExport(definition.TrigSignal, context, simulation); + } + + if (definition.TargSignal != null && definition.TargSignal != definition.TrigSignal) + { + targExport = GenerateExport(definition.TargSignal, context, simulation); + } + else + { + targExport = trigExport; + } + } + + // Hook into simulation events for data collection + if (definition.Type == MeasType.TrigTarg) + { + var trigData = new List<(double X, double Y)>(); + var targData = new List<(double X, double Y)>(); + + simulation.EventExportData += (_, __) => + { + double x = GetIndependentVariable(simulation); + if (trigExport != null) + { + try { trigData.Add((x, trigExport.Extract())); } + catch { trigData.Add((x, double.NaN)); } + } + + if (targExport != null && targExport != trigExport) + { + try { targData.Add((x, targExport.Extract())); } + catch { targData.Add((x, double.NaN)); } + } + }; + + simulation.EventAfterExecute += (_, __) => + { + var data = trigExport == targExport ? trigData : targData; + + double? trigX = MeasurementEvaluator.FindCrossing( + trigData, definition.TrigVal, definition.TrigEdge, definition.TrigEdgeNumber, definition.TrigTd, null, null); + double? targX = MeasurementEvaluator.FindCrossing( + data, definition.TargVal, definition.TargEdge, definition.TargEdgeNumber, definition.TargTd, null, null); + + MeasurementResult result; + if (trigX.HasValue && targX.HasValue) + { + result = new MeasurementResult(definition.Name, targX.Value - trigX.Value, true, "TRIG_TARG", simulation.Name); + } + else + { + result = new MeasurementResult(definition.Name, double.NaN, false, "TRIG_TARG", simulation.Name); + } + + AddMeasurementResult(context, result); + }; + } + else + { + simulation.EventExportData += (_, __) => + { + double x = GetIndependentVariable(simulation); + + if (primaryExport != null) + { + try { evaluator.CollectDataPoint(x, primaryExport.Extract()); } + catch { evaluator.CollectDataPoint(x, double.NaN); } + } + + if (findExport != null) + { + try { evaluator.CollectFindDataPoint(x, findExport.Extract()); } + catch { evaluator.CollectFindDataPoint(x, double.NaN); } + } + }; + + simulation.EventAfterExecute += (_, __) => + { + var result = evaluator.ComputeResult(simulation.Name); + AddMeasurementResult(context, result); + }; + } + } + + private static Parameter GetPrimarySignalParameter(MeasurementDefinition definition) + { + switch (definition.Type) + { + case MeasType.When: + return definition.WhenSignal; + case MeasType.FindWhen: + return definition.WhenSignal; + case MeasType.Min: + case MeasType.Max: + case MeasType.Avg: + case MeasType.Rms: + case MeasType.Pp: + case MeasType.Integ: + case MeasType.Deriv: + return definition.Signal; + default: + return null; + } + } + + private static double GetIndependentVariable(ISimulationWithEvents simulation) + { + if (simulation is Transient t) + { + return t.Time; + } + + if (simulation is AC a) + { + return a.Frequency; + } + + if (simulation is DC d) + { + return d.GetCurrentSweepValue().Last(); + } + + return 0; + } + + private static void AddMeasurementResult(IReadingContext context, MeasurementResult result) + { + context.Result.Measurements.AddOrUpdate( + result.Name, + _ => new List { result }, + (_, list) => + { + lock (list) + { + list.Add(result); + } + + return list; + }); + } + + private void ComputeParamMeasurement(MeasurementDefinition definition, string simulationName, IReadingContext context) + { + try + { + // Set measurement values as parameters in the evaluation context + var simContext = context.EvaluationContext; + foreach (var kvp in context.Result.Measurements) + { + var results = kvp.Value; + + // Find the result for this simulation, or the first/last available + MeasurementResult matchingResult = null; + lock (results) + { + matchingResult = results.FirstOrDefault(r => r.SimulationName == simulationName) + ?? results.LastOrDefault(); + } + + if (matchingResult != null && matchingResult.Success) + { + simContext.SetParameter(kvp.Key, matchingResult.Value); + } + } + + double value = context.Evaluator.EvaluateDouble(definition.ParamExpression); + var result = new MeasurementResult(definition.Name, value, true, "PARAM", simulationName); + AddMeasurementResult(context, result); + } + catch + { + var result = new MeasurementResult(definition.Name, double.NaN, false, "PARAM", simulationName); + AddMeasurementResult(context, result); + } + } + + private MeasurementDefinition ParseDefinition(Control statement, IReadingContext context) + { + // Syntax: .MEAS + // Parameters[0] = analysis type (TRAN, AC, DC, OP, NOISE) + // Parameters[1] = measurement name + // Parameters[2..] = measurement specification + + string analysisType = statement.Parameters[0].Value.ToUpper(); + string measName = statement.Parameters[1].Value; + + var definition = new MeasurementDefinition + { + Name = measName, + AnalysisType = analysisType, + }; + + // Parse the measurement specification starting at parameter index 2 + var specParams = new List(); + for (int i = 2; i < statement.Parameters.Count; i++) + { + specParams.Add(statement.Parameters[i]); + } + + if (specParams.Count == 0) + { + return null; + } + + // Detect keyword: for AssignmentParameter like PARAM='expr', the Name is the keyword + string keyword; + if (specParams[0] is AssignmentParameter assignParam) + { + keyword = assignParam.Name.ToUpper(); + } + else + { + keyword = specParams[0].Value.ToUpper(); + } + + switch (keyword) + { + case "TRIG": + return ParseTrigTarg(definition, specParams, context); + case "WHEN": + return ParseWhen(definition, specParams, context); + case "FIND": + return ParseFindWhen(definition, specParams, context); + case "MIN": + return ParseStatistical(definition, MeasType.Min, specParams, context); + case "MAX": + return ParseStatistical(definition, MeasType.Max, specParams, context); + case "AVG": + return ParseStatistical(definition, MeasType.Avg, specParams, context); + case "RMS": + return ParseStatistical(definition, MeasType.Rms, specParams, context); + case "PP": + return ParseStatistical(definition, MeasType.Pp, specParams, context); + case "INTEG": + return ParseStatistical(definition, MeasType.Integ, specParams, context); + case "DERIV": + return ParseDeriv(definition, specParams, context); + case "PARAM": + return ParseParam(definition, specParams); + default: + context.Result.ValidationResult.AddError( + ValidationEntrySource.Reader, + $".MEAS: Unrecognized measurement type '{keyword}'"); + return null; + } + } + + private MeasurementDefinition ParseTrigTarg(MeasurementDefinition definition, List specParams, IReadingContext context) + { + // .MEAS TRAN name TRIG VAL= [RISE|FALL|CROSS=] [TD=] + // TARG VAL= [RISE|FALL|CROSS=] [TD=] + definition.Type = MeasType.TrigTarg; + + int targIndex = -1; + for (int i = 1; i < specParams.Count; i++) + { + if (specParams[i].Value.ToUpper() == "TARG") + { + targIndex = i; + break; + } + } + + if (targIndex < 0) + { + context.Result.ValidationResult.AddError( + ValidationEntrySource.Reader, + ".MEAS TRIG/TARG: Missing TARG keyword"); + return null; + } + + // Parse trigger section: TRIG [VAL=] [RISE|FALL|CROSS=] [TD=] + var trigParams = specParams.GetRange(1, targIndex - 1); + ParseThresholdSection(trigParams, out Parameter trigSignal, out double trigVal, out EdgeType trigEdge, out int trigEdgeNum, out double? trigTd, context); + + definition.TrigSignal = trigSignal; + definition.TrigVal = trigVal; + definition.TrigEdge = trigEdge; + definition.TrigEdgeNumber = trigEdgeNum; + definition.TrigTd = trigTd; + + // Parse target section: TARG [VAL=] [RISE|FALL|CROSS=] [TD=] + var targParams = specParams.GetRange(targIndex + 1, specParams.Count - targIndex - 1); + ParseThresholdSection(targParams, out Parameter targSignal, out double targVal, out EdgeType targEdge, out int targEdgeNum, out double? targTd, context); + + definition.TargSignal = targSignal; + definition.TargVal = targVal; + definition.TargEdge = targEdge; + definition.TargEdgeNumber = targEdgeNum; + definition.TargTd = targTd; + + return definition; + } + + private void ParseThresholdSection( + List parameters, + out Parameter signal, + out double val, + out EdgeType edge, + out int edgeNumber, + out double? td, + IReadingContext context) + { + signal = null; + val = 0; + edge = EdgeType.Cross; + edgeNumber = 1; + td = null; + + foreach (var param in parameters) + { + if (param is AssignmentParameter ap) + { + switch (ap.Name.ToUpper()) + { + case "VAL": + val = context.Evaluator.EvaluateDouble(ap.Value); + break; + case "RISE": + edge = EdgeType.Rise; + edgeNumber = (int)context.Evaluator.EvaluateDouble(ap.Value); + break; + case "FALL": + edge = EdgeType.Fall; + edgeNumber = (int)context.Evaluator.EvaluateDouble(ap.Value); + break; + case "CROSS": + edge = EdgeType.Cross; + edgeNumber = (int)context.Evaluator.EvaluateDouble(ap.Value); + break; + case "TD": + td = context.Evaluator.EvaluateDouble(ap.Value); + break; + } + } + else if (param is BracketParameter || param is ReferenceParameter) + { + signal = param; + } + else if (signal == null) + { + signal = param; + } + } + } + + private MeasurementDefinition ParseWhen(MeasurementDefinition definition, List specParams, IReadingContext context) + { + // .MEAS TRAN name WHEN V(out)=0.5 [RISE|FALL|CROSS=] + // .MEAS TRAN name WHEN V(out) VAL=0.5 [RISE|FALL|CROSS=] + definition.Type = MeasType.When; + + ParseWhenCondition(specParams, 1, definition, context); + + return definition; + } + + private MeasurementDefinition ParseFindWhen(MeasurementDefinition definition, List specParams, IReadingContext context) + { + // .MEAS TRAN name FIND V(out) WHEN V(in)=0.5 [RISE|FALL|CROSS=] + definition.Type = MeasType.FindWhen; + + // Find the WHEN keyword + int whenIndex = -1; + for (int i = 1; i < specParams.Count; i++) + { + if (specParams[i].Value.ToUpper() == "WHEN") + { + whenIndex = i; + break; + } + } + + if (whenIndex < 0) + { + context.Result.ValidationResult.AddError( + ValidationEntrySource.Reader, + ".MEAS FIND: Missing WHEN keyword"); + return null; + } + + // The FIND signal is between FIND keyword and WHEN keyword + for (int i = 1; i < whenIndex; i++) + { + if (specParams[i] is BracketParameter || specParams[i] is ReferenceParameter) + { + definition.FindSignal = specParams[i]; + break; + } + else if (definition.FindSignal == null) + { + definition.FindSignal = specParams[i]; + } + } + + // Parse the WHEN condition + var whenParams = specParams.GetRange(whenIndex, specParams.Count - whenIndex); + ParseWhenCondition(whenParams, 1, definition, context); + + return definition; + } + + private void ParseWhenCondition(List specParams, int startIndex, MeasurementDefinition definition, IReadingContext context) + { + if (startIndex >= specParams.Count) + { + return; + } + + var param = specParams[startIndex]; + + // Combined syntax: WHEN V(out)=0.5 — parsed as AssignmentParameter + if (param is AssignmentParameter ap) + { + // AssignmentParameter: Name = "V", Arguments = ["out"], Values = ["0.5"] + if (ap.HasFunctionSyntax || (ap.Arguments != null && ap.Arguments.Count > 0)) + { + // Reconstruct signal as a BracketParameter for export generation + var signalParams = new ParameterCollection(new List()); + if (ap.Arguments != null) + { + foreach (var arg in ap.Arguments) + { + signalParams.Add(new WordParameter(arg, null)); + } + } + + definition.WhenSignal = new BracketParameter(ap.Name, signalParams, null); + definition.WhenVal = context.Evaluator.EvaluateDouble(ap.Value); + } + else + { + // Simple assignment: just treat name as signal name + definition.WhenSignal = new WordParameter(ap.Name, null); + definition.WhenVal = context.Evaluator.EvaluateDouble(ap.Value); + } + } + else if (param is BracketParameter bp) + { + // Separate syntax: WHEN V(out) VAL=0.5 CROSS=1 + definition.WhenSignal = bp; + + // Parse remaining qualifiers + for (int i = startIndex + 1; i < specParams.Count; i++) + { + if (specParams[i] is AssignmentParameter qap) + { + switch (qap.Name.ToUpper()) + { + case "VAL": + definition.WhenVal = context.Evaluator.EvaluateDouble(qap.Value); + break; + case "RISE": + definition.WhenEdge = EdgeType.Rise; + definition.WhenEdgeNumber = (int)context.Evaluator.EvaluateDouble(qap.Value); + break; + case "FALL": + definition.WhenEdge = EdgeType.Fall; + definition.WhenEdgeNumber = (int)context.Evaluator.EvaluateDouble(qap.Value); + break; + case "CROSS": + definition.WhenEdge = EdgeType.Cross; + definition.WhenEdgeNumber = (int)context.Evaluator.EvaluateDouble(qap.Value); + break; + case "FROM": + definition.From = context.Evaluator.EvaluateDouble(qap.Value); + break; + case "TO": + definition.To = context.Evaluator.EvaluateDouble(qap.Value); + break; + } + } + } + } + else + { + // Fallback: treat as word parameter for signal name + definition.WhenSignal = param; + } + + // Parse edge qualifiers after the combined syntax + for (int i = startIndex + 1; i < specParams.Count; i++) + { + if (specParams[i] is AssignmentParameter edgeAp) + { + switch (edgeAp.Name.ToUpper()) + { + case "RISE": + definition.WhenEdge = EdgeType.Rise; + definition.WhenEdgeNumber = (int)context.Evaluator.EvaluateDouble(edgeAp.Value); + break; + case "FALL": + definition.WhenEdge = EdgeType.Fall; + definition.WhenEdgeNumber = (int)context.Evaluator.EvaluateDouble(edgeAp.Value); + break; + case "CROSS": + definition.WhenEdge = EdgeType.Cross; + definition.WhenEdgeNumber = (int)context.Evaluator.EvaluateDouble(edgeAp.Value); + break; + case "FROM": + definition.From = context.Evaluator.EvaluateDouble(edgeAp.Value); + break; + case "TO": + definition.To = context.Evaluator.EvaluateDouble(edgeAp.Value); + break; + } + } + } + } + + private MeasurementDefinition ParseStatistical(MeasurementDefinition definition, MeasType type, List specParams, IReadingContext context) + { + // .MEAS TRAN name MAX V(out) [FROM=] [TO=] + definition.Type = type; + + // Signal is the first non-keyword parameter + for (int i = 1; i < specParams.Count; i++) + { + var param = specParams[i]; + if (param is AssignmentParameter ap) + { + switch (ap.Name.ToUpper()) + { + case "FROM": + definition.From = context.Evaluator.EvaluateDouble(ap.Value); + break; + case "TO": + definition.To = context.Evaluator.EvaluateDouble(ap.Value); + break; + } + } + else if (definition.Signal == null) + { + definition.Signal = param; + } + } + + return definition; + } + + private MeasurementDefinition ParseDeriv(MeasurementDefinition definition, List specParams, IReadingContext context) + { + // .MEAS TRAN name DERIV V(out) AT= + definition.Type = MeasType.Deriv; + + for (int i = 1; i < specParams.Count; i++) + { + var param = specParams[i]; + if (param is AssignmentParameter ap) + { + switch (ap.Name.ToUpper()) + { + case "AT": + definition.At = context.Evaluator.EvaluateDouble(ap.Value); + break; + case "FROM": + definition.From = context.Evaluator.EvaluateDouble(ap.Value); + break; + case "TO": + definition.To = context.Evaluator.EvaluateDouble(ap.Value); + break; + } + } + else if (definition.Signal == null) + { + definition.Signal = param; + } + } + + return definition; + } + + private MeasurementDefinition ParseParam(MeasurementDefinition definition, List specParams) + { + // .MEAS TRAN name PARAM='expression' or PARAM {expression} + definition.Type = MeasType.Param; + + // Case 1: PARAM='expr' parsed as AssignmentParameter — expression is in the Value + if (specParams[0] is AssignmentParameter ap) + { + string expr = ap.Value; + if (expr.StartsWith("'") && expr.EndsWith("'")) + { + expr = expr.Substring(1, expr.Length - 2); + } + + definition.ParamExpression = expr; + } + else if (specParams.Count > 1) + { + // Case 2: PARAM 'expression' — separate parameters + string expr = specParams[1].Value; + if (expr.StartsWith("'") && expr.EndsWith("'")) + { + expr = expr.Substring(1, expr.Length - 2); + } + + definition.ParamExpression = expr; + } + + return definition; + } + } +} diff --git a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/Controls/Measurements/MeasurementDefinition.cs b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/Controls/Measurements/MeasurementDefinition.cs new file mode 100644 index 00000000..35f48b06 --- /dev/null +++ b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/Controls/Measurements/MeasurementDefinition.cs @@ -0,0 +1,153 @@ +using SpiceSharpParser.Models.Netlist.Spice.Objects; + +namespace SpiceSharpParser.ModelReaders.Netlist.Spice.Readers.Controls.Measurements +{ + /// + /// The type of measurement to perform. + /// + public enum MeasType + { + /// Measure time/x between trigger and target threshold crossings. + TrigTarg, + + /// Find x-value where a condition is met. + When, + + /// Find the value of one signal at the x-value where another signal crosses a threshold. + FindWhen, + + /// Find the minimum value of a signal. + Min, + + /// Find the maximum value of a signal. + Max, + + /// Compute the average value of a signal. + Avg, + + /// Compute the RMS value of a signal. + Rms, + + /// Compute the peak-to-peak value of a signal. + Pp, + + /// Integrate a signal over the simulation range. + Integ, + + /// Compute the derivative of a signal at a point. + Deriv, + + /// Compute a parameter expression from other measurement results. + Param, + } + + /// + /// The type of edge to detect for threshold crossings. + /// + public enum EdgeType + { + /// Rising edge (signal crosses threshold going up). + Rise, + + /// Falling edge (signal crosses threshold going down). + Fall, + + /// Any crossing (rising or falling). + Cross, + } + + /// + /// Represents the parsed definition of a .MEAS/.MEASURE statement. + /// + public class MeasurementDefinition + { + /// + /// Gets or sets the user-defined measurement name. + /// + public string Name { get; set; } + + /// + /// Gets or sets the analysis type (e.g. "TRAN", "AC", "DC", "NOISE", "OP"). + /// + public string AnalysisType { get; set; } + + /// + /// Gets or sets the type of measurement. + /// + public MeasType Type { get; set; } + + // --- TRIG/TARG properties --- + + /// Gets or sets the trigger signal parameter. + public Parameter TrigSignal { get; set; } + + /// Gets or sets the trigger threshold value. + public double TrigVal { get; set; } + + /// Gets or sets the trigger edge type. + public EdgeType TrigEdge { get; set; } = EdgeType.Cross; + + /// Gets or sets the trigger edge number (1-based). + public int TrigEdgeNumber { get; set; } = 1; + + /// Gets or sets the trigger time delay offset. + public double? TrigTd { get; set; } + + /// Gets or sets the target signal parameter. + public Parameter TargSignal { get; set; } + + /// Gets or sets the target threshold value. + public double TargVal { get; set; } + + /// Gets or sets the target edge type. + public EdgeType TargEdge { get; set; } = EdgeType.Cross; + + /// Gets or sets the target edge number (1-based). + public int TargEdgeNumber { get; set; } = 1; + + /// Gets or sets the target time delay offset. + public double? TargTd { get; set; } + + // --- WHEN properties --- + + /// Gets or sets the WHEN signal parameter. + public Parameter WhenSignal { get; set; } + + /// Gets or sets the WHEN threshold value. + public double WhenVal { get; set; } + + /// Gets or sets the WHEN edge type. + public EdgeType WhenEdge { get; set; } = EdgeType.Cross; + + /// Gets or sets the WHEN edge number (1-based). + public int WhenEdgeNumber { get; set; } = 1; + + // --- FIND/WHEN properties --- + + /// Gets or sets the FIND signal parameter (signal to evaluate at the WHEN crossing point). + public Parameter FindSignal { get; set; } + + // --- Windowing --- + + /// Gets or sets the FROM bound of the measurement window (inclusive). + public double? From { get; set; } + + /// Gets or sets the TO bound of the measurement window (inclusive). + public double? To { get; set; } + + // --- DERIV --- + + /// Gets or sets the AT point for DERIV measurements. + public double? At { get; set; } + + // --- Statistical measurement signal --- + + /// Gets or sets the signal parameter for MIN/MAX/AVG/RMS/PP/INTEG/DERIV measurements. + public Parameter Signal { get; set; } + + // --- PARAM --- + + /// Gets or sets the expression string for PARAM measurements. + public string ParamExpression { get; set; } + } +} diff --git a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/Controls/Measurements/MeasurementEvaluator.cs b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/Controls/Measurements/MeasurementEvaluator.cs new file mode 100644 index 00000000..87728104 --- /dev/null +++ b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/Controls/Measurements/MeasurementEvaluator.cs @@ -0,0 +1,466 @@ +using System; +using System.Collections.Generic; + +namespace SpiceSharpParser.ModelReaders.Netlist.Spice.Readers.Controls.Measurements +{ + /// + /// Collects simulation data points and computes measurement results. + /// One instance is created per simulation per measurement, so there are no thread-safety concerns. + /// + public class MeasurementEvaluator + { + private readonly List<(double X, double Y)> _data = new List<(double X, double Y)>(); + private readonly List<(double X, double Y)> _findData = new List<(double X, double Y)>(); + + /// + /// Gets the measurement definition being evaluated. + /// + public MeasurementDefinition Definition { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The measurement definition. + public MeasurementEvaluator(MeasurementDefinition definition) + { + Definition = definition ?? throw new ArgumentNullException(nameof(definition)); + } + + /// + /// Collects a data point for the primary signal. + /// + /// The independent variable (time, frequency, sweep value). + /// The signal value at x. + public void CollectDataPoint(double x, double y) + { + _data.Add((x, y)); + } + + /// + /// Collects a data point for the FIND signal (used in FIND/WHEN measurements). + /// + /// The independent variable. + /// The FIND signal value at x. + public void CollectFindDataPoint(double x, double y) + { + _findData.Add((x, y)); + } + + /// + /// Computes the measurement result from collected data. + /// + /// The name of the simulation. + /// The computed measurement result. + public MeasurementResult ComputeResult(string simulationName) + { + switch (Definition.Type) + { + case MeasType.TrigTarg: + return ComputeTrigTarg(simulationName); + case MeasType.When: + return ComputeWhen(simulationName); + case MeasType.FindWhen: + return ComputeFindWhen(simulationName); + case MeasType.Min: + return ComputeMin(simulationName); + case MeasType.Max: + return ComputeMax(simulationName); + case MeasType.Avg: + return ComputeAvg(simulationName); + case MeasType.Rms: + return ComputeRms(simulationName); + case MeasType.Pp: + return ComputePp(simulationName); + case MeasType.Integ: + return ComputeInteg(simulationName); + case MeasType.Deriv: + return ComputeDeriv(simulationName); + default: + return new MeasurementResult(Definition.Name, double.NaN, false, Definition.Type.ToString().ToUpper(), simulationName); + } + } + + /// + /// Finds the x-value at which the signal crosses the given threshold at the nth specified edge. + /// + internal static double? FindCrossing( + List<(double X, double Y)> data, + double threshold, + EdgeType edgeType, + int edgeNumber, + double? td, + double? fromX, + double? toX) + { + int edgeCount = 0; + + for (int i = 1; i < data.Count; i++) + { + double x0 = data[i - 1].X; + double x1 = data[i].X; + + if (td.HasValue && x1 < td.Value) + { + continue; + } + + if (fromX.HasValue && x1 < fromX.Value) + { + continue; + } + + if (toX.HasValue && x0 > toX.Value) + { + break; + } + + double y0 = data[i - 1].Y - threshold; + double y1 = data[i].Y - threshold; + + if (y0 == 0.0 && y1 == 0.0) + { + continue; + } + + bool crosses = y0 * y1 < 0 || (y0 == 0.0 && y1 != 0.0); + + if (!crosses) + { + continue; + } + + bool isRising = y1 > y0; + + bool matchesEdge; + switch (edgeType) + { + case EdgeType.Rise: + matchesEdge = isRising; + break; + case EdgeType.Fall: + matchesEdge = !isRising; + break; + default: + matchesEdge = true; + break; + } + + if (!matchesEdge) + { + continue; + } + + edgeCount++; + if (edgeCount == edgeNumber) + { + // Linear interpolation to find precise crossing point + double dy = data[i].Y - data[i - 1].Y; + if (Math.Abs(dy) < 1e-30) + { + return data[i].X; + } + + double fraction = (threshold - data[i - 1].Y) / dy; + return data[i - 1].X + fraction * (data[i].X - data[i - 1].X); + } + } + + return null; + } + + /// + /// Interpolates the y-value at a given x from a data series using linear interpolation. + /// + internal static double InterpolateY(List<(double X, double Y)> data, double targetX) + { + if (data.Count == 0) + { + return double.NaN; + } + + if (targetX <= data[0].X) + { + return data[0].Y; + } + + if (targetX >= data[data.Count - 1].X) + { + return data[data.Count - 1].Y; + } + + for (int i = 1; i < data.Count; i++) + { + if (data[i].X >= targetX) + { + double dx = data[i].X - data[i - 1].X; + if (Math.Abs(dx) < 1e-30) + { + return data[i].Y; + } + + double fraction = (targetX - data[i - 1].X) / dx; + return data[i - 1].Y + fraction * (data[i].Y - data[i - 1].Y); + } + } + + return data[data.Count - 1].Y; + } + + private List<(double X, double Y)> GetWindowedData(List<(double X, double Y)> data) + { + if (!Definition.From.HasValue && !Definition.To.HasValue) + { + return data; + } + + var result = new List<(double X, double Y)>(); + foreach (var point in data) + { + if (Definition.From.HasValue && point.X < Definition.From.Value) + { + continue; + } + + if (Definition.To.HasValue && point.X > Definition.To.Value) + { + break; + } + + result.Add(point); + } + + return result; + } + + private MeasurementResult ComputeTrigTarg(string simulationName) + { + double? trigX = FindCrossing(_data, Definition.TrigVal, Definition.TrigEdge, Definition.TrigEdgeNumber, Definition.TrigTd, null, null); + if (!trigX.HasValue) + { + return new MeasurementResult(Definition.Name, double.NaN, false, "TRIG_TARG", simulationName); + } + + double? targX = FindCrossing(_data, Definition.TargVal, Definition.TargEdge, Definition.TargEdgeNumber, Definition.TargTd, null, null); + if (!targX.HasValue) + { + return new MeasurementResult(Definition.Name, double.NaN, false, "TRIG_TARG", simulationName); + } + + double value = targX.Value - trigX.Value; + return new MeasurementResult(Definition.Name, value, true, "TRIG_TARG", simulationName); + } + + private MeasurementResult ComputeWhen(string simulationName) + { + double? crossX = FindCrossing(_data, Definition.WhenVal, Definition.WhenEdge, Definition.WhenEdgeNumber, null, Definition.From, Definition.To); + if (!crossX.HasValue) + { + return new MeasurementResult(Definition.Name, double.NaN, false, "WHEN", simulationName); + } + + return new MeasurementResult(Definition.Name, crossX.Value, true, "WHEN", simulationName); + } + + private MeasurementResult ComputeFindWhen(string simulationName) + { + // Use the primary data (_data) for the WHEN condition + double? crossX = FindCrossing(_data, Definition.WhenVal, Definition.WhenEdge, Definition.WhenEdgeNumber, null, Definition.From, Definition.To); + if (!crossX.HasValue) + { + return new MeasurementResult(Definition.Name, double.NaN, false, "FIND_WHEN", simulationName); + } + + // Interpolate the FIND signal at the crossing x-value + var findSource = _findData.Count > 0 ? _findData : _data; + double value = InterpolateY(findSource, crossX.Value); + return new MeasurementResult(Definition.Name, value, true, "FIND_WHEN", simulationName); + } + + private MeasurementResult ComputeMin(string simulationName) + { + var windowed = GetWindowedData(_data); + if (windowed.Count == 0) + { + return new MeasurementResult(Definition.Name, double.NaN, false, "MIN", simulationName); + } + + double min = double.MaxValue; + foreach (var point in windowed) + { + if (point.Y < min) + { + min = point.Y; + } + } + + return new MeasurementResult(Definition.Name, min, true, "MIN", simulationName); + } + + private MeasurementResult ComputeMax(string simulationName) + { + var windowed = GetWindowedData(_data); + if (windowed.Count == 0) + { + return new MeasurementResult(Definition.Name, double.NaN, false, "MAX", simulationName); + } + + double max = double.MinValue; + foreach (var point in windowed) + { + if (point.Y > max) + { + max = point.Y; + } + } + + return new MeasurementResult(Definition.Name, max, true, "MAX", simulationName); + } + + private MeasurementResult ComputeAvg(string simulationName) + { + var windowed = GetWindowedData(_data); + if (windowed.Count < 2) + { + if (windowed.Count == 1) + { + return new MeasurementResult(Definition.Name, windowed[0].Y, true, "AVG", simulationName); + } + + return new MeasurementResult(Definition.Name, double.NaN, false, "AVG", simulationName); + } + + // Trapezoidal mean: integral(y dx) / (xEnd - xStart) + double integral = 0; + for (int i = 1; i < windowed.Count; i++) + { + double dx = windowed[i].X - windowed[i - 1].X; + integral += (windowed[i].Y + windowed[i - 1].Y) * 0.5 * dx; + } + + double span = windowed[windowed.Count - 1].X - windowed[0].X; + if (Math.Abs(span) < 1e-30) + { + return new MeasurementResult(Definition.Name, windowed[0].Y, true, "AVG", simulationName); + } + + return new MeasurementResult(Definition.Name, integral / span, true, "AVG", simulationName); + } + + private MeasurementResult ComputeRms(string simulationName) + { + var windowed = GetWindowedData(_data); + if (windowed.Count < 2) + { + if (windowed.Count == 1) + { + return new MeasurementResult(Definition.Name, Math.Abs(windowed[0].Y), true, "RMS", simulationName); + } + + return new MeasurementResult(Definition.Name, double.NaN, false, "RMS", simulationName); + } + + // Trapezoidal RMS: sqrt(integral(y^2 dx) / (xEnd - xStart)) + double integral = 0; + for (int i = 1; i < windowed.Count; i++) + { + double dx = windowed[i].X - windowed[i - 1].X; + double y0Sq = windowed[i - 1].Y * windowed[i - 1].Y; + double y1Sq = windowed[i].Y * windowed[i].Y; + integral += (y0Sq + y1Sq) * 0.5 * dx; + } + + double span = windowed[windowed.Count - 1].X - windowed[0].X; + if (Math.Abs(span) < 1e-30) + { + return new MeasurementResult(Definition.Name, Math.Abs(windowed[0].Y), true, "RMS", simulationName); + } + + return new MeasurementResult(Definition.Name, Math.Sqrt(integral / span), true, "RMS", simulationName); + } + + private MeasurementResult ComputePp(string simulationName) + { + var windowed = GetWindowedData(_data); + if (windowed.Count == 0) + { + return new MeasurementResult(Definition.Name, double.NaN, false, "PP", simulationName); + } + + double min = double.MaxValue; + double max = double.MinValue; + foreach (var point in windowed) + { + if (point.Y < min) min = point.Y; + if (point.Y > max) max = point.Y; + } + + return new MeasurementResult(Definition.Name, max - min, true, "PP", simulationName); + } + + private MeasurementResult ComputeInteg(string simulationName) + { + var windowed = GetWindowedData(_data); + if (windowed.Count < 2) + { + return new MeasurementResult(Definition.Name, windowed.Count == 1 ? 0 : double.NaN, windowed.Count == 1, "INTEG", simulationName); + } + + double integral = 0; + for (int i = 1; i < windowed.Count; i++) + { + double dx = windowed[i].X - windowed[i - 1].X; + integral += (windowed[i].Y + windowed[i - 1].Y) * 0.5 * dx; + } + + return new MeasurementResult(Definition.Name, integral, true, "INTEG", simulationName); + } + + private MeasurementResult ComputeDeriv(string simulationName) + { + if (!Definition.At.HasValue) + { + return new MeasurementResult(Definition.Name, double.NaN, false, "DERIV", simulationName); + } + + double targetX = Definition.At.Value; + var windowed = GetWindowedData(_data); + + if (windowed.Count < 2) + { + return new MeasurementResult(Definition.Name, double.NaN, false, "DERIV", simulationName); + } + + // Find the interval containing targetX + for (int i = 1; i < windowed.Count; i++) + { + if (windowed[i].X >= targetX) + { + // Use central difference if possible + if (i > 0 && i < windowed.Count - 1) + { + double dx = windowed[i + 1].X - windowed[i - 1].X; + if (Math.Abs(dx) < 1e-30) + { + return new MeasurementResult(Definition.Name, double.NaN, false, "DERIV", simulationName); + } + + double deriv = (windowed[i + 1].Y - windowed[i - 1].Y) / dx; + return new MeasurementResult(Definition.Name, deriv, true, "DERIV", simulationName); + } + + // Forward/backward difference as fallback + double dxFb = windowed[i].X - windowed[i - 1].X; + if (Math.Abs(dxFb) < 1e-30) + { + return new MeasurementResult(Definition.Name, double.NaN, false, "DERIV", simulationName); + } + + double derivFb = (windowed[i].Y - windowed[i - 1].Y) / dxFb; + return new MeasurementResult(Definition.Name, derivFb, true, "DERIV", simulationName); + } + } + + return new MeasurementResult(Definition.Name, double.NaN, false, "DERIV", simulationName); + } + } +} diff --git a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/Controls/Measurements/MeasurementResult.cs b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/Controls/Measurements/MeasurementResult.cs new file mode 100644 index 00000000..8c64d82d --- /dev/null +++ b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/Controls/Measurements/MeasurementResult.cs @@ -0,0 +1,51 @@ +namespace SpiceSharpParser.ModelReaders.Netlist.Spice.Readers.Controls.Measurements +{ + /// + /// Represents the result of a single .MEAS/.MEASURE evaluation. + /// + public class MeasurementResult + { + /// + /// Initializes a new instance of the class. + /// + /// The measurement name. + /// The computed measurement value. + /// Whether the measurement succeeded. + /// The type of measurement performed. + /// The name of the simulation that produced this result. + public MeasurementResult(string name, double value, bool success, string measurementType, string simulationName) + { + Name = name; + Value = value; + Success = success; + MeasurementType = measurementType; + SimulationName = simulationName; + } + + /// + /// Gets the measurement name (user-defined identifier). + /// + public string Name { get; } + + /// + /// Gets the computed measurement value. + /// + public double Value { get; } + + /// + /// Gets a value indicating whether the measurement was computed successfully. + /// + public bool Success { get; } + + /// + /// Gets the type of measurement that was performed (e.g. "TRIG_TARG", "MIN", "MAX", "AVG", "WHEN", "FIND_WHEN", "RMS", "PP", "INTEG", "DERIV", "PARAM"). + /// + public string MeasurementType { get; } + + /// + /// Gets the name of the simulation that produced this result. + /// Useful for identifying sweep points when used with .STEP. + /// + public string SimulationName { get; } + } +} diff --git a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/SpiceObjectMappings.cs b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/SpiceObjectMappings.cs index 0f6c16cc..588a29ea 100644 --- a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/SpiceObjectMappings.cs +++ b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/SpiceObjectMappings.cs @@ -69,6 +69,8 @@ public SpiceObjectMappings() Controls.Map("SAVE", new SaveControl(Exporters, new ExportFactory())); Controls.Map("PLOT", new PlotControl(Exporters, new ExportFactory())); Controls.Map("PRINT", new PrintControl(Exporters, new ExportFactory())); + Controls.Map("MEAS", new MeasControl(Exporters, new ExportFactory())); + Controls.Map("MEASURE", new MeasControl(Exporters, new ExportFactory())); Controls.Map("IC", new ICControl()); Controls.Map("NODESET", new NodeSetControl()); Controls.Map("WAVE", new WaveControl(Exporters, new ExportFactory())); diff --git a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/SpiceSharpModel.cs b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/SpiceSharpModel.cs index fda6e602..7bc9ac52 100644 --- a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/SpiceSharpModel.cs +++ b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/SpiceSharpModel.cs @@ -3,9 +3,11 @@ using SpiceSharpParser.Common; using SpiceSharpParser.Common.Validation; using SpiceSharpParser.ModelReaders.Netlist.Spice.Readers.Controls.Exporters; +using SpiceSharpParser.ModelReaders.Netlist.Spice.Readers.Controls.Measurements; using SpiceSharpParser.ModelReaders.Netlist.Spice.Readers.Controls.Plots; using SpiceSharpParser.ModelReaders.Netlist.Spice.Readers.Controls.Prints; using SpiceSharpParser.ModelReaders.Netlist.Spice.Readers.Controls.Simulations; +using System.Collections.Concurrent; using System.Collections.Generic; namespace SpiceSharpParser.ModelReaders.Netlist.Spice @@ -66,6 +68,12 @@ public SpiceSharpModel(Circuit circuit, string title) /// public int? Seed { get; set; } + /// + /// Gets the measurement results from .MEAS/.MEASURE statements. + /// Each key is the measurement name, and the list contains one result per simulation (sweep point). + /// + public ConcurrentDictionary> Measurements { get; } = new ConcurrentDictionary>(); + /// /// Gets the validation result. /// diff --git a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/SpiceStatementsOrderer.cs b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/SpiceStatementsOrderer.cs index 0b99a27b..abe2b049 100644 --- a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/SpiceStatementsOrderer.cs +++ b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/SpiceStatementsOrderer.cs @@ -8,7 +8,7 @@ public class SpiceStatementsOrderer : ISpiceStatementsOrderer { protected List TopControls { get; set; } = new List { "st_r", "step_r", "param", "sparam", "func", "options", "distribution" }; - protected List ControlsAfterComponents { get; set; } = new List { "plot", "print", "save", "wave" }; + protected List ControlsAfterComponents { get; set; } = new List { "plot", "print", "save", "wave", "meas", "measure" }; protected List Controls { get; set; } = new List { "temp", "step", "st", "nodeset", "ic", "mc", "op", "ac", "tran", "dc", "noise" }; diff --git a/src/docs/articles/ac.md b/src/docs/articles/ac.md new file mode 100644 index 00000000..e7ac06d0 --- /dev/null +++ b/src/docs/articles/ac.md @@ -0,0 +1,70 @@ +# .AC Statement + +The `.AC` statement defines an AC small-signal frequency analysis. The circuit is first linearized around the DC operating point, then swept over a range of frequencies. + +## Syntax + +``` +.AC DEC +.AC OCT +.AC LIN +``` + +| Parameter | Description | +|-----------|-------------| +| `DEC` | Logarithmic sweep — *points* per decade | +| `OCT` | Logarithmic sweep — *points* per octave | +| `LIN` | Linear sweep — *points* total between fstart and fstop | +| `fstart` | Starting frequency (Hz). Must be > 0 for DEC/OCT | +| `fstop` | Ending frequency (Hz) | + +## Examples + +```spice +* 10 points per decade from 1 Hz to 1 MHz +.AC DEC 10 1 1MEG + +* 100 linearly spaced points from 60 Hz to 10 kHz +.AC LIN 100 60 10K + +* 5 points per octave from 100 Hz to 100 kHz +.AC OCT 5 100 100K +``` + +## AC Sources + +At least one independent source must have an AC specification: + +```spice +V1 IN 0 DC 0 AC 1 0 +``` + +The `AC` keyword is followed by magnitude and optional phase (degrees). + +## Typical Usage + +```spice +Low-pass filter +V1 IN 0 AC 1 +R1 IN OUT 1k +C1 OUT 0 1u +.AC DEC 10 1 1MEG +.SAVE V(OUT) +.END +``` + +## C# API + +```csharp +var parser = new SpiceNetlistParser(); +var result = parser.ParseNetlist(netlist); +var reader = new SpiceSharpReader(); +var model = reader.Read(result.FinalModel); + +var sim = model.Simulations.Single(); // AC simulation +var vout = model.Exports.Find(e => e.Name == "V(OUT)"); +sim.EventExportData += (s, args) => +{ + Console.WriteLine(vout.Extract()); +}; +``` diff --git a/src/docs/articles/appendmodel.md b/src/docs/articles/appendmodel.md new file mode 100644 index 00000000..4592ad96 --- /dev/null +++ b/src/docs/articles/appendmodel.md @@ -0,0 +1,24 @@ +# .APPENDMODEL Statement + +The `.APPENDMODEL` statement appends additional parameters to an existing model definition, typically used to add tolerance or distribution information for Monte Carlo analysis. + +## Syntax + +``` +.APPENDMODEL (= [= ...]) +``` + +## Example + +```spice +* Define base model +.model 1N914 D(Is=2.52e-9 Rs=0.568 N=1.752) + +* Append tolerance info +.APPENDMODEL 1N914 D (Is=LOT=10% Rs=LOT=5%) +``` + +## Notes + +- The model must already be defined before `.APPENDMODEL` is used. +- This statement is primarily used with Monte Carlo (`.MC`) to add lot-to-lot and device-to-device variation parameters. diff --git a/src/docs/articles/behavioral-source.md b/src/docs/articles/behavioral-source.md new file mode 100644 index 00000000..eb93c18f --- /dev/null +++ b/src/docs/articles/behavioral-source.md @@ -0,0 +1,55 @@ +# B — Arbitrary Behavioral Source + +The behavioral source creates a voltage or current source whose value is defined by a mathematical expression. This is part of analog behavioral modeling (ABM). + +## Syntax + +``` +B V={} +B I={} +``` + +| Parameter | Description | +|-----------|-------------| +| `node+`, `node-` | Terminal nodes | +| `V={expr}` | Behavioral voltage source | +| `I={expr}` | Behavioral current source | + +## Examples + +```spice +* Voltage source: half the input +B1 OUT 0 V={V(IN)*0.5} + +* Current source: proportional to voltage +B2 OUT 0 I={V(CTRL)*1m} + +* Full-wave rectifier +B3 OUT 0 V={abs(V(IN))} + +* Conditional (comparator) +B4 OUT 0 V={if(V(IN)>2.5, 5, 0)} + +* Multiplier +B5 OUT 0 V={V(A)*V(B)} +``` + +## Expressions + +The expression can use: + +- Node voltages: `V(node)`, `V(node1, node2)` +- Branch currents: `I(Vsource)` +- Mathematical functions: `abs()`, `sqrt()`, `exp()`, `log()`, `sin()`, `cos()`, `if()`, `min()`, `max()`, etc. +- Arithmetic operators: `+`, `-`, `*`, `/`, `**` (power) +- Parameters defined with `.PARAM` + +## Supported ABM Forms + +SpiceSharpParser supports these analog behavioral modeling constructs: + +| Form | Description | +|------|-------------| +| `VALUE={expr}` | Expression-based source (equivalent to `V=` or `I=`) | +| `TABLE={expr}` | Lookup table with piecewise-linear interpolation | +| `POLY(n)` | Polynomial transfer function | diff --git a/src/docs/articles/bjt.md b/src/docs/articles/bjt.md new file mode 100644 index 00000000..f3663a39 --- /dev/null +++ b/src/docs/articles/bjt.md @@ -0,0 +1,74 @@ +# Q — Bipolar Junction Transistor (BJT) + +The BJT is a three-terminal semiconductor device used for amplification and switching. + +## Syntax + +``` +Q [] [OFF] [IC=[,]] +``` + +| Parameter | Description | +|-----------|-------------| +| `collector` | Collector node | +| `base` | Base node | +| `emitter` | Emitter node | +| `model_name` | Name of a `.MODEL NPN(...)` or `.MODEL PNP(...)` definition | +| `area` | Area factor | +| `OFF` | Initial guess: device is off | +| `IC=vbe[,vce]` | Initial junction voltages for `UIC` | + +## Examples + +```spice +Q1 C B E 2N3904 +Q2 OUT BASE 0 NPN_MODEL 2.0 +Q3 C B E PNP_MOD IC=0.7,5.0 +``` + +## Model Definition + +### NPN + +```spice +.MODEL 2N3904 NPN(Is=6.734f Bf=416.4 Br=0.7374 Cje=3.638p Cjc=4.493p) +``` + +### PNP + +```spice +.MODEL 2N3906 PNP(Is=1.41f Bf=180 Br=4 Cje=9.7p Cjc=18p) +``` + +### Common Model Parameters + +| Parameter | Description | +|-----------|-------------| +| `Is` | Transport saturation current | +| `Bf` | Ideal forward current gain (β) | +| `Br` | Ideal reverse current gain | +| `Nf` | Forward current emission coefficient | +| `Nr` | Reverse current emission coefficient | +| `Cje` | Base-emitter zero-bias capacitance | +| `Cjc` | Base-collector zero-bias capacitance | +| `Vaf` | Forward Early voltage | +| `Var` | Reverse Early voltage | +| `Rb` | Base resistance | +| `Rc` | Collector resistance | +| `Re` | Emitter resistance | + +## Typical Usage + +```spice +Common-emitter amplifier +VCC VCC 0 12 +V1 IN 0 DC 0 AC 1m +R1 VCC OUT 4.7k +R2 IN BASE 100k +R3 BASE 0 22k +Q1 OUT BASE 0 2N3904 +.model 2N3904 NPN(Is=6.734f Bf=416.4) +.AC DEC 10 10 10MEG +.SAVE V(OUT) +.END +``` diff --git a/src/docs/articles/capacitor.md b/src/docs/articles/capacitor.md new file mode 100644 index 00000000..e4b29262 --- /dev/null +++ b/src/docs/articles/capacitor.md @@ -0,0 +1,46 @@ +# C — Capacitor + +A capacitor stores energy in an electric field between two conductors. + +## Syntax + +``` +C [IC=] [M=] +C [L=] [W=] [IC=] +C VALUE={} +``` + +| Parameter | Description | +|-----------|-------------| +| `node+`, `node-` | Positive and negative terminal nodes | +| `value` | Capacitance in farads | +| `IC=v` | Initial voltage across the capacitor (for `UIC`) | +| `TC=tc1[,tc2]` | Temperature coefficients | +| `M=m` | Multiplier | +| `model_name` | Reference to a `.MODEL` definition | +| `VALUE={expr}` | Behavioral expression | + +## Examples + +```spice +* Basic capacitor +C1 OUT 0 1u + +* With initial condition +C2 OUT 0 100n IC=5 + +* Picofarads +C3 A B 10p + +* Model-based +C4 IN OUT cmod L=10u W=1u IC=1 + +* Behavioral +C5 IN OUT VALUE={M*N*1p} +``` + +## Model Definition + +```spice +.MODEL cmod C(CJ=1e-12) +``` diff --git a/src/docs/articles/cccs.md b/src/docs/articles/cccs.md new file mode 100644 index 00000000..cde40f82 --- /dev/null +++ b/src/docs/articles/cccs.md @@ -0,0 +1,39 @@ +# F — Current-Controlled Current Source (CCCS) + +A current-controlled current source produces an output current proportional to a controlling current (measured through a voltage source). + +## Syntax + +### Linear + +``` +F [M=] +``` + +### Polynomial + +``` +F POLY() +``` + +| Parameter | Description | +|-----------|-------------| +| `out+`, `out-` | Output nodes (current flows from out+ to out-) | +| `Vcontrol` | Name of a voltage source sensing the control current | +| `gain` | Current gain (Iout = gain × Ictrl) | +| `M=m` | Multiplier | + +## Examples + +```spice +* Simple current mirror with gain 2 +F1 OUT 0 Vsense 2 + +* Requires a 0V sense source in the control path +Vsense CTRL_A CTRL_B 0 +``` + +## Notes + +- The controlling current is the current through the named voltage source. +- A 0V voltage source is commonly used as a current sensor (ammeter). diff --git a/src/docs/articles/ccvs.md b/src/docs/articles/ccvs.md new file mode 100644 index 00000000..c7eeaf08 --- /dev/null +++ b/src/docs/articles/ccvs.md @@ -0,0 +1,38 @@ +# H — Current-Controlled Voltage Source (CCVS) + +A current-controlled voltage source produces an output voltage proportional to a controlling current (measured through a voltage source). + +## Syntax + +### Linear + +``` +H +``` + +### Polynomial + +``` +H POLY() +``` + +| Parameter | Description | +|-----------|-------------| +| `out+`, `out-` | Output nodes | +| `Vcontrol` | Name of a voltage source sensing the control current | +| `transimpedance` | Gain in ohms (Vout = Rm × Ictrl) | + +## Examples + +```spice +* Transimpedance of 500 ohms +H1 OUT 0 Vsense 500 + +* Requires a 0V sense source +Vsense A B 0 +``` + +## Notes + +- Like the `F` source, the controlling current is measured through a named voltage source. +- A 0V voltage source acts as an ideal ammeter. diff --git a/src/docs/articles/current-source.md b/src/docs/articles/current-source.md new file mode 100644 index 00000000..75beb940 --- /dev/null +++ b/src/docs/articles/current-source.md @@ -0,0 +1,54 @@ +# I — Independent Current Source + +An independent current source provides a specified current between two nodes. It supports DC, AC, and time-domain waveforms, similar to voltage sources. + +## Syntax + +``` +I [DC ] [AC []] [] [M=] +``` + +| Parameter | Description | +|-----------|-------------| +| `node+`, `node-` | Current flows from node+ through the source to node- | +| `DC ` | DC current value | +| `AC [phase]` | AC magnitude and phase | +| `waveform` | Time-domain waveform (PULSE, SIN, PWL, SFFM, AM) | +| `M=m` | Multiplier (equivalent to m parallel sources) | + +## Examples + +```spice +* DC current source: 1mA +I1 VCC OUT DC 1m + +* AC current source +I2 IN 0 AC 1m 0 + +* Pulse current source +I3 A B PULSE(0 10m 0 1n 1n 5u 10u) + +* Sinusoidal current +I4 IN 0 SIN(0 5m 1k) + +* With multiplier +I5 A B DC 1m M=4 +``` + +## Waveform Types + +Current sources support the same waveform types as voltage sources: + +- `PULSE` — Rectangular pulse train +- `SIN` / `SINE` — Sinusoidal +- `PWL` — Piecewise linear +- `SFFM` — Single-frequency FM +- `AM` — Amplitude modulation + +See the [V — Voltage Source](voltage-source.md) article for waveform syntax details. + +## Behavioral Current Source + +```spice +I1 OUT 0 VALUE={V(CTRL)*1m} +``` diff --git a/src/docs/articles/current-switch.md b/src/docs/articles/current-switch.md new file mode 100644 index 00000000..5446fca7 --- /dev/null +++ b/src/docs/articles/current-switch.md @@ -0,0 +1,39 @@ +# W — Current Switch + +A current-controlled switch changes between high and low resistance states based on the current through a controlling voltage source. + +## Syntax + +``` +W [ON|OFF] [M=] +``` + +| Parameter | Description | +|-----------|-------------| +| `node1`, `node2` | Switch terminal nodes | +| `Vcontrol` | Name of voltage source sensing the control current | +| `model_name` | Name of a `.MODEL ISWITCH(...)` definition | +| `ON` / `OFF` | Initial state | +| `M=m` | Multiplier | + +## Example + +```spice +W1 OUT 0 Vsense ISMOD +Vsense CTRL_A CTRL_B 0 +.MODEL ISMOD ISWITCH(IT=1m IH=0.1m RON=1 ROFF=1MEG) +``` + +## Model Parameters + +| Parameter | Description | +|-----------|-------------| +| `IT` | Threshold current | +| `IH` | Hysteresis current | +| `RON` | On-state resistance | +| `ROFF` | Off-state resistance | + +## Notes + +- A 0V voltage source is used as a current sensor in the control path. +- Behavior is analogous to the voltage switch (S), but controlled by current instead of voltage. diff --git a/src/docs/articles/dc.md b/src/docs/articles/dc.md new file mode 100644 index 00000000..6d36166e --- /dev/null +++ b/src/docs/articles/dc.md @@ -0,0 +1,62 @@ +# .DC Statement + +The `.DC` statement defines a DC sweep analysis. One or more independent source values are swept over a range while the DC operating point is computed at each step. + +## Syntax + +``` +.DC [ ] +``` + +| Parameter | Description | +|-----------|-------------| +| `srcname` | Name of the independent source to sweep (e.g., `V1`) | +| `vstart` | Starting value | +| `vstop` | Ending value | +| `vincr` | Increment step size | + +A second source can be specified for nested (double) sweeps. + +## Examples + +```spice +* Single sweep: V1 from -5V to 5V in 0.1V steps +.DC V1 -5 5 0.1 + +* Double sweep: V1 from 0 to 5V, for each value of V2 from 0 to 3V +.DC V1 0 5 0.1 V2 0 3 1 +``` + +## Typical Usage + +```spice +IV Characteristic +V1 in 0 0 +R1 in 0 10 +.DC V1 -10 10 1e-3 +.SAVE V(in) I(V1) +.END +``` + +## Diode IV Curve + +```spice +Diode characteristic +D1 OUT 0 1N914 +V1 OUT 0 0 +.model 1N914 D(Is=2.52e-9 Rs=0.568 N=1.752) +.DC V1 -1 1 10e-3 +.SAVE I(V1) +.END +``` + +## C# API + +```csharp +var sim = model.Simulations.Single(); // DC simulation +var export = model.Exports.Find(e => e.Name == "I(V1)"); +sim.EventExportData += (s, args) => +{ + Console.WriteLine(export.Extract()); +}; +``` diff --git a/src/docs/articles/diode.md b/src/docs/articles/diode.md new file mode 100644 index 00000000..bb2670cd --- /dev/null +++ b/src/docs/articles/diode.md @@ -0,0 +1,58 @@ +# D — Diode + +The diode is a two-terminal semiconductor device that allows current to flow primarily in one direction. + +## Syntax + +``` +D [] [OFF] [IC=] +``` + +| Parameter | Description | +|-----------|-------------| +| `anode` | Anode node (positive terminal) | +| `cathode` | Cathode node (negative terminal) | +| `model_name` | Name of a `.MODEL D(...)` definition | +| `area` | Area factor (multiplier for current capacity) | +| `OFF` | Initial guess: device is off | +| `IC=vd` | Initial diode voltage for `UIC` | + +## Examples + +```spice +D1 ANODE CATHODE 1N914 +D2 IN OUT MyDiode 2.0 +D3 A B DMOD OFF +``` + +## Model Definition + +```spice +.MODEL 1N914 D(Is=2.52e-9 Rs=0.568 N=1.752 Cjo=4e-12 M=0.4 tt=20e-9) +``` + +### Common Model Parameters + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `Is` | Saturation current | 1e-14 A | +| `Rs` | Series resistance | 0 Ω | +| `N` | Emission coefficient | 1 | +| `Cjo` | Zero-bias junction capacitance | 0 F | +| `M` | Grading coefficient | 0.5 | +| `Vj` | Junction potential | 1 V | +| `tt` | Transit time | 0 s | +| `BV` | Reverse breakdown voltage | ∞ | +| `IBV` | Current at breakdown | 1e-3 A | + +## Typical Usage + +```spice +Diode IV Characteristic +D1 OUT 0 1N914 +V1 OUT 0 0 +.model 1N914 D(Is=2.52e-9 Rs=0.568 N=1.752) +.DC V1 -1 1 10e-3 +.SAVE I(V1) +.END +``` diff --git a/src/docs/articles/distribution.md b/src/docs/articles/distribution.md new file mode 100644 index 00000000..a8fc538e --- /dev/null +++ b/src/docs/articles/distribution.md @@ -0,0 +1,41 @@ +# .DISTRIBUTION Statement + +The `.DISTRIBUTION` statement defines a custom probability density function (PDF) for use with Monte Carlo analysis. + +## Syntax + +``` +.DISTRIBUTION (,) (,) [(,) ...] +``` + +| Parameter | Description | +|-----------|-------------| +| `name` | Name of the distribution | +| `(x, y)` | Coordinate pairs defining the PDF curve | + +The PDF is defined as a piecewise-linear function through the given (x, y) points. + +## Examples + +```spice +* Uniform distribution from -1 to 1 +.DISTRIBUTION uniform (-1,1) (1,1) + +* Triangular distribution centered at 0 +.DISTRIBUTION triangular (-1,0) (0,1) (1,0) +``` + +## Usage with Monte Carlo + +Set the default distribution for `.MC` analysis: + +```spice +.DISTRIBUTION my_dist (-1,0) (0,2) (1,0) +.OPTIONS DISTRIBUTION=my_dist +.MC 100 TRAN V(OUT) MAX +``` + +## Notes + +- The y-values define relative probability — the simulator normalizes automatically. +- Built-in distributions are available without explicit definition. diff --git a/src/docs/articles/func.md b/src/docs/articles/func.md new file mode 100644 index 00000000..883df78f --- /dev/null +++ b/src/docs/articles/func.md @@ -0,0 +1,61 @@ +# .FUNC Statement + +The `.FUNC` statement defines user functions that can be used in expressions throughout the netlist. + +## Syntax + +``` +.FUNC ([, , ...]) = +``` + +An alternative bracket syntax is also supported: + +``` +.FUNC [, ] +``` + +## Examples + +```spice +* Simple function +.FUNC square(x) = x*x + +* Multi-argument function +.FUNC parallel(r1, r2) = (r1*r2)/(r1+r2) + +* Multiple functions on one line +.FUNC db(x)=20*log10(x) rad(x)=x*3.14159/180 +``` + +## Using Functions + +Functions can be called in any expression context using curly braces: + +```spice +.FUNC parallel(r1, r2) = (r1*r2)/(r1+r2) +R1 IN OUT {parallel(1k, 2k)} +``` + +## Built-in Functions + +SpiceSharpParser includes standard math functions: + +| Function | Description | +|----------|-------------| +| `abs(x)` | Absolute value | +| `sqrt(x)` | Square root | +| `exp(x)` | Exponential | +| `log(x)` | Natural logarithm | +| `log10(x)` | Base-10 logarithm | +| `sin(x)`, `cos(x)`, `tan(x)` | Trigonometric | +| `asin(x)`, `acos(x)`, `atan(x)` | Inverse trigonometric | +| `atan2(y, x)` | Two-argument arctangent | +| `sinh(x)`, `cosh(x)`, `tanh(x)` | Hyperbolic | +| `min(x, y)` | Minimum | +| `max(x, y)` | Maximum | +| `pow(x, y)` | Power | +| `pwr(x, y)` | `sgn(x) * pow(abs(x), y)` | +| `floor(x)` | Floor | +| `ceil(x)` | Ceiling | +| `if(cond, t, f)` | Conditional | +| `limit(x, lo, hi)` | Clamp to range | diff --git a/src/docs/articles/global.md b/src/docs/articles/global.md new file mode 100644 index 00000000..c0527689 --- /dev/null +++ b/src/docs/articles/global.md @@ -0,0 +1,43 @@ +# .GLOBAL Statement + +The `.GLOBAL` statement declares nodes as global, making them visible across subcircuit boundaries. Normally, nodes inside a subcircuit are local and isolated from the parent circuit. + +## Syntax + +``` +.GLOBAL [ ...] +``` + +## Examples + +```spice +* Make power rails global +.GLOBAL VCC GND VDD VSS +``` + +## Typical Usage + +```spice +Global power rails +.GLOBAL VCC + +V1 VCC 0 5 + +.SUBCKT amp IN OUT +R1 IN BASE 10k +Q1 VCC BASE OUT NPN_MODEL +.ENDS amp + +X1 SIG OUTPUT amp +.model NPN_MODEL NPN(Is=1e-14 Bf=100) +.OP +.SAVE V(OUTPUT) +.END +``` + +Without `.GLOBAL VCC`, the `VCC` node inside the subcircuit would be a separate local node. With `.GLOBAL`, it refers to the same node as the top-level `VCC`. + +## Notes + +- Ground (node `0`) is always global — it does not need to be declared. +- Global nodes are useful for power supply rails, clocks, resets, and other signals shared across the entire design. diff --git a/src/docs/articles/ic.md b/src/docs/articles/ic.md new file mode 100644 index 00000000..56b02cea --- /dev/null +++ b/src/docs/articles/ic.md @@ -0,0 +1,42 @@ +# .IC Statement + +The `.IC` statement sets initial node voltages for transient analysis. These values are used when the `UIC` (Use Initial Conditions) keyword is specified on the `.TRAN` statement. + +## Syntax + +``` +.IC V()= [V()= ...] +``` + +| Parameter | Description | +|-----------|-------------| +| `V()` | Node name wrapped in `V()` | +| `value` | Initial voltage at that node | + +## Examples + +```spice +.IC V(OUT)=0 V(MID)=2.5 + +.IC V(1)=5 V(2)=0 V(3)=3.3 +``` + +## Typical Usage + +```spice +Capacitor charging +R1 IN OUT 10k +C1 OUT 0 1u +V1 IN 0 10 +.IC V(OUT)=0 +.TRAN 1e-6 50e-3 UIC +.SAVE V(OUT) +.END +``` + +## Notes + +- `.IC` values are only applied when `UIC` is specified on the `.TRAN` line. +- Without `UIC`, the simulator computes an initial DC operating point instead. +- For initial guesses that assist DC convergence (without `UIC`), use `.NODESET` instead. +- Device-level initial conditions can also be set with the `IC=` parameter on individual components (e.g., `C1 1 0 1u IC=5`). diff --git a/src/docs/articles/if.md b/src/docs/articles/if.md new file mode 100644 index 00000000..4b7b6e3f --- /dev/null +++ b/src/docs/articles/if.md @@ -0,0 +1,65 @@ +# .IF / .ELSE / .ENDIF Statements + +The `.IF` / `.ELSE` / `.ENDIF` statements provide conditional inclusion of netlist sections based on parameter expressions evaluated at parse time. + +## Syntax + +``` +.IF () + ...statements included when condition is true... +.ELSE + ...statements included when condition is false... +.ENDIF +``` + +The `.ELSE` block is optional. + +## Examples + +### Simple Condition + +```spice +.PARAM use_fast=1 + +.IF (use_fast == 1) +R1 IN OUT 100 +.ELSE +R1 IN OUT 10k +.ENDIF +``` + +### Without .ELSE + +```spice +.PARAM add_cap=1 + +.IF (add_cap > 0) +C1 OUT 0 100p +.ENDIF +``` + +### Nested (with Parameters) + +```spice +.PARAM version=2 + +.IF (version == 1) +.INCLUDE "model_v1.lib" +.ELSE +.INCLUDE "model_v2.lib" +.ENDIF +``` + +## Condition Expressions + +The condition can use any expression that evaluates to a numeric value: + +- Non-zero = true +- Zero = false + +Standard comparison operators (`==`, `!=`, `>`, `<`, `>=`, `<=`) and logical operators are supported. + +## Notes + +- Conditions are evaluated at parse time using the current parameter values. +- The conditional blocks can contain any valid SPICE statements: components, models, subcircuits, analysis commands, etc. diff --git a/src/docs/articles/include.md b/src/docs/articles/include.md new file mode 100644 index 00000000..edbacac4 --- /dev/null +++ b/src/docs/articles/include.md @@ -0,0 +1,45 @@ +# .INCLUDE Statement + +The `.INCLUDE` statement includes the contents of another file into the current netlist. The included file is parsed as if its contents were inserted at the location of the `.INCLUDE` statement. + +## Syntax + +``` +.INCLUDE "" +.INCLUDE +``` + +| Parameter | Description | +|-----------|-------------| +| `filepath` | Path to the file to include (absolute or relative to the current netlist) | + +## Examples + +```spice +.INCLUDE "models/diodes.lib" +.INCLUDE transistors.mod +``` + +## Typical Usage + +```spice +Main netlist +.INCLUDE "standard_models.lib" +D1 OUT 0 1N914 +V1 OUT 0 0 +.DC V1 -1 1 10e-3 +.SAVE I(V1) +.END +``` + +Where `standard_models.lib` contains: + +```spice +.model 1N914 D(Is=2.52e-9 Rs=0.568 N=1.752) +``` + +## Notes + +- Included files can contain any valid SPICE statements (models, subcircuits, parameters, etc.). +- Multiple levels of nesting are supported (an included file can itself contain `.INCLUDE`). +- File paths are resolved relative to the current working directory or the location of the including file. diff --git a/src/docs/articles/inductor.md b/src/docs/articles/inductor.md new file mode 100644 index 00000000..1d4ab581 --- /dev/null +++ b/src/docs/articles/inductor.md @@ -0,0 +1,40 @@ +# L — Inductor + +An inductor stores energy in a magnetic field created by current flowing through a coil. + +## Syntax + +``` +L [IC=] +``` + +| Parameter | Description | +|-----------|-------------| +| `node+`, `node-` | Positive and negative terminal nodes | +| `value` | Inductance in henries | +| `IC=i` | Initial current through the inductor (for `UIC`) | + +## Examples + +```spice +* Basic inductor +L1 IN OUT 10u + +* With initial current +L2 A B 1m IC=100m + +* Nanohenries +L3 IN OUT 47n +``` + +## Mutual Inductance + +Inductors can be magnetically coupled using the `K` statement: + +```spice +L1 A 0 10u +L2 B 0 10u +K1 L1 L2 0.95 +``` + +See the [K — Mutual Inductance](mutual-inductance.md) article. diff --git a/src/docs/articles/intro.md b/src/docs/articles/intro.md index c0478ced..bf353f1b 100644 --- a/src/docs/articles/intro.md +++ b/src/docs/articles/intro.md @@ -1 +1,108 @@ -# Add your introductions here! +# Getting Started with SpiceSharpParser + +SpiceSharpParser is a .NET library that parses SPICE netlists and simulates them using [SpiceSharp](https://github.com/SpiceSharp/SpiceSharp). It supports a wide subset of PSpice and LTspice syntax including DC, AC, transient, noise, and operating-point analyses. + +## Installation + +Install from NuGet: + +``` +dotnet add package SpiceSharp-Parser +``` + +Or via the NuGet Package Manager: + +``` +Install-Package SpiceSharp-Parser +``` + +## Quick Example + +```csharp +using System; +using System.Linq; +using SpiceSharpParser; + +var netlist = string.Join(Environment.NewLine, + "Low-pass RC filter", + "V1 IN 0 AC 1", + "R1 IN OUT 1k", + "C1 OUT 0 1u", + ".AC DEC 10 1 1MEG", + ".SAVE V(OUT)", + ".END"); + +// Parse +var parser = new SpiceNetlistParser(); +var parseResult = parser.ParseNetlist(netlist); + +// Translate to SpiceSharp objects +var reader = new SpiceSharpReader(); +var spiceModel = reader.Read(parseResult.FinalModel); + +// Run simulation +var sim = spiceModel.Simulations.Single(); +var export = spiceModel.Exports.Find(e => e.Name == "V(OUT)"); +sim.EventExportData += (sender, args) => Console.WriteLine(export.Extract()); +var codes = sim.Run(spiceModel.Circuit, -1); +codes = sim.InvokeEvents(codes); +codes.ToArray(); +``` + +## Workflow Overview + +Using SpiceSharpParser involves three steps: + +1. **Parse** — convert a SPICE netlist string into a parse-tree model with `SpiceNetlistParser.ParseNetlist()`. +2. **Read** — translate the parse-tree model into SpiceSharp simulation objects (circuit, simulations, exports) with `SpiceSharpReader.Read()`. +3. **Simulate** — run the SpiceSharp simulations and collect results via exports and events. + +### The SpiceSharpModel + +`SpiceSharpReader.Read()` returns a `SpiceSharpModel` containing: + +| Property | Description | +|----------|-------------| +| `Circuit` | The SpiceSharp `Circuit` with all components | +| `Simulations` | List of simulation objects (DC, AC, Transient, etc.) | +| `Exports` | Signal exports created by `.SAVE`, `.PRINT`, `.PLOT` | +| `XyPlots` | Plot data from `.PLOT` statements | +| `Prints` | Tabular data from `.PRINT` statements | +| `Measurements` | Results from `.MEAS` / `.MEASURE` statements | +| `Title` | Netlist title (first line) | + +### Collecting Results + +Export data during simulation using the `EventExportData` event: + +```csharp +var export = spiceModel.Exports.Find(e => e.Name == "V(OUT)"); +sim.EventExportData += (sender, args) => +{ + double value = (double)export.Extract(); + // process value +}; +``` + +### Parameter Sweeps (.STEP) + +When `.STEP` is used, the simulation runs multiple times. Each sweep iteration fires `EventExportData` with appropriate parameter values: + +```csharp +sim.EventExportData += (sender, args) => +{ + Console.WriteLine($"{export.Extract()}"); +}; +``` + +## Supported Features + +SpiceSharpParser supports a comprehensive set of SPICE statements and devices. See the individual documentation articles for details on each: + +- **Analysis**: `.AC`, `.DC`, `.TRAN`, `.OP`, `.NOISE` +- **Output**: `.SAVE`, `.PRINT`, `.PLOT`, `.MEAS` +- **Parameters**: `.PARAM`, `.FUNC`, `.LET`, `.SPARAM` +- **Circuit structure**: `.SUBCKT`, `.INCLUDE`, `.LIB`, `.GLOBAL` +- **Simulation control**: `.STEP`, `.MC`, `.TEMP`, `.OPTIONS`, `.IC`, `.NODESET` +- **Behavioral modeling**: `VALUE`, `TABLE`, `POLY(n)`, `B` sources +- **Devices**: R, L, C, K, D, Q, M, J, V, I, E, F, G, H, B, S, W, T, X diff --git a/src/docs/articles/jfet.md b/src/docs/articles/jfet.md new file mode 100644 index 00000000..223158b0 --- /dev/null +++ b/src/docs/articles/jfet.md @@ -0,0 +1,45 @@ +# J — JFET + +The JFET (Junction Field-Effect Transistor) is a three-terminal semiconductor device controlled by a voltage applied to the gate-source junction. + +## Syntax + +``` +J [] [OFF] [IC=[,]] +``` + +| Parameter | Description | +|-----------|-------------| +| `drain` | Drain node | +| `gate` | Gate node | +| `source` | Source node | +| `model_name` | Name of a `.MODEL NJF(...)` or `.MODEL PJF(...)` definition | +| `area` | Area factor | +| `OFF` | Initial guess: device is off | +| `IC=vds[,vgs]` | Initial junction voltages for `UIC` | + +## Examples + +```spice +J1 DRAIN GATE SOURCE J2N3819 +J2 D G S my_njfet 2.0 +``` + +## Model Definition + +```spice +.MODEL J2N3819 NJF(VTO=-3 BETA=1.304m LAMBDA=2.25m IS=33.57f CGS=2.414p CGD=0.3p) +``` + +### Common Model Parameters + +| Parameter | Description | +|-----------|-------------| +| `VTO` | Pinch-off voltage | +| `BETA` | Transconductance coefficient | +| `LAMBDA` | Channel-length modulation | +| `IS` | Gate junction saturation current | +| `CGS` | Gate-source capacitance | +| `CGD` | Gate-drain capacitance | +| `RD` | Drain resistance | +| `RS` | Source resistance | diff --git a/src/docs/articles/let.md b/src/docs/articles/let.md new file mode 100644 index 00000000..638b1a23 --- /dev/null +++ b/src/docs/articles/let.md @@ -0,0 +1,26 @@ +# .LET Statement + +The `.LET` statement defines a named expression that can be referenced later in the netlist. Unlike `.PARAM`, `.LET` stores the expression itself (not just the evaluated value), making it useful for deferred calculations. + +## Syntax + +``` +.LET +``` + +| Parameter | Description | +|-----------|-------------| +| `name` | Name of the expression | +| `expression` | A mathematical expression (may use `{}` delimiters) | + +## Examples + +```spice +.LET power {V(OUT)*I(V1)} +.LET gain {V(OUT)/V(IN)} +``` + +## Difference from .PARAM + +- `.PARAM` stores a parameter value that is evaluated at parse time and used for component values. +- `.LET` stores a named expression that can be evaluated dynamically during simulation, often used with `.MEAS` or other post-processing. diff --git a/src/docs/articles/lib.md b/src/docs/articles/lib.md new file mode 100644 index 00000000..1163d154 --- /dev/null +++ b/src/docs/articles/lib.md @@ -0,0 +1,53 @@ +# .LIB Statement + +The `.LIB` statement includes a specific library section from a file. Unlike `.INCLUDE`, which inserts the entire file, `.LIB` can selectively include only a named section. + +## Syntax + +``` +.LIB "" [] +``` + +| Parameter | Description | +|-----------|-------------| +| `filepath` | Path to the library file | +| `section_name` | Optional — name of the section to include | + +## Library File Format + +Library files can define named sections: + +```spice +.LIB section_name +...models and subcircuits... +.ENDL section_name +``` + +## Examples + +```spice +* Include specific section +.LIB "device_models.lib" NPN_MODELS + +* Include entire file (behaves like .INCLUDE) +.LIB "all_models.lib" +``` + +## Library File Example + +```spice +* device_models.lib +.LIB NPN_MODELS +.model 2N3904 NPN(Is=6.734f Bf=416.4 Br=.7374) +.model 2N2222 NPN(Is=14.34f Bf=255.9 Br=6.092) +.ENDL NPN_MODELS + +.LIB PNP_MODELS +.model 2N3906 PNP(Is=1.41f Bf=180 Br=4) +.ENDL PNP_MODELS +``` + +## Notes + +- When no section name is given, `.LIB` behaves like `.INCLUDE`. +- Library files are commonly used to distribute device models separately from the main netlist. diff --git a/src/docs/articles/mc.md b/src/docs/articles/mc.md new file mode 100644 index 00000000..42980da6 --- /dev/null +++ b/src/docs/articles/mc.md @@ -0,0 +1,40 @@ +# .MC Statement + +The `.MC` statement configures Monte Carlo analysis, running the simulation multiple times with random parameter variations to assess statistical behavior. + +## Syntax + +``` +.MC [SEED=] +``` + +| Parameter | Description | +|-----------|-------------| +| `runs` | Number of Monte Carlo iterations | +| `analysis_type` | `OP`, `DC`, `TRAN`, `AC`, or `NOISE` | +| `output_variable` | Signal to monitor | +| `function` | Statistical analysis function | +| `SEED=` | Optional random seed for reproducibility | + +## Example + +```spice +Monte Carlo analysis +V1 IN 0 1 +R1 IN OUT 1k +C1 OUT 0 1u +.MC 100 TRAN V(OUT) MAX SEED=42 +.TRAN 1e-6 10e-3 +.SAVE V(OUT) +.END +``` + +## Parameter Tolerances + +Monte Carlo analysis varies parameters that have defined tolerances. Tolerances are specified in model parameters or via distribution definitions (see `.DISTRIBUTION`). + +## Notes + +- The `SEED` parameter ensures repeatable results across runs. +- Use `.DISTRIBUTION` to define custom probability density functions. +- Use `.OPTIONS DISTRIBUTION=` to set the default distribution. diff --git a/src/docs/articles/meas.md b/src/docs/articles/meas.md new file mode 100644 index 00000000..139d45ef --- /dev/null +++ b/src/docs/articles/meas.md @@ -0,0 +1,180 @@ +# .MEAS / .MEASURE Statement + +The `.MEAS` (or `.MEASURE`) statement allows you to extract scalar measurements from simulation results — such as rise time, delay, average voltage, RMS, or custom expressions. Both `.MEAS` and `.MEASURE` are accepted as aliases. + +## General Syntax + +``` +.MEAS +.MEASURE +``` + +- **analysis_type**: `TRAN`, `AC`, `DC`, `OP`, or `NOISE` +- **name**: user-defined measurement name (used to access results) +- **measurement_spec**: one of the measurement types described below + +## Supported Measurement Types + +### TRIG/TARG — Timing Measurements + +Measures the time (or frequency/sweep) difference between a trigger event and a target event. + +``` +.MEAS TRAN rise_time TRIG V(out) VAL=1.0 RISE=1 TARG V(out) VAL=9.0 RISE=1 +.MEAS TRAN tpd TRIG V(in) VAL=2.5 RISE=1 TARG V(out) VAL=2.5 RISE=1 +.MEAS TRAN delayed TRIG V(out) VAL=1.0 RISE=1 TD=5u TARG V(out) VAL=9.0 RISE=1 +``` + +**Qualifiers:** +- `VAL=` — threshold value +- `RISE=` — match the nth rising crossing +- `FALL=` — match the nth falling crossing +- `CROSS=` — match the nth crossing (either direction) +- `TD=