From de4b040d801603bf3bc8b6519bad63ad14e96e86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Go=C5=82=C4=99biowski?= Date: Sat, 4 Jul 2026 16:43:53 +0200 Subject: [PATCH 1/2] FinitePulse --- roadmap/ltspice-compatibility-matrix.md | 2 +- roadmap/ltspice-netlist-compatibility-plan.md | 6 +- .../LTspiceCompatibilityP2Tests.cs | 76 ++++++++++++++- .../Spice/Readers/Waveforms/FinitePulse.cs | 96 +++++++++++++++++++ .../Spice/Readers/Waveforms/PulseGenerator.cs | 70 ++++++++++---- 5 files changed, 228 insertions(+), 22 deletions(-) create mode 100644 src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/Waveforms/FinitePulse.cs diff --git a/roadmap/ltspice-compatibility-matrix.md b/roadmap/ltspice-compatibility-matrix.md index 7e8afbb1..395b95ec 100644 --- a/roadmap/ltspice-compatibility-matrix.md +++ b/roadmap/ltspice-compatibility-matrix.md @@ -53,7 +53,7 @@ Compatibility classes: | Expression `**`, single/double `&` and `|`, unary `!` | Accepted | Parsed as power, boolean AND/OR, and NOT | OP/unit supported | Analytic fixture | None expected | Current parser/runtime | `^` remains existing exponent syntax; LTspice boolean XOR is deferred. | | `uplim(...)`, `dnlim(...)`, unary `~` | Accepted in expression text | Rejected in LTspice mode | Not runnable | None | Targeted LTspice expression diagnostic names construct | Current parser/runtime | Smooth limiting and unary inversion semantics are not mapped yet. | | `EXP(...)` source waveform | Accepted | Produces internal exponential waveform | TRAN supported | Analytic fixture | Invalid count/tau diagnostics name `EXP` | Current parser/runtime | P2 supports the six-argument form `EXP(v1 v2 td1 tau1 td2 tau2)`. | -| `PULSE(... Ncycles)` | Accepted | Rejected in LTspice mode | Not runnable | None | Targeted LTspice `PULSE` cycle-count diagnostic | Current parser/runtime | Finite-cycle waveform behavior is deferred. | +| `PULSE(... Ncycles)` | Accepted in LTspice mode | Produces finite-cycle pulse waveform | TRAN supported | Analytic transient fixture | Invalid period or cycle count produces targeted `PULSE` diagnostic | Current parser/runtime | The eighth LTspice argument limits the waveform to `Ncycles` periods, then returns to the initial value. | | `SINE(... Ncycles)` | Accepted | Rejected in LTspice mode | Not runnable | None | Targeted LTspice `SINE` cycle-count diagnostic | Current parser/runtime | Finite-cycle waveform behavior is deferred. | | Independent source `tbl=(expr,x1,y1,...)` | Accepted in LTspice mode | Lowered to behavioral `table(...)` source | OP supported | Analytic fixture | Invalid form names `tbl` | Current parser/runtime | Parser shim uses the existing source/table expression path. | | Source `wavefile= chan= [amplitude=]` | Accepted | Produces wave-file waveform | Existing wave-file behavior | Smoke/diagnostic fixture | Missing `chan` and missing file produce targeted validation | Current parser/runtime | Channel defaults are not inferred. | diff --git a/roadmap/ltspice-netlist-compatibility-plan.md b/roadmap/ltspice-netlist-compatibility-plan.md index 54adc362..b93d3c68 100644 --- a/roadmap/ltspice-netlist-compatibility-plan.md +++ b/roadmap/ltspice-netlist-compatibility-plan.md @@ -37,7 +37,7 @@ This roadmap is intentionally parser-first and evidence-first: - `.TRAN` accepts traditional numeric forms and trailing `UIC`; P1 LTspice mode also accepts `.tran ` and `.tran UIC` by deriving `step = Tstop / 50.0`. LTspice `startup`, `steady`, `nodiscard`, and `step` modifiers remain targeted errors. - LTspice output/viewer `.options` such as `plotwinsize`, `plotreltol`, `plotvntol`, `plotabstol`, `numdgt`, `measdgt`, `meascplxfmt`, `baudrate`, and `fastaccess` are warning no-ops only in LTspice mode. - LTspice behavior-changing `.options` such as `cshunt`, `gshunt`, `srcsteps`, `gminsteps`, `trtol`, `chgtol`, `pivrel`, `pivtol`, and `ptrantau` remain targeted errors. -- Source waveform mappings cover `SIN` / `SINE`, `PULSE`, `EXP`, `PWL`, `AM`, `SFFM`, and wave-file input. LTspice cycle-count arguments remain targeted errors, wave-file channel defaults are not inferred, and topology-changing independent-source instance options remain targeted errors. +- Source waveform mappings cover `SIN` / `SINE`, `PULSE`, `EXP`, `PWL`, `AM`, `SFFM`, and wave-file input. LTspice finite-cycle `PULSE(... Ncycles)` is supported; finite-cycle `SINE` arguments remain targeted errors, wave-file channel defaults are not inferred, and topology-changing independent-source instance options remain targeted errors. - MOS model generation currently covers legacy levels 1, 2, and 3. LTspice `VDMOS` and advanced monolithic levels such as BSIM/EKV/HiSIM variants are runtime or intentional-unsupported candidates. - Distributed-line support currently starts from lossless `T`. LTspice lossy `O` / `LTRA` and uniform RC-line `URC` models need engine triage before runnable support is claimed. - P3 LTspice mode maps R/C model `tc=a[,b]`, switch `von`/`voff`, and current-switch `ion`/`ioff` aliases where they lower to existing parameters. @@ -209,7 +209,7 @@ Implemented P2 behavior: - Kept `^` as the existing exponent operator; LTspice boolean XOR is deferred to avoid changing current semantics. - Added LTspice-mode targeted diagnostics for `uplim(...)`, `dnlim(...)`, and unary `~`. - Added six-argument `EXP(v1 v2 td1 tau1 td2 tau2)` source waveform support with argument-count and positive-tau diagnostics. -- Added LTspice-mode targeted diagnostics for finite-cycle `PULSE` and `SINE` arguments. +- Added LTspice-mode finite-cycle `PULSE(... Ncycles)` support and targeted diagnostics for unsupported finite-cycle `SINE` arguments. - Added LTspice-mode `tbl=(expr,x1,y1,...)` independent-source lowering to the existing behavioral `table(...)` path. - Improved `wavefile= chan= [amplitude=]` validation so missing `chan` and missing files produce targeted diagnostics. - Added LTspice-mode targeted diagnostics for topology-changing source options `Rser`, `Cpar`, `load`, and `R=`. @@ -218,7 +218,7 @@ Remaining follow-up: - Compare existing random functions with LTspice semantics before making numeric claims. - Decide whether LTspice boolean XOR can be added without breaking existing `^` exponent behavior. -- Defer finite-cycle waveform behavior, broader PWL file variants, and source-option topology synthesis until fixture-backed runtime behavior is specified. +- Defer finite-cycle `SINE` behavior, broader PWL file variants, and source-option topology synthesis until fixture-backed runtime behavior is specified. Acceptance criteria: diff --git a/src/SpiceSharpParser.IntegrationTests/LTspiceCompatibility/LTspiceCompatibilityP2Tests.cs b/src/SpiceSharpParser.IntegrationTests/LTspiceCompatibility/LTspiceCompatibilityP2Tests.cs index eae39310..b62caaae 100644 --- a/src/SpiceSharpParser.IntegrationTests/LTspiceCompatibility/LTspiceCompatibilityP2Tests.cs +++ b/src/SpiceSharpParser.IntegrationTests/LTspiceCompatibility/LTspiceCompatibilityP2Tests.cs @@ -83,7 +83,6 @@ public void When_LtspiceSourceOptionChangesTopology_Expect_TargetedError(string } [Theory] - [InlineData("V1 out 0 PULSE(0 1 0 1n 1n 10n 20n 3)", "PULSE")] [InlineData("V1 out 0 SINE(0 1 1k 0 0 0 3)", "SINE")] public void When_LtspiceWaveformHasCycleCount_Expect_TargetedError(string sourceLine, string waveformName) { @@ -101,6 +100,46 @@ public void When_LtspiceWaveformHasCycleCount_Expect_TargetedError(string source AssertErrorContains(model.ValidationResult, "cycle-count"); } + [Fact] + public void When_LtspicePulseHasCycleCount_Expect_TransientStopsAfterFiniteCycles() + { + var model = GetSpiceSharpModelWithCompatibility( + CompatibilityOptions.LTspice, + "LTspice P2 - finite-cycle PULSE", + "V1 out 0 PULSE(0 1 2n 1n 1n 3n 10n 2)", + "R1 out 0 1k", + ".tran 1n 25n", + ".save V(out)", + ".end"); + + AssertNoValidationIssues(model.ValidationResult); + + var exports = RunTransientSimulation(model, "V(out)"); + Assert.NotEmpty(exports); + Assert.True(EqualsWithTol(exports, FinitePulseReference)); + } + + [Theory] + [InlineData("V1 out 0 PULSE(0 1 0 1n 1n 10n 0 3)", "period")] + [InlineData("V1 out 0 PULSE(0 1 0 1n 1n 10n 20n 0)", "cycle-count")] + public void When_LtspicePulseCycleCountArgumentsAreInvalid_Expect_TargetedError( + string sourceLine, + string expectedMessage) + { + var model = GetSpiceSharpModelWithCompatibility( + CompatibilityOptions.LTspice, + "LTspice P2 - invalid finite-cycle PULSE", + sourceLine, + "R1 out 0 1k", + ".tran 1n 50n", + ".save V(out)", + ".end"); + + Assert.True(model.ValidationResult.HasError); + AssertErrorContains(model.ValidationResult, "PULSE"); + AssertErrorContains(model.ValidationResult, expectedMessage); + } + [Theory] [InlineData("uplim(V(in),0,1)", "uplim")] [InlineData("dnlim(V(in),0,1)", "dnlim")] @@ -158,6 +197,41 @@ private static double ExpReference(double time) return value; } + private static double FinitePulseReference(double time) + { + const double initialValue = 0.0; + const double pulsedValue = 1.0; + const double delay = 2e-9; + const double riseTime = 1e-9; + const double fallTime = 1e-9; + const double pulseWidth = 3e-9; + const double period = 10e-9; + const double cycleCount = 2.0; + + if (time < delay || time >= delay + (period * cycleCount)) + { + return initialValue; + } + + var localTime = (time - delay) % period; + if (localTime < riseTime) + { + return initialValue + ((pulsedValue - initialValue) * localTime / riseTime); + } + + if (localTime < riseTime + pulseWidth) + { + return pulsedValue; + } + + if (localTime < riseTime + pulseWidth + fallTime) + { + return pulsedValue + ((initialValue - pulsedValue) * (localTime - riseTime - pulseWidth) / fallTime); + } + + return initialValue; + } + private static SpiceSharpModel GetSpiceSharpModelWithCompatibility(CompatibilityOptions compatibility, params string[] lines) { var text = string.Join(Environment.NewLine, lines); diff --git a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/Waveforms/FinitePulse.cs b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/Waveforms/FinitePulse.cs new file mode 100644 index 00000000..251a5579 --- /dev/null +++ b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/Waveforms/FinitePulse.cs @@ -0,0 +1,96 @@ +using System; +using SpiceSharp.Components; +using SpiceSharp.Entities; +using SpiceSharp.ParameterSets; +using SpiceSharp.Simulations; + +namespace SpiceSharpParser.ModelReaders.Netlist.Spice.Readers.Waveforms +{ + internal sealed class FinitePulse : ParameterSet, IWaveformDescription + { + public double InitialValue { get; set; } + + public double PulsedValue { get; set; } + + public double Delay { get; set; } + + public double RiseTime { get; set; } + + public double FallTime { get; set; } + + public double PulseWidth { get; set; } + + public double Period { get; set; } + + public double CycleCount { get; set; } + + public IWaveform Create(IBindingContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var pulse = new Pulse + { + InitialValue = InitialValue, + PulsedValue = PulsedValue, + Delay = Delay, + RiseTime = RiseTime, + FallTime = FallTime, + PulseWidth = PulseWidth, + Period = Period, + }; + + return new Instance( + context.GetState(), + pulse.Create(context), + InitialValue, + Delay, + Period, + CycleCount); + } + + private sealed class Instance : IWaveform + { + private readonly IIntegrationMethod _method; + private readonly IWaveform _inner; + private readonly double _initialValue; + private readonly double _stopTime; + + public Instance( + IIntegrationMethod method, + IWaveform inner, + double initialValue, + double delay, + double period, + double cycleCount) + { + _method = method ?? throw new ArgumentNullException(nameof(method)); + _inner = inner ?? throw new ArgumentNullException(nameof(inner)); + _initialValue = initialValue; + _stopTime = delay + (period * cycleCount); + Value = initialValue; + } + + public double Value { get; private set; } + + public void Probe() + { + if (_method.Time >= _stopTime) + { + Value = _initialValue; + return; + } + + _inner.Probe(); + Value = _inner.Value; + } + + public void Accept() + { + _inner.Accept(); + } + } + } +} diff --git a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/Waveforms/PulseGenerator.cs b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/Waveforms/PulseGenerator.cs index d32bd18a..26472b0f 100644 --- a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/Waveforms/PulseGenerator.cs +++ b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/Waveforms/PulseGenerator.cs @@ -33,34 +33,25 @@ public override IWaveformDescription Generate(ParameterCollection parameters, IR throw new ArgumentNullException(nameof(context)); } - var argumentCount = parameters.Count == 1 && parameters[0] is VectorParameter vp - ? vp.Elements.Count - : parameters.Count; + parameters = FlattenVector(parameters); + var argumentCount = parameters.Count; - if (argumentCount > 7) + if (argumentCount > 8 || (argumentCount == 8 && !context.ReaderSettings.Compatibility.IsLTspice)) { - if (context.ReaderSettings.Compatibility.IsLTspice) - { - context.Result.ValidationResult.AddError( - ValidationEntrySource.Reader, - "Unsupported LTspice PULSE cycle-count arguments: finite-cycle PULSE sources are not mapped yet.", - parameters.LineInfo); - return null; - } - context.Result.ValidationResult.AddError( ValidationEntrySource.Reader, "Wrong number of arguments for PULSE waveform", parameters.LineInfo); + return null; } - var w = new Pulse(); - - if (parameters.Count == 1 && parameters[0] is VectorParameter v) + if (argumentCount == 8) { - parameters = new ParameterCollection(v.Elements.Select(e => e).Cast().ToList()); + return GenerateFinitePulse(parameters, context); } + var w = new Pulse(); + if (parameters.Count >= 1) { w.InitialValue = context.EvaluationContext.Evaluator.EvaluateDouble(parameters[0].Value); @@ -98,5 +89,50 @@ public override IWaveformDescription Generate(ParameterCollection parameters, IR return w; } + + private static IWaveformDescription GenerateFinitePulse(ParameterCollection parameters, IReadingContext context) + { + var waveform = new FinitePulse + { + InitialValue = context.EvaluationContext.Evaluator.EvaluateDouble(parameters[0].Value), + PulsedValue = context.EvaluationContext.Evaluator.EvaluateDouble(parameters[1].Value), + Delay = context.EvaluationContext.Evaluator.EvaluateDouble(parameters[2].Value), + RiseTime = context.EvaluationContext.Evaluator.EvaluateDouble(parameters[3].Value), + FallTime = context.EvaluationContext.Evaluator.EvaluateDouble(parameters[4].Value), + PulseWidth = context.EvaluationContext.Evaluator.EvaluateDouble(parameters[5].Value), + Period = context.EvaluationContext.Evaluator.EvaluateDouble(parameters[6].Value), + CycleCount = context.EvaluationContext.Evaluator.EvaluateDouble(parameters[7].Value), + }; + + if (waveform.Period <= 0.0 || double.IsNaN(waveform.Period) || double.IsInfinity(waveform.Period)) + { + context.Result.ValidationResult.AddError( + ValidationEntrySource.Reader, + "LTspice finite-cycle PULSE period must be positive.", + parameters[6].LineInfo); + return null; + } + + if (waveform.CycleCount <= 0.0 || double.IsNaN(waveform.CycleCount) || double.IsInfinity(waveform.CycleCount)) + { + context.Result.ValidationResult.AddError( + ValidationEntrySource.Reader, + "LTspice finite-cycle PULSE cycle-count must be positive.", + parameters[7].LineInfo); + return null; + } + + return waveform; + } + + private static ParameterCollection FlattenVector(ParameterCollection parameters) + { + if (parameters.Count == 1 && parameters[0] is VectorParameter vector) + { + return new ParameterCollection(vector.Elements.Select(element => element).Cast().ToList()); + } + + return parameters; + } } } From e8d815dce24908802250efcc10e4fa1a1f266eac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Go=C5=82=C4=99biowski?= Date: Sat, 4 Jul 2026 16:51:50 +0200 Subject: [PATCH 2/2] Update of README.md --- README.md | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 41677b86..06e4985d 100644 --- a/README.md +++ b/README.md @@ -104,14 +104,32 @@ Using SpiceSharpParser involves three steps: |----------|---------| | Passive | R (resistor), C (capacitor), L (inductor), K (mutual inductance), T (transmission line) | | Semiconductor | D (diode), Q (BJT), M (MOSFET), J (JFET) | -| Sources | V (voltage), I (current) — DC, AC, PULSE, SIN, PWL, SFFM, AM | +| Sources | V (voltage), I (current) — DC, AC, PULSE, SIN / SINE, EXP, PWL, SFFM, AM, wave-file input | | Controlled sources | E (VCVS), F (CCCS), G (VCCS), H (CCVS) | | Behavioral | B (arbitrary behavioral source with V= or I= expressions) | | Switches | S (voltage-controlled), W (current-controlled) | +### LTspice Compatibility + +LTspice-specific behavior is opt-in so the default parser and reader behavior stays dialect-neutral: + +```csharp +var parser = new SpiceNetlistParser(); +parser.Settings.Compatibility = CompatibilityOptions.LTspice; +var parseResult = parser.ParseNetlist(netlist); + +var reader = new SpiceSharpReader(); +reader.Settings.Compatibility = CompatibilityOptions.LTspice; +var model = reader.Read(parseResult.FinalModel); +``` + +LTspice mode covers fixture-backed generated-netlist syntax such as `.backanno` and selected output/viewer options as warning no-ops, one-argument `.TRAN`, scalar expression aliases, finite-cycle `PULSE(... Ncycles)`, `EXP(...)`, independent-source `tbl=(...)`, R/C model `tc=a[,b]`, and switch threshold aliases. Behavior-changing constructs that are not represented by SpiceSharp are reported with targeted diagnostics instead of being silently ignored. + +See the [LTspice compatibility matrix](roadmap/ltspice-compatibility-matrix.md) and [LTspice netlist compatibility plan](roadmap/ltspice-netlist-compatibility-plan.md) for the current support classes, known gaps, and evidence policy. LTspice schematic and symbol import (`.asc` / `.asy`) is out of scope. + ### Custom Components -`SpiceSharpParser.CustomComponents` adds opt-in parser mappings for LTspice-style +The optional `SpiceSharpParser.CustomComponents` package/project adds opt-in parser mappings for LTspice-style ideal diode models and nonlinear passive devices: ```spice @@ -122,7 +140,8 @@ C1 out 0 Q=1u*x+100n*x*x L1 in out Flux=1m*x+100u*x*x ``` -Enable the mappings with `reader.Settings.UseCustomComponents()` before calling `Read()`. +Add `using SpiceSharpParser.CustomComponents;` and enable the mappings with +`reader.Settings.UseCustomComponents()` before calling `Read()`. Ideal diode models support LTspice-style `Ron`, `Roff`, `Vfwd`, `Vrev`, `Rrev`, `Ilimit`, `RevIlimit`, `Epsilon`, `RevEpsilon`, `M`, and `N` behavior, while ordinary diode models still fall back to SpiceSharp's built-in semiconductor diode.