Skip to content

Commit 9eade47

Browse files
LAPLACE Phase3
1 parent c0a0f5a commit 9eade47

11 files changed

Lines changed: 1094 additions & 27 deletions

File tree

roadmap/laplace-pspice-feasibility.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,13 @@ This approach keeps the implementation small, aligns with existing source-genera
6868

6969
## Current Status
7070

71-
`LAPLACE` source support is not currently implemented by SpiceSharpParser.
71+
Canonical `E`-source `LAPLACE` support is implemented by SpiceSharpParser. `G` source mapping remains Phase 4.
7272

7373
Phase 1 grammar groundwork is implemented: expression-to-expression assignments such as `{V(in)} = {1/(1+s*tau)}` and `{V(in1,in2)} = {1/(1+s*tau)}` are preserved as `ExpressionAssignmentParameter`.
7474

75-
Phase 2 transfer-function math is implemented: internal polynomial, rational-polynomial, transfer-function, and expression-parser types convert rational expressions in `s` into ascending numerator / denominator coefficients with focused validation and broad unit coverage. Source-reader mapping, validation-entry diagnostics, and simulation support remain later-phase work.
75+
Phase 2 transfer-function math is implemented: internal polynomial, rational-polynomial, transfer-function, and expression-parser types convert rational expressions in `s` into ascending numerator / denominator coefficients with focused validation and broad unit coverage.
76+
77+
Phase 3 `E`-source mapping is implemented: canonical `Ename out+ out- LAPLACE {V(ctrl)}` `= {H(s)}` and `V(ctrl+,ctrl-)` forms map to `LaplaceVoltageControlledVoltageSource`, produce reader validation errors for semantic failures, and have OP / AC integration coverage. `G` mapping remains Phase 4.
7678

7779
Adjacent features already exist:
7880

@@ -651,6 +653,8 @@ Status: implemented.
651653

652654
### Phase 3: `E` Source Mapping
653655

656+
Status: implemented for canonical `E` source syntax. `G` mapping remains Phase 4.
657+
654658
1. Add `LaplaceSourceParser` and source definition models.
655659
2. Add `CanonicalExpressionAssignmentRecognizer` as the first syntax recognizer.
656660
3. Detect `LAPLACE` in `VoltageSourceGenerator.CreateCustomVoltageSource(...)`.
Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using SpiceSharp.Components;
5+
using SpiceSharpParser.ModelReaders.Netlist.Spice;
6+
using Xunit;
7+
8+
namespace SpiceSharpParser.IntegrationTests.AnalogBehavioralModeling
9+
{
10+
public class LaplaceTests : BaseTests
11+
{
12+
public static IEnumerable<object[]> InvalidLaplaceInputs
13+
{
14+
get
15+
{
16+
yield return new object[] { "V(a)-V(b)", "input expression" };
17+
yield return new object[] { "I(Vsense)", "input expression" };
18+
yield return new object[] { "V(a,b,c)", "input expression" };
19+
yield return new object[] { "V(a+1)", "input expression" };
20+
}
21+
}
22+
23+
public static IEnumerable<object[]> RejectedLaplaceTransfers
24+
{
25+
get
26+
{
27+
yield return new object[] { "1/s", "singular DC gain" };
28+
yield return new object[] { "s", "improper" };
29+
yield return new object[] { "sin(s)", "rational polynomial" };
30+
}
31+
}
32+
33+
public static IEnumerable<object[]> UnsupportedLaplaceOptions
34+
{
35+
get
36+
{
37+
yield return new object[] { "M=2", "multiplier" };
38+
yield return new object[] { "TD=1n", "delay" };
39+
yield return new object[] { "DELAY=1n", "delay" };
40+
}
41+
}
42+
43+
[Fact]
44+
public void When_ELaplaceLowPassRunsOp_Expect_DcGainNearOne()
45+
{
46+
var model = GetSpiceSharpModel(
47+
"E LAPLACE low-pass OP",
48+
".PARAM tau=1u",
49+
"VIN in 0 1",
50+
"ELOW out 0 LAPLACE {V(in)} = {1/(1+s*tau)}",
51+
"RLOAD out 0 1k",
52+
".OP",
53+
".SAVE V(out)",
54+
".END");
55+
56+
Assert.NotNull(model);
57+
Assert.False(model.ValidationResult.HasWarning);
58+
Assert.False(model.ValidationResult.HasError);
59+
Assert.IsType<LaplaceVoltageControlledVoltageSource>(model.Circuit["ELOW"]);
60+
61+
double export = RunOpSimulation(model, "V(out)");
62+
Assert.True(EqualsWithTol(1.0, export), $"Expected OP gain near 1, got {export}.");
63+
}
64+
65+
[Fact]
66+
public void When_ELaplaceLowPassUsesDifferentialInput_Expect_ControlNodeDifference()
67+
{
68+
var model = GetSpiceSharpModel(
69+
"E LAPLACE differential OP",
70+
".PARAM tau=1u",
71+
"VINP inp 0 2",
72+
"VINN inn 0 0.5",
73+
"ELOW out 0 LAPLACE {V(inp,inn)} = {1/(1+s*tau)}",
74+
"RLOAD out 0 1k",
75+
".OP",
76+
".SAVE V(out)",
77+
".END");
78+
79+
Assert.NotNull(model);
80+
Assert.False(model.ValidationResult.HasWarning);
81+
Assert.False(model.ValidationResult.HasError);
82+
83+
double export = RunOpSimulation(model, "V(out)");
84+
Assert.True(EqualsWithTol(1.5, export), $"Expected OP gain from differential input near 1.5, got {export}.");
85+
}
86+
87+
[Fact]
88+
public void When_ELaplaceLowPassRunsAcAtCutoff_Expect_ExpectedMagnitudeAndPhase()
89+
{
90+
var model = GetSpiceSharpModel(
91+
"E LAPLACE low-pass AC",
92+
".PARAM fc=1k",
93+
".PARAM wc={2*PI*fc}",
94+
"VIN in 0 AC 1",
95+
"ELOW out 0 LAPLACE {V(in)} = {wc/(s+wc)}",
96+
"RLOAD out 0 1k",
97+
".AC DEC 20 10 100k",
98+
".MEAS AC vm_fc FIND VM(out) AT=1k",
99+
".MEAS AC vp_fc FIND VP(out) AT=1k",
100+
".END");
101+
102+
Assert.NotNull(model);
103+
Assert.False(model.ValidationResult.HasWarning);
104+
Assert.False(model.ValidationResult.HasError);
105+
106+
RunSimulations(model);
107+
108+
AssertMeasurementSuccess(model, "vm_fc");
109+
AssertMeasurementSuccess(model, "vp_fc");
110+
double magnitude = model.Measurements["vm_fc"][0].Value;
111+
double phase = model.Measurements["vp_fc"][0].Value;
112+
Assert.True(Math.Abs(magnitude - (1.0 / Math.Sqrt(2.0))) < 0.03, $"Expected low-pass cutoff magnitude near 0.707, got {magnitude}.");
113+
Assert.True(Math.Abs(phase - (-Math.PI / 4.0)) < 0.08, $"Expected low-pass cutoff phase near -pi/4, got {phase}.");
114+
}
115+
116+
[Fact]
117+
public void When_ELaplaceHighPassRunsAcAtCutoff_Expect_ExpectedMagnitudeAndPhase()
118+
{
119+
var model = GetSpiceSharpModel(
120+
"E LAPLACE high-pass AC",
121+
".PARAM fc=1k",
122+
".PARAM wc={2*PI*fc}",
123+
"VIN in 0 AC 1",
124+
"EHIGH out 0 LAPLACE {V(in)} = {s/(s+wc)}",
125+
"RLOAD out 0 1k",
126+
".AC DEC 20 10 100k",
127+
".MEAS AC vm_fc FIND VM(out) AT=1k",
128+
".MEAS AC vp_fc FIND VP(out) AT=1k",
129+
".END");
130+
131+
Assert.NotNull(model);
132+
Assert.False(model.ValidationResult.HasWarning);
133+
Assert.False(model.ValidationResult.HasError);
134+
135+
RunSimulations(model);
136+
137+
AssertMeasurementSuccess(model, "vm_fc");
138+
AssertMeasurementSuccess(model, "vp_fc");
139+
double magnitude = model.Measurements["vm_fc"][0].Value;
140+
double phase = model.Measurements["vp_fc"][0].Value;
141+
Assert.True(Math.Abs(magnitude - (1.0 / Math.Sqrt(2.0))) < 0.03, $"Expected high-pass cutoff magnitude near 0.707, got {magnitude}.");
142+
Assert.True(Math.Abs(phase - (Math.PI / 4.0)) < 0.08, $"Expected high-pass cutoff phase near pi/4, got {phase}.");
143+
}
144+
145+
[Fact]
146+
public void When_ELaplaceIsMixedWithExistingAbmSources_Expect_AllOutputs()
147+
{
148+
var model = GetSpiceSharpModel(
149+
"E LAPLACE mixed ABM OP",
150+
"VIN in 0 2",
151+
"EVALUE vout 0 VALUE = {V(in)+1}",
152+
"EPOLY pout 0 POLY(1) in 0 2 1",
153+
"ETABLE tout 0 TABLE {V(in)} = (0,0) (2,5) (4,9)",
154+
"ELAPLACE lout 0 LAPLACE {V(in)} = {1/(1+s*1u)}",
155+
"RV vout 0 1k",
156+
"RP pout 0 1k",
157+
"RT tout 0 1k",
158+
"RL lout 0 1k",
159+
".OP",
160+
".SAVE V(vout) V(pout) V(tout) V(lout)",
161+
".END");
162+
163+
AssertNoValidationErrors(model);
164+
165+
double[] exports = RunOpSimulation(model, "V(vout)", "V(pout)", "V(tout)", "V(lout)");
166+
Assert.True(EqualsWithTol(exports, new[] { 3.0, 4.0, 5.0, 2.0 }));
167+
}
168+
169+
[Theory]
170+
[MemberData(nameof(InvalidLaplaceInputs))]
171+
public void When_ELaplaceInputIsInvalid_Expect_ReaderValidationError(
172+
string inputExpression,
173+
string expectedMessage)
174+
{
175+
var model = ReadSingleLaplaceSource(inputExpression, "1/(1+s)");
176+
177+
AssertReaderErrorContains(model, expectedMessage);
178+
}
179+
180+
[Theory]
181+
[MemberData(nameof(RejectedLaplaceTransfers))]
182+
public void When_ELaplaceTransferIsRejected_Expect_ReaderValidationError(
183+
string transferExpression,
184+
string expectedMessage)
185+
{
186+
var model = ReadSingleLaplaceSource("V(in)", transferExpression);
187+
188+
AssertReaderErrorContains(model, expectedMessage);
189+
}
190+
191+
[Theory]
192+
[MemberData(nameof(UnsupportedLaplaceOptions))]
193+
public void When_ELaplaceUnsupportedOptionIsUsed_Expect_ReaderValidationError(
194+
string option,
195+
string expectedMessage)
196+
{
197+
var model = GetSpiceSharpModel(
198+
"E LAPLACE unsupported option",
199+
"VIN in 0 1",
200+
$"EBAD out 0 LAPLACE {{V(in)}} = {{1/(1+s)}} {option}",
201+
"RLOAD out 0 1k",
202+
".OP",
203+
".SAVE V(out)",
204+
".END");
205+
206+
AssertReaderErrorContains(model, expectedMessage);
207+
}
208+
209+
[Fact]
210+
public void When_GLaplaceIsUsed_Expect_ReaderValidationError()
211+
{
212+
var model = GetSpiceSharpModel(
213+
"G LAPLACE unsupported",
214+
"VIN in 0 1",
215+
"GBAD out 0 LAPLACE {V(in)} = {1/(1+s)}",
216+
"RLOAD out 0 1k",
217+
".OP",
218+
".SAVE V(out)",
219+
".END");
220+
221+
AssertReaderErrorContains(model, "G mapping remains unsupported");
222+
}
223+
224+
[Fact]
225+
public void When_HLaplaceIsUsed_Expect_ReaderValidationError()
226+
{
227+
var model = GetSpiceSharpModel(
228+
"H LAPLACE unsupported",
229+
"VIN in 0 1",
230+
"HBAD out 0 LAPLACE {V(in)} = {1/(1+s)}",
231+
"RLOAD out 0 1k",
232+
".OP",
233+
".SAVE V(out)",
234+
".END");
235+
236+
AssertReaderErrorContains(model, "only for E");
237+
}
238+
239+
private static SpiceSharpModel ReadSingleLaplaceSource(string inputExpression, string transferExpression)
240+
{
241+
return GetSpiceSharpModel(
242+
"E LAPLACE validation",
243+
"VIN in 0 1",
244+
$"EBAD out 0 LAPLACE {{{inputExpression}}} = {{{transferExpression}}}",
245+
"RLOAD out 0 1k",
246+
".OP",
247+
".SAVE V(out)",
248+
".END");
249+
}
250+
251+
private static void AssertNoValidationErrors(SpiceSharpModel model)
252+
{
253+
Assert.NotNull(model);
254+
Assert.False(model.ValidationResult.HasWarning);
255+
Assert.False(model.ValidationResult.HasError);
256+
}
257+
258+
private static void AssertReaderErrorContains(SpiceSharpModel model, string expectedMessage)
259+
{
260+
Assert.NotNull(model);
261+
Assert.False(model.ValidationResult.HasWarning);
262+
Assert.True(model.ValidationResult.HasError);
263+
var messages = string.Join(Environment.NewLine, model.ValidationResult.Errors.Select(error => error.Message));
264+
Assert.Contains(expectedMessage, messages, StringComparison.OrdinalIgnoreCase);
265+
}
266+
}
267+
}

0 commit comments

Comments
 (0)