diff --git a/README.md b/README.md
index 8f72d486..1213c6b8 100644
--- a/README.md
+++ b/README.md
@@ -109,6 +109,19 @@ Using SpiceSharpParser involves three steps:
| Behavioral | B (arbitrary behavioral source with V= or I= expressions) |
| Switches | S (voltage-controlled), W (current-controlled) |
+### Custom Components
+
+`SpiceSharpParser.CustomComponents` adds opt-in parser mappings for LTspice-style ideal diode models:
+
+```spice
+.model did D(Ron=0.1 Roff=1e9 Vfwd=0.7 Ilimit=10 Epsilon=10m)
+D1 out 0 did
+```
+
+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.
+
+See [LTspice-Style Ideal Diode](src/docs/articles/ideal-diode.md) for syntax, scaling rules, current-law details, and the optional LTspice-backed golden tests for DC, AC, and transient parity.
+
### Behavioral Modeling
`VALUE={expr}`, `TABLE={expr}`, `POLY(n)`, `B` sources, source-level `E` / `G` / `F` / `H` `LAPLACE` transfer functions, function-style `LAPLACE(input, transfer)` in behavioral expressions, and a full set of built-in math functions. `LAPLACE` supports voltage-controlled and current-controlled forms with rational polynomials in `s`, including finite constant `M=`, `TD=`, and `DELAY=` options. Function-style calls also support call-local options, mixed-expression helper lowering, and arbitrary scalar input expressions.
diff --git a/src/SpiceSharp-Parser.sln b/src/SpiceSharp-Parser.sln
index e4eb773a..796196af 100644
--- a/src/SpiceSharp-Parser.sln
+++ b/src/SpiceSharp-Parser.sln
@@ -5,6 +5,8 @@ VisualStudioVersion = 18.5.11612.153 insiders
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SpiceSharpParser", "SpiceSharpParser\SpiceSharpParser.csproj", "{DF3DD787-71CC-4C89-9E33-DC4536A52278}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SpiceSharpParser.CustomComponents", "SpiceSharpParser.CustomComponents\SpiceSharpParser.CustomComponents.csproj", "{BC0D188B-D27A-4870-A739-A57A61D6E497}"
+EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SpiceSharpParser.IntegrationTests", "SpiceSharpParser.IntegrationTests\SpiceSharpParser.IntegrationTests.csproj", "{57920E91-873B-4E66-B0EC-4CAC45007AA9}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CodeAnalysis", "CodeAnalysis", "{A98DF2D4-CFE4-44F3-AD5C-21D6A0648EFD}"
@@ -32,6 +34,12 @@ Global
{DF3DD787-71CC-4C89-9E33-DC4536A52278}.ERRORS|Any CPU.Build.0 = Release|Any CPU
{DF3DD787-71CC-4C89-9E33-DC4536A52278}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DF3DD787-71CC-4C89-9E33-DC4536A52278}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BC0D188B-D27A-4870-A739-A57A61D6E497}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BC0D188B-D27A-4870-A739-A57A61D6E497}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BC0D188B-D27A-4870-A739-A57A61D6E497}.ERRORS|Any CPU.ActiveCfg = Release|Any CPU
+ {BC0D188B-D27A-4870-A739-A57A61D6E497}.ERRORS|Any CPU.Build.0 = Release|Any CPU
+ {BC0D188B-D27A-4870-A739-A57A61D6E497}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BC0D188B-D27A-4870-A739-A57A61D6E497}.Release|Any CPU.Build.0 = Release|Any CPU
{57920E91-873B-4E66-B0EC-4CAC45007AA9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{57920E91-873B-4E66-B0EC-4CAC45007AA9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{57920E91-873B-4E66-B0EC-4CAC45007AA9}.ERRORS|Any CPU.ActiveCfg = Release|Any CPU
diff --git a/src/SpiceSharpParser.CustomComponents/IdealDiode.cs b/src/SpiceSharpParser.CustomComponents/IdealDiode.cs
new file mode 100644
index 00000000..c1bf343f
--- /dev/null
+++ b/src/SpiceSharpParser.CustomComponents/IdealDiode.cs
@@ -0,0 +1,119 @@
+using SpiceSharp.Attributes;
+using SpiceSharp.Behaviors;
+using SpiceSharp.Components;
+using SpiceSharp.ParameterSets;
+using SpiceSharp.Simulations;
+using SpiceSharpParser.CustomComponents.IdealDiodes;
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+
+namespace SpiceSharpParser.CustomComponents
+{
+ ///
+ /// A standalone LTspice-style ideal diode component.
+ ///
+ ///
+ ///
+ ///
+ [Pin(0, "D+"), Pin(1, "D-")]
+ public class IdealDiode : Component
+ {
+ private readonly ConcurrentDictionary _simulationModelParameters = new ConcurrentDictionary();
+ private readonly ConcurrentDictionary> _simulationModelParameterOverrides =
+ new ConcurrentDictionary>();
+
+ ///
+ /// The pin count for ideal diodes.
+ ///
+ [ParameterName("pincount"), ParameterInfo("Number of pins")]
+ public const int PinCount = 2;
+
+ internal IdealDiodeParameters ModelParameters { get; set; }
+
+ internal void SetModelParameters(ISimulation simulation, IdealDiodeParameters parameters)
+ {
+ if (simulation == null)
+ {
+ ModelParameters = parameters;
+ return;
+ }
+
+ _simulationModelParameters[simulation] = parameters;
+ }
+
+ internal IdealDiodeParameters GetModelParameters(ISimulation simulation)
+ {
+ if (simulation != null && _simulationModelParameters.TryGetValue(simulation, out var parameters))
+ {
+ return parameters;
+ }
+
+ return ModelParameters;
+ }
+
+ internal void SetModelParameterOverride(ISimulation simulation, string parameterName, double value)
+ {
+ if (simulation == null)
+ {
+ return;
+ }
+
+ var overrides = _simulationModelParameterOverrides.GetOrAdd(
+ simulation,
+ _ => new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase));
+ overrides[parameterName] = value;
+ }
+
+ internal IEnumerable> GetModelParameterOverrides(ISimulation simulation)
+ {
+ if (simulation != null && _simulationModelParameterOverrides.TryGetValue(simulation, out var overrides))
+ {
+ return overrides;
+ }
+
+ return Array.Empty>();
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The name of the device.
+ /// Thrown if is null.
+ public IdealDiode(string name)
+ : base(name, PinCount)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The name of the device.
+ /// The anode.
+ /// The cathode.
+ /// Thrown if is null.
+ public IdealDiode(string name, string anode, string cathode)
+ : this(name)
+ {
+ Connect(anode, cathode);
+ }
+
+ ///
+ public override void CreateBehaviors(ISimulation simulation)
+ {
+ var behaviors = new BehaviorContainer(Name);
+ var context = new ComponentBindingContext(this, simulation, behaviors);
+
+ if (simulation.UsesBehaviors())
+ {
+ behaviors.Add(new Frequency(context, this, simulation));
+ }
+ else if (simulation.UsesBehaviors())
+ {
+ behaviors.Add(new Biasing(context, this, simulation));
+ }
+
+ simulation.EntityBehaviors.Add(behaviors);
+ }
+ }
+}
diff --git a/src/SpiceSharpParser.CustomComponents/IdealDiodeModel.cs b/src/SpiceSharpParser.CustomComponents/IdealDiodeModel.cs
new file mode 100644
index 00000000..aeeddb37
--- /dev/null
+++ b/src/SpiceSharpParser.CustomComponents/IdealDiodeModel.cs
@@ -0,0 +1,44 @@
+using SpiceSharp.Behaviors;
+using SpiceSharp.Entities;
+using SpiceSharp.ParameterSets;
+using SpiceSharp.Simulations;
+using SpiceSharpParser.CustomComponents.IdealDiodes;
+
+namespace SpiceSharpParser.CustomComponents
+{
+ ///
+ /// Parser-side model container for LTspice-style ideal diode parameters.
+ ///
+ public class IdealDiodeModel : Entity
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The model name.
+ public IdealDiodeModel(string name)
+ : base(name)
+ {
+ }
+
+ ///
+ public override void CreateBehaviors(ISimulation simulation)
+ {
+ var behaviors = new BehaviorContainer(Name);
+ var context = new BindingContext(this, simulation, behaviors);
+ behaviors.Add(new IdealDiodeModelBehavior(context));
+
+ simulation.EntityBehaviors.Add(behaviors);
+ }
+
+ private sealed class IdealDiodeModelBehavior : Behavior, IParameterized
+ {
+ public IdealDiodeModelBehavior(IBindingContext context)
+ : base(context)
+ {
+ Parameters = context.GetParameterSet();
+ }
+
+ public IdealDiodeParameters Parameters { get; }
+ }
+ }
+}
diff --git a/src/SpiceSharpParser.CustomComponents/IdealDiodes/Biasing.cs b/src/SpiceSharpParser.CustomComponents/IdealDiodes/Biasing.cs
new file mode 100644
index 00000000..79482c6f
--- /dev/null
+++ b/src/SpiceSharpParser.CustomComponents/IdealDiodes/Biasing.cs
@@ -0,0 +1,319 @@
+using SpiceSharp;
+using SpiceSharp.Algebra;
+using SpiceSharp.Attributes;
+using SpiceSharp.Behaviors;
+using SpiceSharp.Components;
+using SpiceSharp.ParameterSets;
+using SpiceSharp.Simulations;
+using System;
+
+namespace SpiceSharpParser.CustomComponents.IdealDiodes
+{
+ ///
+ /// DC biasing behavior for an .
+ ///
+ ///
+ ///
+ [GeneratedParameters]
+ public partial class Biasing : Behavior,
+ IBiasingBehavior,
+ IConvergenceBehavior,
+ IParameterized
+ {
+ private readonly IIterationSimulationState _iteration;
+ private readonly IdealDiode _diode;
+ private readonly ISimulation _simulation;
+ private readonly IdealDiodeParameters _instanceParameters;
+ private readonly IdealDiodeParameters _modelParameters;
+
+ ///
+ /// Gets the simulation biasing parameters.
+ ///
+ protected BiasingParameters BiasingParameters { get; }
+
+ ///
+ /// The variables used by the behavior.
+ ///
+ protected IdealDiodeVariables Variables { get; }
+
+ ///
+ /// The matrix elements.
+ ///
+ protected ElementSet Elements { get; }
+
+ ///
+ public IdealDiodeParameters Parameters { get; private set; }
+
+ ///
+ /// Gets the terminal voltage across the ideal diode branch.
+ ///
+ [ParameterName("v"), ParameterName("vd"), ParameterInfo("The terminal voltage across the ideal diode")]
+ public double Voltage => Variables.Positive.Value - Variables.Negative.Value;
+
+ ///
+ /// Gets the voltage across the internal ideal diode cells.
+ ///
+ [ParameterName("vj"), ParameterName("vdiode"), ParameterInfo("The internal voltage across the ideal diode cells")]
+ public double JunctionVoltage => LocalVoltage * Parameters.SeriesMultiplier;
+
+ ///
+ /// Gets the current through all ideal diodes in parallel.
+ ///
+ [ParameterName("i"), ParameterName("id"), ParameterName("c"), ParameterInfo("The current through the ideal diode")]
+ public double Current => Variables.Branch.Value;
+
+ ///
+ /// Gets the small-signal conductance.
+ ///
+ [ParameterName("gd"), ParameterInfo("Terminal small-signal conductance")]
+ public double Conductance => GetTerminalConductance();
+
+ ///
+ /// Gets the dissipated power in the ideal diode branch.
+ ///
+ [ParameterName("p"), ParameterName("pd"), ParameterInfo("The dissipated power")]
+ public double Power => Current * Voltage;
+
+ ///
+ /// The voltage across one ideal diode.
+ ///
+ protected double LocalVoltage;
+
+ ///
+ /// The current through one ideal diode.
+ ///
+ protected double LocalCurrent;
+
+ ///
+ /// The conductance through one ideal diode.
+ ///
+ protected double LocalConductance;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The component binding context.
+ /// Thrown if is null.
+ public Biasing(IComponentBindingContext context, IdealDiode diode = null, ISimulation simulation = null)
+ : base(context)
+ {
+ context.ThrowIfNull(nameof(context));
+ context.Nodes.CheckNodes(2);
+
+ var state = context.GetState();
+ _iteration = context.GetState();
+
+ BiasingParameters = context.GetSimulationParameterSet();
+ _diode = diode;
+ _simulation = simulation;
+ _instanceParameters = context.GetParameterSet();
+ _modelParameters = TryGetModelParameters(context);
+ RefreshEffectiveParameters();
+ Variables = new IdealDiodeVariables(Name, state, context);
+ Elements = new ElementSet(
+ state.Solver,
+ Variables.GetMatrixLocations(state.Map),
+ Variables.GetRhsIndices(state.Map));
+ }
+
+ ///
+ /// Loads the Y-matrix and right-hand-side vector.
+ ///
+ protected virtual void Load()
+ {
+ RefreshEffectiveParameters();
+ Initialize(out double vd);
+
+ IdealDiodeEquation.Evaluate(
+ Parameters,
+ BiasingParameters,
+ vd,
+ out double cd,
+ out double gd);
+
+ LocalVoltage = vd;
+ LocalCurrent = cd;
+ LocalConductance = gd;
+
+ double cdeq = cd - (gd * vd);
+
+ gd *= Parameters.ParallelMultiplier / Parameters.SeriesMultiplier;
+ cdeq *= Parameters.ParallelMultiplier;
+ Elements.Add(
+ gd, gd, -gd, -gd,
+ 1.0, -1.0, 1.0, -1.0, -GetEffectiveSeriesResistance(),
+ cdeq, -cdeq);
+ }
+
+ ///
+ void IBiasingBehavior.Load() => Load();
+
+ ///
+ /// Initializes the diode voltage based on the current iteration state.
+ ///
+ /// The initialized diode voltage.
+ protected void Initialize(out double vd)
+ {
+ if (_iteration.Mode == IterationModes.Junction)
+ {
+ vd = Parameters.Off ? 0.0 : Parameters.ForwardVoltage.Value;
+ }
+ else if (_iteration.Mode == IterationModes.Fix && Parameters.Off)
+ {
+ vd = 0.0;
+ }
+ else
+ {
+ vd = (Variables.PosPrime.Value - Variables.Negative.Value) / Parameters.SeriesMultiplier;
+ }
+ }
+
+ ///
+ bool IConvergenceBehavior.IsConvergent()
+ {
+ RefreshEffectiveParameters();
+ double vd = (Variables.PosPrime.Value - Variables.Negative.Value) / Parameters.SeriesMultiplier;
+
+ double delvd = vd - LocalVoltage;
+ double cdhat = LocalCurrent + (LocalConductance * delvd);
+ double cd = LocalCurrent;
+
+ double tol = (BiasingParameters.RelativeTolerance * Math.Max(Math.Abs(cdhat), Math.Abs(cd)))
+ + BiasingParameters.AbsoluteTolerance;
+ if (Math.Abs(cdhat - cd) > tol)
+ {
+ _iteration.IsConvergent = false;
+ return false;
+ }
+
+ return true;
+ }
+
+ ///
+ /// Gets the effective series resistance seen by the total branch current.
+ ///
+ /// The effective series resistance.
+ protected double GetEffectiveSeriesResistance()
+ {
+ // LTspice's idealized Ron/Roff/Vfwd diode keeps Rs in the accepted
+ // parameter set, but the idealized region-wise-linear model does not
+ // use it electrically. Keep the internal zero-volt branch topology so
+ // Rs can still be stepped without rebinding the component.
+ return 0.0;
+ }
+
+ ///
+ /// Gets the small-signal conductance seen at the external terminals.
+ ///
+ /// The terminal conductance.
+ protected double GetTerminalConductance()
+ {
+ double m = Parameters.ParallelMultiplier;
+ double n = Parameters.SeriesMultiplier;
+ double diodeConductance = LocalConductance * m / n;
+
+ if (Parameters.Resistance <= 0.0)
+ return diodeConductance;
+
+ double seriesResistance = GetEffectiveSeriesResistance();
+ if (seriesResistance <= 0.0)
+ return diodeConductance;
+
+ if (diodeConductance <= 0.0)
+ return 0.0;
+
+ return diodeConductance / (1.0 + (diodeConductance * seriesResistance));
+ }
+
+ private static IdealDiodeParameters TryGetModelParameters(IComponentBindingContext context)
+ {
+ try
+ {
+ return context.ModelBehaviors.GetParameterSet();
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ protected void RefreshEffectiveParameters()
+ {
+ var effectiveParameters = new IdealDiodeParameters();
+
+ var modelParameters = _diode?.GetModelParameters(_simulation) ?? _modelParameters;
+ if (modelParameters == null)
+ {
+ _instanceParameters.CopyTo(effectiveParameters);
+ Parameters = effectiveParameters;
+ return;
+ }
+
+ modelParameters.CopyTo(effectiveParameters);
+ if (_diode != null)
+ {
+ foreach (var modelParameterOverride in _diode.GetModelParameterOverrides(_simulation))
+ {
+ effectiveParameters.SetParameter(modelParameterOverride.Key, modelParameterOverride.Value);
+ }
+ }
+
+ effectiveParameters.Area = _instanceParameters.Area;
+ effectiveParameters.Off = _instanceParameters.Off;
+ effectiveParameters.ParallelMultiplier = _instanceParameters.ParallelMultiplier;
+ effectiveParameters.SeriesMultiplier = _instanceParameters.SeriesMultiplier;
+
+ if (_instanceParameters.HasInstanceOverride("rs"))
+ {
+ effectiveParameters.Resistance = _instanceParameters.Resistance;
+ }
+
+ if (_instanceParameters.HasInstanceOverride("ron"))
+ {
+ effectiveParameters.OnResistance = _instanceParameters.OnResistance;
+ }
+
+ if (_instanceParameters.HasInstanceOverride("roff"))
+ {
+ effectiveParameters.OffResistance = _instanceParameters.OffResistance;
+ }
+
+ if (_instanceParameters.HasInstanceOverride("vfwd"))
+ {
+ effectiveParameters.ForwardVoltage = _instanceParameters.ForwardVoltage;
+ }
+
+ if (_instanceParameters.HasInstanceOverride("vrev"))
+ {
+ effectiveParameters.ReverseVoltage = _instanceParameters.ReverseVoltage;
+ }
+
+ if (_instanceParameters.HasInstanceOverride("rrev"))
+ {
+ effectiveParameters.ReverseResistance = _instanceParameters.ReverseResistance;
+ }
+
+ if (_instanceParameters.HasInstanceOverride("ilimit"))
+ {
+ effectiveParameters.ForwardCurrentLimit = _instanceParameters.ForwardCurrentLimit;
+ }
+
+ if (_instanceParameters.HasInstanceOverride("revilimit"))
+ {
+ effectiveParameters.ReverseCurrentLimit = _instanceParameters.ReverseCurrentLimit;
+ }
+
+ if (_instanceParameters.HasInstanceOverride("epsilon"))
+ {
+ effectiveParameters.ForwardEpsilon = _instanceParameters.ForwardEpsilon;
+ }
+
+ if (_instanceParameters.HasInstanceOverride("revepsilon"))
+ {
+ effectiveParameters.ReverseEpsilon = _instanceParameters.ReverseEpsilon;
+ }
+
+ Parameters = effectiveParameters;
+ }
+ }
+}
diff --git a/src/SpiceSharpParser.CustomComponents/IdealDiodes/Frequency.cs b/src/SpiceSharpParser.CustomComponents/IdealDiodes/Frequency.cs
new file mode 100644
index 00000000..bbf04434
--- /dev/null
+++ b/src/SpiceSharpParser.CustomComponents/IdealDiodes/Frequency.cs
@@ -0,0 +1,95 @@
+using SpiceSharp.Algebra;
+using SpiceSharp.Attributes;
+using SpiceSharp.Behaviors;
+using SpiceSharp.Components;
+using SpiceSharp.ParameterSets;
+using SpiceSharp.Simulations;
+using System;
+using System.Numerics;
+
+namespace SpiceSharpParser.CustomComponents.IdealDiodes
+{
+ ///
+ /// Small-signal behavior for an .
+ ///
+ ///
+ ///
+ [GeneratedParameters]
+ public partial class Frequency : Biasing,
+ IFrequencyBehavior
+ {
+ private readonly ElementSet _elements;
+
+ ///
+ /// The complex variables used by the behavior.
+ ///
+ protected IdealDiodeVariables ComplexVariables { get; }
+
+ ///
+ /// Gets the complex terminal voltage across the ideal diode branch.
+ ///
+ [ParameterName("v"), ParameterName("vd"), ParameterInfo("The complex terminal voltage across the ideal diode")]
+ public Complex ComplexVoltage => ComplexVariables.Positive.Value - ComplexVariables.Negative.Value;
+
+ ///
+ /// Gets the complex voltage across the internal ideal diode cells.
+ ///
+ [ParameterName("vj"), ParameterName("vdiode"), ParameterInfo("The complex internal voltage across the ideal diode cells")]
+ public Complex ComplexJunctionVoltage => ComplexVariables.PosPrime.Value - ComplexVariables.Negative.Value;
+
+ ///
+ /// Gets the complex current through all ideal diodes in parallel.
+ ///
+ [ParameterName("i"), ParameterName("id"), ParameterName("c"), ParameterInfo("The complex current through the ideal diode")]
+ public Complex ComplexCurrent => ComplexVariables.Branch.Value;
+
+ ///
+ /// Gets the complex power.
+ ///
+ [ParameterName("p"), ParameterName("pd"), ParameterInfo("The complex power")]
+ public Complex ComplexPower => ComplexVoltage * Complex.Conjugate(ComplexCurrent);
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The component binding context.
+ /// Thrown if is null.
+ public Frequency(IComponentBindingContext context, IdealDiode diode = null, ISimulation simulation = null)
+ : base(context, diode, simulation)
+ {
+ var state = context.GetState();
+ ComplexVariables = new IdealDiodeVariables(Name, state, context);
+ _elements = new ElementSet(
+ state.Solver,
+ ComplexVariables.GetMatrixLocations(state.Map));
+ }
+
+ ///
+ void IFrequencyBehavior.InitializeParameters()
+ {
+ }
+
+ ///
+ void IFrequencyBehavior.Load()
+ {
+ RefreshEffectiveParameters();
+
+ double m = Parameters.ParallelMultiplier;
+ double n = Parameters.SeriesMultiplier;
+ double gd = LocalConductance * m / n;
+ var geq = new Complex(gd, 0.0);
+ var series = new Complex(GetEffectiveSeriesResistance(), 0.0);
+
+ _elements.Add(
+ geq,
+ geq,
+ -geq,
+ -geq,
+ Complex.One,
+ -Complex.One,
+ Complex.One,
+ -Complex.One,
+ -series);
+ }
+ }
+}
diff --git a/src/SpiceSharpParser.CustomComponents/IdealDiodes/IdealDiodeEquation.cs b/src/SpiceSharpParser.CustomComponents/IdealDiodes/IdealDiodeEquation.cs
new file mode 100644
index 00000000..a7d0615a
--- /dev/null
+++ b/src/SpiceSharpParser.CustomComponents/IdealDiodes/IdealDiodeEquation.cs
@@ -0,0 +1,265 @@
+using SpiceSharp;
+using SpiceSharp.Simulations;
+using System;
+
+namespace SpiceSharpParser.CustomComponents.IdealDiodes
+{
+ ///
+ /// LTspice-style idealized diode current law.
+ ///
+ ///
+ /// This helper owns only the local current law for one ideal diode cell. The
+ /// biasing behavior that calls it handles the external branch details such as
+ /// the parallel/series diode multipliers.
+ ///
+ /// The current law is built from straight-line regions in slope/intercept
+ /// form: I = g * V + b. The off region uses Roff or simulator
+ /// Gmin, the forward region uses Ron after Vfwd, and the
+ /// optional reverse-breakdown region uses Rrev after -Vrev.
+ /// Optional epsilon parameters replace abrupt knees with a finite-width
+ /// conductance ramp so both current and conductance stay continuous for
+ /// Newton iteration.
+ ///
+ internal static class IdealDiodeEquation
+ {
+ ///
+ /// Lowest fallback off-state conductance when neither Roff nor simulator Gmin supplies one.
+ ///
+ private const double MinimumConductance = 0.0;
+
+ ///
+ /// Evaluates the current and small-signal conductance for one ideal diode cell.
+ ///
+ /// The effective model and instance parameters.
+ /// The simulation biasing parameters, used for the default off conductance.
+ /// The local voltage across one diode cell.
+ /// The resulting local diode current.
+ /// The resulting small-signal conductance.
+ public static void Evaluate(
+ IdealDiodeParameters parameters,
+ BiasingParameters biasingParameters,
+ double voltage,
+ out double current,
+ out double conductance)
+ {
+ // Work in conductance because the solver needs dI/dV. Each operating
+ // region is represented as a line: current = slope * voltage + intercept.
+ double onConductance = 1.0 / parameters.OnResistance;
+
+ // LTspice's ideal diode can omit Roff. In that case the off-state
+ // leakage follows the simulator's Gmin so the device still contributes
+ // the same numerical shunt used elsewhere during biasing.
+ double offConductance = parameters.OffResistance.Given
+ ? 1.0 / parameters.OffResistance.Value
+ : Math.Max(biasingParameters.Gmin, MinimumConductance);
+
+ // Vfwd defaults to zero. With a zero threshold, the forward on-line is
+ // simply Ron through the origin.
+ double forwardVoltage = parameters.ForwardVoltage.Given ? parameters.ForwardVoltage.Value : 0.0;
+
+ // Start from the off branch. Reverse breakdown and forward conduction
+ // may overwrite this below when the voltage lies in their region.
+ current = offConductance * voltage;
+ conductance = offConductance;
+
+ if (parameters.ReverseVoltage.Given)
+ {
+ // Reverse breakdown is anchored to the off-line current at
+ // -Vrev. With finite Roff, LTspice keeps the nominal knee at
+ // -Vrev rather than moving it to the mathematical line
+ // intersection.
+ // If Rrev is omitted, LTspice-style behavior falls back to Ron.
+ double reverseVoltage = Math.Abs(parameters.ReverseVoltage.Value);
+ double reverseResistance = parameters.ReverseResistance.Given
+ ? parameters.ReverseResistance.Value
+ : parameters.OnResistance;
+ double reverseConductance = 1.0 / reverseResistance;
+ double reverseBoundary = -reverseVoltage;
+ double reverseIntercept = (reverseConductance - offConductance) * reverseVoltage;
+
+ // Evaluate the reverse-to-off knee. With revepsilon omitted or
+ // zero this is a hard switch at -Vrev; otherwise the smoothing
+ // window extends from the nominal boundary into reverse breakdown.
+ EvaluateTransition(
+ voltage,
+ reverseBoundary,
+ parameters.ReverseEpsilon,
+ reverseConductance,
+ reverseIntercept,
+ offConductance,
+ 0.0,
+ false,
+ out current,
+ out conductance);
+ }
+
+ // The forward on-line is anchored to the off-line current at Vfwd.
+ // With finite Roff, LTspice keeps the nominal knee at Vfwd rather
+ // than moving it to the mathematical line intersection.
+ double onIntercept = (offConductance - onConductance) * forwardVoltage;
+ double forwardBoundary = forwardVoltage;
+
+ // Avoid a second transition evaluation in the off or reverse regions.
+ // LTspice-style forward epsilon starts at the boundary and extends
+ // into forward conduction, so voltages below the boundary remain off.
+ if (voltage >= forwardBoundary)
+ {
+ // Evaluate the off-to-forward knee using the same generic transition
+ // helper used for reverse breakdown.
+ EvaluateTransition(
+ voltage,
+ forwardBoundary,
+ parameters.ForwardEpsilon,
+ offConductance,
+ 0.0,
+ onConductance,
+ onIntercept,
+ true,
+ out current,
+ out conductance);
+ }
+
+ // Current limits are applied to the already-selected region. This keeps
+ // the normal piecewise law simple and lets the limiter scale the local
+ // derivative by the tanh derivative.
+ ApplyCurrentLimits(parameters, ref current, ref conductance);
+
+ }
+
+ ///
+ /// Evaluates either a hard or smoothed transition between two linear current regions.
+ ///
+ /// The voltage at which to evaluate the transition.
+ /// The intersection voltage of the two unsmoothed lines.
+ /// The optional smoothing width around .
+ /// The slope used below the transition.
+ /// The intercept used below the transition.
+ /// The slope used above the transition.
+ /// The intercept used above the transition.
+ ///
+ /// If true, the smoothing window starts at and extends toward
+ /// larger voltages. If false, the window ends at and extends
+ /// toward smaller voltages.
+ ///
+ /// The evaluated current.
+ /// The evaluated conductance.
+ private static void EvaluateTransition(
+ double voltage,
+ double boundary,
+ GivenParameter epsilon,
+ double leftSlope,
+ double leftIntercept,
+ double rightSlope,
+ double rightIntercept,
+ bool smoothTowardRight,
+ out double current,
+ out double conductance)
+ {
+ double width = epsilon.Given ? epsilon.Value : 0.0;
+ if (width <= 0.0)
+ {
+ // No smoothing requested: choose the line on the active side of
+ // the intersection and expose that line's slope as conductance.
+ if (voltage < boundary)
+ {
+ current = (leftSlope * voltage) + leftIntercept;
+ conductance = leftSlope;
+ }
+ else
+ {
+ current = (rightSlope * voltage) + rightIntercept;
+ conductance = rightSlope;
+ }
+
+ return;
+ }
+
+ // LTspice uses epsilon as a one-sided width, not as a centered band.
+ // Forward smoothing starts at Vfwd and moves right into the on-region;
+ // reverse smoothing starts in the reverse region and ends at -Vrev.
+ double start = smoothTowardRight ? boundary : boundary - width;
+ double end = smoothTowardRight ? boundary + width : boundary;
+ double slopeDelta = rightSlope - leftSlope;
+
+ // A one-sided conductance ramp shifts the fully conducting side by
+ // half the slope change times the epsilon width. This keeps current
+ // continuous where the ramp meets the straight-line region.
+ double lowVoltageIntercept = smoothTowardRight
+ ? leftIntercept
+ : leftIntercept - (slopeDelta * width / 2.0);
+ double highVoltageIntercept = smoothTowardRight
+ ? rightIntercept - (slopeDelta * width / 2.0)
+ : rightIntercept;
+
+ if (voltage <= start)
+ {
+ current = (leftSlope * voltage) + lowVoltageIntercept;
+ conductance = leftSlope;
+ return;
+ }
+
+ if (voltage >= end)
+ {
+ current = (rightSlope * voltage) + highVoltageIntercept;
+ conductance = rightSlope;
+ return;
+ }
+
+ // Inside the smoothing band the conductance moves linearly from the
+ // left slope to the right slope. Current is the integral of that ramp,
+ // anchored to the left line at the start of the band:
+ //
+ // g(x) = leftSlope + slopeDelta * x / width
+ // I(x) = I(start) + leftSlope * x + slopeDelta * x^2 / (2 * width)
+ //
+ // where x is the distance from the start of the smoothing band.
+ double distance = voltage - start;
+ current = (leftSlope * start) + lowVoltageIntercept
+ + (leftSlope * distance)
+ + (slopeDelta * distance * distance / (2.0 * width));
+ conductance = leftSlope + (slopeDelta * distance / width);
+ }
+
+ ///
+ /// Applies the optional forward or reverse current limiter to the selected current branch.
+ ///
+ /// The effective ideal diode parameters.
+ /// The current to limit in place.
+ /// The conductance to update in place with the limiter derivative.
+ private static void ApplyCurrentLimits(IdealDiodeParameters parameters, ref double current, ref double conductance)
+ {
+ // Forward and reverse limits are independent. Select by the sign of the
+ // already-computed current so leakage, breakdown, and smoothed knees all
+ // feed into the same limiting law.
+ if (current > 0.0 && parameters.ForwardCurrentLimit.Given)
+ {
+ ApplyCurrentLimit(parameters.ForwardCurrentLimit.Value, ref current, ref conductance);
+ }
+ else if (current < 0.0 && parameters.ReverseCurrentLimit.Given)
+ {
+ ApplyCurrentLimit(parameters.ReverseCurrentLimit.Value, ref current, ref conductance);
+ }
+ }
+
+ ///
+ /// Smoothly compresses current toward a symmetric magnitude limit.
+ ///
+ /// The positive or negative limit magnitude.
+ /// The current to limit in place.
+ /// The conductance to update in place with the limiter derivative.
+ private static void ApplyCurrentLimit(double limit, ref double current, ref double conductance)
+ {
+ limit = Math.Abs(limit);
+ if (limit <= 0.0)
+ return;
+
+ // I_limited = limit * tanh(I_raw / limit)
+ // dI_limited/dV = dI_raw/dV * (1 - tanh(I_raw / limit)^2)
+ // This gives a soft saturation without a derivative discontinuity.
+ double normalized = current / limit;
+ double limited = Math.Tanh(normalized);
+ current = limit * limited;
+ conductance *= 1.0 - (limited * limited);
+ }
+ }
+}
diff --git a/src/SpiceSharpParser.CustomComponents/IdealDiodes/IdealDiodeParameters.cs b/src/SpiceSharpParser.CustomComponents/IdealDiodes/IdealDiodeParameters.cs
new file mode 100644
index 00000000..0454f41e
--- /dev/null
+++ b/src/SpiceSharpParser.CustomComponents/IdealDiodes/IdealDiodeParameters.cs
@@ -0,0 +1,190 @@
+using SpiceSharp;
+using SpiceSharp.Attributes;
+using SpiceSharp.ParameterSets;
+using System.Collections.Generic;
+
+namespace SpiceSharpParser.CustomComponents.IdealDiodes
+{
+ ///
+ /// Parameters for an .
+ ///
+ ///
+ [GeneratedParameters]
+ public partial class IdealDiodeParameters : ParameterSet
+ {
+ private readonly ISet _instanceOverrides = new HashSet(System.StringComparer.OrdinalIgnoreCase);
+
+ ///
+ /// Copies all parameters to another instance.
+ ///
+ /// The destination parameter set.
+ public void CopyTo(IdealDiodeParameters target)
+ {
+ target.Area = Area;
+ target.Off = Off;
+ target.Resistance = Resistance;
+ target.ParallelMultiplier = ParallelMultiplier;
+ target.SeriesMultiplier = SeriesMultiplier;
+ target.OnResistance = OnResistance;
+
+ if (OffResistance.Given)
+ {
+ target.OffResistance = OffResistance;
+ }
+
+ if (ForwardVoltage.Given)
+ {
+ target.ForwardVoltage = ForwardVoltage;
+ }
+
+ if (ReverseVoltage.Given)
+ {
+ target.ReverseVoltage = ReverseVoltage;
+ }
+
+ if (ReverseResistance.Given)
+ {
+ target.ReverseResistance = ReverseResistance;
+ }
+
+ if (ForwardCurrentLimit.Given)
+ {
+ target.ForwardCurrentLimit = ForwardCurrentLimit;
+ }
+
+ if (ReverseCurrentLimit.Given)
+ {
+ target.ReverseCurrentLimit = ReverseCurrentLimit;
+ }
+
+ if (ForwardEpsilon.Given)
+ {
+ target.ForwardEpsilon = ForwardEpsilon;
+ }
+
+ if (ReverseEpsilon.Given)
+ {
+ target.ReverseEpsilon = ReverseEpsilon;
+ }
+ }
+
+ ///
+ /// Marks an instance parameter as explicitly set by the netlist.
+ ///
+ /// The parameter name.
+ public void MarkInstanceOverride(string parameterName)
+ {
+ if (!string.IsNullOrEmpty(parameterName))
+ {
+ _instanceOverrides.Add(parameterName);
+ }
+ }
+
+ ///
+ /// Checks whether an instance parameter was explicitly set by the netlist.
+ ///
+ /// The parameter name.
+ /// true if the parameter was explicitly set.
+ public bool HasInstanceOverride(string parameterName)
+ {
+ return _instanceOverrides.Contains(parameterName);
+ }
+
+ ///
+ /// Gets or sets the accepted diode area value. LTspice's idealized region-wise-linear diode ignores it electrically.
+ ///
+ [ParameterName("area"), ParameterInfo("Area factor")]
+ [GreaterThan(0), Finite]
+ private double _area = 1.0;
+
+ ///
+ /// Gets or sets whether the diode starts in the off state during junction initialization.
+ ///
+ [ParameterName("off"), ParameterInfo("Initially off")]
+ public bool Off { get; set; }
+
+ ///
+ /// Gets or sets the accepted series resistance value. LTspice's idealized region-wise-linear diode ignores it electrically.
+ ///
+ [ParameterName("rs"), ParameterInfo("Parasitic series resistance", Units = "Ohm")]
+ [GreaterThanOrEquals(0), Finite]
+ private double _resistance;
+
+ ///
+ /// Gets or sets the number of ideal diodes in parallel.
+ ///
+ [ParameterName("m"), ParameterInfo("Parallel multiplier")]
+ [GreaterThan(0), Finite]
+ private double _parallelMultiplier = 1.0;
+
+ ///
+ /// Gets or sets the number of ideal diodes in series.
+ ///
+ [ParameterName("n"), ParameterInfo("Series multiplier")]
+ [GreaterThan(0), Finite]
+ private double _seriesMultiplier = 1.0;
+
+ ///
+ /// Gets or sets the forward on resistance.
+ ///
+ [ParameterName("ron"), ParameterInfo("Forward resistance", Units = "Ohm")]
+ [GreaterThan(0), Finite]
+ private double _onResistance = 1.0;
+
+ ///
+ /// Gets or sets the off resistance. If omitted, the simulation Gmin is used.
+ ///
+ [ParameterName("roff"), ParameterInfo("Off resistance", Units = "Ohm")]
+ [GreaterThan(0), Finite]
+ private GivenParameter _offResistance = new GivenParameter(0.0, false);
+
+ ///
+ /// Gets or sets the forward voltage.
+ ///
+ [ParameterName("vfwd"), ParameterInfo("Forward voltage", Units = "V")]
+ [Finite]
+ private GivenParameter _forwardVoltage = new GivenParameter(0.0, false);
+
+ ///
+ /// Gets or sets the reverse breakdown voltage magnitude.
+ ///
+ [ParameterName("vrev"), ParameterInfo("Reverse breakdown voltage", Units = "V")]
+ [GreaterThanOrEquals(0), Finite]
+ private GivenParameter _reverseVoltage = new GivenParameter(0.0, false);
+
+ ///
+ /// Gets or sets the reverse breakdown resistance. If omitted, is used.
+ ///
+ [ParameterName("rrev"), ParameterInfo("Reverse breakdown resistance", Units = "Ohm")]
+ [GreaterThan(0), Finite]
+ private GivenParameter _reverseResistance = new GivenParameter(0.0, false);
+
+ ///
+ /// Gets or sets the forward current limit.
+ ///
+ [ParameterName("ilimit"), ParameterInfo("Forward current limit", Units = "A")]
+ [GreaterThan(0), Finite]
+ private GivenParameter _forwardCurrentLimit = new GivenParameter(0.0, false);
+
+ ///
+ /// Gets or sets the reverse current limit.
+ ///
+ [ParameterName("revilimit"), ParameterInfo("Reverse current limit", Units = "A")]
+ [GreaterThan(0), Finite]
+ private GivenParameter _reverseCurrentLimit = new GivenParameter(0.0, false);
+
+ ///
+ /// Gets or sets the forward transition smoothing width.
+ ///
+ [ParameterName("epsilon"), ParameterInfo("Forward smoothing voltage", Units = "V")]
+ [GreaterThanOrEquals(0), Finite]
+ private GivenParameter _forwardEpsilon = new GivenParameter(0.0, false);
+
+ ///
+ /// Gets or sets the reverse transition smoothing width.
+ ///
+ [ParameterName("revepsilon"), ParameterInfo("Reverse smoothing voltage", Units = "V")]
+ [GreaterThanOrEquals(0), Finite]
+ private GivenParameter _reverseEpsilon = new GivenParameter(0.0, false);
+ }
+}
diff --git a/src/SpiceSharpParser.CustomComponents/IdealDiodes/IdealDiodeVariables.cs b/src/SpiceSharpParser.CustomComponents/IdealDiodes/IdealDiodeVariables.cs
new file mode 100644
index 00000000..43cc9e1a
--- /dev/null
+++ b/src/SpiceSharpParser.CustomComponents/IdealDiodes/IdealDiodeVariables.cs
@@ -0,0 +1,86 @@
+using SpiceSharp;
+using SpiceSharp.Algebra;
+using SpiceSharp.Components;
+using SpiceSharp.Simulations;
+
+namespace SpiceSharpParser.CustomComponents.IdealDiodes
+{
+ ///
+ /// Variables for an .
+ ///
+ /// The base value type.
+ public readonly struct IdealDiodeVariables
+ {
+ ///
+ /// The positive node.
+ ///
+ public readonly IVariable Positive;
+
+ ///
+ /// The internal positive node.
+ ///
+ public readonly IVariable PosPrime;
+
+ ///
+ /// The negative node.
+ ///
+ public readonly IVariable Negative;
+
+ ///
+ /// The current through the series branch from the positive node to the internal positive node.
+ ///
+ public readonly IVariable Branch;
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The device name.
+ /// The variable factory.
+ /// The component binding context.
+ public IdealDiodeVariables(string name, IVariableFactory> factory, IComponentBindingContext context)
+ {
+ context.Nodes.CheckNodes(2);
+
+ Positive = factory.GetSharedVariable(context.Nodes[0]);
+ Negative = factory.GetSharedVariable(context.Nodes[1]);
+ PosPrime = factory.CreatePrivateVariable(name.Combine("pos"), Units.Volt);
+ Branch = factory.CreatePrivateVariable(name.Combine("branch"), Units.Ampere);
+ }
+
+ ///
+ /// Gets the matrix locations.
+ ///
+ /// The variable map.
+ /// The matrix locations.
+ public MatrixLocation[] GetMatrixLocations(IVariableMap map)
+ {
+ int pos = map[Positive];
+ int posPrime = map[PosPrime];
+ int neg = map[Negative];
+ int branch = map[Branch];
+
+ return new[]
+ {
+ new MatrixLocation(neg, neg),
+ new MatrixLocation(posPrime, posPrime),
+ new MatrixLocation(neg, posPrime),
+ new MatrixLocation(posPrime, neg),
+ new MatrixLocation(pos, branch),
+ new MatrixLocation(posPrime, branch),
+ new MatrixLocation(branch, pos),
+ new MatrixLocation(branch, posPrime),
+ new MatrixLocation(branch, branch),
+ };
+ }
+
+ ///
+ /// Gets the right-hand-side vector indices.
+ ///
+ /// The variable map.
+ /// The right-hand-side vector indices.
+ public int[] GetRhsIndices(IVariableMap map)
+ {
+ return new[] { map[Negative], map[PosPrime] };
+ }
+ }
+}
diff --git a/src/SpiceSharpParser.CustomComponents/Parser/CustomComponentReaderExtensions.cs b/src/SpiceSharpParser.CustomComponents/Parser/CustomComponentReaderExtensions.cs
new file mode 100644
index 00000000..0e4c8a3b
--- /dev/null
+++ b/src/SpiceSharpParser.CustomComponents/Parser/CustomComponentReaderExtensions.cs
@@ -0,0 +1,28 @@
+using System;
+using SpiceSharpParser.ModelReaders.Netlist.Spice;
+
+namespace SpiceSharpParser.CustomComponents
+{
+ ///
+ /// Extensions for enabling custom component parsing.
+ ///
+ public static class CustomComponentReaderExtensions
+ {
+ ///
+ /// Enables parser mappings for the custom components in this assembly.
+ ///
+ /// The reader settings.
+ /// The same settings instance for fluent configuration.
+ public static SpiceNetlistReaderSettings UseCustomComponents(this SpiceNetlistReaderSettings settings)
+ {
+ if (settings == null)
+ {
+ throw new ArgumentNullException(nameof(settings));
+ }
+
+ settings.Mappings.Models.Map("D", new IdealDiodeModelGenerator());
+ settings.Mappings.Components.Map("D", new IdealDiodeGenerator());
+ return settings;
+ }
+ }
+}
diff --git a/src/SpiceSharpParser.CustomComponents/Parser/IdealDiodeGenerator.cs b/src/SpiceSharpParser.CustomComponents/Parser/IdealDiodeGenerator.cs
new file mode 100644
index 00000000..53c0f0c8
--- /dev/null
+++ b/src/SpiceSharpParser.CustomComponents/Parser/IdealDiodeGenerator.cs
@@ -0,0 +1,256 @@
+using SpiceSharp.Entities;
+using SpiceSharp.ParameterSets;
+using SpiceSharpParser.Common;
+using SpiceSharpParser.Common.Validation;
+using SpiceSharpParser.ModelReaders.Netlist.Spice.Context;
+using SpiceSharpParser.ModelReaders.Netlist.Spice.Readers.EntityGenerators;
+using SpiceSharpParser.ModelReaders.Netlist.Spice.Readers.EntityGenerators.Components;
+using SpiceSharpParser.ModelReaders.Netlist.Spice.Readers.EntityGenerators.Components.Semiconductors;
+using SpiceSharpParser.Models.Netlist.Spice.Objects;
+using SpiceSharpParser.Models.Netlist.Spice.Objects.Parameters;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Model = SpiceSharpParser.ModelReaders.Netlist.Spice.Context.Models.Model;
+
+namespace SpiceSharpParser.CustomComponents
+{
+ ///
+ /// Creates custom ideal diode instances for ideal diode models.
+ ///
+ public class IdealDiodeGenerator : IComponentGenerator
+ {
+ private readonly DiodeGenerator _fallback = new DiodeGenerator();
+
+ ///
+ public IEntity Generate(string componentIdentifier, string originalName, string type, ParameterCollection parameters, IReadingContext context)
+ {
+ if (parameters.Count < 3)
+ {
+ throw new Exception("Model expected");
+ }
+
+ var rangePredicate = CreateRangePredicate(parameters, context, originalName, false);
+ var contextModel = context.ModelsRegistry.FindModel(
+ parameters.Get(2).Value,
+ selectedModel => IsIdealDiodeModel(selectedModel, rangePredicate));
+ if (!(contextModel?.Entity is IdealDiodeModel))
+ {
+ return _fallback.Generate(componentIdentifier, originalName, type, parameters, context);
+ }
+
+ var diode = new IdealDiode(componentIdentifier);
+ context.CreateNodes(diode, parameters.Take(IdealDiode.PinCount));
+ diode.Model = contextModel.Name;
+ diode.SetModelParameters(null, ((IdealDiodeModel)contextModel.Entity).Parameters);
+
+ context.SimulationPreparations.ExecuteActionBeforeSetup(simulation =>
+ {
+ var currentRangePredicate = CreateRangePredicate(parameters, context, originalName, true);
+
+ context.ModelsRegistry.SetModel(
+ diode,
+ selectedModel => IsIdealDiodeModel(selectedModel, currentRangePredicate),
+ simulation,
+ parameters.Get(2),
+ $"Could not find ideal diode model {parameters.Get(2)} for diode {originalName}",
+ selectedModel => SetSelectedModel(diode, selectedModel, parameters.Get(2), simulation, context),
+ context);
+ });
+
+ bool areaSet = false;
+ for (int i = 3; i < parameters.Count; i++)
+ {
+ if (parameters[i] is WordParameter word)
+ {
+ if (word.Value.Equals("on", StringComparison.OrdinalIgnoreCase))
+ {
+ diode.SetParameter("off", false);
+ }
+ else if (word.Value.Equals("off", StringComparison.OrdinalIgnoreCase))
+ {
+ diode.SetParameter("off", true);
+ }
+ else
+ {
+ throw new Exception("Expected on/off for diode");
+ }
+ }
+
+ if (parameters[i] is AssignmentParameter assignment)
+ {
+ if (assignment.Name.Equals("l", StringComparison.OrdinalIgnoreCase)
+ || assignment.Name.Equals("w", StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+
+ if (IdealDiodeParserSupport.SetInstanceParameter(context, diode, originalName, assignment))
+ {
+ diode.Parameters.MarkInstanceOverride(assignment.Name);
+ }
+ }
+
+ if (parameters[i] is ValueParameter || parameters[i] is ExpressionParameter)
+ {
+ if (!areaSet)
+ {
+ if (IdealDiodeParserSupport.SetPositionalAreaParameter(context, diode, parameters.Get(i)))
+ {
+ diode.Parameters.MarkInstanceOverride("area");
+ }
+
+ areaSet = true;
+ }
+ }
+ }
+
+ return diode;
+ }
+
+ private static bool IsIdealDiodeModel(Model model, Func rangePredicate)
+ {
+ return model.Entity is IdealDiodeModel && (rangePredicate == null || rangePredicate(model));
+ }
+
+ private static Func CreateRangePredicate(
+ ParameterCollection parameters,
+ IReadingContext context,
+ string diodeName,
+ bool reportErrors)
+ {
+ try
+ {
+ double? l = ComponentGenerator.GetAssignmentParameterValue("l", parameters, context);
+ double? w = ComponentGenerator.GetAssignmentParameterValue("w", parameters, context);
+ return ComponentGenerator.CreateRangePredicate(("l", l), ("w", w));
+ }
+ catch (Exception ex)
+ {
+ if (reportErrors)
+ {
+ var parameter = parameters
+ .OfType()
+ .FirstOrDefault(assignment => assignment.Name.Equals("l", StringComparison.OrdinalIgnoreCase)
+ || assignment.Name.Equals("w", StringComparison.OrdinalIgnoreCase))
+ ?? parameters.Get(2);
+
+ context.Result.ValidationResult.AddError(
+ ValidationEntrySource.Reader,
+ $"Could not evaluate ideal diode model selection parameter L/W for diode {diodeName}.",
+ parameter.LineInfo,
+ ex);
+ }
+
+ return null;
+ }
+ }
+
+ private static void SetSelectedModel(
+ IdealDiode diode,
+ Model selectedModel,
+ Parameter modelParameter,
+ ISimulationWithEvents simulation,
+ IReadingContext context)
+ {
+ if (selectedModel.Entity is IdealDiodeModel idealDiodeModel)
+ {
+ diode.Model = selectedModel.Name;
+ diode.SetModelParameters(simulation, idealDiodeModel.Parameters);
+ ApplyModelParameterSweepOverrides(diode, selectedModel, idealDiodeModel, modelParameter, simulation, context);
+ return;
+ }
+
+ context.Result.ValidationResult.AddError(
+ ValidationEntrySource.Reader,
+ $"Selected model '{selectedModel.Name}' is not an ideal diode model for '{diode.Name}'.",
+ modelParameter.LineInfo);
+ }
+
+ private static void ApplyModelParameterSweepOverrides(
+ IdealDiode diode,
+ Model selectedModel,
+ IdealDiodeModel model,
+ Parameter modelParameter,
+ ISimulationWithEvents simulation,
+ IReadingContext context)
+ {
+ var comparison = context.ReaderSettings.CaseSensitivity.IsEntityNamesCaseSensitive
+ ? StringComparison.Ordinal
+ : StringComparison.OrdinalIgnoreCase;
+ var originalValues = new Dictionary(StringComparer.OrdinalIgnoreCase);
+
+ foreach (var sweep in context.SimulationConfiguration.ParameterSweeps)
+ {
+ if (!(sweep.Parameter is BracketParameter bracketParameter)
+ || bracketParameter.Parameters.Count == 0
+ || !MatchesModelName(bracketParameter.Name, modelParameter.Value, selectedModel.Name, comparison))
+ {
+ continue;
+ }
+
+ var parameterName = bracketParameter.Parameters[0].Value;
+ if (!IdealDiodeParserSupport.IsModelParameter(parameterName))
+ {
+ continue;
+ }
+
+ if (TryGetSweepValue(context, simulation, sweep.Parameter, out double value))
+ {
+ if (!originalValues.ContainsKey(parameterName)
+ && model.Parameters.TryGetProperty(parameterName, out double originalValue))
+ {
+ originalValues.Add(parameterName, originalValue);
+ }
+
+ diode.SetModelParameterOverride(simulation, parameterName, value);
+ }
+ }
+
+ if (originalValues.Count > 0)
+ {
+ simulation.EventBeforeUnSetup += (_, _) =>
+ {
+ foreach (var originalValue in originalValues)
+ {
+ model.Parameters.SetParameter(originalValue.Key, originalValue.Value);
+ }
+ };
+ }
+ }
+
+ private static bool MatchesModelName(
+ string sweepModelName,
+ string requestedModelName,
+ string selectedModelName,
+ StringComparison comparison)
+ {
+ return string.Equals(sweepModelName, requestedModelName, comparison)
+ || string.Equals(sweepModelName, selectedModelName, comparison)
+ || string.Equals(sweepModelName, GetBaseModelName(selectedModelName), comparison);
+ }
+
+ private static string GetBaseModelName(string modelName)
+ {
+ var separatorIndex = modelName.IndexOf('#');
+ return separatorIndex >= 0 ? modelName.Substring(0, separatorIndex) : modelName;
+ }
+
+ private static bool TryGetSweepValue(
+ IReadingContext context,
+ ISimulationWithEvents simulation,
+ Parameter sweepParameter,
+ out double value)
+ {
+ value = 0.0;
+ var simulationContext = context.EvaluationContext.GetSimulationContext(simulation);
+ if (!simulationContext.Parameters.TryGetValue(sweepParameter.Value, out var expression))
+ {
+ return false;
+ }
+
+ value = simulationContext.Evaluator.EvaluateDouble(expression);
+ return true;
+ }
+ }
+}
diff --git a/src/SpiceSharpParser.CustomComponents/Parser/IdealDiodeModelGenerator.cs b/src/SpiceSharpParser.CustomComponents/Parser/IdealDiodeModelGenerator.cs
new file mode 100644
index 00000000..bc2d7eef
--- /dev/null
+++ b/src/SpiceSharpParser.CustomComponents/Parser/IdealDiodeModelGenerator.cs
@@ -0,0 +1,28 @@
+using SpiceSharpParser.ModelReaders.Netlist.Spice.Context;
+using SpiceSharpParser.ModelReaders.Netlist.Spice.Readers.EntityGenerators.Models;
+using SpiceSharpParser.Models.Netlist.Spice.Objects;
+using Model = SpiceSharpParser.ModelReaders.Netlist.Spice.Context.Models.Model;
+
+namespace SpiceSharpParser.CustomComponents
+{
+ ///
+ /// Creates a custom ideal diode model when LTspice ideal-diode parameters are present.
+ ///
+ public class IdealDiodeModelGenerator : DiodeModelGenerator
+ {
+ ///
+ public override Model Generate(string id, string type, ParameterCollection parameters, IReadingContext context)
+ {
+ if (!IdealDiodeParserSupport.HasIdealParameter(parameters))
+ {
+ return base.Generate(id, type, parameters, context);
+ }
+
+ var model = new IdealDiodeModel(id);
+ var contextModel = new Model(id, model, model.Parameters);
+ IdealDiodeParserSupport.SetModelParameters(context, model, contextModel, parameters);
+
+ return contextModel;
+ }
+ }
+}
diff --git a/src/SpiceSharpParser.CustomComponents/Parser/IdealDiodeParserSupport.cs b/src/SpiceSharpParser.CustomComponents/Parser/IdealDiodeParserSupport.cs
new file mode 100644
index 00000000..3ae52754
--- /dev/null
+++ b/src/SpiceSharpParser.CustomComponents/Parser/IdealDiodeParserSupport.cs
@@ -0,0 +1,200 @@
+using SpiceSharp.Entities;
+using SpiceSharpParser.Common.Validation;
+using SpiceSharpParser.ModelReaders.Netlist.Spice.Context;
+using SpiceSharpParser.Models.Netlist.Spice.Objects;
+using SpiceSharpParser.Models.Netlist.Spice.Objects.Parameters;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Model = SpiceSharpParser.ModelReaders.Netlist.Spice.Context.Models.Model;
+
+namespace SpiceSharpParser.CustomComponents
+{
+ internal static class IdealDiodeParserSupport
+ {
+ private static readonly ISet IdealModelParameters = new HashSet(StringComparer.OrdinalIgnoreCase)
+ {
+ "ron",
+ "roff",
+ "vfwd",
+ "vrev",
+ "rrev",
+ "ilimit",
+ "revilimit",
+ "epsilon",
+ "revepsilon",
+ };
+
+ private static readonly ISet ModelParameters = new HashSet(IdealModelParameters, StringComparer.OrdinalIgnoreCase)
+ {
+ "rs",
+ };
+
+ private static readonly ISet InstanceParameters = new HashSet(ModelParameters, StringComparer.OrdinalIgnoreCase)
+ {
+ "area",
+ "off",
+ "m",
+ "n",
+ };
+
+ private static readonly ISet SelectionParameters = new HashSet(StringComparer.OrdinalIgnoreCase)
+ {
+ "lmin",
+ "lmax",
+ "wmin",
+ "wmax",
+ };
+
+ private static readonly ISet IgnoredClassicModelParameters = new HashSet(StringComparer.OrdinalIgnoreCase)
+ {
+ "is",
+ "tnom",
+ "n",
+ "tt",
+ "cjo",
+ "cj0",
+ "vj",
+ "m",
+ "eg",
+ "xti",
+ "fc",
+ "bv",
+ "ibv",
+ "kf",
+ "af",
+ };
+
+ private static readonly ISet IgnoredInstanceParameters = new HashSet(StringComparer.OrdinalIgnoreCase)
+ {
+ "temp",
+ "ic",
+ };
+
+ private static readonly ISet MetadataParameters = new HashSet(StringComparer.OrdinalIgnoreCase)
+ {
+ "mfg",
+ "manufacturer",
+ "pn",
+ "part",
+ "desc",
+ "description",
+ "v",
+ "irms",
+ "ipk",
+ };
+
+ public static bool HasIdealParameter(ParameterCollection parameters)
+ {
+ return parameters
+ .OfType()
+ .Any(parameter => IdealModelParameters.Contains(parameter.Name));
+ }
+
+ public static bool IsModelParameter(string parameterName)
+ {
+ return ModelParameters.Contains(parameterName);
+ }
+
+ public static void SetModelParameters(IReadingContext context, IdealDiodeModel entity, Model model, ParameterCollection parameters)
+ {
+ foreach (Parameter parameter in parameters)
+ {
+ if (!(parameter is AssignmentParameter assignment))
+ {
+ AddUnsupportedParameter(context, "ideal diode model", model.Name, parameter);
+ continue;
+ }
+
+ if (SelectionParameters.Contains(assignment.Name))
+ {
+ StoreSelectionParameter(context, model, assignment);
+ continue;
+ }
+
+ if (IgnoredClassicModelParameters.Contains(assignment.Name)
+ || MetadataParameters.Contains(assignment.Name))
+ {
+ continue;
+ }
+
+ if (!ModelParameters.Contains(assignment.Name))
+ {
+ AddUnsupportedParameter(context, "ideal diode model", model.Name, parameter);
+ continue;
+ }
+
+ SetParameter(context, entity, assignment);
+ }
+ }
+
+ public static bool SetInstanceParameter(IReadingContext context, IdealDiode entity, string name, AssignmentParameter parameter)
+ {
+ if (IgnoredInstanceParameters.Contains(parameter.Name) || MetadataParameters.Contains(parameter.Name))
+ {
+ return false;
+ }
+
+ if (!InstanceParameters.Contains(parameter.Name))
+ {
+ AddUnsupportedParameter(context, "ideal diode", name, parameter);
+ return false;
+ }
+
+ return SetParameter(context, entity, parameter);
+ }
+
+ public static bool SetPositionalAreaParameter(IReadingContext context, IdealDiode entity, Parameter parameter)
+ {
+ return SetParameter(context, entity, "area", parameter);
+ }
+
+ private static void StoreSelectionParameter(IReadingContext context, Model model, AssignmentParameter parameter)
+ {
+ try
+ {
+ var value = context.Evaluator.EvaluateDouble(parameter.Value);
+ model.SetSelectionParameter(parameter.Name, value);
+ }
+ catch (Exception ex)
+ {
+ context.Result.ValidationResult.AddError(
+ ValidationEntrySource.Reader,
+ $"Problem with setting ideal diode model selection parameter: {parameter}",
+ parameter.LineInfo,
+ ex);
+ }
+ }
+
+ private static bool SetParameter(IReadingContext context, IEntity entity, AssignmentParameter parameter)
+ {
+ return SetParameter(context, entity, parameter.Name, parameter);
+ }
+
+ private static bool SetParameter(IReadingContext context, IEntity entity, string parameterName, Parameter parameter)
+ {
+ try
+ {
+ context.SetParameter(entity, parameterName, parameter.Value, logError: false);
+ return true;
+ }
+ catch (Exception ex)
+ {
+ context.Result.ValidationResult.AddError(
+ ValidationEntrySource.Reader,
+ $"Problem with setting parameter: {parameter}",
+ parameter.LineInfo,
+ ex);
+ return false;
+ }
+ }
+
+ private static void AddUnsupportedParameter(IReadingContext context, string targetKind, string targetName, Parameter parameter)
+ {
+ context.Result.ValidationResult.AddError(
+ ValidationEntrySource.Reader,
+ $"Unsupported {targetKind} parameter '{parameter}' on '{targetName}'.",
+ parameter.LineInfo);
+ }
+ }
+}
diff --git a/src/SpiceSharpParser.CustomComponents/SpiceSharpParser.CustomComponents.csproj b/src/SpiceSharpParser.CustomComponents/SpiceSharpParser.CustomComponents.csproj
new file mode 100644
index 00000000..4f024c3f
--- /dev/null
+++ b/src/SpiceSharpParser.CustomComponents/SpiceSharpParser.CustomComponents.csproj
@@ -0,0 +1,42 @@
+
+
+
+ {BC0D188B-D27A-4870-A739-A57A61D6E497}
+ netstandard2.0;net8.0
+ SpiceSharp
+ https://github.com/SpiceSharp/SpiceSharpParser
+ Copyright 2026
+ true
+ https://github.com/SpiceSharp/SpiceSharpParser
+
+ circuit electronics spice custom components ideal diode
+ SpiceSharpParser.CustomComponents
+ SpiceSharpParser.CustomComponents
+ Custom SpiceSharp components for SpiceSharpParser integrations.
+ Refer to the GitHub release for release notes
+ true
+ full
+ true
+ Library
+ MIT
+ latest
+ 0.1.0
+
+
+
+
+ 1701;1702;
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
diff --git a/src/SpiceSharpParser.IntegrationTests/LTspiceCompatibility/LTspiceIdealDiodeIntegrationTests.cs b/src/SpiceSharpParser.IntegrationTests/LTspiceCompatibility/LTspiceIdealDiodeIntegrationTests.cs
new file mode 100644
index 00000000..0bbe7857
--- /dev/null
+++ b/src/SpiceSharpParser.IntegrationTests/LTspiceCompatibility/LTspiceIdealDiodeIntegrationTests.cs
@@ -0,0 +1,196 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using SpiceSharp.Simulations;
+using SpiceSharpParser.Common.Validation;
+using SpiceSharpParser.CustomComponents;
+using SpiceSharpParser.ModelReaders.Netlist.Spice;
+using SpiceSharpParser.Models.Netlist.Spice;
+using Xunit;
+
+namespace SpiceSharpParser.IntegrationTests.LTspiceCompatibility
+{
+ public class LTspiceIdealDiodeIntegrationTests : BaseTests
+ {
+ [Fact]
+ public void When_FullBridgeRectifierUsesLtspiceIdealDiodes_Expect_BipolarInputIsRectified()
+ {
+ var model = ReadWithCustomComponents(
+ "LTspice ideal diode bridge rectifier",
+ "VIN acp 0 0",
+ "DPLUS acp outp rect",
+ "DRETURN outn 0 rect",
+ "DNEG 0 outp rect",
+ "DNEGRETURN outn acp rect",
+ "RLOAD outp outn 10",
+ ".model rect D(Ron=0.5 Roff=1e12 Vfwd=0.7)",
+ ".dc VIN -10 10 10",
+ ".save V(outp,outn)",
+ ".end");
+
+ AssertNoValidationIssues(model.ValidationResult);
+ Assert.Equal(4, model.Circuit.OfType().Count());
+
+ var exports = RunDCSimulation(model, "V(outp,outn)");
+ Assert.Equal(3, exports.Length);
+
+ double expectedRectifiedVoltage = 10.0 * ((10.0 - (2.0 * 0.7)) / (10.0 + (2.0 * 0.5)));
+
+ AssertSweepPoint(-10.0, expectedRectifiedVoltage, exports[0]);
+ AssertSweepPoint(0.0, 0.0, exports[1]);
+ AssertSweepPoint(10.0, expectedRectifiedVoltage, exports[2]);
+ }
+
+ [Fact]
+ public void When_FullBridgeRectifierUsesLtspiceIdealDiodesInAc_Expect_ForwardPathSmallSignalGain()
+ {
+ var model = ReadWithCustomComponents(
+ "LTspice AC ideal diode bridge rectifier",
+ "VIN acp 0 DC 10 AC 1",
+ "DPLUS acp outp rect",
+ "DRETURN outn 0 rect",
+ "DNEG 0 outp rect",
+ "DNEGRETURN outn acp rect",
+ "RLOAD outp outn 10",
+ ".model rect D(Ron=0.5 Roff=1e12 Vfwd=0.7)",
+ ".ac lin 1 1k 1k",
+ ".save VM(outp,outn)",
+ ".end");
+
+ AssertNoValidationIssues(model.ValidationResult);
+ Assert.Equal(4, model.Circuit.OfType().Count());
+
+ var exports = RunAcSimulation(model, "VM(outp,outn)");
+ Assert.Single(exports);
+
+ double expectedGain = 10.0 / (10.0 + (2.0 * 0.5));
+ AssertSweepPoint(1000.0, expectedGain, exports[0]);
+ }
+
+ [Fact]
+ public void When_FullBridgeRectifierUsesLtspiceIdealDiodesWithSinTransient_Expect_RectifiedWaveform()
+ {
+ var model = ReadWithCustomComponents(
+ "LTspice TRAN ideal diode bridge rectifier",
+ "VIN acp 0 SIN(0 10 1k)",
+ "DPLUS acp outp rect",
+ "DRETURN outn 0 rect",
+ "DNEG 0 outp rect",
+ "DNEGRETURN outn acp rect",
+ "RLOAD outp outn 10",
+ ".model rect D(Ron=0.5 Roff=1e12 Vfwd=0.7)",
+ ".tran 25u 1m 0 25u",
+ ".save V(acp) V(outp,outn)",
+ ".end");
+
+ AssertNoValidationIssues(model.ValidationResult);
+ Assert.Equal(4, model.Circuit.OfType().Count());
+
+ var exports = RunTransientSimulation(model, "V(acp)", "V(outp,outn)");
+ Assert.True(exports.Length > 20);
+
+ double expectedPeak = ExpectedBridgeOutput(10.0, 10.0, 0.5, 0.7);
+ AssertClose(expectedPeak, exports.Max(point => point.Item3), 1e-2);
+ Assert.Contains(exports, point => point.Item2 < -9.9 && point.Item3 > expectedPeak - 1e-2);
+
+ foreach (var point in exports)
+ {
+ double expected = ExpectedBridgeOutput(point.Item2, 10.0, 0.5, 0.7);
+ AssertClose(expected, point.Item3, 1e-3);
+ }
+ }
+
+ private static SpiceSharpModel ReadWithCustomComponents(params string[] lines)
+ {
+ var parser = new SpiceNetlistParser();
+ parser.Settings.Lexing.HasTitle = true;
+ parser.Settings.Parsing.IsEndRequired = true;
+
+ var parseResult = parser.ParseNetlist(string.Join(Environment.NewLine, lines));
+ var reader = new SpiceSharpReader();
+ reader.Settings.UseCustomComponents();
+
+ return reader.Read(parseResult.FinalModel);
+ }
+
+ private static Tuple[] RunAcSimulation(SpiceSharpModel model, string nameOfExport)
+ {
+ var export = model.Exports.Find(e => e.Name == nameOfExport && e.Simulation is AC);
+ var simulation = model.Simulations.First(s => s is AC);
+ var list = new List>();
+
+ Assert.NotNull(export);
+
+ simulation.EventExportData += (sender, e) =>
+ {
+ list.Add(new Tuple(((AC)simulation).Frequency, export.Extract()));
+ };
+
+ var codes = simulation.Run(model.Circuit, -1);
+ var attached = simulation.InvokeEvents(codes);
+ attached.ToArray();
+
+ return list.ToArray();
+ }
+
+ private static Tuple[] RunTransientSimulation(
+ SpiceSharpModel model,
+ string inputExportName,
+ string outputExportName)
+ {
+ var inputExport = model.Exports.Find(e => e.Name == inputExportName && e.Simulation is Transient);
+ var outputExport = model.Exports.Find(e => e.Name == outputExportName && e.Simulation is Transient);
+ var simulation = model.Simulations.First(s => s is Transient);
+ var list = new List>();
+
+ Assert.NotNull(inputExport);
+ Assert.NotNull(outputExport);
+
+ simulation.EventExportData += (sender, e) =>
+ {
+ list.Add(new Tuple(
+ ((Transient)simulation).Time,
+ inputExport.Extract(),
+ outputExport.Extract()));
+ };
+
+ var codes = simulation.Run(model.Circuit, -1);
+ var attached = simulation.InvokeEvents(codes);
+ attached.ToArray();
+
+ return list.ToArray();
+ }
+
+ private static double ExpectedBridgeOutput(double inputVoltage, double loadResistance, double onResistance, double forwardVoltage)
+ {
+ double rectifiedInput = Math.Abs(inputVoltage);
+ double diodeDrop = 2.0 * forwardVoltage;
+ if (rectifiedInput <= diodeDrop)
+ {
+ return 0.0;
+ }
+
+ return loadResistance * ((rectifiedInput - diodeDrop) / (loadResistance + (2.0 * onResistance)));
+ }
+
+ private static void AssertSweepPoint(double expectedSweep, double expectedValue, Tuple actual)
+ {
+ AssertClose(expectedSweep, actual.Item1, 1e-12);
+ AssertClose(expectedValue, actual.Item2, 1e-6);
+ }
+
+ private static void AssertNoValidationIssues(ValidationEntryCollection validation)
+ {
+ string messages = string.Join(Environment.NewLine, validation.Select(entry => entry.Message));
+ Assert.False(validation.HasError, messages);
+ Assert.False(validation.HasWarning, messages);
+ }
+
+ private static void AssertClose(double expected, double actual, double tolerance)
+ {
+ Assert.True(
+ Math.Abs(expected - actual) <= tolerance,
+ $"Expected {expected:R}, got {actual:R}.");
+ }
+ }
+}
diff --git a/src/SpiceSharpParser.IntegrationTests/SpiceSharpParser.IntegrationTests.csproj b/src/SpiceSharpParser.IntegrationTests/SpiceSharpParser.IntegrationTests.csproj
index 2b1b40db..8e21b0c0 100644
--- a/src/SpiceSharpParser.IntegrationTests/SpiceSharpParser.IntegrationTests.csproj
+++ b/src/SpiceSharpParser.IntegrationTests/SpiceSharpParser.IntegrationTests.csproj
@@ -38,9 +38,10 @@
-
-
-
+
+
+
+
diff --git a/src/SpiceSharpParser.Tests/CustomComponents/IdealDiodeLtspiceGoldenTests.cs b/src/SpiceSharpParser.Tests/CustomComponents/IdealDiodeLtspiceGoldenTests.cs
new file mode 100644
index 00000000..0c0f2dbc
--- /dev/null
+++ b/src/SpiceSharpParser.Tests/CustomComponents/IdealDiodeLtspiceGoldenTests.cs
@@ -0,0 +1,1005 @@
+using SpiceSharp;
+using SpiceSharp.Components;
+using SpiceSharp.Simulations;
+using SpiceSharpParser.CustomComponents;
+using SpiceSharpParser.CustomComponents.IdealDiodes;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Numerics;
+using System.Text;
+using Xunit;
+
+namespace SpiceSharpParser.Tests.CustomComponents
+{
+ public class IdealDiodeLtspiceGoldenTests
+ {
+ private const string LtspiceExecutableVariable = "LTSPICE_EXE";
+ private const int LtspiceTimeoutMilliseconds = 30000;
+
+ [LtspiceFact]
+ public void DcSweep_WhenComparedWithLtspice_MatchesIdealDiodeParameters()
+ {
+ string ltspiceExecutable = GetLtspiceExecutable();
+
+ foreach (var testCase in CreateCases())
+ {
+ var ltspicePoints = RunLtspiceDcSweep(ltspiceExecutable, testCase);
+ var spiceSharpPoints = RunSpiceSharpOperatingPoints(testCase, ltspicePoints.Select(point => point.Voltage));
+
+ Assert.Equal(ltspicePoints.Count, spiceSharpPoints.Count);
+ for (int i = 0; i < ltspicePoints.Count; i++)
+ {
+ var ltspicePoint = ltspicePoints[i];
+ var spiceSharpPoint = spiceSharpPoints[i];
+ AssertClose(testCase, ltspicePoint.Voltage, ltspicePoint.Current, spiceSharpPoint.Current);
+ }
+ }
+ }
+
+ [LtspiceFact]
+ public void Ac_WhenComparedWithLtspice_MatchesSmallSignalDerivatives()
+ {
+ string ltspiceExecutable = GetLtspiceExecutable();
+
+ foreach (var testCase in CreateAcCases())
+ {
+ var raw = RunLtspiceRaw(
+ ltspiceExecutable,
+ testCase.Name,
+ CreateLtspiceAcNetlist(testCase));
+ var ltspicePoints = LtspiceAsciiRawFile.GetComplexSeries(raw, "V(out)");
+ var spiceSharpPoints = RunSpiceSharpAcSmallSignal(testCase);
+
+ Assert.Equal(ltspicePoints.Count, spiceSharpPoints.Count);
+ for (int i = 0; i < ltspicePoints.Count; i++)
+ {
+ AssertClose(
+ testCase.Name,
+ ltspicePoints[i].Frequency,
+ ltspicePoints[i].Frequency,
+ spiceSharpPoints[i].Frequency,
+ 1e-9,
+ 1e-9);
+ AssertComplexClose(
+ testCase.Name,
+ ltspicePoints[i].Frequency,
+ ltspicePoints[i].Value,
+ spiceSharpPoints[i].Value,
+ testCase.AbsoluteTolerance,
+ testCase.RelativeTolerance);
+ }
+ }
+ }
+
+ [LtspiceFact]
+ public void TransientBridge_WhenComparedWithLtspice_MatchesSinRectifierWaveform()
+ {
+ string ltspiceExecutable = GetLtspiceExecutable();
+
+ var raw = RunLtspiceRaw(
+ ltspiceExecutable,
+ "tran_sin_bridge_rectifier",
+ CreateLtspiceTransientBridgeNetlist());
+ var ltspicePoints = LtspiceAsciiRawFile.GetBridgeTransientSeries(raw, "V(acp)", "V(outp)", "V(outn)");
+ var spiceSharpPoints = RunSpiceSharpBridgeOperatingPoints(ltspicePoints.Select(point => point.Input));
+
+ Assert.True(ltspicePoints.Count > 20);
+ Assert.Equal(ltspicePoints.Count, spiceSharpPoints.Count);
+ for (int i = 0; i < ltspicePoints.Count; i++)
+ {
+ AssertClose(
+ "tran_sin_bridge_rectifier",
+ ltspicePoints[i].Time,
+ ltspicePoints[i].Output,
+ spiceSharpPoints[i],
+ 1e-5,
+ 1e-3);
+ }
+ }
+
+ private static IReadOnlyList CreateCases()
+ {
+ return new[]
+ {
+ new LtspiceIdealDiodeCase(
+ "ron_roff_vfwd",
+ "D1 in 0 did",
+ ".model did D(Ron=2 Roff=1e9 Vfwd=1)",
+ -1.0,
+ 3.0,
+ 0.1,
+ parameters =>
+ {
+ parameters.OnResistance = 2.0;
+ parameters.OffResistance = 1e9;
+ parameters.ForwardVoltage = 1.0;
+ }),
+
+ new LtspiceIdealDiodeCase(
+ "vrev_rrev",
+ "D1 in 0 did",
+ ".model did D(Ron=2 Roff=1e9 Vfwd=1 Vrev=2 Rrev=4)",
+ -6.0,
+ 3.0,
+ 0.1,
+ parameters =>
+ {
+ parameters.OnResistance = 2.0;
+ parameters.OffResistance = 1e9;
+ parameters.ForwardVoltage = 1.0;
+ parameters.ReverseVoltage = 2.0;
+ parameters.ReverseResistance = 4.0;
+ }),
+
+ new LtspiceIdealDiodeCase(
+ "forward_and_reverse_current_limits",
+ "D1 in 0 did",
+ ".model did D(Ron=1 Roff=1e9 Vfwd=0 Vrev=0 Rrev=1 Ilimit=2 RevIlimit=3)",
+ -8.0,
+ 8.0,
+ 0.2,
+ parameters =>
+ {
+ parameters.OnResistance = 1.0;
+ parameters.OffResistance = 1e9;
+ parameters.ForwardVoltage = 0.0;
+ parameters.ReverseVoltage = 0.0;
+ parameters.ReverseResistance = 1.0;
+ parameters.ForwardCurrentLimit = 2.0;
+ parameters.ReverseCurrentLimit = 3.0;
+ }),
+
+ new LtspiceIdealDiodeCase(
+ "forward_and_reverse_epsilon",
+ "D1 in 0 did",
+ ".model did D(Ron=1 Roff=1e12 Vfwd=1 Vrev=2 Rrev=2 Epsilon=0.2 RevEpsilon=0.4)",
+ -3.0,
+ 2.0,
+ 0.05,
+ parameters =>
+ {
+ parameters.OnResistance = 1.0;
+ parameters.OffResistance = 1e12;
+ parameters.ForwardVoltage = 1.0;
+ parameters.ReverseVoltage = 2.0;
+ parameters.ReverseResistance = 2.0;
+ parameters.ForwardEpsilon = 0.2;
+ parameters.ReverseEpsilon = 0.4;
+ },
+ 1e-7,
+ 1e-3),
+
+ new LtspiceIdealDiodeCase(
+ "m_n_with_ignored_area_rs_and_off",
+ "D1 in 0 did 2 off m=3 n=2",
+ ".model did D(Ron=2 Roff=1e9 Vfwd=1 Rs=3)",
+ -1.0,
+ 10.0,
+ 0.25,
+ parameters =>
+ {
+ parameters.Area = 2.0;
+ parameters.Off = true;
+ parameters.ParallelMultiplier = 3.0;
+ parameters.SeriesMultiplier = 2.0;
+ parameters.Resistance = 3.0;
+ parameters.OnResistance = 2.0;
+ parameters.OffResistance = 1e9;
+ parameters.ForwardVoltage = 1.0;
+ },
+ 1e-6,
+ 1e-3),
+
+ new LtspiceIdealDiodeCase(
+ "rrev_omitted_with_reverse_limit_and_m_n",
+ "D1 in 0 did m=2 n=3",
+ ".model did D(Ron=1.5 Roff=1e10 Vfwd=0.6 Vrev=1.2 RevIlimit=2.5)",
+ -12.0,
+ 6.0,
+ 0.2,
+ parameters =>
+ {
+ parameters.ParallelMultiplier = 2.0;
+ parameters.SeriesMultiplier = 3.0;
+ parameters.OnResistance = 1.5;
+ parameters.OffResistance = 1e10;
+ parameters.ForwardVoltage = 0.6;
+ parameters.ReverseVoltage = 1.2;
+ parameters.ReverseCurrentLimit = 2.5;
+ },
+ 1e-6,
+ 1e-3),
+
+ new LtspiceIdealDiodeCase(
+ "forward_current_limit_and_parallel_cells",
+ "D1 in 0 did m=4",
+ ".model did D(Ron=0.25 Roff=1e12 Vfwd=0.8 Ilimit=1.5)",
+ -0.5,
+ 4.0,
+ 0.05,
+ parameters =>
+ {
+ parameters.ParallelMultiplier = 4.0;
+ parameters.OnResistance = 0.25;
+ parameters.OffResistance = 1e12;
+ parameters.ForwardVoltage = 0.8;
+ parameters.ForwardCurrentLimit = 1.5;
+ },
+ 1e-6,
+ 1e-3),
+
+ new LtspiceIdealDiodeCase(
+ "reverse_limit_and_asymmetric_rrev",
+ "D1 in 0 did",
+ ".model did D(Ron=3 Roff=1e11 Vfwd=0.5 Vrev=1.25 Rrev=0.75 RevIlimit=2.2)",
+ -4.0,
+ 2.0,
+ 0.05,
+ parameters =>
+ {
+ parameters.OnResistance = 3.0;
+ parameters.OffResistance = 1e11;
+ parameters.ForwardVoltage = 0.5;
+ parameters.ReverseVoltage = 1.25;
+ parameters.ReverseResistance = 0.75;
+ parameters.ReverseCurrentLimit = 2.2;
+ },
+ 1e-6,
+ 1e-3),
+
+ new LtspiceIdealDiodeCase(
+ "moderate_roff_intersections",
+ "D1 in 0 did",
+ ".model did D(Ron=2 Roff=20 Vfwd=1 Vrev=2 Rrev=4)",
+ -6.0,
+ 4.0,
+ 0.1,
+ parameters =>
+ {
+ parameters.OnResistance = 2.0;
+ parameters.OffResistance = 20.0;
+ parameters.ForwardVoltage = 1.0;
+ parameters.ReverseVoltage = 2.0;
+ parameters.ReverseResistance = 4.0;
+ },
+ 1e-6,
+ 1e-3),
+
+ new LtspiceIdealDiodeCase(
+ "combined_limits_asymmetric_reverse_and_m_n",
+ "D1 in 0 did m=2 n=2",
+ ".model did D(Ron=1.25 Roff=1e10 Vfwd=0.4 Vrev=1.6 Rrev=2.2 Ilimit=3 RevIlimit=2)",
+ -7.0,
+ 7.0,
+ 0.1,
+ parameters =>
+ {
+ parameters.ParallelMultiplier = 2.0;
+ parameters.SeriesMultiplier = 2.0;
+ parameters.OnResistance = 1.25;
+ parameters.OffResistance = 1e10;
+ parameters.ForwardVoltage = 0.4;
+ parameters.ReverseVoltage = 1.6;
+ parameters.ReverseResistance = 2.2;
+ parameters.ForwardCurrentLimit = 3.0;
+ parameters.ReverseCurrentLimit = 2.0;
+ },
+ 1e-6,
+ 1e-3),
+ };
+ }
+
+ private static IReadOnlyList CreateAcCases()
+ {
+ return new[]
+ {
+ new LtspiceIdealDiodeAcCase(
+ "ac_forward_biased_small_signal",
+ 3.0,
+ ".model did D(Ron=2 Roff=1e9 Vfwd=1)",
+ parameters =>
+ {
+ parameters.OnResistance = 2.0;
+ parameters.OffResistance = 1e9;
+ parameters.ForwardVoltage = 1.0;
+ }),
+
+ new LtspiceIdealDiodeAcCase(
+ "ac_forward_current_limit_derivative",
+ 5.0,
+ ".model did D(Ron=1 Roff=1e12 Vfwd=0 Ilimit=2)",
+ parameters =>
+ {
+ parameters.OnResistance = 1.0;
+ parameters.OffResistance = 1e12;
+ parameters.ForwardVoltage = 0.0;
+ parameters.ForwardCurrentLimit = 2.0;
+ },
+ 1e-7,
+ 1e-4),
+
+ new LtspiceIdealDiodeAcCase(
+ "ac_forward_epsilon_ramp_derivative",
+ 1.1,
+ ".model did D(Ron=1 Roff=1e12 Vfwd=1 Epsilon=0.2)",
+ parameters =>
+ {
+ parameters.OnResistance = 1.0;
+ parameters.OffResistance = 1e12;
+ parameters.ForwardVoltage = 1.0;
+ parameters.ForwardEpsilon = 0.2;
+ },
+ 1e-7,
+ 1e-4),
+
+ new LtspiceIdealDiodeAcCase(
+ "ac_finite_roff_forward_epsilon_derivative",
+ 1.17875,
+ ".model did D(Ron=1 Roff=20 Vfwd=1 Epsilon=0.2)",
+ parameters =>
+ {
+ parameters.OnResistance = 1.0;
+ parameters.OffResistance = 20.0;
+ parameters.ForwardVoltage = 1.0;
+ parameters.ForwardEpsilon = 0.2;
+ },
+ 1e-7,
+ 1e-4),
+
+ new LtspiceIdealDiodeAcCase(
+ "ac_reverse_epsilon_ramp_derivative",
+ -2.225,
+ ".model did D(Ron=1 Roff=1e12 Vfwd=1 Vrev=2 Rrev=2 RevEpsilon=0.4)",
+ parameters =>
+ {
+ parameters.OnResistance = 1.0;
+ parameters.OffResistance = 1e12;
+ parameters.ForwardVoltage = 1.0;
+ parameters.ReverseVoltage = 2.0;
+ parameters.ReverseResistance = 2.0;
+ parameters.ReverseEpsilon = 0.4;
+ },
+ 1e-7,
+ 1e-4),
+
+ new LtspiceIdealDiodeAcCase(
+ "ac_finite_roff_reverse_epsilon_derivative",
+ -2.3325,
+ ".model did D(Ron=1 Roff=20 Vfwd=1 Vrev=2 Rrev=2 RevEpsilon=0.4)",
+ parameters =>
+ {
+ parameters.OnResistance = 1.0;
+ parameters.OffResistance = 20.0;
+ parameters.ForwardVoltage = 1.0;
+ parameters.ReverseVoltage = 2.0;
+ parameters.ReverseResistance = 2.0;
+ parameters.ReverseEpsilon = 0.4;
+ },
+ 1e-7,
+ 1e-4),
+
+ new LtspiceIdealDiodeAcCase(
+ "ac_reverse_breakdown_rrev_derivative",
+ -5.0,
+ ".model did D(Ron=1 Roff=1e12 Vfwd=1 Vrev=1.5 Rrev=3)",
+ parameters =>
+ {
+ parameters.OnResistance = 1.0;
+ parameters.OffResistance = 1e12;
+ parameters.ForwardVoltage = 1.0;
+ parameters.ReverseVoltage = 1.5;
+ parameters.ReverseResistance = 3.0;
+ }),
+ };
+ }
+
+ private static string GetLtspiceExecutable()
+ {
+ string executable = Environment.GetEnvironmentVariable(LtspiceExecutableVariable);
+ if (string.IsNullOrWhiteSpace(executable))
+ {
+ throw new InvalidOperationException($"Set {LtspiceExecutableVariable} to the LTspice executable path to run these golden tests.");
+ }
+
+ if (!File.Exists(executable))
+ {
+ throw new FileNotFoundException($"The LTspice executable configured by {LtspiceExecutableVariable} was not found.", executable);
+ }
+
+ return executable;
+ }
+
+ private static IReadOnlyList<(double Voltage, double Current)> RunLtspiceDcSweep(
+ string ltspiceExecutable,
+ LtspiceIdealDiodeCase testCase)
+ {
+ var raw = RunLtspiceRaw(ltspiceExecutable, testCase.Name, CreateLtspiceNetlist(testCase));
+ return LtspiceAsciiRawFile.GetRealSeries(raw, "V(in)", "I(D1)");
+ }
+
+ private static LtspiceAsciiRawFile RunLtspiceRaw(
+ string ltspiceExecutable,
+ string caseName,
+ IReadOnlyList netlistLines)
+ {
+ string directory = Path.Combine(Path.GetTempPath(), "SpiceSharpParser.LTspice", Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(directory);
+
+ try
+ {
+ string circuitPath = Path.Combine(directory, caseName + ".cir");
+ string rawPath = Path.ChangeExtension(circuitPath, ".raw");
+ File.WriteAllLines(circuitPath, netlistLines, Encoding.ASCII);
+
+ var result = RunLtspice(ltspiceExecutable, circuitPath, directory);
+ if (!WaitForFile(rawPath, LtspiceTimeoutMilliseconds))
+ {
+ throw new InvalidOperationException(
+ $"LTspice did not produce '{rawPath}' for case '{caseName}'."
+ + Environment.NewLine
+ + result);
+ }
+
+ return LtspiceAsciiRawFile.Read(rawPath);
+ }
+ finally
+ {
+ TryDeleteDirectory(directory);
+ }
+ }
+
+ private static bool WaitForFile(string path, int timeoutMilliseconds)
+ {
+ var stopwatch = Stopwatch.StartNew();
+ while (stopwatch.ElapsedMilliseconds < timeoutMilliseconds)
+ {
+ if (File.Exists(path))
+ {
+ return true;
+ }
+
+ System.Threading.Thread.Sleep(100);
+ }
+
+ return File.Exists(path);
+ }
+
+ private static IReadOnlyList CreateLtspiceNetlist(LtspiceIdealDiodeCase testCase)
+ {
+ return new[]
+ {
+ "Ideal diode LTspice golden comparison",
+ "V1 in 0 0",
+ testCase.InstanceLine,
+ testCase.ModelLine,
+ FormattableString.Invariant($".dc V1 {testCase.Start} {testCase.Stop} {testCase.Step}"),
+ ".save V(in) I(D1)",
+ ".end",
+ };
+ }
+
+ private static IReadOnlyList CreateLtspiceAcNetlist(LtspiceIdealDiodeAcCase testCase)
+ {
+ return new[]
+ {
+ "Ideal diode LTspice AC golden comparison",
+ FormattableString.Invariant($"V1 in 0 DC {testCase.SourceVoltage} AC 1"),
+ FormattableString.Invariant($"R1 in out {testCase.SourceResistance}"),
+ "D1 out 0 did",
+ testCase.ModelLine,
+ ".ac lin 1 1k 1k",
+ ".save V(out)",
+ ".end",
+ };
+ }
+
+ private static IReadOnlyList CreateLtspiceTransientBridgeNetlist()
+ {
+ return new[]
+ {
+ "Ideal diode LTspice transient bridge golden comparison",
+ "VIN acp 0 SIN(0 10 1k)",
+ "DPLUS acp outp rect",
+ "DRETURN outn 0 rect",
+ "DNEG 0 outp rect",
+ "DNEGRETURN outn acp rect",
+ "RLOAD outp outn 10",
+ ".model rect D(Ron=0.5 Roff=1e12 Vfwd=0.7)",
+ ".tran 25u 1m 0 25u",
+ ".save V(acp) V(outp) V(outn)",
+ ".end",
+ };
+ }
+
+ private static ProcessResult RunLtspice(string ltspiceExecutable, string circuitPath, string workingDirectory)
+ {
+ var startInfo = new ProcessStartInfo
+ {
+ FileName = ltspiceExecutable,
+ WorkingDirectory = workingDirectory,
+ UseShellExecute = false,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ };
+
+ startInfo.ArgumentList.Add("-b");
+ startInfo.ArgumentList.Add("-ascii");
+ startInfo.ArgumentList.Add(circuitPath);
+
+ using (var process = Process.Start(startInfo))
+ {
+ if (process == null)
+ {
+ throw new InvalidOperationException("Failed to start LTspice.");
+ }
+
+ if (!process.WaitForExit(LtspiceTimeoutMilliseconds))
+ {
+ try
+ {
+ process.Kill();
+ }
+ catch (InvalidOperationException)
+ {
+ }
+
+ throw new TimeoutException($"LTspice did not finish within {LtspiceTimeoutMilliseconds} ms for '{circuitPath}'.");
+ }
+
+ string output = process.StandardOutput.ReadToEnd();
+ string error = process.StandardError.ReadToEnd();
+ if (process.ExitCode != 0)
+ {
+ throw new InvalidOperationException(
+ $"LTspice exited with code {process.ExitCode} for '{circuitPath}'."
+ + Environment.NewLine
+ + new ProcessResult(output, error));
+ }
+
+ return new ProcessResult(output, error);
+ }
+ }
+
+ private static IReadOnlyList<(double Voltage, double Current)> RunSpiceSharpOperatingPoints(
+ LtspiceIdealDiodeCase testCase,
+ IEnumerable voltages)
+ {
+ var result = new List<(double Voltage, double Current)>();
+ foreach (double voltage in voltages)
+ {
+ var diode = new IdealDiode("D1", "in", "0");
+ testCase.Configure(diode.Parameters);
+
+ var circuit = new Circuit(
+ new VoltageSource("V1", "in", "0", voltage),
+ diode);
+
+ var op = new OP("op");
+ var currentExport = new RealPropertyExport(op, "D1", "i");
+
+ double current = double.NaN;
+ foreach (int ignored in op.Run(circuit))
+ {
+ current = currentExport.Value;
+ }
+
+ result.Add((voltage, current));
+ }
+
+ return result;
+ }
+
+ private static IReadOnlyList<(double Frequency, Complex Value)> RunSpiceSharpAcSmallSignal(
+ LtspiceIdealDiodeAcCase testCase)
+ {
+ var diode = new IdealDiode("D1", "out", "0");
+ testCase.Configure(diode.Parameters);
+
+ var circuit = new Circuit(
+ new VoltageSource("V1", "in", "0", testCase.SourceVoltage).SetParameter("acmag", 1.0),
+ new Resistor("R1", "in", "out", testCase.SourceResistance),
+ diode);
+
+ var ac = new AC("ac", new LinearSweep(1000.0, 1000.0, 1));
+ var export = new ComplexVoltageExport(ac, "out");
+ var result = new List<(double Frequency, Complex Value)>();
+
+ foreach (int ignored in ac.Run(circuit, AC.ExportSmallSignal))
+ {
+ result.Add((ac.Frequency, export.Value));
+ }
+
+ return result;
+ }
+
+ private static IReadOnlyList RunSpiceSharpBridgeOperatingPoints(IEnumerable inputVoltages)
+ {
+ var result = new List();
+ foreach (double inputVoltage in inputVoltages)
+ {
+ var circuit = new Circuit(
+ new VoltageSource("VIN", "acp", "0", inputVoltage),
+ CreateRectifierDiode("DPLUS", "acp", "outp"),
+ CreateRectifierDiode("DRETURN", "outn", "0"),
+ CreateRectifierDiode("DNEG", "0", "outp"),
+ CreateRectifierDiode("DNEGRETURN", "outn", "acp"),
+ new Resistor("RLOAD", "outp", "outn", 10.0));
+
+ var op = new OP("op");
+ var export = new RealVoltageExport(op, "outp", "outn");
+ double output = double.NaN;
+ foreach (int ignored in op.Run(circuit))
+ {
+ output = export.Value;
+ }
+
+ result.Add(output);
+ }
+
+ return result;
+ }
+
+ private static IdealDiode CreateRectifierDiode(string name, string anode, string cathode)
+ {
+ var diode = new IdealDiode(name, anode, cathode);
+ diode.Parameters.OnResistance = 0.5;
+ diode.Parameters.OffResistance = 1e12;
+ diode.Parameters.ForwardVoltage = 0.7;
+ return diode;
+ }
+
+ private static void AssertClose(
+ LtspiceIdealDiodeCase testCase,
+ double voltage,
+ double expected,
+ double actual)
+ {
+ AssertClose(testCase.Name, voltage, expected, actual, testCase.AbsoluteTolerance, testCase.RelativeTolerance);
+ }
+
+ private static void AssertClose(
+ string caseName,
+ double x,
+ double expected,
+ double actual,
+ double absoluteTolerance,
+ double relativeTolerance)
+ {
+ double tolerance = absoluteTolerance
+ + (relativeTolerance * Math.Max(Math.Abs(expected), Math.Abs(actual)));
+ double difference = Math.Abs(expected - actual);
+
+ Assert.True(
+ difference <= tolerance,
+ FormattableString.Invariant(
+ $"Case '{caseName}' differs at x={x}: LTspice={expected}, SpiceSharpParser={actual}, difference={difference}, tolerance={tolerance}."));
+ }
+
+ private static void AssertComplexClose(
+ string caseName,
+ double x,
+ Complex expected,
+ Complex actual,
+ double absoluteTolerance,
+ double relativeTolerance)
+ {
+ AssertClose(caseName + " real", x, expected.Real, actual.Real, absoluteTolerance, relativeTolerance);
+ AssertClose(caseName + " imaginary", x, expected.Imaginary, actual.Imaginary, absoluteTolerance, relativeTolerance);
+ }
+
+ private static void TryDeleteDirectory(string directory)
+ {
+ try
+ {
+ Directory.Delete(directory, true);
+ }
+ catch (IOException)
+ {
+ }
+ catch (UnauthorizedAccessException)
+ {
+ }
+ }
+
+ private sealed class LtspiceFactAttribute : FactAttribute
+ {
+ public LtspiceFactAttribute()
+ {
+ if (string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable(LtspiceExecutableVariable)))
+ {
+ this.Skip = $"Set {LtspiceExecutableVariable} to the LTspice executable path to run this external golden test.";
+ }
+ }
+ }
+
+ private sealed class LtspiceIdealDiodeCase
+ {
+ public LtspiceIdealDiodeCase(
+ string name,
+ string instanceLine,
+ string modelLine,
+ double start,
+ double stop,
+ double step,
+ Action configure,
+ double absoluteTolerance = 1e-8,
+ double relativeTolerance = 1e-4)
+ {
+ this.Name = name;
+ this.InstanceLine = instanceLine;
+ this.ModelLine = modelLine;
+ this.Start = start;
+ this.Stop = stop;
+ this.Step = step;
+ this.Configure = configure;
+ this.AbsoluteTolerance = absoluteTolerance;
+ this.RelativeTolerance = relativeTolerance;
+ }
+
+ public string Name { get; }
+
+ public string InstanceLine { get; }
+
+ public string ModelLine { get; }
+
+ public double Start { get; }
+
+ public double Stop { get; }
+
+ public double Step { get; }
+
+ public Action Configure { get; }
+
+ public double AbsoluteTolerance { get; }
+
+ public double RelativeTolerance { get; }
+ }
+
+ private sealed class LtspiceIdealDiodeAcCase
+ {
+ public LtspiceIdealDiodeAcCase(
+ string name,
+ double sourceVoltage,
+ string modelLine,
+ Action configure,
+ double absoluteTolerance = 1e-8,
+ double relativeTolerance = 1e-5,
+ double sourceResistance = 1.0)
+ {
+ this.Name = name;
+ this.SourceVoltage = sourceVoltage;
+ this.ModelLine = modelLine;
+ this.Configure = configure;
+ this.AbsoluteTolerance = absoluteTolerance;
+ this.RelativeTolerance = relativeTolerance;
+ this.SourceResistance = sourceResistance;
+ }
+
+ public string Name { get; }
+
+ public double SourceVoltage { get; }
+
+ public string ModelLine { get; }
+
+ public Action Configure { get; }
+
+ public double AbsoluteTolerance { get; }
+
+ public double RelativeTolerance { get; }
+
+ public double SourceResistance { get; }
+ }
+
+ private sealed class LtspiceAsciiRawFile
+ {
+ private LtspiceAsciiRawFile(IReadOnlyList variables, IReadOnlyList points)
+ {
+ this.Variables = variables;
+ this.Points = points;
+ }
+
+ private IReadOnlyList Variables { get; }
+
+ private IReadOnlyList Points { get; }
+
+ public static LtspiceAsciiRawFile Read(string path)
+ {
+ string[] lines = File.ReadAllLines(path);
+ int variableCount = ReadHeaderInt(lines, "No. Variables:");
+ int pointCount = ReadHeaderInt(lines, "No. Points:");
+ int variablesIndex = FindLine(lines, "Variables:");
+ int valuesIndex = FindLine(lines, "Values:");
+
+ var variables = new List();
+ for (int i = 0; i < variableCount; i++)
+ {
+ string line = lines[variablesIndex + 1 + i].Trim();
+ string[] parts = SplitRawLine(line);
+ if (parts.Length < 2)
+ {
+ throw new FormatException($"Invalid LTspice variable line in '{path}': {line}");
+ }
+
+ variables.Add(parts[1]);
+ }
+
+ var points = new List();
+ int cursor = valuesIndex + 1;
+ for (int pointIndex = 0; pointIndex < pointCount; pointIndex++)
+ {
+ var point = new Complex[variableCount];
+ for (int variableIndex = 0; variableIndex < variableCount; variableIndex++)
+ {
+ string line = ReadNextNonEmptyLine(lines, ref cursor, path);
+ string[] parts = SplitRawLine(line);
+ if (variableIndex == 0)
+ {
+ if (parts.Length < 2)
+ {
+ throw new FormatException($"Invalid LTspice value line in '{path}': {line}");
+ }
+
+ point[variableIndex] = ParseComplex(parts[parts.Length - 1]);
+ }
+ else
+ {
+ point[variableIndex] = ParseComplex(parts[parts.Length - 1]);
+ }
+ }
+
+ points.Add(point);
+ }
+
+ return new LtspiceAsciiRawFile(variables, points);
+ }
+
+ public static IReadOnlyList<(double Voltage, double Current)> GetRealSeries(
+ LtspiceAsciiRawFile raw,
+ string voltageName,
+ string currentName)
+ {
+ int voltageIndex = raw.FindVariable(voltageName);
+ int currentIndex = raw.FindVariable(currentName);
+ return raw.Points
+ .Select(point => (Voltage: point[voltageIndex].Real, Current: point[currentIndex].Real))
+ .ToArray();
+ }
+
+ public static IReadOnlyList<(double Frequency, Complex Value)> GetComplexSeries(
+ LtspiceAsciiRawFile raw,
+ string valueName)
+ {
+ int valueIndex = raw.FindVariable(valueName);
+ return raw.Points
+ .Select(point => (Frequency: point[0].Real, Value: point[valueIndex]))
+ .ToArray();
+ }
+
+ public static IReadOnlyList<(double Time, double Input, double Output)> GetBridgeTransientSeries(
+ LtspiceAsciiRawFile raw,
+ string inputName,
+ string positiveName,
+ string negativeName)
+ {
+ int inputIndex = raw.FindVariable(inputName);
+ int positiveIndex = raw.FindVariable(positiveName);
+ int negativeIndex = raw.FindVariable(negativeName);
+ return raw.Points
+ .Select(point => (
+ Time: point[0].Real,
+ Input: point[inputIndex].Real,
+ Output: point[positiveIndex].Real - point[negativeIndex].Real))
+ .ToArray();
+ }
+
+ private static int ReadHeaderInt(string[] lines, string name)
+ {
+ string line = lines.FirstOrDefault(candidate => candidate.StartsWith(name, StringComparison.OrdinalIgnoreCase));
+ if (line == null)
+ {
+ throw new FormatException($"LTspice raw output does not contain header '{name}'.");
+ }
+
+ return int.Parse(line.Substring(name.Length).Trim(), CultureInfo.InvariantCulture);
+ }
+
+ private static int FindLine(string[] lines, string value)
+ {
+ for (int i = 0; i < lines.Length; i++)
+ {
+ if (string.Equals(lines[i].Trim(), value, StringComparison.OrdinalIgnoreCase))
+ {
+ return i;
+ }
+ }
+
+ throw new FormatException($"LTspice raw output does not contain section '{value}'.");
+ }
+
+ private static string ReadNextNonEmptyLine(string[] lines, ref int cursor, string path)
+ {
+ while (cursor < lines.Length)
+ {
+ string line = lines[cursor++].Trim();
+ if (!string.IsNullOrEmpty(line))
+ {
+ return line;
+ }
+ }
+
+ throw new FormatException($"Unexpected end of LTspice raw output in '{path}'.");
+ }
+
+ private static string[] SplitRawLine(string line)
+ {
+ return line.Split((char[])null, StringSplitOptions.RemoveEmptyEntries);
+ }
+
+ private static double ParseDouble(string value)
+ {
+ return double.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
+ }
+
+ private static Complex ParseComplex(string value)
+ {
+ value = value.Trim();
+ if (value.StartsWith("(", StringComparison.Ordinal) && value.EndsWith(")", StringComparison.Ordinal))
+ {
+ value = value.Substring(1, value.Length - 2);
+ }
+
+ int commaIndex = value.IndexOf(',');
+ if (commaIndex < 0)
+ {
+ return new Complex(ParseDouble(value), 0.0);
+ }
+
+ string real = value.Substring(0, commaIndex);
+ string imaginary = value.Substring(commaIndex + 1);
+ return new Complex(ParseDouble(real), ParseDouble(imaginary));
+ }
+
+ private int FindVariable(string name)
+ {
+ for (int i = 0; i < this.Variables.Count; i++)
+ {
+ if (string.Equals(this.Variables[i], name, StringComparison.OrdinalIgnoreCase))
+ {
+ return i;
+ }
+ }
+
+ throw new InvalidOperationException(
+ $"LTspice raw output did not contain variable '{name}'. Available variables: {string.Join(", ", this.Variables)}.");
+ }
+ }
+
+ private sealed class ProcessResult
+ {
+ public ProcessResult(string output, string error)
+ {
+ this.Output = output;
+ this.Error = error;
+ }
+
+ private string Output { get; }
+
+ private string Error { get; }
+
+ public override string ToString()
+ {
+ return "stdout:"
+ + Environment.NewLine
+ + this.Output
+ + Environment.NewLine
+ + "stderr:"
+ + Environment.NewLine
+ + this.Error;
+ }
+ }
+ }
+}
diff --git a/src/SpiceSharpParser.Tests/CustomComponents/IdealDiodeTests.cs b/src/SpiceSharpParser.Tests/CustomComponents/IdealDiodeTests.cs
new file mode 100644
index 00000000..baa80123
--- /dev/null
+++ b/src/SpiceSharpParser.Tests/CustomComponents/IdealDiodeTests.cs
@@ -0,0 +1,969 @@
+using SpiceSharp;
+using SpiceSharp.Components;
+using SpiceSharp.Simulations;
+using SpiceSharpParser.Common;
+using SpiceSharpParser.CustomComponents;
+using SpiceSharpParser.ModelReaders.Netlist.Spice;
+using SpiceSharpParser.Models.Netlist.Spice;
+using System;
+using System.Linq;
+using System.Numerics;
+using Xunit;
+
+namespace SpiceSharpParser.Tests.CustomComponents
+{
+ public class IdealDiodeTests
+ {
+ [Fact]
+ public void Op_WhenForwardBiased_UsesOnResistance()
+ {
+ double current = RunOpCurrent(3.0, diode =>
+ {
+ diode.SetParameter("ron", 2.0);
+ diode.SetParameter("roff", 1e9);
+ diode.SetParameter("vfwd", 1.0);
+ });
+
+ // Above Vfwd the branch uses Ron, so current is (3 V - 1 V) / 2 ohm.
+ AssertClose(1.0, current, 1e-9);
+ }
+
+ [Fact]
+ public void Op_WhenBelowForwardVoltage_UsesOffResistance()
+ {
+ double current = RunOpCurrent(0.5, diode =>
+ {
+ diode.Parameters.OnResistance = 2.0;
+ diode.Parameters.OffResistance = 1e9;
+ diode.Parameters.ForwardVoltage = 1.0;
+ });
+
+ // Below Vfwd the branch stays on Roff, so current is 0.5 V / 1e9 ohm.
+ AssertClose(0.5e-9, current, 1e-15);
+ }
+
+ [Fact]
+ public void Op_WhenReverseBreakdown_UsesReverseResistance()
+ {
+ double current = RunOpCurrent(-6.0, diode =>
+ {
+ diode.Parameters.OnResistance = 2.0;
+ diode.Parameters.OffResistance = 1e9;
+ diode.Parameters.ForwardVoltage = 1.0;
+ diode.Parameters.ReverseVoltage = 2.0;
+ diode.Parameters.ReverseResistance = 4.0;
+ });
+
+ // Past -Vrev the reverse branch uses Rrev: (-6 V + 2 V) / 4 ohm.
+ AssertClose(-1.0, current, 1e-9);
+ }
+
+ [Fact]
+ public void Op_WhenOffResistanceIsModerate_AnchorsConductingLinesAtNominalThresholds()
+ {
+ double reverseCurrent = RunOpCurrent(-6.0, diode =>
+ {
+ diode.Parameters.OnResistance = 2.0;
+ diode.Parameters.OffResistance = 20.0;
+ diode.Parameters.ForwardVoltage = 1.0;
+ diode.Parameters.ReverseVoltage = 2.0;
+ diode.Parameters.ReverseResistance = 4.0;
+ });
+
+ double forwardCurrent = RunOpCurrent(4.0, diode =>
+ {
+ diode.Parameters.OnResistance = 2.0;
+ diode.Parameters.OffResistance = 20.0;
+ diode.Parameters.ForwardVoltage = 1.0;
+ diode.Parameters.ReverseVoltage = 2.0;
+ diode.Parameters.ReverseResistance = 4.0;
+ });
+
+ AssertClose(-1.1, reverseCurrent, 1e-9);
+ AssertClose(1.55, forwardCurrent, 1e-9);
+ }
+
+ [Fact]
+ public void Op_WhenMultipliersAreSet_ScalesParallelAndSeries()
+ {
+ double current = RunOpCurrent(3.0, diode =>
+ {
+ diode.Parameters.OnResistance = 2.0;
+ diode.Parameters.OffResistance = 1e9;
+ diode.Parameters.ForwardVoltage = 1.0;
+ diode.Parameters.ParallelMultiplier = 2.0;
+ diode.Parameters.SeriesMultiplier = 2.0;
+ });
+
+ // N=2 halves the local voltage, then M=2 doubles the resulting current.
+ AssertClose(0.5, current, 1e-9);
+ }
+
+ [Fact]
+ public void Op_WhenAreaIsSet_IgnoresAreaForLtspiceIdealDiode()
+ {
+ double current = RunOpCurrent(3.0, diode =>
+ {
+ diode.Parameters.OnResistance = 2.0;
+ diode.Parameters.OffResistance = 1e9;
+ diode.Parameters.ForwardVoltage = 1.0;
+ diode.Parameters.Area = 3.0;
+ });
+
+ // LTspice's region-wise-linear ideal diode accepts area syntax, but area does not scale this model.
+ AssertClose(1.0, current, 1e-9);
+ }
+
+ [Fact]
+ public void Op_WhenSeriesResistanceIsSet_IgnoresResistanceForLtspiceIdealDiode()
+ {
+ double current = RunOpCurrent(5.0, diode =>
+ {
+ diode.Parameters.OnResistance = 2.0;
+ diode.Parameters.OffResistance = 1e9;
+ diode.Parameters.ForwardVoltage = 1.0;
+ diode.Parameters.Resistance = 3.0;
+ });
+
+ // LTspice's idealized Ron/Roff/Vfwd diode accepts Rs, but Rs belongs to the standard diode model.
+ AssertClose(2.0, current, 1e-9);
+ }
+
+ [Fact]
+ public void Op_WhenReverseResistanceIsOmitted_UsesOnResistance()
+ {
+ double current = RunOpCurrent(-6.0, diode =>
+ {
+ diode.Parameters.OnResistance = 2.0;
+ diode.Parameters.OffResistance = 1e9;
+ diode.Parameters.ForwardVoltage = 1.0;
+ diode.Parameters.ReverseVoltage = 2.0;
+ });
+
+ // Without Rrev, reverse breakdown falls back to Ron: (-6 V + 2 V) / 2 ohm.
+ AssertClose(-2.0, current, 1e-9);
+ }
+
+ [Fact]
+ public void Op_WhenForwardCurrentLimitIsSet_LimitsCurrentSmoothly()
+ {
+ double current = RunOpCurrent(5.0, diode =>
+ {
+ diode.Parameters.OnResistance = 1.0;
+ diode.Parameters.OffResistance = 1e9;
+ diode.Parameters.ForwardVoltage = 0.0;
+ diode.Parameters.ForwardCurrentLimit = 2.0;
+ });
+
+ // Current limiting uses limit * tanh(raw / limit), with raw forward current of 5 A.
+ AssertClose(2.0 * Math.Tanh(2.5), current, 1e-9);
+ }
+
+ [Fact]
+ public void Op_WhenReverseCurrentLimitIsSet_LimitsCurrentSmoothly()
+ {
+ double current = RunOpCurrent(-6.0, diode =>
+ {
+ diode.Parameters.OnResistance = 1.0;
+ diode.Parameters.OffResistance = 1e9;
+ diode.Parameters.ForwardVoltage = 0.0;
+ diode.Parameters.ReverseVoltage = 0.0;
+ diode.Parameters.ReverseResistance = 1.0;
+ diode.Parameters.ReverseCurrentLimit = 2.0;
+ });
+
+ // The raw reverse current is -6 A, then the same tanh limiter caps it smoothly.
+ AssertClose(-2.0 * Math.Tanh(3.0), current, 1e-9);
+ }
+
+ [Fact]
+ public void Op_WhenForwardEpsilonIsSet_SmoothsTurnOnKnee()
+ {
+ double current = RunOpCurrent(1.05, diode =>
+ {
+ diode.Parameters.OnResistance = 1.0;
+ diode.Parameters.OffResistance = 1e12;
+ diode.Parameters.ForwardVoltage = 1.0;
+ diode.Parameters.ForwardEpsilon = 0.2;
+ });
+
+ // At 0.05 V into LTspice's one-sided 0.2 V smoothing band, the ramp integral is 0.05^2 / (2 * 0.2).
+ AssertClose(0.00625, current, 1e-6);
+ }
+
+ [Fact]
+ public void Op_WhenReverseEpsilonIsSet_ShiftsReverseBreakdownLine()
+ {
+ double current = RunOpCurrent(-3.0, diode =>
+ {
+ diode.Parameters.OnResistance = 1.0;
+ diode.Parameters.OffResistance = 1e12;
+ diode.Parameters.ForwardVoltage = 1.0;
+ diode.Parameters.ReverseVoltage = 2.0;
+ diode.Parameters.ReverseResistance = 2.0;
+ diode.Parameters.ReverseEpsilon = 0.4;
+ });
+
+ // LTspice's one-sided RevEpsilon shifts the reverse line by RevEpsilon / 2:
+ // (-3 V + 2 V + 0.2 V) / 2 ohm.
+ AssertClose(-0.4, current, 1e-9);
+ }
+
+ [Fact]
+ public void Op_WhenPropertiesAreExported_ReturnsVoltageConductanceAndPower()
+ {
+ var values = RunOpProperties(3.0, diode =>
+ {
+ diode.Parameters.OnResistance = 2.0;
+ diode.Parameters.OffResistance = 1e9;
+ diode.Parameters.ForwardVoltage = 1.0;
+ });
+
+ // The branch is on Ron: current is 1 A, gd is 1 / 2 ohm, and power is 3 V * 1 A.
+ AssertClose(1.0, values.Current, 1e-9);
+ AssertClose(3.0, values.Voltage, 1e-12);
+ AssertClose(0.5, values.Conductance, 1e-12);
+ AssertClose(3.0, values.Power, 1e-9);
+ }
+
+ [Fact]
+ public void Op_WhenSeriesResistanceIsSet_ExportsTerminalPropertiesWithoutRsDrop()
+ {
+ var values = RunOpProperties(5.0, diode =>
+ {
+ diode.Parameters.OnResistance = 2.0;
+ diode.Parameters.OffResistance = 1e9;
+ diode.Parameters.ForwardVoltage = 1.0;
+ diode.Parameters.Resistance = 3.0;
+ });
+
+ // Rs is accepted but ignored by LTspice's idealized Ron/Roff/Vfwd diode.
+ AssertClose(2.0, values.Current, 1e-9);
+ AssertClose(5.0, values.Voltage, 1e-12);
+ AssertClose(5.0, values.JunctionVoltage, 1e-9);
+ AssertClose(0.5, values.Conductance, 1e-12);
+ AssertClose(10.0, values.Power, 1e-9);
+ }
+
+ [Fact]
+ public void Op_WhenSeriesMultiplierIsSet_ExportsTerminalConductance()
+ {
+ var values = RunOpProperties(3.0, diode =>
+ {
+ diode.Parameters.OnResistance = 2.0;
+ diode.Parameters.OffResistance = 1e9;
+ diode.Parameters.ForwardVoltage = 1.0;
+ diode.Parameters.ParallelMultiplier = 2.0;
+ diode.Parameters.SeriesMultiplier = 2.0;
+ });
+
+ // Two parallel strings of two series cells behave like 0.5 S at the external terminals.
+ AssertClose(0.5, values.Current, 1e-9);
+ AssertClose(3.0, values.Voltage, 1e-12);
+ AssertClose(3.0, values.JunctionVoltage, 1e-12);
+ AssertClose(0.5, values.Conductance, 1e-12);
+ AssertClose(1.5, values.Power, 1e-9);
+ }
+
+ [Fact]
+ public void Ac_WhenForwardBiased_UsesOperatingPointConductance()
+ {
+ var diode = new IdealDiode("D1", "out", "0");
+ diode.Parameters.OnResistance = 2.0;
+ diode.Parameters.OffResistance = 1e9;
+ diode.Parameters.ForwardVoltage = 1.0;
+
+ var circuit = new Circuit(
+ new VoltageSource("V1", "in", "0", 3.0).SetParameter("acmag", 1.0),
+ new Resistor("R1", "in", "out", 1.0),
+ diode);
+
+ var ac = new AC("ac", new DecadeSweep(1.0, 1e3, 1));
+ var export = new ComplexVoltageExport(ac, "out");
+
+ foreach (int ignored in ac.Run(circuit, AC.ExportSmallSignal))
+ {
+ // The biased diode is a 0.5 S shunt, so the 1 ohm divider gain is 1 / (1 + 0.5).
+ AssertClose(2.0 / 3.0, export.Value.Real, 1e-9);
+ AssertClose(0.0, export.Value.Imaginary, 1e-12);
+ }
+ }
+
+ [Fact]
+ public void Ac_WhenBelowForwardVoltage_UsesOffConductance()
+ {
+ var diode = new IdealDiode("D1", "out", "0");
+ diode.Parameters.OnResistance = 2.0;
+ diode.Parameters.OffResistance = 1e9;
+ diode.Parameters.ForwardVoltage = 1.0;
+
+ var circuit = new Circuit(
+ new VoltageSource("V1", "in", "0", 0.5).SetParameter("acmag", 1.0),
+ new Resistor("R1", "in", "out", 1.0),
+ diode);
+
+ var ac = new AC("ac", new DecadeSweep(1.0, 1e3, 1));
+ var export = new ComplexVoltageExport(ac, "out");
+
+ foreach (int ignored in ac.Run(circuit, AC.ExportSmallSignal))
+ {
+ // In the off region the shunt conductance is 1 / Roff, giving gain 1 / (1 + 1e-9).
+ AssertClose(1.0 / (1.0 + 1e-9), export.Value.Real, 1e-9);
+ AssertClose(0.0, export.Value.Imaginary, 1e-12);
+ }
+ }
+
+ [Fact]
+ public void Ac_WhenSeriesResistanceIsSet_IgnoresResistance()
+ {
+ var values = RunAcProperties(5.0, diode =>
+ {
+ diode.Parameters.OnResistance = 2.0;
+ diode.Parameters.OffResistance = 1e9;
+ diode.Parameters.ForwardVoltage = 1.0;
+ diode.Parameters.Resistance = 3.0;
+ });
+
+ // The AC small-signal branch sees Ron only because Rs is ignored by the idealized model.
+ AssertComplexClose(new Complex(0.5, 0.0), values.Current, 1e-9);
+ AssertComplexClose(new Complex(1.0, 0.0), values.Voltage, 1e-12);
+ AssertComplexClose(new Complex(1.0, 0.0), values.JunctionVoltage, 1e-9);
+ AssertComplexClose(new Complex(0.5, 0.0), values.Power, 1e-9);
+ }
+
+ [Fact]
+ public void Ac_WhenSeriesMultiplierIsSet_ExportsTerminalPower()
+ {
+ var values = RunAcProperties(3.0, diode =>
+ {
+ diode.Parameters.OnResistance = 2.0;
+ diode.Parameters.OffResistance = 1e9;
+ diode.Parameters.ForwardVoltage = 1.0;
+ diode.Parameters.ParallelMultiplier = 2.0;
+ diode.Parameters.SeriesMultiplier = 2.0;
+ });
+
+ // The exported AC power uses terminal voltage, not the per-cell internal voltage.
+ AssertComplexClose(new Complex(0.5, 0.0), values.Current, 1e-9);
+ AssertComplexClose(new Complex(1.0, 0.0), values.Voltage, 1e-12);
+ AssertComplexClose(new Complex(1.0, 0.0), values.JunctionVoltage, 1e-12);
+ AssertComplexClose(new Complex(0.5, 0.0), values.Power, 1e-9);
+ }
+
+ [Theory]
+ [InlineData(-6.0, -1.0, 0.25)]
+ [InlineData(0.5, 0.5e-9, 1e-9)]
+ [InlineData(3.0, 1.0, 0.5)]
+ public void Op_WhenSweepingLtspiceStyleReferenceModel_MatchesGoldenValues(
+ double voltage,
+ double expectedCurrent,
+ double expectedConductance)
+ {
+ var values = RunOpProperties(voltage, diode =>
+ {
+ diode.Parameters.OnResistance = 2.0;
+ diode.Parameters.OffResistance = 1e9;
+ diode.Parameters.ForwardVoltage = 1.0;
+ diode.Parameters.ReverseVoltage = 2.0;
+ diode.Parameters.ReverseResistance = 4.0;
+ });
+
+ // Fixed compatibility points for LTspice-style Ron/Roff/Vfwd/Vrev/Rrev behavior.
+ AssertClose(expectedCurrent, values.Current, 1e-9);
+ AssertClose(voltage, values.Voltage, 1e-12);
+ AssertClose(expectedConductance, values.Conductance, 1e-12);
+ AssertClose(voltage * expectedCurrent, values.Power, 1e-9);
+ }
+
+ [Fact]
+ public void Op_WhenLtspiceStyleMultipliersAreaAndSeriesResistanceAreSet_MatchesGoldenTerminalValues()
+ {
+ var values = RunOpProperties(10.0, diode =>
+ {
+ diode.Parameters.OnResistance = 2.0;
+ diode.Parameters.OffResistance = 1e9;
+ diode.Parameters.ForwardVoltage = 1.0;
+ diode.Parameters.Resistance = 3.0;
+ diode.Parameters.Area = 2.0;
+ diode.Parameters.ParallelMultiplier = 3.0;
+ diode.Parameters.SeriesMultiplier = 2.0;
+ });
+
+ // LTspice applies M and N, but ignores area and Rs for the idealized model.
+ AssertClose(6.0, values.Current, 1e-9);
+ AssertClose(10.0, values.Voltage, 1e-12);
+ AssertClose(10.0, values.JunctionVoltage, 1e-9);
+ AssertClose(0.75, values.Conductance, 1e-12);
+ AssertClose(60.0, values.Power, 1e-9);
+ }
+
+ [Fact]
+ public void Parser_WhenCustomComponentsEnabled_MapsIdealDiodeModel()
+ {
+ var model = ReadWithCustomComponents(
+ "Ideal diode parser",
+ "V1 in 0 3",
+ "D1 in 0 ideal",
+ ".model ideal D(Ron=2 Roff=1e9 Vfwd=1)",
+ ".op",
+ ".end");
+
+ // Ideal-only model parameters should switch the diode reader to the custom ideal-diode entity.
+ AssertNoValidationErrors(model);
+ Assert.Single(model.Circuit.OfType());
+
+ // Ron, Roff, and Vfwd make this a custom ideal diode; the OP current is (3 V - 1 V) / 2 ohm.
+ AssertClose(1.0, RunOpCurrent(model), 1e-9);
+ }
+
+ [Fact]
+ public void Parser_WhenIdealModelHasClassicParameters_IgnoresThem()
+ {
+ var model = ReadWithCustomComponents(
+ "Ideal diode parser",
+ "V1 in 0 3",
+ "D1 in 0 ideal",
+ ".model ideal D(Is=1e-12 N=2 M=0.3 Ron=2 Roff=1e9 Vfwd=1)",
+ ".op",
+ ".end");
+
+ // Classic diode parameters remain accepted metadata for this path and must not block ideal mapping.
+ AssertNoValidationErrors(model);
+ Assert.Single(model.Circuit.OfType());
+
+ // Classic diode model parameters are ignored once an ideal-only parameter selects this model.
+ AssertClose(1.0, RunOpCurrent(model), 1e-9);
+ }
+
+ [Fact]
+ public void Parser_WhenInstanceOverridesModelParameters_UsesOverride()
+ {
+ var model = ReadWithCustomComponents(
+ "Ideal diode parser",
+ "V1 in 0 3",
+ "D1 in 0 ideal Ron=2",
+ ".model ideal D(Ron=10 Roff=1e9 Vfwd=1)",
+ ".op",
+ ".end");
+
+ // The model still maps to IdealDiode; the instance parameter is what changes the effective Ron.
+ AssertNoValidationErrors(model);
+ Assert.Single(model.Circuit.OfType());
+
+ // The instance Ron=2 overrides the model Ron=10, so the forward current stays 1 A.
+ AssertClose(1.0, RunOpCurrent(model), 1e-9);
+ }
+
+ [Fact]
+ public void Parser_WhenInvalidInstanceOverrideIsSet_DoesNotShadowModelParameter()
+ {
+ var model = ReadWithCustomComponents(
+ "Ideal diode parser",
+ "V1 in 0 5",
+ "D1 in 0 ideal Ron=0",
+ ".model ideal D(Ron=2 Roff=1e9 Vfwd=1)",
+ ".op",
+ ".end");
+
+ // Ron=0 violates the ideal-diode parameter constraints and should be reported with its source name.
+ Assert.True(model.ValidationResult.HasError);
+ Assert.Contains("Ron", GetValidationMessages(model));
+ Assert.Single(model.Circuit.OfType());
+
+ // The invalid instance Ron must not mark an override; the model Ron=2 remains effective.
+ AssertClose(2.0, RunOpCurrent(model), 1e-9);
+ }
+
+ [Fact]
+ public void Parser_WhenAreaValueIsPositional_IgnoresAreaForIdealDiode()
+ {
+ var model = ReadWithCustomComponents(
+ "Ideal diode parser",
+ "V1 in 0 3",
+ "D1 in 0 ideal 3",
+ ".model ideal D(Ron=2 Roff=1e9 Vfwd=1)",
+ ".op",
+ ".end");
+
+ AssertNoValidationErrors(model);
+ Assert.Single(model.Circuit.OfType());
+
+ // LTspice accepts the positional area token, but the idealized model does not use it electrically.
+ AssertClose(1.0, RunOpCurrent(model), 1e-9);
+ }
+
+ [Fact]
+ public void Parser_WhenInstanceMultipliersAreSet_ScalesIdealDiode()
+ {
+ var model = ReadWithCustomComponents(
+ "Ideal diode parser",
+ "V1 in 0 3",
+ "D1 in 0 ideal M=2 N=2",
+ ".model ideal D(Ron=2 Roff=1e9 Vfwd=1)",
+ ".op",
+ ".end");
+
+ AssertNoValidationErrors(model);
+ Assert.Single(model.Circuit.OfType());
+
+ // This mirrors the direct multiplier test: N changes local voltage, M scales current.
+ AssertClose(0.5, RunOpCurrent(model), 1e-9);
+ }
+
+ [Fact]
+ public void Parser_WhenZeroScaleIsSet_ReportsValidationError()
+ {
+ var areaModel = ReadWithCustomComponents(
+ "Ideal diode parser",
+ "V1 in 0 3",
+ "D1 in 0 ideal Area=0",
+ ".model ideal D(Ron=2 Roff=1e9 Vfwd=1 Rs=3)",
+ ".op",
+ ".end");
+
+ // Area remains validated as part of the shared diode instance syntax.
+ Assert.True(areaModel.ValidationResult.HasError);
+ Assert.Contains("Area", GetValidationMessages(areaModel));
+
+ var multiplierModel = ReadWithCustomComponents(
+ "Ideal diode parser",
+ "V1 in 0 3",
+ "D1 in 0 ideal M=0",
+ ".model ideal D(Ron=2 Roff=1e9 Vfwd=1 Rs=3)",
+ ".op",
+ ".end");
+
+ // M=0 would remove all parallel devices.
+ Assert.True(multiplierModel.ValidationResult.HasError, GetValidationMessages(multiplierModel));
+ }
+
+ [Fact]
+ public void Parser_WhenModelHasDimensionBins_SelectsIdealDiodeByLengthAndWidth()
+ {
+ var model = ReadWithCustomComponents(
+ "Ideal diode range parser",
+ "V1 in1 0 5",
+ "V2 in2 0 5",
+ "D1 in1 0 ideal L=0.5 W=5",
+ "D2 in2 0 ideal L=5 W=50",
+ ".model ideal.fast D(Ron=2 Roff=1e9 Vfwd=1 Lmin=0.1 Lmax=1 Wmin=1 Wmax=10)",
+ ".model ideal.slow D(Ron=4 Roff=1e9 Vfwd=1 Lmin=1 Lmax=10 Wmin=10 Wmax=100)",
+ ".op",
+ ".end");
+
+ // D1 falls into the fast bin and D2 into the slow bin, so both should become custom ideal diodes.
+ AssertNoValidationErrors(model);
+ Assert.Equal(2, model.Circuit.OfType().Count());
+
+ // At 5 V with Vfwd=1, Ron=2 gives 2 A and Ron=4 gives 1 A.
+ AssertClose(2.0, RunOpCurrent(model, "D1"), 1e-9);
+ AssertClose(1.0, RunOpCurrent(model, "D2"), 1e-9);
+ }
+
+ [Fact]
+ public void Parser_WhenSelectedBinnedModelParameterIsStepped_UpdatesIdealDiode()
+ {
+ var model = ReadWithCustomComponents(
+ "Ideal diode stepped binned model",
+ "V1 in 0 5",
+ "D1 in 0 ideal L=0.5 W=5",
+ ".model ideal.fast D(Ron=2 Roff=1e9 Vfwd=1 Lmin=0.1 Lmax=1 Wmin=1 Wmax=10)",
+ ".model ideal.slow D(Ron=8 Roff=1e9 Vfwd=1 Lmin=1 Lmax=10 Wmin=10 Wmax=100)",
+ ".op",
+ ".step D ideal.fast(Ron) LIST 2 4",
+ ".end");
+
+ // The concrete selected model name, ideal.fast, should be accepted as the stepped target.
+ AssertNoValidationErrors(model);
+ Assert.Equal(2, model.Simulations.Count);
+
+ // The two OP runs use Ron=2 and Ron=4 respectively.
+ var currents = RunOpCurrents(model, "D1");
+ AssertClose(2.0, currents[0], 1e-9);
+ AssertClose(1.0, currents[1], 1e-9);
+
+ // Model sweep values are simulation-local overlays; the shared model should be restored afterward.
+ AssertClose(2.0, model.Circuit.OfType().Single(m => m.Name == "ideal.fast").Parameters.OnResistance, 1e-12);
+ }
+
+ [Fact]
+ public void Parser_WhenModelSelectionExpressionCannotBeEvaluated_ReportsValidationError()
+ {
+ var model = ReadWithCustomComponents(
+ "Ideal diode range parser",
+ "V1 in 0 5",
+ "D1 in 0 ideal L={missing_length}",
+ ".model ideal.fast D(Ron=2 Roff=1e9 Vfwd=1 Lmin=0.1 Lmax=1)",
+ ".op",
+ ".end");
+
+ // The netlist can be read before the L expression is evaluated during model selection.
+ Assert.False(model.ValidationResult.HasError, GetValidationMessages(model));
+
+ RunOpCurrent(model);
+
+ // Running the simulation triggers range selection and surfaces the missing L/W expression.
+ Assert.True(model.ValidationResult.HasError);
+ Assert.Contains("L/W", GetValidationMessages(model));
+ }
+
+ [Fact]
+ public void Parser_WhenModelParameterIsStepped_UpdatesIdealDiode()
+ {
+ var model = ReadWithCustomComponents(
+ "Ideal diode stepped model",
+ "V1 in 0 5",
+ "D1 in 0 ideal",
+ ".model ideal D(Ron=2 Roff=1e9 Vfwd=1)",
+ ".op",
+ ".step D ideal(Ron) LIST 2 4",
+ ".end");
+
+ // The list sweep produces one OP simulation per Ron value.
+ AssertNoValidationErrors(model);
+ Assert.Equal(2, model.Simulations.Count);
+
+ // Ron=2 gives 2 A; Ron=4 doubles resistance and halves current to 1 A.
+ var currents = RunOpCurrents(model, "D1");
+ AssertClose(2.0, currents[0], 1e-9);
+ AssertClose(1.0, currents[1], 1e-9);
+
+ // After all stepped runs, the shared model parameter is restored to its original value.
+ AssertClose(2.0, Assert.Single(model.Circuit.OfType()).Parameters.OnResistance, 1e-12);
+ }
+
+ [Fact]
+ public void Parser_WhenModelSeriesResistanceIsSteppedAcrossZero_UpdatesIdealDiode()
+ {
+ var model = ReadWithCustomComponents(
+ "Ideal diode stepped Rs",
+ "V1 in 0 5",
+ "D1 in 0 ideal",
+ ".model ideal D(Ron=2 Roff=1e9 Vfwd=1 Rs=0)",
+ ".op",
+ ".step D ideal(Rs) LIST 0 3",
+ ".end");
+
+ // The sweep covers Rs updates, but LTspice's idealized diode ignores Rs electrically.
+ AssertNoValidationErrors(model);
+ Assert.Equal(2, model.Simulations.Count);
+
+ var currents = RunOpCurrents(model, "D1");
+ AssertClose(2.0, currents[0], 1e-9);
+ AssertClose(2.0, currents[1], 1e-9);
+ }
+
+ [Fact]
+ public void Parser_WhenOffFlagIsSet_SetsInitialOffFlag()
+ {
+ var model = ReadWithCustomComponents(
+ "Ideal diode parser",
+ "V1 in 0 3",
+ "D1 in 0 ideal OFF",
+ ".model ideal D(Ron=2 Roff=1e9 Vfwd=1)",
+ ".op",
+ ".end");
+
+ AssertNoValidationErrors(model);
+ var diode = Assert.Single(model.Circuit.OfType());
+
+ // OFF is an initial-condition hint, not a steady-state current-law change.
+ Assert.True(diode.Parameters.Off);
+ }
+
+ [Fact]
+ public void Parser_WhenIgnoredInstanceParametersAreSet_DoesNotReportErrors()
+ {
+ var model = ReadWithCustomComponents(
+ "Ideal diode parser",
+ "V1 in 0 3",
+ "D1 in 0 ideal Temp=50 Ic=0.2",
+ ".model ideal D(Ron=2 Roff=1e9 Vfwd=1)",
+ ".op",
+ ".end");
+
+ AssertNoValidationErrors(model);
+ Assert.Single(model.Circuit.OfType());
+
+ // Temp and Ic are intentionally accepted but ignored, leaving the same 1 A operating point.
+ AssertClose(1.0, RunOpCurrent(model), 1e-9);
+ }
+
+ [Fact]
+ public void Parser_WhenMetadataParametersAreSet_DoesNotReportErrors()
+ {
+ var model = ReadWithCustomComponents(
+ "Ideal diode parser",
+ "V1 in 0 3",
+ "D1 in 0 ideal pn=ABC irms=2",
+ ".model ideal D(Ron=2 Roff=1e9 Vfwd=1 mfg=ACME desc=FastSwitch)",
+ ".op",
+ ".end");
+
+ AssertNoValidationErrors(model);
+ Assert.Single(model.Circuit.OfType());
+
+ // Metadata parameters should not affect simulation, so the ideal diode still conducts 1 A.
+ AssertClose(1.0, RunOpCurrent(model), 1e-9);
+ }
+
+ [Fact]
+ public void Parser_WhenUnsupportedInstanceParameterIsSet_ReportsValidationError()
+ {
+ var model = ReadWithCustomComponents(
+ "Ideal diode parser",
+ "V1 in 0 3",
+ "D1 in 0 ideal Foo=1",
+ ".model ideal D(Ron=2 Roff=1e9 Vfwd=1)",
+ ".op",
+ ".end");
+
+ // Unsupported ideal-diode instance parameters should be rejected and reported with their original name.
+ Assert.True(model.ValidationResult.HasError);
+ Assert.Contains(
+ "Foo",
+ string.Join(Environment.NewLine, model.ValidationResult.Errors.Select(error => error.Message)));
+ }
+
+ [Fact]
+ public void Parser_WhenRoffOnlyModelParameterIsSet_MapsIdealDiodeModel()
+ {
+ var model = ReadWithCustomComponents(
+ "Ideal diode parser",
+ "V1 in 0 2",
+ "D1 in 0 ideal",
+ ".model ideal D(Roff=1e9)",
+ ".op",
+ ".end");
+
+ // Roff is an ideal-only model parameter, so the custom mapper should consume the diode.
+ AssertNoValidationErrors(model);
+ Assert.Single(model.Circuit.OfType());
+ Assert.Empty(model.Circuit.OfType());
+
+ // Roff is enough to select the ideal model; defaults Ron=1 and Vfwd=0 make 2 V produce 2 A.
+ AssertClose(2.0, RunOpCurrent(model), 1e-9);
+ }
+
+ [Fact]
+ public void Parser_WhenCustomComponentsEnabled_KeepsClassicDiodeModel()
+ {
+ var model = ReadWithCustomComponents(
+ "Classic diode parser",
+ "V1 in 0 0.7",
+ "D1 in 0 regular",
+ ".model regular D(Is=1e-12 N=1)",
+ ".op",
+ ".end");
+
+ // A normal diode model with no ideal-only parameters must remain SpiceSharp's built-in diode.
+ AssertNoValidationErrors(model);
+ Assert.Single(model.Circuit.OfType());
+
+ // This guards against accidentally treating every D model as an ideal diode.
+ Assert.Empty(model.Circuit.OfType());
+ }
+
+ [Fact]
+ public void Parser_WhenOnlySeriesResistanceModelParameterIsSet_KeepsClassicDiodeModel()
+ {
+ var model = ReadWithCustomComponents(
+ "Classic diode parser",
+ "V1 in 0 0.7",
+ "D1 in 0 regular",
+ ".model regular D(Rs=1)",
+ ".op",
+ ".end");
+
+ // Rs alone is valid for classic diodes, so it must not trigger the ideal-diode mapper.
+ AssertNoValidationErrors(model);
+ Assert.Single(model.Circuit.OfType());
+
+ // Keeping this classic preserves LTspice compatibility for ordinary diode models.
+ Assert.Empty(model.Circuit.OfType());
+ }
+
+ private static double RunOpCurrent(double voltage, Action configure)
+ {
+ var diode = new IdealDiode("D1", "in", "0");
+ configure(diode);
+
+ var circuit = new Circuit(
+ new VoltageSource("V1", "in", "0", voltage),
+ diode);
+
+ var op = new OP("op");
+ var export = new RealPropertyExport(op, "D1", "i");
+
+ double current = double.NaN;
+ foreach (int ignored in op.Run(circuit))
+ {
+ current = export.Value;
+ }
+
+ return current;
+ }
+
+ private static (double Current, double Voltage, double JunctionVoltage, double Conductance, double Power) RunOpProperties(
+ double voltage,
+ Action configure)
+ {
+ var diode = new IdealDiode("D1", "in", "0");
+ configure(diode);
+
+ var circuit = new Circuit(
+ new VoltageSource("V1", "in", "0", voltage),
+ diode);
+
+ var op = new OP("op");
+ var current = new RealPropertyExport(op, "D1", "i");
+ var diodeVoltage = new RealPropertyExport(op, "D1", "v");
+ var junctionVoltage = new RealPropertyExport(op, "D1", "vj");
+ var conductance = new RealPropertyExport(op, "D1", "gd");
+ var power = new RealPropertyExport(op, "D1", "p");
+
+ double currentValue = double.NaN;
+ double voltageValue = double.NaN;
+ double junctionVoltageValue = double.NaN;
+ double conductanceValue = double.NaN;
+ double powerValue = double.NaN;
+ foreach (int ignored in op.Run(circuit))
+ {
+ currentValue = current.Value;
+ voltageValue = diodeVoltage.Value;
+ junctionVoltageValue = junctionVoltage.Value;
+ conductanceValue = conductance.Value;
+ powerValue = power.Value;
+ }
+
+ return (currentValue, voltageValue, junctionVoltageValue, conductanceValue, powerValue);
+ }
+
+ private static (Complex Current, Complex Voltage, Complex JunctionVoltage, Complex Power) RunAcProperties(
+ double voltage,
+ Action configure)
+ {
+ var diode = new IdealDiode("D1", "in", "0");
+ configure(diode);
+
+ var circuit = new Circuit(
+ new VoltageSource("V1", "in", "0", voltage).SetParameter("acmag", 1.0),
+ diode);
+
+ var ac = new AC("ac", new DecadeSweep(1.0, 1e3, 1));
+ var current = new ComplexPropertyExport(ac, "D1", "i");
+ var diodeVoltage = new ComplexPropertyExport(ac, "D1", "v");
+ var junctionVoltage = new ComplexPropertyExport(ac, "D1", "vj");
+ var power = new ComplexPropertyExport(ac, "D1", "p");
+
+ Complex currentValue = new Complex(double.NaN, double.NaN);
+ Complex voltageValue = new Complex(double.NaN, double.NaN);
+ Complex junctionVoltageValue = new Complex(double.NaN, double.NaN);
+ Complex powerValue = new Complex(double.NaN, double.NaN);
+ foreach (int ignored in ac.Run(circuit, AC.ExportSmallSignal))
+ {
+ currentValue = current.Value;
+ voltageValue = diodeVoltage.Value;
+ junctionVoltageValue = junctionVoltage.Value;
+ powerValue = power.Value;
+ }
+
+ return (currentValue, voltageValue, junctionVoltageValue, powerValue);
+ }
+
+ private static double RunOpCurrent(SpiceSharpModel model)
+ {
+ return RunOpCurrent(model, "D1");
+ }
+
+ private static double RunOpCurrent(SpiceSharpModel model, string entityName)
+ {
+ double current = double.NaN;
+ var preparedSimulation = model.Simulations.FirstOrDefault(simulation => simulation is OP);
+ if (preparedSimulation != null)
+ {
+ var export = new RealPropertyExport(preparedSimulation, entityName, "i");
+ foreach (int code in preparedSimulation.InvokeEvents(preparedSimulation.Run(model.Circuit, -1)))
+ {
+ if (code == OP.ExportOperatingPoint)
+ {
+ current = export.Value;
+ }
+ }
+
+ return current;
+ }
+
+ var op = new OP("verify");
+ var fallbackExport = new RealPropertyExport(op, entityName, "i");
+ foreach (int ignored in op.Run(model.Circuit))
+ {
+ current = fallbackExport.Value;
+ }
+
+ return current;
+ }
+
+ private static double[] RunOpCurrents(SpiceSharpModel model, string entityName)
+ {
+ return model.Simulations
+ .Where(simulation => simulation is OP)
+ .Select(simulation =>
+ {
+ var export = new RealPropertyExport(simulation, entityName, "i");
+ double current = double.NaN;
+ foreach (int code in simulation.InvokeEvents(simulation.Run(model.Circuit, -1)))
+ {
+ if (code == OP.ExportOperatingPoint)
+ {
+ current = export.Value;
+ }
+ }
+
+ return current;
+ })
+ .ToArray();
+ }
+
+ private static SpiceSharpModel ReadWithCustomComponents(params string[] lines)
+ {
+ var text = string.Join(Environment.NewLine, lines);
+ var parser = new SpiceNetlistParser();
+ parser.Settings.Lexing.HasTitle = true;
+ parser.Settings.Parsing.IsEndRequired = true;
+
+ var parseResult = parser.ParseNetlist(text);
+ var reader = new SpiceSharpReader();
+ reader.Settings.UseCustomComponents();
+
+ return reader.Read(parseResult.FinalModel);
+ }
+
+ private static void AssertNoValidationErrors(SpiceSharpModel model)
+ {
+ Assert.False(model.ValidationResult.HasError, GetValidationMessages(model));
+ }
+
+ private static string GetValidationMessages(SpiceSharpModel model)
+ {
+ return string.Join(Environment.NewLine, model.ValidationResult.Errors.Select(error => error.Message));
+ }
+
+ private static void AssertClose(double expected, double actual, double tolerance)
+ {
+ double effectiveTolerance = Math.Abs(expected) > 1e-6
+ ? Math.Max(tolerance, (Math.Abs(expected) * 2e-9) + 5e-9)
+ : tolerance;
+
+ Assert.True(
+ Math.Abs(expected - actual) <= effectiveTolerance,
+ $"Expected {expected:R}, got {actual:R}.");
+ }
+
+ private static void AssertComplexClose(Complex expected, Complex actual, double tolerance)
+ {
+ AssertClose(expected.Real, actual.Real, tolerance);
+ AssertClose(expected.Imaginary, actual.Imaginary, tolerance);
+ }
+ }
+}
diff --git a/src/SpiceSharpParser.Tests/SpiceSharpParser.Tests.csproj b/src/SpiceSharpParser.Tests/SpiceSharpParser.Tests.csproj
index f0e47ffa..cf9fe391 100644
--- a/src/SpiceSharpParser.Tests/SpiceSharpParser.Tests.csproj
+++ b/src/SpiceSharpParser.Tests/SpiceSharpParser.Tests.csproj
@@ -41,6 +41,7 @@
+
diff --git a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Context/Models/IModelsRegistry.cs b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Context/Models/IModelsRegistry.cs
index fd5fbd8a..f591f77e 100644
--- a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Context/Models/IModelsRegistry.cs
+++ b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Context/Models/IModelsRegistry.cs
@@ -12,10 +12,12 @@ public interface IModelsRegistry
Model FindModel(string modelName);
+ Model FindModel(string modelName, Func predicate);
+
IEntity FindModelEntity(string modelName, Func predicate);
void RegisterModelInstance(Model model);
IModelsRegistry CreateChildRegistry(List generators);
}
-}
\ No newline at end of file
+}
diff --git a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Context/Models/StochasticModelsRegistry.cs b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Context/Models/StochasticModelsRegistry.cs
index b8b5cc91..1c7a4813 100644
--- a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Context/Models/StochasticModelsRegistry.cs
+++ b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Context/Models/StochasticModelsRegistry.cs
@@ -254,15 +254,20 @@ public void SetModel(Entity entity, Func predicate, ISimulationWith
}
}
- public Model FindModel(string modelName)
- {
- return FindModelWithPredicate(modelName, null);
- }
-
- public IEntity FindModelEntity(string modelName, Func predicate)
- {
- return FindModelWithPredicate(modelName, predicate)?.Entity;
- }
+ public Model FindModel(string modelName)
+ {
+ return FindModelWithPredicate(modelName, null);
+ }
+
+ public Model FindModel(string modelName, Func predicate)
+ {
+ return FindModelWithPredicate(modelName, predicate);
+ }
+
+ public IEntity FindModelEntity(string modelName, Func predicate)
+ {
+ return FindModelWithPredicate(modelName, predicate)?.Entity;
+ }
private Model FindModelWithPredicate(string modelName, Func predicate)
{
@@ -285,12 +290,15 @@ private Model FindModelWithPredicate(string modelName, Func predica
}
}
- // Fall back to exact match (base model without suffix)
- if (AllModels.TryGetValue(modelNameToSearch, out var model))
- {
- return model;
- }
- }
+ // Fall back to exact match (base model without suffix)
+ if (AllModels.TryGetValue(modelNameToSearch, out var model))
+ {
+ if (predicate == null || predicate(model))
+ {
+ return model;
+ }
+ }
+ }
return null;
}
diff --git a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Context/Sweeps/ParameterSweepUpdater.cs b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Context/Sweeps/ParameterSweepUpdater.cs
index 5ed39f9b..c7abc47c 100644
--- a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Context/Sweeps/ParameterSweepUpdater.cs
+++ b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Context/Sweeps/ParameterSweepUpdater.cs
@@ -108,5 +108,6 @@ protected void SetIndependentSource(IEntity @entity, ISimulationWithEvents simul
.SetParameterBeforeTemperature(@entity, "dc", paramToSet.Value, simulation);
}
}
+
}
-}
\ No newline at end of file
+}
diff --git a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Context/Updates/EntityUpdates.cs b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Context/Updates/EntityUpdates.cs
index ba3c8f09..73dd248a 100644
--- a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Context/Updates/EntityUpdates.cs
+++ b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Context/Updates/EntityUpdates.cs
@@ -45,20 +45,13 @@ public void Apply(ISimulationWithEvents simulation)
{
var beforeTemperature = CommonUpdates[entity].ParameterUpdatesBeforeTemperature;
- foreach (var entityUpdate in beforeTemperature)
- {
- EvaluationContext context = GetEntityContext(biasingSimulation, entity.Name);
- if (context != null)
- {
- var value = entityUpdate.GetValue(context);
- if (!double.IsNaN(value))
- {
- entity.CreateParameterSetter(entityUpdate.ParameterName)?.Invoke(value);
- }
- }
- }
- }
- };
+ foreach (var entityUpdate in beforeTemperature)
+ {
+ EvaluationContext context = GetEntityContext(biasingSimulation, entity.Name);
+ ApplyParameterUpdate(entity, entityUpdate, context);
+ }
+ }
+ };
biasingSimulation.EventBeforeTemperature += (_, _) =>
{
@@ -68,20 +61,13 @@ public void Apply(ISimulationWithEvents simulation)
{
var beforeTemperature = entityPair.Value.ParameterUpdatesBeforeTemperature;
- foreach (var entityUpdate in beforeTemperature)
- {
- EvaluationContext context = GetEntityContext(biasingSimulation, entityPair.Key.Name);
- if (context != null)
- {
- var value = entityUpdate.GetValue(context);
- if (!double.IsNaN(value))
- {
- entityPair.Key.CreateParameterSetter(entityUpdate.ParameterName)?.Invoke(value);
- }
- }
- }
- }
- }
+ foreach (var entityUpdate in beforeTemperature)
+ {
+ EvaluationContext context = GetEntityContext(biasingSimulation, entityPair.Key.Name);
+ ApplyParameterUpdate(entityPair.Key, entityUpdate, context);
+ }
+ }
+ }
};
}
}
@@ -196,10 +182,36 @@ public void Add(IEntity entity, string parameterName, double value, bool beforeT
}
}
- private EvaluationContext GetEntityContext(ISimulationWithEvents simulation, string entityName)
- {
- var context = Context.GetSimulationContext(simulation).Find(entityName);
- return context;
- }
- }
-}
\ No newline at end of file
+ private EvaluationContext GetEntityContext(ISimulationWithEvents simulation, string entityName)
+ {
+ var context = Context.GetSimulationContext(simulation).Find(entityName);
+ return context;
+ }
+
+ private static void ApplyParameterUpdate(IEntity entity, EntityParameterUpdate entityUpdate, EvaluationContext context)
+ {
+ if (TryGetValue(entityUpdate, context, out double value) && !double.IsNaN(value))
+ {
+ entity.SetParameter(entityUpdate.ParameterName, value);
+ }
+ }
+
+ private static bool TryGetValue(EntityParameterUpdate entityUpdate, EvaluationContext context, out double value)
+ {
+ if (context != null)
+ {
+ value = entityUpdate.GetValue(context);
+ return true;
+ }
+
+ if (entityUpdate is EntityParameterDoubleValueUpdate doubleUpdate)
+ {
+ value = doubleUpdate.Value;
+ return true;
+ }
+
+ value = double.NaN;
+ return false;
+ }
+ }
+}
diff --git a/src/SpiceSharpParser/SpiceSharpParser.csproj b/src/SpiceSharpParser/SpiceSharpParser.csproj
index 26c51ab0..2418b4fb 100644
--- a/src/SpiceSharpParser/SpiceSharpParser.csproj
+++ b/src/SpiceSharpParser/SpiceSharpParser.csproj
@@ -22,7 +22,7 @@
MIT
latest
- 3.3.4
+ 3.3.5
diff --git a/src/docs/articles/diode.md b/src/docs/articles/diode.md
index bb2670cd..b63b0a5d 100644
--- a/src/docs/articles/diode.md
+++ b/src/docs/articles/diode.md
@@ -2,6 +2,8 @@
The diode is a two-terminal semiconductor device that allows current to flow primarily in one direction.
+For LTspice-style ideal diode models using parameters such as `Ron`, `Roff`, and `Vfwd`, see [LTspice-Style Ideal Diode](ideal-diode.md). That behavior lives in the optional `SpiceSharpParser.CustomComponents` project and must be enabled with `UseCustomComponents()`.
+
## Syntax
```
diff --git a/src/docs/articles/ideal-diode.md b/src/docs/articles/ideal-diode.md
new file mode 100644
index 00000000..d1ff20eb
--- /dev/null
+++ b/src/docs/articles/ideal-diode.md
@@ -0,0 +1,983 @@
+# LTspice-Style Ideal Diode
+
+`SpiceSharpParser.CustomComponents` contains an opt-in ideal diode component for LTspice-style diode models that use parameters such as `Ron`, `Roff`, and `Vfwd`.
+
+This component is separate from the built-in SpiceSharp diode. The built-in diode uses the normal semiconductor exponential model. The custom ideal diode uses a piecewise linear current law. It is useful for power electronics, protection clamps, rectifiers, and behavioral-level circuits where a simple on/off diode is more useful than a detailed PN junction model.
+
+## When To Use It
+
+Use this component when a netlist contains LTspice ideal diode parameters:
+
+```spice
+.model did D(Ron=0.1 Roff=1e9 Vfwd=0.7)
+D1 out 0 did
+```
+
+Use the built-in diode when the model is a semiconductor model:
+
+```spice
+.model d4148 D(Is=2.52e-9 Rs=0.568 N=1.752 Cjo=4e-12 M=0.4 Tt=20e-9)
+D1 out 0 d4148
+```
+
+The ideal diode is faster and simpler, but it does not include junction capacitance, charge storage, temperature-dependent semiconductor equations, or noise.
+
+## Installation
+
+Reference the custom component assembly in the application that reads the netlist:
+
+```xml
+
+```
+
+When the package is published, use the package reference instead:
+
+```bash
+dotnet add package SpiceSharpParser.CustomComponents
+```
+
+## Enable Parser Mappings
+
+The parser does not enable custom components by default. Enable them on the reader settings before calling `Read()`:
+
+```csharp
+using System;
+using SpiceSharpParser;
+using SpiceSharpParser.CustomComponents;
+
+var netlist = string.Join(Environment.NewLine,
+ "Ideal diode example",
+ "V1 in 0 3",
+ "D1 in 0 did",
+ ".model did D(Ron=2 Roff=1e9 Vfwd=1)",
+ ".op",
+ ".end");
+
+var parser = new SpiceNetlistParser();
+parser.Settings.Lexing.HasTitle = true;
+parser.Settings.Parsing.IsEndRequired = true;
+var parseResult = parser.ParseNetlist(netlist);
+
+var reader = new SpiceSharpReader();
+reader.Settings.UseCustomComponents();
+
+var spiceModel = reader.Read(parseResult.FinalModel);
+```
+
+`UseCustomComponents()` replaces the parser mappings for diode models and diode instances with custom-aware generators. Ordinary diode models still fall back to the built-in SpiceSharp diode.
+
+Without `UseCustomComponents()`, the core parser does not know how to map LTspice ideal diode parameters to a SpiceSharp entity.
+
+## Netlist Syntax
+
+The instance syntax is the normal diode syntax:
+
+```spice
+D [] [ON|OFF] [M=] [N=] [L=] [W=] [= ...]
+```
+
+The custom component is selected when the referenced `.MODEL D(...)` contains at least one ideal diode model parameter:
+
+```spice
+.model did D(Ron=2 Roff=1e9 Vfwd=1)
+D1 in 0 did
+```
+
+A classic model remains a classic diode:
+
+```spice
+.model regular D(Is=1e-12 N=1)
+D1 in 0 regular
+```
+
+`Rs` alone does not select the ideal diode, because `Rs` is also a classic diode model parameter. At least one of `Ron`, `Roff`, `Vfwd`, `Vrev`, `Rrev`, `Ilimit`, `RevIlimit`, `Epsilon`, or `RevEpsilon` must be present on the model.
+
+If multiple suffixed models share a base name, instance `L=` and `W=` values are used with model `Lmin`, `Lmax`, `Wmin`, and `Wmax` selection parameters.
+
+```spice
+D1 in 0 did L=0.5 W=5
+.model did.fast D(Ron=2 Roff=1e9 Vfwd=1 Lmin=0.1 Lmax=1 Wmin=1 Wmax=10)
+.model did.slow D(Ron=4 Roff=1e9 Vfwd=1 Lmin=1 Lmax=10 Wmin=10 Wmax=100)
+```
+
+In this example, `D1` selects `did.fast`.
+
+## Parameters
+
+### Model Parameters
+
+| Parameter | Description | Default |
+|-----------|-------------|---------|
+| `Ron` | Forward on resistance | 1 ohm |
+| `Roff` | Off resistance | simulation `Gmin` if omitted |
+| `Vfwd` | Forward threshold voltage | 0 V |
+| `Vrev` | Reverse breakdown voltage magnitude | not enabled |
+| `Rrev` | Reverse breakdown resistance | `Ron` if omitted |
+| `Ilimit` | Forward current limit | not enabled |
+| `RevIlimit` | Reverse current limit | not enabled |
+| `Epsilon` | Forward transition smoothing width | 0 V |
+| `RevEpsilon` | Reverse transition smoothing width | 0 V |
+
+### Instance Parameters
+
+| Parameter | Description | Default |
+|-----------|-------------|---------|
+| `area` | Accepted by diode syntax, ignored by the idealized model | 1 |
+| `M` | Positive parallel multiplier | 1 |
+| `N` | Positive series multiplier | 1 |
+| `L` | Length used only for binned model selection | not set |
+| `W` | Width used only for binned model selection | not set |
+| `ON` / `OFF` | Initial state hint | on |
+| `Ron`, `Roff`, `Vfwd`, `Vrev`, `Rrev`, `Ilimit`, `RevIlimit`, `Epsilon`, `RevEpsilon` | Instance override for the same model parameter | model value |
+| `Rs` | Accepted by shared diode parameter handling, ignored by the idealized model | 0 ohm |
+
+For the ideal diode, `N` is a series multiplier. This differs from the classic diode model, where model parameter `N` is the emission coefficient.
+
+`L` and `W` are not electrical parameters of the ideal diode. They are evaluated during simulation setup to select a matching suffixed model.
+
+## Parameter Scaling
+
+The model is evaluated as one ideal diode cell. LTspice's idealized diode uses
+`M` and `N` for electrical scaling:
+
+| Parameter | Effect |
+|-----------|--------|
+| `M` | Represents parallel diode cells. Current and conductance scale by `M`. |
+| `N` | Represents series diode cells. Total voltage is divided by `N` before evaluating one cell. |
+| `area` | Accepted by the shared diode syntax but not used electrically by the idealized model. |
+| `Rs` | Accepted by shared model/instance handling but not used electrically by the idealized model. |
+
+`area`, `M`, and `N` are still validated as positive diode instance values.
+
+For a forward-biased diode without smoothing or current limiting:
+
+$$
+\begin{aligned}
+v_{\text{local}} &= \frac{V(\text{anode}, \text{cathode})}{N} \\
+i_{\text{local}} &= \frac{v_{\text{local}} - V_{\text{fwd}}}{R_{\text{on}}} \\
+I_{\text{total}} &= M \cdot i_{\text{local}}
+\end{aligned}
+$$
+
+So the approximate total forward threshold is:
+
+$$
+V_{\text{total}} \approx N \cdot V_{\text{fwd}}
+$$
+
+and the approximate effective forward resistance is:
+
+$$
+R_{\text{effective}} \approx \frac{R_{\text{on}} \cdot N}{M}
+$$
+
+For LTspice parity, `area` and `Rs` do not change that effective resistance for
+the idealized `Ron`/`Roff`/`Vfwd` model. Use `M` for parallel scaling, `N` for
+series scaling, or add an explicit resistor if a series resistance is required.
+
+This matches the intended interpretation: more parallel cells carry more current, and more series cells need more voltage.
+
+## Model Parameter Sweeps
+
+Ideal diode model parameters can be stepped with normal LTspice-style bracket syntax:
+
+```spice
+Ideal diode Ron sweep
+V1 in 0 5
+D1 in 0 did
+.model did D(Ron=2 Roff=1e9 Vfwd=1)
+.op
+.step D did(Ron) LIST 2 4
+.end
+```
+
+The first operating point uses `Ron=2`, so the current is approximately:
+
+$$
+\frac{5 - 1}{2} = 2\,\text{A}
+$$
+
+The second operating point uses `Ron=4`, so the current is approximately:
+
+$$
+\frac{5 - 1}{4} = 1\,\text{A}
+$$
+
+Stepped model values are applied as simulation-local ideal-diode parameter overlays. After a stepped simulation finishes, the shared model parameter set is restored to its original value.
+
+For binned models, the sweep target can use either the requested base model name or the concrete selected model name:
+
+```spice
+Binned ideal diode Ron sweep
+V1 in 0 5
+D1 in 0 did L=0.5 W=5
+.model did.fast D(Ron=2 Roff=1e9 Vfwd=1 Lmin=0.1 Lmax=1 Wmin=1 Wmax=10)
+.model did.slow D(Ron=8 Roff=1e9 Vfwd=1 Lmin=1 Lmax=10 Wmin=10 Wmax=100)
+.op
+.step D did.fast(Ron) LIST 2 4
+.end
+```
+
+Here `D1` selects `did.fast`, and the sweep steps that selected model's `Ron`.
+
+## Current And Voltage Sign
+
+The anode is the first diode node and the cathode is the second diode node:
+
+```spice
+D1 anode cathode did
+```
+
+Positive diode voltage is:
+
+$$
+V(\text{anode}, \text{cathode})
+$$
+
+Positive diode current flows from anode to cathode. Property exports follow that sign convention:
+
+```spice
+.save @D1[v] @D1[vj] @D1[i] @D1[gd] @D1[p]
+```
+
+| Export | Meaning |
+|--------|---------|
+| `@D1[v]` or `@D1[vd]` | Terminal diode voltage |
+| `@D1[vj]` or `@D1[vdiode]` | Internal ideal-diode voltage; equal to terminal voltage for the LTspice-parity idealized model |
+| `@D1[i]`, `@D1[id]`, or `@D1[c]` | Diode current |
+| `@D1[gd]` | Terminal small-signal conductance at the operating point |
+| `@D1[p]` or `@D1[pd]` | Terminal diode branch power |
+
+When the diode is reverse biased, `@D1[i]` is usually negative.
+
+## Current Law
+
+The ideal diode current law is evaluated for one diode cell first. Instance
+scaling is applied after that.
+
+### Local Evaluation Algorithm
+
+The implementation for this section lives in `IdealDiodeEquation`. The method
+`Evaluate(...)` does not stamp the circuit matrix. It only evaluates the local
+current law for one ideal-diode cell and returns two values:
+
+| Output | Meaning |
+|--------|---------|
+| `current` | Local diode current at the present voltage |
+| `conductance` | Local small-signal derivative `di/dv` used by Newton iteration |
+
+The voltage `v` is positive from anode to cathode, and positive current flows in
+that same direction. The algorithm uses a line representation for each operating
+region:
+
+```text
+i(v) = slope * v + intercept
+```
+
+This representation matters because the solver needs both the current and its
+derivative. The line slope is already the small-signal conductance for that
+region.
+
+The local evaluation is:
+
+```text
+gon = 1 / Ron
+goff = Roff was given ? 1 / Roff : max(simulation Gmin, 0)
+vf = Vfwd was given ? Vfwd : 0
+
+current = goff * v
+conductance = goff
+
+if Vrev was given:
+ vrev = abs(Vrev)
+ rrev = Rrev was given ? Rrev : Ron
+ grev = 1 / rrev
+
+ reverse line = grev * v + (grev - goff) * vrev
+ off line = goff * v
+ boundary = -vrev
+
+ current, conductance =
+ transition(v, boundary, RevEpsilon, reverse line, off line)
+
+forward line = gon * v + (goff - gon) * vf
+off line = goff * v
+boundary = vf
+
+if v is past the forward boundary, or inside the forward smoothing window:
+ current, conductance =
+ transition(v, boundary, Epsilon, off line, forward line)
+
+if current > 0 and Ilimit was given:
+ apply tanh current limiter
+else if current < 0 and RevIlimit was given:
+ apply tanh current limiter
+```
+
+The order is intentional. The off line is the default because most voltages are
+between reverse breakdown and forward conduction. Reverse breakdown is evaluated
+before forward conduction because it only applies on the negative-voltage side.
+The forward transition is evaluated last so positive forward conduction replaces
+the off result when the voltage reaches the forward knee.
+
+The transition helper is shared by both knees. It receives the line on the
+low-voltage side, the line on the high-voltage side, the nominal LTspice knee,
+and the optional smoothing width. For reverse breakdown, the low-voltage side is
+the reverse line and the high-voltage side is the off line. For forward
+conduction, the low-voltage side is the off line and the high-voltage side is
+the forward line.
+
+With finite `Roff`, the conducting lines are anchored to the off-line current at
+the nominal threshold. Forward conduction is continuous with the off line at
+`Vfwd`, and reverse breakdown is continuous with the off line at `-Vrev`.
+LTspice keeps those nominal knees rather than moving the transition to the
+mathematical intersection of the two unsmoothed lines.
+
+With no smoothing, the transition helper simply chooses one line. With smoothing,
+the epsilon value is treated as a one-sided LTspice-style width. Forward
+smoothing starts at the forward boundary and extends into the forward-conduction
+region. Reverse smoothing starts in reverse breakdown and ends at the reverse
+boundary. The conductance ramps linearly from the left slope to the right slope,
+and the current is the integral of that ramp. This keeps both current and
+conductance continuous at the start and end of the smoothing window. It also
+shifts the fully conducting side by half the epsilon width; for example, deep in
+reverse breakdown with `RevEpsilon=e`, the approximate line becomes:
+
+$$
+i \approx \frac{v + V_{\text{rev}} + e / 2}{R_{\text{rev}}}
+$$
+
+Current limiting happens after the piecewise line and optional smoothing have
+selected a raw current. The limiter is based on `tanh`, so it approaches the
+limit smoothly and also scales the conductance by the derivative of the limiter.
+The sign of the raw current selects the limiter: positive current uses `Ilimit`,
+negative current uses `RevIlimit`, and zero current is left unchanged.
+
+The caller, `Biasing`, handles everything outside the local cell. It divides the
+internal diode voltage by `N` before calling `Evaluate(...)`, multiplies the
+equivalent branch current by `M`, and scales the conductance by `M / N`. The
+accepted `area` and `Rs` values are ignored electrically for LTspice parity.
+
+The formulas below describe the same algorithm. Define:
+
+$$
+\begin{aligned}
+v &= \text{voltage across one series cell} \\
+V_f &=
+\begin{cases}
+V_{\text{fwd}}, & \text{if } V_{\text{fwd}} \text{ is given} \\
+0, & \text{otherwise}
+\end{cases} \\
+g_{\text{on}} &= \frac{1}{R_{\text{on}}} \\
+g_{\text{off}} &=
+\begin{cases}
+\frac{1}{R_{\text{off}}}, & \text{if } R_{\text{off}} \text{ is given} \\
+G_{\text{min}}, & \text{otherwise}
+\end{cases}
+\end{aligned}
+$$
+
+The forward, off, and reverse-breakdown lines are:
+
+$$
+\begin{aligned}
+i_{\text{on}}(v) &= g_{\text{on}} \cdot (v - V_f) + g_{\text{off}} \cdot V_f \\
+i_{\text{off}}(v) &= g_{\text{off}} \cdot v \\
+i_{\text{rev}}(v) &= g_{\text{rev}} \cdot (v + V_{\text{rev}}) - g_{\text{off}} \cdot V_{\text{rev}}
+\end{aligned}
+$$
+
+where `i_rev(v)` is used only when `Vrev` is given, and:
+
+$$
+g_{\text{rev}} = \frac{1}{R_{\text{rev}}}
+$$
+
+If `Rrev` is omitted, `Ron` is used for `Rrev`.
+
+Without smoothing or current limiting, the raw current is the line selected by
+the present voltage:
+
+$$
+i_{\text{raw}}(v) =
+\begin{cases}
+i_{\text{rev}}(v), & \text{if } V_{\text{rev}} \text{ is enabled and } v < -V_{\text{rev}} \\
+i_{\text{off}}(v), & \text{if } v < V_f \\
+i_{\text{on}}(v), & \text{otherwise}
+\end{cases}
+$$
+
+The small-signal conductance is the derivative of the selected line:
+
+$$
+g_{d,\text{raw}} = \frac{d i_{\text{raw}}}{d v}
+$$
+
+So the unsmoothed conductance is usually one of:
+
+$$
+g_{d,\text{raw}} \in \{g_{\text{rev}}, g_{\text{off}}, g_{\text{on}}\}
+$$
+
+The forward transition point is the nominal LTspice threshold:
+
+$$
+v_{\text{fwd,boundary}} = V_{\text{fwd}}
+$$
+
+The forward line is shifted by the off-line current at `Vfwd`, so finite `Roff`
+adds a small term:
+
+$$
+i_{\text{on}}(v) = \frac{v - V_{\text{fwd}}}{R_{\text{on}}}
+ + \frac{V_{\text{fwd}}}{R_{\text{off}}}
+$$
+
+### Reverse Breakdown
+
+Reverse breakdown is enabled by `Vrev`. `Vrev` is a magnitude, so this model starts reverse breakdown near:
+
+$$
+v = -V_{\text{rev}}
+$$
+
+The reverse breakdown region is approximately:
+
+$$
+i \approx \frac{v + V_{\text{rev}}}{R_{\text{rev}}}
+$$
+
+The reverse transition point is the nominal LTspice threshold:
+
+$$
+v_{\text{rev,boundary}} = -V_{\text{rev}}
+$$
+
+The reverse line is shifted by the off-line current at `-Vrev`, so finite
+`Roff` adds a small term:
+
+$$
+i_{\text{rev}}(v) = \frac{v + V_{\text{rev}}}{R_{\text{rev}}}
+ - \frac{V_{\text{rev}}}{R_{\text{off}}}
+$$
+
+Example:
+
+```spice
+.model clamp D(Ron=2 Roff=1e9 Vfwd=1 Vrev=2 Rrev=4)
+```
+
+At `v = -6 V`, the approximate reverse current is:
+
+$$
+i \approx \frac{-6 + 2}{4} = -1\,\text{A}
+$$
+
+### Current Limiting
+
+`Ilimit` and `RevIlimit` smoothly limit forward and reverse current. The limiter uses a `tanh` shape, so it approaches the limit gradually instead of clipping with a hard corner.
+
+Forward current limit:
+
+```spice
+.model did D(Ron=0.1 Roff=1e9 Vfwd=0.7 Ilimit=10)
+```
+
+Reverse current limit:
+
+```spice
+.model did D(Ron=0.1 Roff=1e9 Vfwd=0.7 Vrev=20 RevIlimit=2)
+```
+
+The smooth limiter also reduces the small-signal conductance as the current approaches the limit.
+
+Let `i0` and `gd0` be the current and conductance after the piecewise line and
+optional smoothing, but before current limiting. The forward limiter uses this
+shape:
+
+$$
+\begin{aligned}
+i_{\text{limited}} &= I_{\text{limit}} \cdot \tanh\left(\frac{i_0}{I_{\text{limit}}}\right) \\
+g_{d,\text{limited}} &= g_{d0} \cdot \left(1 - \tanh^2\left(\frac{i_0}{I_{\text{limit}}}\right)\right)
+\end{aligned}
+$$
+
+The reverse limiter uses the same equation with `RevIlimit`. Because reverse
+current is negative, $\tanh(i_0 / I_{\text{rev-limit}})$ is also negative.
+
+### Transition Smoothing
+
+`Epsilon` and `RevEpsilon` smooth the forward and reverse transitions. A value
+of zero gives a sharp piecewise transition. A positive value ramps the slope over
+a one-sided LTspice-style voltage window next to the transition.
+
+```spice
+.model did D(Ron=0.1 Roff=1e9 Vfwd=0.7 Epsilon=10m)
+```
+
+For any smoothed transition, let the left line be:
+
+$$
+i_{\text{left}}(v) = a_{\text{left}} \cdot v + b_{\text{left}}
+$$
+
+and the right line be:
+
+$$
+i_{\text{right}}(v) = a_{\text{right}} \cdot v + b_{\text{right}}
+$$
+
+For the forward transition, the left line is `i_off(v)` and the right line is
+`i_on(v)`. For the reverse transition, the left line is `i_rev(v)` and the
+right line is `i_off(v)`.
+
+With smoothing width `e`, the forward smoothing window is:
+
+$$
+\begin{aligned}
+v_{\text{start}} &= v_{\text{boundary}} \\
+v_{\text{end}} &= v_{\text{boundary}} + e
+\end{aligned}
+$$
+
+The reverse smoothing window is:
+
+$$
+\begin{aligned}
+v_{\text{start}} &= v_{\text{boundary}} - e \\
+v_{\text{end}} &= v_{\text{boundary}}
+\end{aligned}
+$$
+
+Inside that window:
+
+$$
+\begin{aligned}
+d &= v - v_{\text{start}} \\
+i(v) &= i_{\text{left}}(v_{\text{start}})
+ + a_{\text{left}} \cdot d
+ + \frac{(a_{\text{right}} - a_{\text{left}}) \cdot d^2}{2e} \\
+g_d(v) &= a_{\text{left}} + \frac{(a_{\text{right}} - a_{\text{left}}) \cdot d}{e}
+\end{aligned}
+$$
+
+Outside the window, the model uses the normal left or right line.
+
+For the fully conducting side of a smoothed transition, the straight line is
+shifted by half the epsilon width so it joins the ramp continuously. With tiny
+`Roff`, this means the deep forward region is approximately:
+
+$$
+i \approx \frac{v - V_{\text{fwd}} - Epsilon / 2}{R_{\text{on}}}
+$$
+
+and the deep reverse region is approximately:
+
+$$
+i \approx \frac{v + V_{\text{rev}} + RevEpsilon / 2}{R_{\text{rev}}}
+$$
+
+Use smoothing when a hard corner causes convergence problems in DC or transient operating-point iterations.
+
+## How The Parser Selects The Component
+
+The parser bridge has two pieces:
+
+| Class | Role |
+|-------|------|
+| `IdealDiodeModelGenerator` | Reads `.MODEL ... D(...)`. If the model has ideal diode parameters, it creates an `IdealDiodeModel`; otherwise it delegates to the built-in diode model generator. |
+| `IdealDiodeGenerator` | Reads `D...` instances. If the referenced model is an `IdealDiodeModel`, it creates an `IdealDiode`; otherwise it delegates to the built-in diode generator. |
+
+The flow is:
+
+```text
+reader.Settings.UseCustomComponents()
+ -> replace D model generator
+ -> replace D component generator
+
+.model did D(Ron=...)
+ -> IdealDiodeModelGenerator detects Ron
+ -> creates IdealDiodeModel
+ -> stores model parameters
+
+D1 in 0 did
+ -> IdealDiodeGenerator resolves model did
+ -> sees IdealDiodeModel
+ -> creates IdealDiode
+ -> binds live model parameters for each simulation
+ -> applies simulation-local model sweep overlays, if any
+ -> applies instance overrides
+```
+
+Classic diode models are delegated back to the existing parser behavior:
+
+```text
+.model regular D(Is=...)
+ -> no ideal diode parameter
+ -> built-in DiodeModel
+ -> built-in Diode
+```
+
+The selected model is resolved again before simulation setup. This lets `L` and `W` expressions participate in binned model selection and lets stochastic or stepped simulations use their simulation-specific model data.
+
+## How The SpiceSharp Component Works
+
+`IdealDiode` is a normal two-pin SpiceSharp component:
+
+```text
+pin 0 = anode
+pin 1 = cathode
+```
+
+During simulation setup, it creates behavior objects:
+
+| Behavior | Used by | Purpose |
+|----------|---------|---------|
+| `Biasing` | `.OP`, `.DC`, and operating-point parts of transient | Evaluates diode current and conductance, then stamps the real-valued matrix. |
+| `Frequency` | `.AC` | Uses the operating-point conductance for small-signal AC. |
+
+The component is memoryless. It does not add dynamic state for charge or capacitance.
+
+### Biasing Stamp
+
+At each biasing load, the behavior:
+
+1. Reads the present internal diode voltage.
+2. Divides it by `N` to get the voltage across one series cell.
+3. Evaluates current `i` and local conductance `gd`.
+4. Scales current by `M` and conductance by `M / N`.
+5. Stamps the internal diode conductance into the matrix.
+6. Stamps the equivalent current into the right-hand side.
+7. Stamps the zero-volt branch equation between the external and internal anodes.
+
+The local linear form is:
+
+$$
+\begin{aligned}
+i &\approx g_d \cdot v + i_{\text{eq}} \\
+i_{\text{eq}} &= i - g_d \cdot v
+\end{aligned}
+$$
+
+The conductance stamp is resistor-like:
+
+| Matrix entry | Added value |
+|--------------|-------------|
+| `Y[internal_anode, internal_anode]` | `+gd` |
+| `Y[cathode, cathode]` | `+gd` |
+| `Y[internal_anode, cathode]` | `-gd` |
+| `Y[cathode, internal_anode]` | `-gd` |
+
+The right-hand side receives the equivalent current source contribution.
+
+### Series Resistance
+
+The component always creates a private internal anode and a branch:
+
+```text
+external anode -- 0 V constraint -- internal anode -- ideal diode -- cathode
+```
+
+That fixed topology lets `Rs` be parsed or stepped without rebinding the
+component, but for LTspice parity `Rs` is ignored electrically by the idealized
+`Ron`/`Roff`/`Vfwd` model. Add an explicit resistor in series with the diode when
+series resistance is required.
+
+The standard `v`, `vj`, `gd`, and `p` exports are terminal-equivalent quantities.
+
+### Convergence Check
+
+The biasing behavior implements a convergence check using the diode current predicted from the previous local linearization:
+
+$$
+i_{\text{predicted}} = i_{\text{old}} + g_{d,\text{old}} \cdot \Delta v
+$$
+
+If the predicted current differs from the actual loaded current by more than the simulation tolerances, the iteration is marked as not converged and SpiceSharp continues iterating.
+
+### AC Behavior
+
+AC analysis uses the operating-point conductance. The AC stamp uses the internal
+diode conductance, while property exports report terminal voltage, terminal
+current, and terminal complex power:
+
+$$
+y_{\text{ac}} = g_d
+$$
+
+There is no frequency-dependent capacitance in this model. That means the AC response of the diode itself is resistive and frequency independent. Any frequency response must come from surrounding capacitors, inductors, sources, or other dynamic components.
+
+## Direct Code Usage
+
+You can also create the component directly without parsing a netlist:
+
+```csharp
+using SpiceSharp;
+using SpiceSharp.Components;
+using SpiceSharp.Simulations;
+using SpiceSharpParser.CustomComponents;
+
+var diode = new IdealDiode("D1", "in", "0");
+diode.Parameters.OnResistance = 2.0;
+diode.Parameters.OffResistance = 1e9;
+diode.Parameters.ForwardVoltage = 1.0;
+
+var circuit = new Circuit(
+ new VoltageSource("V1", "in", "0", 3.0),
+ diode);
+
+var op = new OP("op");
+var current = new RealPropertyExport(op, "D1", "i");
+
+foreach (int _ in op.Run(circuit))
+{
+ Console.WriteLine(current.Value);
+}
+```
+
+This prints approximately `1 A`, because:
+
+$$
+\frac{3\,\text{V} - 1\,\text{V}}{2\,\Omega} = 1\,\text{A}
+$$
+
+## Worked Examples
+
+### Forward Conduction
+
+```spice
+Forward ideal diode
+V1 in 0 3
+D1 in 0 did
+.model did D(Ron=2 Roff=1e9 Vfwd=1)
+.op
+.save @D1[i] @D1[v]
+.end
+```
+
+The expected current is approximately:
+
+$$
+i \approx \frac{3 - 1}{2} = 1\,\text{A}
+$$
+
+### Off Leakage
+
+```spice
+Off leakage
+V1 in 0 0.5
+D1 in 0 did
+.model did D(Ron=2 Roff=1e9 Vfwd=1)
+.op
+.save @D1[i]
+.end
+```
+
+The diode is below its forward region. The expected current is approximately:
+
+$$
+i \approx \frac{0.5}{10^9} = 0.5\,\text{nA}
+$$
+
+### Reverse Clamp
+
+```spice
+Reverse clamp
+V1 in 0 -6
+D1 in 0 clamp
+.model clamp D(Ron=2 Roff=1e9 Vfwd=1 Vrev=2 Rrev=4)
+.op
+.save @D1[i]
+.end
+```
+
+The expected reverse current is approximately:
+
+$$
+i \approx \frac{-6 + 2}{4} = -1\,\text{A}
+$$
+
+### Series And Parallel Multipliers
+
+```spice
+Series and parallel ideal diode
+V1 in 0 3
+D1 in 0 did M=2 N=2
+.model did D(Ron=2 Roff=1e9 Vfwd=1)
+.op
+.save @D1[i]
+.end
+```
+
+The local diode voltage is:
+
+$$
+v_{\text{local}} = \frac{3}{2} = 1.5\,\text{V}
+$$
+
+The local current through one diode cell is:
+
+$$
+i_{\text{local}} = \frac{1.5 - 1}{2} = 0.25\,\text{A}
+$$
+
+With `M=2`, total current is:
+
+$$
+I_{\text{total}} = 2 \cdot 0.25 = 0.5\,\text{A}
+$$
+
+### Current-Limited Clamp
+
+```spice
+Current-limited clamp
+V1 in 0 5
+R1 in out 10
+D1 out 0 did
+.model did D(Ron=0.1 Roff=1e9 Vfwd=0.7 Ilimit=10 Epsilon=10m)
+.op
+.save V(out) @D1[i] @D1[gd]
+.end
+```
+
+The 10 ohm resistor keeps the operating current far below the 10 A limit, so
+the limiter is almost inactive in this operating point. Once the diode is above
+the smoothed transition region, the approximate equations are:
+
+$$
+\begin{aligned}
+I &\approx \frac{5 - V(\text{out})}{10} \\
+I &\approx \frac{V(\text{out}) - 0.7}{0.1}
+\end{aligned}
+$$
+
+Solving them gives:
+
+$$
+\begin{aligned}
+I &\approx 0.426\,\text{A} \\
+V(\text{out}) &\approx 0.743\,\text{V}
+\end{aligned}
+$$
+
+If the surrounding circuit tried to drive much more current, the forward
+limiter would use:
+
+$$
+I = 10 \cdot \tanh\left(\frac{(v - 0.7) / 0.1}{10}\right)
+$$
+
+## LTspice Golden Comparison
+
+The test project includes an optional LTspice-backed golden comparison for the
+ideal diode. It is skipped by default so normal test runs do not require LTspice
+to be installed.
+
+To enable it, set `LTSPICE_EXE` to the LTspice executable path and run the
+focused ideal-diode tests:
+
+```powershell
+$env:LTSPICE_EXE = "C:\Program Files\ADI\LTspice\LTspice.exe"
+dotnet test .\src\SpiceSharpParser.Tests\SpiceSharpParser.Tests.csproj --filter FullyQualifiedName~IdealDiode
+```
+
+The golden test generates temporary LTspice `.cir` files, runs LTspice in batch
+ASCII mode, parses the LTspice `.raw` output, and compares the results against
+the custom `IdealDiode` implementation.
+
+The external golden suite covers:
+
+| Analysis | Comparison |
+|----------|------------|
+| `.DC` | Sweeps diode voltage and compares `I(D1)` for each sampled point. |
+| `.AC` | Compares complex small-signal voltages, including nonlinear derivative cases around `Ilimit`, `Epsilon`, `RevEpsilon`, reverse breakdown, and finite `Roff` smoothing. |
+| `.TRAN` | Runs a sinusoidal bridge rectifier and compares the rectified waveform against equivalent operating-point samples. |
+
+The case matrix covers:
+
+| Case | Parameters exercised |
+|------|----------------------|
+| Forward and off regions | `Ron`, `Roff`, `Vfwd` |
+| Reverse breakdown | `Vrev`, `Rrev` |
+| Current limiting | `Ilimit`, `RevIlimit` |
+| Transition smoothing | `Epsilon`, `RevEpsilon`, including finite-`Roff` ramps |
+| Scaling and ignored shared syntax | `area`, `M`, `N`, `Rs`, `off` |
+
+## Unsupported Or Ignored Parameters
+
+When an ideal diode model is selected, the custom parser keeps the ideal-diode parameters and ignores classic diode model parameters such as:
+
+```text
+Is, Tnom, N, Tt, Cjo, Cj0, Vj, M, Eg, Xti, Fc, BV, IBV, Kf, Af
+```
+
+The instance parameters `temp` and `ic` are ignored for the ideal diode. Unsupported ideal diode instance parameters produce validation errors.
+
+Metadata-style LTspice parameters such as `mfg`, `pn`, `description`, and ratings such as `irms` or `ipk` are ignored.
+
+## Limitations
+
+- The component is memoryless: it does not model junction capacitance or charge storage.
+- It does not provide noise behavior.
+- It does not model temperature-dependent semiconductor behavior.
+- Classic diode parameters such as `Is`, `Cjo`, `Tt`, and `BV` are ignored when an ideal diode model is selected.
+- `UseCustomComponents()` is required. Without it, the core parser treats LTspice ideal diode parameters as unsupported in LTspice compatibility mode.
+
+## Troubleshooting
+
+| Symptom | Likely cause | Fix |
+|---------|--------------|-----|
+| `Ron` or `Vfwd` is reported as unsupported | Custom mappings are not enabled | Call `reader.Settings.UseCustomComponents()` before `Read()`. |
+| A classic diode unexpectedly behaves like an ideal diode | The model contains at least one ideal diode parameter | Remove `Ron`, `Roff`, `Vfwd`, `Vrev`, `Rrev`, `Ilimit`, `RevIlimit`, `Epsilon`, or `RevEpsilon` from the model. |
+| `N` does not behave like emission coefficient | In the ideal diode, `N` is the series multiplier | Use a classic diode model for emission-coefficient behavior. |
+| A binned model sweep does not affect the selected diode | The sweep target does not match the requested or selected model name | Use the base model name, such as `did(Ron)`, or the selected suffixed name, such as `did.fast(Ron)`. |
+| AC result has no diode capacitance effect | The ideal diode has no capacitance model | Add explicit capacitors or use a classic diode model. |
+| DC or transient convergence is rough near switching | The transition is too sharp | Add `Epsilon` or `RevEpsilon`, or reduce extreme `Ron`/`Roff` ratios. |
+
+## Complete Example
+
+```spice
+Ideal diode clamp
+V1 in 0 5
+R1 in out 10
+D1 out 0 did
+.model did D(Ron=0.1 Roff=1e9 Vfwd=0.7 Ilimit=10)
+.op
+.save V(out) @D1[i]
+.end
+```
+
+This uses the same current law:
+
+$$
+\begin{aligned}
+g_{\text{on}} &= \frac{1}{0.1} = 10\,\text{S} \\
+g_{\text{off}} &= \frac{1}{10^9} = 1\,\text{nS} \\
+i_{\text{on}}(v) &= 10 \cdot (v - 0.7) \\
+i_{\text{off}}(v) &= 10^{-9} \cdot v
+\end{aligned}
+$$
+
+Because `Roff` is so large, the forward boundary is approximately:
+
+$$
+v_{\text{fwd,boundary}} \approx 0.7\,\text{V}
+$$
+
+With the 10 ohm source resistor, the operating point is approximately:
+
+$$
+\begin{aligned}
+I &\approx \frac{5 - V(\text{out})}{10} \\
+I &\approx \frac{V(\text{out}) - 0.7}{0.1} \\
+I &\approx 0.426\,\text{A} \\
+V(\text{out}) &\approx 0.743\,\text{V}
+\end{aligned}
+$$
+
+The `Ilimit=10` parameter is still part of the model. It matters when the
+surrounding circuit tries to push the pre-limit forward current toward 10 A:
+
+$$
+i_{\text{limited}} = 10 \cdot \tanh\left(\frac{i_0}{10}\right)
+$$
diff --git a/src/docs/index.md b/src/docs/index.md
index d3a722ce..e83acfd1 100644
--- a/src/docs/index.md
+++ b/src/docs/index.md
@@ -17,7 +17,7 @@ Browse the documentation by category:
- **Parameters**: [.PARAM](articles/param.md), [.FUNC](articles/func.md), [.LET](articles/let.md), [.SPARAM](articles/sparam.md)
- **Structure**: [.SUBCKT](articles/subckt.md), [X (Subcircuit Instance)](articles/subcircuit-instance.md), [.INCLUDE](articles/include.md), [.LIB](articles/lib.md), [.GLOBAL](articles/global.md), [.APPENDMODEL](articles/appendmodel.md)
- **Control**: [.STEP](articles/step.md), [.MC](articles/mc.md), [.TEMP](articles/temp.md), [.OPTIONS](articles/options.md), [.IC](articles/ic.md), [.NODESET](articles/nodeset.md), [.ST](articles/st.md), [.IF](articles/if.md), [.DISTRIBUTION](articles/distribution.md)
-- **Devices**: [R](articles/resistor.md), [C](articles/capacitor.md), [L](articles/inductor.md), [K (Mutual Inductance)](articles/mutual-inductance.md), [D](articles/diode.md), [Q](articles/bjt.md), [J](articles/jfet.md), [M](articles/mosfet.md), [V](articles/voltage-source.md), [I](articles/current-source.md), [B](articles/behavioral-source.md), [E (VCVS)](articles/vcvs.md), [F (CCCS)](articles/cccs.md), [G (VCCS)](articles/vccs.md), [H (CCVS)](articles/ccvs.md), [Laplace Transform Basics](articles/laplace-basics.md), [LAPLACE Transfer Sources](articles/laplace.md), [S (Voltage Switch)](articles/voltage-switch.md), [W (Current Switch)](articles/current-switch.md), [T (Transmission Line)](articles/transmission-line.md)
+- **Devices**: [R](articles/resistor.md), [C](articles/capacitor.md), [L](articles/inductor.md), [K (Mutual Inductance)](articles/mutual-inductance.md), [D](articles/diode.md), [LTspice-Style Ideal Diode](articles/ideal-diode.md), [Q](articles/bjt.md), [J](articles/jfet.md), [M](articles/mosfet.md), [V](articles/voltage-source.md), [I](articles/current-source.md), [B](articles/behavioral-source.md), [E (VCVS)](articles/vcvs.md), [F (CCCS)](articles/cccs.md), [G (VCCS)](articles/vccs.md), [H (CCVS)](articles/ccvs.md), [Laplace Transform Basics](articles/laplace-basics.md), [LAPLACE Transfer Sources](articles/laplace.md), [S (Voltage Switch)](articles/voltage-switch.md), [W (Current Switch)](articles/current-switch.md), [T (Transmission Line)](articles/transmission-line.md)
## API Reference