diff --git a/roadmap/ltspice-compatibility-matrix.md b/roadmap/ltspice-compatibility-matrix.md index 395b95ec..002f9629 100644 --- a/roadmap/ltspice-compatibility-matrix.md +++ b/roadmap/ltspice-compatibility-matrix.md @@ -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 @@ -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 ` 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 ` | Accepted | Lib processor inserts file content | OP supported | Smoke fixture | None expected | Current parser/runtime | Synthetic local fixture only. | -| Selected-section `.lib
` | Accepted | Lib processor selects section | OP supported | Smoke fixture | None expected | Current parser/runtime | Synthetic local fixture only. | +| Selected-section `.lib
` | 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`. | diff --git a/roadmap/ltspice-netlist-compatibility-plan.md b/roadmap/ltspice-netlist-compatibility-plan.md index b93d3c68..fd3bc273 100644 --- a/roadmap/ltspice-netlist-compatibility-plan.md +++ b/roadmap/ltspice-netlist-compatibility-plan.md @@ -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 @@ -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 ` and `.tran UIC` by deriving `step = Tstop / 50.0`. LTspice `startup`, `steady`, `nodiscard`, and `step` modifiers remain targeted errors. - LTspice output/viewer `.options` such as `plotwinsize`, `plotreltol`, `plotvntol`, `plotabstol`, `numdgt`, `measdgt`, `meascplxfmt`, `baudrate`, and `fastaccess` are warning no-ops only in LTspice mode. - LTspice behavior-changing `.options` such as `cshunt`, `gshunt`, `srcsteps`, `gminsteps`, `trtol`, `chgtol`, `pivrel`, `pivtol`, and `ptrantau` remain targeted errors. @@ -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 ` and `.tran 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: diff --git a/src/SpiceSharpParser.IntegrationTests/LTspiceCompatibility/LTspiceCompatibilityP1Tests.cs b/src/SpiceSharpParser.IntegrationTests/LTspiceCompatibility/LTspiceCompatibilityP1Tests.cs index 2c6f07ab..b1f7e0e0 100644 --- a/src/SpiceSharpParser.IntegrationTests/LTspiceCompatibility/LTspiceCompatibilityP1Tests.cs +++ b/src/SpiceSharpParser.IntegrationTests/LTspiceCompatibility/LTspiceCompatibilityP1Tests.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using System.Linq; using System.Text; using SpiceSharp.Simulations; @@ -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); @@ -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)); diff --git a/src/SpiceSharpParser/Common/Processors/IncludesProcessor.cs b/src/SpiceSharpParser/Common/Processors/IncludesProcessor.cs index 799130bf..901c79cf 100644 --- a/src/SpiceSharpParser/Common/Processors/IncludesProcessor.cs +++ b/src/SpiceSharpParser/Common/Processors/IncludesProcessor.cs @@ -81,7 +81,7 @@ public Statements Process(Statements statements) /// /// Statements. /// Current directory path. - protected Statements Process(Statements statements, string currentDirectoryPath) + public Statements Process(Statements statements, string currentDirectoryPath) { var subCircuits = statements.OfType().ToList(); @@ -172,4 +172,4 @@ private void ReadSingleInclude(Statements statements, string currentDirectoryPat } } } -} \ No newline at end of file +} diff --git a/src/SpiceSharpParser/Common/Processors/LibProcessor.cs b/src/SpiceSharpParser/Common/Processors/LibProcessor.cs index 1bb95777..cd27570d 100644 --- a/src/SpiceSharpParser/Common/Processors/LibProcessor.cs +++ b/src/SpiceSharpParser/Common/Processors/LibProcessor.cs @@ -86,13 +86,12 @@ public Statements Process(Statements statements) return Process(statements, InitialDirectoryPath); } - private static void ReadSingleLibWithOneArgument(Statements statements, Control lib, List allStatements) + private static Statements GetSingleLibStatementsWithOneArgument(List allStatements) { - var libStatements = allStatements; - statements.Replace(lib, libStatements); + return ToStatements(allStatements); } - private static void ReadSingleLibWithTwoArguments(Statements statements, Control lib, List allStatements) + private static Statements GetSingleLibStatementsWithTwoArguments(Control lib, List 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); @@ -114,9 +113,22 @@ private static void ReadSingleLibWithTwoArguments(Statements statements, Control else { var libStatements = allStatements.Skip(libPosition + 1).Take(position - libPosition - 1); - statements.Replace(lib, libStatements); + return ToStatements(libStatements); } } + + return null; + } + + private static Statements ToStatements(IEnumerable statements) + { + var result = new Statements(); + foreach (var statement in statements) + { + result.Add(statement); + } + + return result; } /// @@ -130,13 +142,25 @@ private Statements Process(Statements statements, string currentDirectoryPath) 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; @@ -211,14 +235,23 @@ private void ReadSingleLib(Statements statements, string currentDirectoryPath, C 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 @@ -230,4 +263,4 @@ private void ReadSingleLib(Statements statements, string currentDirectoryPath, C } } } -} \ No newline at end of file +}