Skip to content
Merged

Pulse #185

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion roadmap/ltspice-compatibility-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=<path> chan=<n> [amplitude=<value>]` | 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. |
Expand Down
6 changes: 3 additions & 3 deletions roadmap/ltspice-netlist-compatibility-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <Tstop>` and `.tran <Tstop> 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.
Expand Down Expand Up @@ -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=<path> chan=<n> [amplitude=<value>]` 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=<value>`.
Expand All @@ -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:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand All @@ -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")]
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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>, 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<IIntegrationMethod>(),
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();
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<Parameter>().ToList());
return GenerateFinitePulse(parameters, context);
}

var w = new Pulse();

if (parameters.Count >= 1)
{
w.InitialValue = context.EvaluationContext.Evaluator.EvaluateDouble(parameters[0].Value);
Expand Down Expand Up @@ -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<Parameter>().ToList());
}

return parameters;
}
}
}
Loading