Skip to content

Commit 0d93062

Browse files
LAPLACE Phase4
1 parent 9eade47 commit 0d93062

10 files changed

Lines changed: 238 additions & 40 deletions

File tree

roadmap/laplace-pspice-feasibility.md

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

6969
## Current Status
7070

71-
Canonical `E`-source `LAPLACE` support is implemented by SpiceSharpParser. `G` source mapping remains Phase 4.
71+
Canonical `E`-source and `G`-source `LAPLACE` support is implemented by SpiceSharpParser. `F`, `H`, `B`, alternate syntaxes, delay, and `M=` support remain deferred.
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

7575
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.
7676

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.
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.
78+
79+
Phase 4 `G`-source mapping is implemented: canonical `Gname out+ out- LAPLACE {V(ctrl)}` `= {H(s)}` and `V(ctrl+,ctrl-)` forms map to `LaplaceVoltageControlledCurrentSource`, preserve the existing `G` current-source sign convention, reject unsupported `M=` / delay options, and have OP / AC integration coverage.
7880

7981
Adjacent features already exist:
8082

@@ -472,7 +474,7 @@ Prefer parsing with the existing expression parser and accepting only the expect
472474

473475
Do not silently ignore `M=`.
474476

475-
Recommended MVP behavior: reject `M=` on Laplace sources with a clear diagnostic until tests define the intended semantics. If support is added, multiply the numerator coefficients by `M` after evaluating `M` to a constant.
477+
Recommended MVP behavior: reject `M=` on Laplace sources with a clear diagnostic until tests define the intended semantics. `M=` is a multiplier for the effective source or device contribution, typically equivalent to multiple parallel instances or a scaled current/voltage contribution. If support is added for Laplace sources, multiply the transfer output by `M`, most likely by evaluating `M` to a finite constant and scaling the numerator coefficients.
476478

477479
### Delay Policy
478480

@@ -653,7 +655,7 @@ Status: implemented.
653655

654656
### Phase 3: `E` Source Mapping
655657

656-
Status: implemented for canonical `E` source syntax. `G` mapping remains Phase 4.
658+
Status: implemented for canonical `E` source syntax.
657659

658660
1. Add `LaplaceSourceParser` and source definition models.
659661
2. Add `CanonicalExpressionAssignmentRecognizer` as the first syntax recognizer.
@@ -665,6 +667,8 @@ Status: implemented for canonical `E` source syntax. `G` mapping remains Phase 4
665667

666668
### Phase 4: `G` Source Mapping
667669

670+
Status: implemented for canonical `G` source syntax. `M=`, delay options, `F`, and `H` remain deferred.
671+
668672
1. Detect `LAPLACE` in `CurrentSourceGenerator.CreateCustomCurrentSource(...)`.
669673
2. Reuse the same source parser and transfer-function builder.
670674
3. Map to `LaplaceVoltageControlledCurrentSource`.

src/SpiceSharpParser.IntegrationTests/AnalogBehavioralModeling/LaplaceTests.cs

Lines changed: 103 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,104 @@ public void When_ELaplaceHighPassRunsAcAtCutoff_Expect_ExpectedMagnitudeAndPhase
142142
Assert.True(Math.Abs(phase - (Math.PI / 4.0)) < 0.08, $"Expected high-pass cutoff phase near pi/4, got {phase}.");
143143
}
144144

145+
[Fact]
146+
public void When_GLaplaceLowPassRunsOp_Expect_LoadVoltageWithCurrentSourceSign()
147+
{
148+
var model = GetSpiceSharpModel(
149+
"G LAPLACE low-pass OP",
150+
".PARAM gm=1m",
151+
".PARAM tau=1u",
152+
"VIN in 0 1",
153+
"GLOW out 0 LAPLACE {V(in)} = {gm/(1+s*tau)}",
154+
"RLOAD out 0 1k",
155+
".OP",
156+
".SAVE V(out)",
157+
".END");
158+
159+
AssertNoValidationErrors(model);
160+
Assert.IsType<LaplaceVoltageControlledCurrentSource>(model.Circuit["GLOW"]);
161+
162+
double export = RunOpSimulation(model, "V(out)");
163+
Assert.True(EqualsWithTol(-1.0, export), $"Expected OP load voltage near -1, got {export}.");
164+
}
165+
166+
[Fact]
167+
public void When_GLaplaceLowPassUsesDifferentialInput_Expect_ControlNodeDifference()
168+
{
169+
var model = GetSpiceSharpModel(
170+
"G LAPLACE differential OP",
171+
".PARAM gm=1m",
172+
".PARAM tau=1u",
173+
"VINP inp 0 2",
174+
"VINN inn 0 0.5",
175+
"GLOW out 0 LAPLACE {V(inp,inn)} = {gm/(1+s*tau)}",
176+
"RLOAD out 0 1k",
177+
".OP",
178+
".SAVE V(out)",
179+
".END");
180+
181+
AssertNoValidationErrors(model);
182+
183+
double export = RunOpSimulation(model, "V(out)");
184+
Assert.True(EqualsWithTol(-1.5, export), $"Expected OP load voltage from differential input near -1.5, got {export}.");
185+
}
186+
187+
[Fact]
188+
public void When_GLaplaceLowPassRunsAcAtCutoff_Expect_ExpectedMagnitudeAndPhase()
189+
{
190+
var model = GetSpiceSharpModel(
191+
"G LAPLACE low-pass AC",
192+
".PARAM gm=1m",
193+
".PARAM fc=1k",
194+
".PARAM wc={2*PI*fc}",
195+
"VIN in 0 AC 1",
196+
"GLOW out 0 LAPLACE {V(in)} = {gm*wc/(s+wc)}",
197+
"RLOAD out 0 1k",
198+
".AC DEC 20 10 100k",
199+
".MEAS AC vm_fc FIND VM(out) AT=1k",
200+
".MEAS AC vp_fc FIND VP(out) AT=1k",
201+
".END");
202+
203+
AssertNoValidationErrors(model);
204+
205+
RunSimulations(model);
206+
207+
AssertMeasurementSuccess(model, "vm_fc");
208+
AssertMeasurementSuccess(model, "vp_fc");
209+
double magnitude = model.Measurements["vm_fc"][0].Value;
210+
double phase = model.Measurements["vp_fc"][0].Value;
211+
Assert.True(Math.Abs(magnitude - (1.0 / Math.Sqrt(2.0))) < 0.03, $"Expected G low-pass cutoff magnitude near 0.707, got {magnitude}.");
212+
Assert.True(Math.Abs(phase - (3.0 * Math.PI / 4.0)) < 0.08, $"Expected G low-pass cutoff phase near 3*pi/4, got {phase}.");
213+
}
214+
215+
[Fact]
216+
public void When_GLaplaceHighPassRunsAcAtCutoff_Expect_ExpectedMagnitudeAndPhase()
217+
{
218+
var model = GetSpiceSharpModel(
219+
"G LAPLACE high-pass AC",
220+
".PARAM gm=1m",
221+
".PARAM fc=1k",
222+
".PARAM wc={2*PI*fc}",
223+
"VIN in 0 AC 1",
224+
"GHIGH out 0 LAPLACE {V(in)} = {gm*s/(s+wc)}",
225+
"RLOAD out 0 1k",
226+
".AC DEC 20 10 100k",
227+
".MEAS AC vm_fc FIND VM(out) AT=1k",
228+
".MEAS AC vp_fc FIND VP(out) AT=1k",
229+
".END");
230+
231+
AssertNoValidationErrors(model);
232+
233+
RunSimulations(model);
234+
235+
AssertMeasurementSuccess(model, "vm_fc");
236+
AssertMeasurementSuccess(model, "vp_fc");
237+
double magnitude = model.Measurements["vm_fc"][0].Value;
238+
double phase = model.Measurements["vp_fc"][0].Value;
239+
Assert.True(Math.Abs(magnitude - (1.0 / Math.Sqrt(2.0))) < 0.03, $"Expected G high-pass cutoff magnitude near 0.707, got {magnitude}.");
240+
Assert.True(Math.Abs(phase - (-3.0 * Math.PI / 4.0)) < 0.08, $"Expected G high-pass cutoff phase near -3*pi/4, got {phase}.");
241+
}
242+
145243
[Fact]
146244
public void When_ELaplaceIsMixedWithExistingAbmSources_Expect_AllOutputs()
147245
{
@@ -207,18 +305,18 @@ public void When_ELaplaceUnsupportedOptionIsUsed_Expect_ReaderValidationError(
207305
}
208306

209307
[Fact]
210-
public void When_GLaplaceIsUsed_Expect_ReaderValidationError()
308+
public void When_GLaplaceUnsupportedMultiplierIsUsed_Expect_ReaderValidationError()
211309
{
212310
var model = GetSpiceSharpModel(
213-
"G LAPLACE unsupported",
311+
"G LAPLACE unsupported multiplier",
214312
"VIN in 0 1",
215-
"GBAD out 0 LAPLACE {V(in)} = {1/(1+s)}",
313+
"GBAD out 0 LAPLACE {V(in)} = {1/(1+s)} M=2",
216314
"RLOAD out 0 1k",
217315
".OP",
218316
".SAVE V(out)",
219317
".END");
220318

221-
AssertReaderErrorContains(model, "G mapping remains unsupported");
319+
AssertReaderErrorContains(model, "multiplier");
222320
}
223321

224322
[Fact]
@@ -233,7 +331,7 @@ public void When_HLaplaceIsUsed_Expect_ReaderValidationError()
233331
".SAVE V(out)",
234332
".END");
235333

236-
AssertReaderErrorContains(model, "only for E");
334+
AssertReaderErrorContains(model, "E and G");
237335
}
238336

239337
private static SpiceSharpModel ReadSingleLaplaceSource(string inputExpression, string transferExpression)

src/SpiceSharpParser.Tests/ModelReaders/Spice/Readers/EntityGenerators/Components/Sources/LaplaceSourceParserTests.cs

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ public void When_SingleEndedLaplaceSourceIsParsed_Expect_ControlNegativeGround()
6060
context.EvaluationContext.SetParameter("tau", 1e-6);
6161
var parser = new LaplaceSourceParser();
6262

63-
var definition = parser.ParseVoltageControlledVoltageSource(
63+
var definition = parser.ParseVoltageControlledSource(
6464
"E1",
6565
CreateLaplaceParameters("V(in)", "1/(1+s*tau)"),
6666
context);
@@ -82,7 +82,7 @@ public void When_DifferentialLaplaceSourceIsParsed_Expect_ControlNodeOrdering()
8282
var context = CreateReadingContext();
8383
var parser = new LaplaceSourceParser();
8484

85-
var definition = parser.ParseVoltageControlledVoltageSource(
85+
var definition = parser.ParseVoltageControlledSource(
8686
"E1",
8787
CreateLaplaceParameters("V(inp,inn)", "10/(1+s)"),
8888
context);
@@ -120,14 +120,39 @@ public void When_VoltageSourceGeneratorMapsLaplace_Expect_LaplaceEntityAndNodeOr
120120
Assert.Equal(0.0, laplace.Parameters.Delay);
121121
}
122122

123+
[Fact]
124+
public void When_CurrentSourceGeneratorMapsLaplace_Expect_LaplaceEntityAndNodeOrder()
125+
{
126+
var context = CreateReadingContext();
127+
ParameterCollection createdNodes = null;
128+
context.When(x => x.CreateNodes(Arg.Any<IComponent>(), Arg.Any<ParameterCollection>()))
129+
.Do(call => createdNodes = call.ArgAt<ParameterCollection>(1));
130+
var generator = new CurrentSourceGenerator();
131+
132+
var entity = generator.Generate(
133+
"G1",
134+
"G1",
135+
"g",
136+
CreateLaplaceParameters("V(inp,inn)", "1m*1000/(s+1000)"),
137+
context);
138+
139+
var laplace = Assert.IsType<LaplaceVoltageControlledCurrentSource>(entity);
140+
Assert.False(context.Result.ValidationResult.HasError);
141+
Assert.NotNull(createdNodes);
142+
Assert.Equal(new[] { "out", "0", "inp", "inn" }, createdNodes.Select(parameter => parameter.Value).ToArray());
143+
AssertCoefficients(new[] { 1.0 }, laplace.Parameters.Numerator);
144+
AssertCoefficients(new[] { 1000.0, 1.0 }, laplace.Parameters.Denominator);
145+
Assert.Equal(0.0, laplace.Parameters.Delay);
146+
}
147+
123148
[Theory]
124149
[MemberData(nameof(InvalidInputExpressions))]
125150
public void When_InputShapeIsUnsupported_Expect_ReaderValidationError(string inputExpression)
126151
{
127152
var context = CreateReadingContext();
128153
var parser = new LaplaceSourceParser();
129154

130-
var definition = parser.ParseVoltageControlledVoltageSource(
155+
var definition = parser.ParseVoltageControlledSource(
131156
"E1",
132157
CreateLaplaceParameters(inputExpression, "1/(1+s)"),
133158
context);
@@ -143,7 +168,7 @@ public void When_TransferIsUnsupported_Expect_ReaderValidationError(string trans
143168
var context = CreateReadingContext();
144169
var parser = new LaplaceSourceParser();
145170

146-
var definition = parser.ParseVoltageControlledVoltageSource(
171+
var definition = parser.ParseVoltageControlledSource(
147172
"E1",
148173
CreateLaplaceParameters("V(in)", transferExpression),
149174
context);
@@ -159,7 +184,7 @@ public void When_UnsupportedOptionIsPresent_Expect_ReaderValidationError(Paramet
159184
var context = CreateReadingContext();
160185
var parser = new LaplaceSourceParser();
161186

162-
var definition = parser.ParseVoltageControlledVoltageSource(
187+
var definition = parser.ParseVoltageControlledSource(
163188
"E1",
164189
CreateLaplaceParameters("V(in)", "1/(1+s)", option),
165190
context);
@@ -181,7 +206,7 @@ public void When_InputAssignmentIsMissing_Expect_ReaderValidationError()
181206
new ExpressionParameter("V(in)", null),
182207
};
183208

184-
var definition = parser.ParseVoltageControlledVoltageSource("E1", parameters, context);
209+
var definition = parser.ParseVoltageControlledSource("E1", parameters, context);
185210

186211
Assert.Null(definition);
187212
AssertSingleReaderError(context, "separated by '='");
@@ -199,7 +224,7 @@ public void When_InputExpressionIsMissing_Expect_ReaderValidationError()
199224
new WordParameter("LAPLACE"),
200225
};
201226

202-
var definition = parser.ParseVoltageControlledVoltageSource("E1", parameters, context);
227+
var definition = parser.ParseVoltageControlledSource("E1", parameters, context);
203228

204229
Assert.Null(definition);
205230
AssertSingleReaderError(context, "expects input expression");
@@ -219,24 +244,24 @@ public void When_HSourceUsesLaplace_Expect_UnsupportedReaderValidationError()
219244
context);
220245

221246
Assert.Null(entity);
222-
AssertSingleReaderError(context, "only for E");
247+
AssertSingleReaderError(context, "E and G");
223248
}
224249

225250
[Fact]
226-
public void When_GSourceUsesLaplace_Expect_UnsupportedReaderValidationError()
251+
public void When_FSourceUsesLaplace_Expect_UnsupportedReaderValidationError()
227252
{
228253
var context = CreateReadingContext();
229254
var generator = new CurrentSourceGenerator();
230255

231256
var entity = generator.Generate(
232-
"G1",
233-
"G1",
234-
"g",
257+
"F1",
258+
"F1",
259+
"f",
235260
CreateLaplaceParameters("V(in)", "1/(1+s)"),
236261
context);
237262

238263
Assert.Null(entity);
239-
AssertSingleReaderError(context, "G mapping remains unsupported");
264+
AssertSingleReaderError(context, "E and G");
240265
}
241266

242267
private static ParameterCollection CreateLaplaceParameters(

src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Components/Sources/CurrentSourceGenerator.cs

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -215,11 +215,22 @@ private IEntity CreateCustomCurrentSource(string name, ParameterCollection param
215215

216216
if (laplaceParser.IsLaplaceSource(parameters))
217217
{
218-
context.Result.ValidationResult.AddError(
219-
ValidationEntrySource.Reader,
220-
"laplace is currently supported only for E voltage-controlled voltage sources; G mapping remains unsupported",
221-
parameters[2].LineInfo);
222-
return null;
218+
if (!isVoltageControlled)
219+
{
220+
context.Result.ValidationResult.AddError(
221+
ValidationEntrySource.Reader,
222+
"laplace is currently supported only for voltage-controlled E and G sources",
223+
parameters[2].LineInfo);
224+
return null;
225+
}
226+
227+
var definition = laplaceParser.ParseVoltageControlledSource(name, parameters, context);
228+
if (definition == null)
229+
{
230+
return null;
231+
}
232+
233+
return CreateLaplaceVoltageControlledCurrentSource(name, definition, context);
223234
}
224235

225236
if (parameters.Any(p => p is AssignmentParameter ap && ap.Name.ToLower() == "value"))
@@ -292,8 +303,30 @@ private IEntity CreateCustomCurrentSource(string name, ParameterCollection param
292303
return null;
293304
}
294305
}
295-
296-
return null;
297-
}
298-
}
306+
307+
return null;
308+
}
309+
310+
private static IEntity CreateLaplaceVoltageControlledCurrentSource(
311+
string name,
312+
LaplaceSourceDefinition definition,
313+
IReadingContext context)
314+
{
315+
var entity = new LaplaceVoltageControlledCurrentSource(name);
316+
var nodes = new ParameterCollection(new List<Parameter>())
317+
{
318+
new IdentifierParameter(definition.OutputPositiveNode, definition.LineInfo),
319+
new IdentifierParameter(definition.OutputNegativeNode, definition.LineInfo),
320+
new IdentifierParameter(definition.Input.ControlPositiveNode, definition.LineInfo),
321+
new IdentifierParameter(definition.Input.ControlNegativeNode, definition.LineInfo),
322+
};
323+
324+
context.CreateNodes(entity, nodes);
325+
entity.Parameters.Numerator = definition.TransferFunction.NumeratorCoefficients;
326+
entity.Parameters.Denominator = definition.TransferFunction.DenominatorCoefficients;
327+
entity.Parameters.Delay = definition.Delay;
328+
329+
return entity;
330+
}
331+
}
299332
}

src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Components/Sources/LaplaceSourceParser.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public bool IsLaplaceSource(ParameterCollection parameters)
2525
&& string.Equals(wordParameter.Value, "laplace", StringComparison.OrdinalIgnoreCase);
2626
}
2727

28-
public LaplaceSourceDefinition ParseVoltageControlledVoltageSource(
28+
public LaplaceSourceDefinition ParseVoltageControlledSource(
2929
string sourceName,
3030
ParameterCollection parameters,
3131
IReadingContext context)

src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Components/Sources/VoltageSourceGenerator.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -198,12 +198,12 @@ protected IEntity CreateCustomVoltageSource(
198198
{
199199
context.Result.ValidationResult.AddError(
200200
ValidationEntrySource.Reader,
201-
"laplace is currently supported only for E voltage-controlled voltage sources",
201+
"laplace is currently supported only for voltage-controlled E and G sources",
202202
parameters[2].LineInfo);
203203
return null;
204204
}
205205

206-
var definition = laplaceParser.ParseVoltageControlledVoltageSource(name, parameters, context);
206+
var definition = laplaceParser.ParseVoltageControlledSource(name, parameters, context);
207207
if (definition == null)
208208
{
209209
return null;

0 commit comments

Comments
 (0)