Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions roadmap/ltspice-compatibility-matrix.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
---
title: LTspice Compatibility Matrix
status: P3 Baseline + .FOUR Support Refresh + Custom Passives + Ideal Diodes + Golden Evidence
status: P3 Baseline + .FOUR Support Refresh + Custom Passives + Ideal Diodes + Golden Evidence + Include/Lib Path Evidence
scope: SpiceSharpParser + SpiceSharp
last_reviewed: 2026-07-02
last_reviewed: 2026-07-04
---

# LTspice Compatibility Matrix
Expand Down Expand Up @@ -31,10 +31,12 @@ Compatibility classes:
| Function-style `LAPLACE(input, transfer)` | Accepted | Lowered through Laplace source/helper path | OP/AC/TRAN covered elsewhere | Analytic fixture | None expected | Current parser/runtime | Existing Laplace tests plus LTspice P0 runnable fixture. |
| `.param` and `.func` baseline expressions | Accepted | Evaluation context functions/parameters | OP supported | Analytic fixture | None expected | Current parser/runtime | LTspice-specific scoping parity is not claimed. |
| `.tran <step> <stop>` with `.save` and `.meas` | Accepted | Produces transient simulation, exports, measurements | TRAN supported | Smoke/analytic fixture | None expected | Current parser/runtime | LTspice one-argument `.tran` is a separate gap. |
| Quoted `.include` | Accepted | Include processor inserts file content | OP supported | Smoke fixture | None expected | Current parser/runtime | Synthetic local fixture only. |
| Relative `.include` under working directory | Accepted | Include processor resolves relative path | OP supported | Smoke fixture | None expected | Current parser/runtime | Synthetic local fixture only. |
| Quoted `.include` | Accepted | Include processor inserts file content | OP supported | Smoke fixture | None expected | Current parser/runtime | Synthetic local fixture only; P1 evidence includes quoted Windows-style paths. |
| Relative `.include` under working directory | Accepted | Include processor resolves relative path | OP supported | Smoke fixture | None expected | Current parser/runtime | Synthetic local fixture only; P1 evidence includes slash-separated paths. |
| Nested relative `.include` | Accepted | Include processor resolves nested paths relative to the including file | OP supported | Smoke fixture | None expected | Current parser/runtime | Synthetic local fixture only; P1 evidence covers an included file that includes a sibling subdirectory file. |
| One-argument `.lib <file>` | Accepted | Lib processor inserts file content | OP supported | Smoke fixture | None expected | Current parser/runtime | Synthetic local fixture only. |
| Selected-section `.lib <file> <section>` | Accepted | Lib processor selects section | OP supported | Smoke fixture | None expected | Current parser/runtime | Synthetic local fixture only. |
| Selected-section `.lib <file> <section>` | Accepted | Lib processor selects section | OP supported | Smoke fixture | None expected | Current parser/runtime | Synthetic local fixture only; P1 evidence includes quoted Windows-style library paths. |
| Nested selected `.lib` relative paths | Accepted | Lib processor resolves nested selected libraries and their includes relative to the parent library file | OP supported | Smoke fixture | None expected | Current parser/runtime | Synthetic local fixture only; P1 evidence covers `.lib "vendor\\outer.lib" selected` selecting an inner library under the outer library directory. |
| `.backanno` | Accepted as control | Default: rejected. LTspice: warning no-op. | LTspice OP smoke supported | Smoke fixture | Default: targeted error. LTspice: warning names `.backanno`. | Current parser/runtime | Generated annotation metadata is not used by SpiceSharpParser. |
| `.tf` | Accepted as control | Rejected | Not runnable | None | Targeted unsupported LTspice diagnostic | Current parser/runtime | Possible future small-signal feature. |
| `.four` | Accepted as control | Creates transient Fourier post-processing for `.TRAN` | TRAN supported | Analytic integration/unit evidence | Targeted diagnostics for missing `.TRAN`, missing signal, invalid frequency, too-short transient, and missing signal export | Current parser/runtime | Results are exposed through `model.FourierAnalyses`; one result is produced per signal and per stepped transient simulation. Evidence: `FourTests`, `FourierAnalysisCalculatorTests`, README `FourierAnalyses`. |
Expand Down
6 changes: 4 additions & 2 deletions roadmap/ltspice-netlist-compatibility-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: LTspice Netlist Compatibility Plan
status: Draft / Roadmap
scope: SpiceSharpParser + SpiceSharp
last_reviewed: 2026-07-02
last_reviewed: 2026-07-04
---

# LTspice Netlist Compatibility Plan
Expand Down Expand Up @@ -34,6 +34,7 @@ This roadmap is intentionally parser-first and evidence-first:
- `.FOUR` transient Fourier post-processing is implemented as dialect-neutral output support. Results are exposed through `SpiceSharpModel.FourierAnalyses`, with coverage for multiple signals, current signals, parameterized frequencies, stepped transient runs, and targeted failure diagnostics.
- Default reader behavior still rejects LTspice `.backanno`, `.tf`, `.net`, `.ferret`, `.loadbias`, `.savebias`, and `.machine` / `.endmachine` with targeted diagnostics.
- In LTspice mode, `.backanno` is a warning no-op. The other known unsupported LTspice controls remain targeted errors.
- Include/lib path evidence covers quoted paths, Windows and slash separators, nested `.include` resolution relative to the including file, selected `.lib` sections, and nested selected `.lib` sections resolved relative to the parent library file.
- `.TRAN` accepts traditional numeric forms and trailing `UIC`; P1 LTspice mode also accepts `.tran <Tstop>` and `.tran <Tstop> UIC` by deriving `step = Tstop / 50.0`. LTspice `startup`, `steady`, `nodiscard`, and `step` modifiers remain targeted errors.
- LTspice output/viewer `.options` such as `plotwinsize`, `plotreltol`, `plotvntol`, `plotabstol`, `numdgt`, `measdgt`, `meascplxfmt`, `baudrate`, and `fastaccess` are warning no-ops only in LTspice mode.
- LTspice behavior-changing `.options` such as `cshunt`, `gshunt`, `srcsteps`, `gminsteps`, `trtol`, `chgtol`, `pivrel`, `pivtol`, and `ptrantau` remain targeted errors.
Expand Down Expand Up @@ -184,12 +185,13 @@ Implemented P1 behavior:
- Added a recognized-no-op control path for `.backanno` in LTspice mode.
- Kept targeted unsupported diagnostics for `.tf`, `.net`, `.ferret`, `.loadbias`, `.savebias`, and `.machine` / `.endmachine`.
- Added option classification tables for warning no-ops and behavior-changing unsupported LTspice options.
- Added include/lib path fixtures for quoted paths, Windows and slash separators, nested includes, selected library sections, and nested selected library sections.
- Lowered LTspice-mode `.tran <Tstop>` and `.tran <Tstop> UIC` to an explicit compatibility policy: `step = Tstop / 50.0`, with `maxStep = step`.
- Classified LTspice `.tran` modifiers: `UIC` is supported, while `startup`, `steady`, `nodiscard`, and `step` produce targeted diagnostics.

Remaining follow-up:

- Tighten `.include` / `.lib` fixtures for nested includes, Windows separators, and additional relative-path variants before changing path behavior.
- Add new `.include` / `.lib` path fixtures only when real-world decks expose additional variants or diagnostics gaps.

Acceptance criteria:

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.IO;
using System.Linq;
using System.Text;
using SpiceSharp.Simulations;
Expand Down Expand Up @@ -209,13 +210,166 @@ public void When_UnsupportedLtspiceTranModifierIsRead_Expect_TargetedError(strin
AssertErrorContains(model.ValidationResult, modifier);
}

[Theory]
[InlineData(".include \"vendor\\models\\load.inc\"")]
[InlineData(".include vendor/models/load.inc")]
public void When_LtspiceIncludePathUsesQuotesOrSeparators_Expect_IncludedElement(string includeStatement)
{
string tempDirectory = CreateTempDirectory();

try
{
string modelDirectory = Path.Combine(tempDirectory, "vendor", "models");
Directory.CreateDirectory(modelDirectory);
File.WriteAllText(Path.Combine(modelDirectory, "load.inc"), "RLOAD out 0 1k" + Environment.NewLine);

var model = GetSpiceSharpModelWithCompatibilityAndWorkingDirectory(
tempDirectory,
CompatibilityOptions.LTspice,
"LTspice P1 - include path fixture",
"VIN out 0 1",
includeStatement,
".op",
".save V(out)",
".end");

AssertNoValidationIssues(model.ValidationResult);
Assert.NotNull(model.Circuit["RLOAD"]);
Assert.True(EqualsWithTol(1.0, RunOpSimulation(model, "V(out)")));
}
finally
{
DeleteDirectory(tempDirectory);
}
}

[Fact]
public void When_LtspiceNestedIncludeUsesIncludedFileDirectory_Expect_IncludedElement()
{
string tempDirectory = CreateTempDirectory();

try
{
string vendorDirectory = Path.Combine(tempDirectory, "vendor");
string partsDirectory = Path.Combine(vendorDirectory, "parts");
Directory.CreateDirectory(partsDirectory);
File.WriteAllText(Path.Combine(vendorDirectory, "top.inc"), ".include \"parts\\load.inc\"" + Environment.NewLine);
File.WriteAllText(Path.Combine(partsDirectory, "load.inc"), "RLOAD out 0 1k" + Environment.NewLine);

var model = GetSpiceSharpModelWithCompatibilityAndWorkingDirectory(
tempDirectory,
CompatibilityOptions.LTspice,
"LTspice P1 - nested include path fixture",
"VIN out 0 1",
".include \"vendor/top.inc\"",
".op",
".save V(out)",
".end");

AssertNoValidationIssues(model.ValidationResult);
Assert.NotNull(model.Circuit["RLOAD"]);
Assert.True(EqualsWithTol(1.0, RunOpSimulation(model, "V(out)")));
}
finally
{
DeleteDirectory(tempDirectory);
}
}

[Fact]
public void When_LtspiceSelectedLibUsesQuotedWindowsPath_Expect_SelectedContent()
{
string tempDirectory = CreateTempDirectory();

try
{
string vendorDirectory = Path.Combine(tempDirectory, "vendor");
Directory.CreateDirectory(vendorDirectory);
File.WriteAllText(
Path.Combine(vendorDirectory, "loads.lib"),
".lib fast" + Environment.NewLine
+ "RFAST out 0 10k" + Environment.NewLine
+ ".endl" + Environment.NewLine
+ ".lib slow" + Environment.NewLine
+ "RSLOW out 0 1k" + Environment.NewLine
+ ".endl" + Environment.NewLine);

var model = GetSpiceSharpModelWithCompatibilityAndWorkingDirectory(
tempDirectory,
CompatibilityOptions.LTspice,
"LTspice P1 - selected lib path fixture",
"VIN out 0 1",
".lib \"vendor\\loads.lib\" slow",
".op",
".save V(out)",
".end");

AssertNoValidationIssues(model.ValidationResult);
Assert.NotNull(model.Circuit["RSLOW"]);
Assert.True(EqualsWithTol(1.0, RunOpSimulation(model, "V(out)")));
}
finally
{
DeleteDirectory(tempDirectory);
}
}

[Fact]
public void When_LtspiceNestedSelectedLibUsesParentLibDirectory_Expect_SelectedContent()
{
string tempDirectory = CreateTempDirectory();

try
{
string vendorDirectory = Path.Combine(tempDirectory, "vendor");
string innerDirectory = Path.Combine(vendorDirectory, "inner");
string modelDirectory = Path.Combine(innerDirectory, "models");
Directory.CreateDirectory(modelDirectory);
File.WriteAllText(
Path.Combine(vendorDirectory, "outer.lib"),
".lib selected" + Environment.NewLine
+ ".lib \"inner\\loads.lib\" load" + Environment.NewLine
+ ".endl" + Environment.NewLine);
File.WriteAllText(
Path.Combine(innerDirectory, "loads.lib"),
".lib load" + Environment.NewLine
+ ".include \"models\\load.inc\"" + Environment.NewLine
+ ".endl" + Environment.NewLine);
File.WriteAllText(Path.Combine(modelDirectory, "load.inc"), "RLOAD out 0 1k" + Environment.NewLine);

var model = GetSpiceSharpModelWithCompatibilityAndWorkingDirectory(
tempDirectory,
CompatibilityOptions.LTspice,
"LTspice P1 - nested selected lib path fixture",
"VIN out 0 1",
".lib \"vendor\\outer.lib\" selected",
".op",
".save V(out)",
".end");

AssertNoValidationIssues(model.ValidationResult);
Assert.NotNull(model.Circuit["RLOAD"]);
Assert.True(EqualsWithTol(1.0, RunOpSimulation(model, "V(out)")));
}
finally
{
DeleteDirectory(tempDirectory);
}
}

private static SpiceSharpModel GetSpiceSharpModelWithCompatibility(CompatibilityOptions compatibility, params string[] lines)
{
return GetSpiceSharpModelWithCompatibilityAndWorkingDirectory(null, compatibility, lines);
}

private static SpiceSharpModel GetSpiceSharpModelWithCompatibilityAndWorkingDirectory(string workingDirectory, CompatibilityOptions compatibility, params string[] lines)
{
var text = string.Join(Environment.NewLine, lines);
var parser = new SpiceNetlistParser();

parser.Settings.Lexing.HasTitle = true;
parser.Settings.Parsing.IsEndRequired = true;
parser.Settings.WorkingDirectory = workingDirectory;
parser.Settings.Compatibility = compatibility;

var parserResult = parser.ParseNetlist(text);
Expand All @@ -232,6 +386,21 @@ private static SpiceSharpModel GetSpiceSharpModelWithCompatibility(Compatibility
return spiceSharpReader.Read(parserResult.FinalModel);
}

private static string CreateTempDirectory()
{
string tempDirectory = Path.Combine(Path.GetTempPath(), "SpiceSharpParserLtspiceP1_" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(tempDirectory);
return tempDirectory;
}

private static void DeleteDirectory(string tempDirectory)
{
if (Directory.Exists(tempDirectory))
{
Directory.Delete(tempDirectory, true);
}
}

private static void AssertNoErrors(ValidationEntryCollection validation)
{
string messages = string.Join(Environment.NewLine, validation.Select(entry => entry.Message));
Expand Down
4 changes: 2 additions & 2 deletions src/SpiceSharpParser/Common/Processors/IncludesProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ public Statements Process(Statements statements)
/// </summary>
/// <param name="statements">Statements.</param>
/// <param name="currentDirectoryPath">Current directory path.</param>
protected Statements Process(Statements statements, string currentDirectoryPath)
public Statements Process(Statements statements, string currentDirectoryPath)
{
var subCircuits = statements.OfType<SubCircuit>().ToList();

Expand Down Expand Up @@ -172,4 +172,4 @@ private void ReadSingleInclude(Statements statements, string currentDirectoryPat
}
}
}
}
}
51 changes: 42 additions & 9 deletions src/SpiceSharpParser/Common/Processors/LibProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,12 @@
return Process(statements, InitialDirectoryPath);
}

private static void ReadSingleLibWithOneArgument(Statements statements, Control lib, List<Statement> allStatements)
private static Statements GetSingleLibStatementsWithOneArgument(List<Statement> allStatements)
{
var libStatements = allStatements;
statements.Replace(lib, libStatements);
return ToStatements(allStatements);
}

private static void ReadSingleLibWithTwoArguments(Statements statements, Control lib, List<Statement> allStatements)
private static Statements GetSingleLibStatementsWithTwoArguments(Control lib, List<Statement> allStatements)
{
// Find lib by entry
var libEntry = allStatements.SingleOrDefault(s => s is Control c && c.Name == "lib" && c.Parameters.Get(0).Value == lib.Parameters.Get(1).Value);
Expand All @@ -114,9 +113,22 @@
else
{
var libStatements = allStatements.Skip(libPosition + 1).Take(position - libPosition - 1);
statements.Replace(lib, libStatements);
return ToStatements(libStatements);
}
}

return null;

Check warning on line 120 in src/SpiceSharpParser/Common/Processors/LibProcessor.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Return an empty collection instead of null.

See more on https://sonarcloud.io/project/issues?id=SpiceSharp_SpiceSharpParser&issues=AZ8tw_Yp_PZEko836soz&open=AZ8tw_Yp_PZEko836soz&pullRequest=186
}

private static Statements ToStatements(IEnumerable<Statement> statements)
{
var result = new Statements();
foreach (var statement in statements)
{
result.Add(statement);
}

return result;
}

/// <summary>
Expand All @@ -130,13 +142,25 @@

while (libFound)
{
IncludesPreprocessor.Process(statements);
ProcessIncludes(statements, currentDirectoryPath);
libFound = ReadLibs(statements, currentDirectoryPath);
}

return statements;
}

private void ProcessIncludes(Statements statements, string currentDirectoryPath)
{
if (IncludesPreprocessor is IncludesProcessor includesProcessor)
{
includesProcessor.Process(statements, currentDirectoryPath);
}
else
{
IncludesPreprocessor.Process(statements);
}
}

private bool ReadLibs(Statements statements, string currentDirectoryPath)
{
bool result = false;
Expand Down Expand Up @@ -211,14 +235,23 @@
SpiceNetlist includeModel = SpiceNetlistParser.Parse(tokens);

var allStatements = includeModel.Statements.ToList();
Statements libStatements = null;

if (lib.Parameters.Count == 2)
{
ReadSingleLibWithTwoArguments(statements, lib, allStatements);
libStatements = GetSingleLibStatementsWithTwoArguments(lib, allStatements);
}
else if (lib.Parameters.Count == 1)
{
ReadSingleLibWithOneArgument(statements, lib, allStatements);
libStatements = GetSingleLibStatementsWithOneArgument(allStatements);
}

if (libStatements != null)
{
var libDirectory = Path.GetDirectoryName(libFullPath);
ProcessIncludes(libStatements, libDirectory);
Process(libStatements, libDirectory);
statements.Replace(lib, libStatements);
}
}
else
Expand All @@ -230,4 +263,4 @@
}
}
}
}
}
Loading