Skip to content

Commit 3032446

Browse files
Enhance LAPLACE source support in SpiceSharpParser
- Renamed UnsupportedLaplaceOptions to InvalidLaplaceOptions and updated test cases to reflect new validation rules for LAPLACE options. - Added tests for multiplier and delay functionality in LAPLACE sources, ensuring correct scaling and validation. - Implemented scaling of numerator coefficients in LaplaceTransferFunction based on multiplier. - Updated LaplaceSourceParser to handle multiplier and delay options, enforcing single-instance constraints and proper error handling. - Revised documentation to clarify supported LAPLACE syntax and options, including multiplier and delay usage.
1 parent ec892f1 commit 3032446

10 files changed

Lines changed: 456 additions & 92 deletions

File tree

roadmap/laplace-pspice-feasibility.md

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

6969
## Current Status
7070

71-
`E`-source and `G`-source `LAPLACE` support is implemented by SpiceSharpParser for canonical assignment, no-equals expression-pair, and equals-after-keyword expression-pair syntax. `F`, `H`, `B`, function-like `VALUE={LAPLACE(...)}`, delay, and `M=` support remain deferred.
71+
`E`-source and `G`-source `LAPLACE` support is implemented by SpiceSharpParser for canonical assignment, no-equals expression-pair, and equals-after-keyword expression-pair syntax. Constant `M=`, `TD=`, and `DELAY=` options are implemented for those supported voltage-controlled forms. `F`, `H`, `B`, function-like `VALUE={LAPLACE(...)}`, and explicit internal-state options 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

7777
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.
7878

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.
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, support constant `M=` / delay options, and have OP / AC integration coverage.
8080

8181
Phase 5 syntax compatibility is implemented: `E` / `G` sources accept `LAPLACE {V(...)} = {H(s)}`, `LAPLACE {V(...)} {H(s)}`, and `LAPLACE = {V(...)} {H(s)}` as equivalent voltage-controlled forms, while known deferred `VALUE={LAPLACE(...)}` and `B`-source forms produce targeted reader diagnostics.
8282

83+
Phase 6 option and transient verification is implemented: supported `E` / `G` Laplace forms accept one constant `M=` multiplier and either `TD=` or `DELAY=` as a constant non-negative runtime delay parameter. Transient first-order step-response tests cover undelayed `E` / `G` low-pass behavior; delayed transient response shape is not currently claimed.
84+
8385
Adjacent features already exist:
8486

8587
- `VALUE={expr}` behavioral sources.
@@ -168,7 +170,7 @@ Input-expression subset:
168170
- Alternative `E` / `G` Laplace syntax variants that omit `=` or put `=` directly after `LAPLACE`.
169171
- `VALUE={LAPLACE(...)}` / function-like ABM forms.
170172
- Non-rational transfer expressions.
171-
- Explicit delay syntax such as `TD=` or `DELAY=`.
173+
- Explicit internal-state syntax.
172174
- Initial-condition syntax for Laplace internal state.
173175
- Generated C# writer support, unless the MVP delivery explicitly includes writer parity.
174176

@@ -476,16 +478,18 @@ Prefer parsing with the existing expression parser and accepting only the expect
476478

477479
Do not silently ignore `M=`.
478480

479-
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.
481+
Implemented behavior: accept one `M=` assignment on supported `E` / `G` Laplace sources. Evaluate `M` as a finite constant and scale the numerator coefficients, equivalent to putting the multiplier directly in `H(s)`.
480482

481483
### Delay Policy
482484

483-
The runtime supports `Delay`, but PSpice delay syntax for Laplace sources needs confirmation.
485+
The runtime exposes `Delay`; SpiceSharpParser accepts one constant non-negative delay assignment on supported `E` / `G` Laplace sources and maps it to the runtime delay parameter.
484486

485-
MVP behavior:
487+
Implemented behavior:
486488

487-
- Set `Delay = 0.0`.
488-
- Reject `TD=`, `DELAY=`, or trailing delay-like parameters with a clear diagnostic.
489+
- `TD=<expr>` and `DELAY=<expr>` are aliases.
490+
- The expression must evaluate to a finite non-negative constant in seconds.
491+
- Only one delay option may be used.
492+
- Bare forms such as `TD 1n` remain unsupported.
489493

490494
## Diagnostics
491495

@@ -503,8 +507,12 @@ laplace transfer function is improper; numerator degree exceeds denominator degr
503507
laplace transfer function has singular DC gain
504508
laplace transfer coefficients must be constant expressions
505509
laplace transfer expression reserves symbol 's'; use a different parameter name
506-
laplace delay syntax is not supported yet
507-
laplace multiplier M is not supported yet
510+
laplace delay must be a finite constant expression
511+
laplace delay must be non-negative
512+
laplace delay can be specified only once
513+
laplace multiplier M must be a finite constant expression
514+
laplace multiplier M can be specified only once
515+
laplace options must use assignment syntax
508516
laplace syntax variant is recognized but not supported yet
509517
laplace function-like VALUE form is not supported yet; use E/G LAPLACE {V(...)} = {H(s)}
510518
laplace source supports only E and G voltage-controlled forms in this version
@@ -527,8 +535,8 @@ Good diagnostics are part of compatibility. A precise rejection is better than f
527535
| `E ... LAPLACE {V(n1,n2)} = {H(s)}` | Support | Keep |
528536
| `G ... LAPLACE {V(n)} = {H(s)}` | Support | Keep |
529537
| `G ... LAPLACE {V(n1,n2)} = {H(s)}` | Support | Keep |
530-
| `E` / `G ... LAPLACE {V(n)} {H(s)}` | Targeted unsupported diagnostic until canonical form is stable | Add recognizer that lowers to the same model |
531-
| `E` / `G ... LAPLACE = {V(n)} {H(s)}` | Targeted unsupported diagnostic until canonical form is stable | Add recognizer that lowers to the same model |
538+
| `E` / `G ... LAPLACE {V(n)} {H(s)}` | Support | Keep |
539+
| `E` / `G ... LAPLACE = {V(n)} {H(s)}` | Support | Keep |
532540
| `F` / `H` current-controlled forms | Targeted unsupported diagnostic | Map to built-in current-controlled Laplace entities |
533541
| `B` source Laplace forms | Targeted unsupported diagnostic | Investigate PSpice ABM syntax |
534542
| `VALUE={LAPLACE(...)}` function-like form | Targeted unsupported diagnostic | Investigate as source-level special form |
@@ -537,8 +545,8 @@ Good diagnostics are part of compatibility. A precise rejection is better than f
537545
| Non-rational functions of `s` | Reject | Likely keep rejected |
538546
| Singular DC gain (`1/s`) | Reject unless verified safe | Possible AC-only or IC-aware mode |
539547
| Improper transfer (`deg N > deg D`) | Reject | Allow only if proven safe for claimed analyses |
540-
| Explicit delay | Reject | Map after syntax and behavior tests |
541-
| Transient | Verify before documenting | Document verified subset |
548+
| Explicit delay | Support `TD=` / `DELAY=` assignments on `E` / `G` | Other delay-like forms remain unsupported |
549+
| Transient | Verified for undelayed first-order `E` / `G` low-pass step responses | Delayed transient shape not currently claimed |
542550
| Generated C# writer | Defer or include as final polish | Emit built-in Laplace entities |
543551

544552
### PSpice Variants to Investigate Later
@@ -606,7 +614,7 @@ The last recognizer is deliberate. If a user writes a known PSpice Laplace varia
606614
| Current-controlled | `F1 out 0 LAPLACE {I(Vsense)} = {H(s)}` | Later milestone | Runtime entities exist, but controlling-source parsing and PSpice compatibility need tests. |
607615
| Function-like `VALUE` | `E1 out 0 VALUE = {LAPLACE(V(in), H(s))}` | Investigate later | Must be handled as source-level Laplace, not a scalar expression function. |
608616
| `B` source ABM | `B1 out 0 V = {LAPLACE(V(in), H(s))}` | Investigate later | May require arbitrary input-expression lowering or custom behavior. |
609-
| Delay/options | `TD=...`, `DELAY=...` | Reject initially | Runtime has `Delay`; PSpice syntax and semantics need confirmation. |
617+
| Delay/options | `TD=...`, `DELAY=...`, `M=...` | Supported for `E` / `G` voltage-controlled forms | Values must be constant; only one delay option may be used. |
610618

611619
### Normalization Rules
612620

@@ -669,7 +677,7 @@ Status: implemented for canonical `E` source syntax.
669677

670678
### Phase 4: `G` Source Mapping
671679

672-
Status: implemented for canonical `G` source syntax. `M=`, delay options, `F`, and `H` remain deferred.
680+
Status: implemented for canonical `G` source syntax and constant `M=` / delay options. `F` and `H` remain deferred.
673681

674682
1. Detect `LAPLACE` in `CurrentSourceGenerator.CreateCustomCurrentSource(...)`.
675683
2. Reuse the same source parser and transfer-function builder.
@@ -678,7 +686,7 @@ Status: implemented for canonical `G` source syntax. `M=`, delay options, `F`, a
678686

679687
### Phase 5: Syntax Compatibility Layer
680688

681-
Status: implemented for `E` / `G` no-equals and equals-after-keyword voltage-controlled syntax. `VALUE={LAPLACE(...)}`, `B`, `F`, `H`, `M=`, and delay options remain diagnostic-only / deferred.
689+
Status: implemented for `E` / `G` no-equals and equals-after-keyword voltage-controlled syntax. `VALUE={LAPLACE(...)}`, `B`, `F`, and `H` remain diagnostic-only / deferred.
682690

683691
1. Add `UnsupportedKnownVariantRecognizer` so common deferred forms receive targeted diagnostics.
684692
2. Add `NoEqualsExpressionPairRecognizer` for `LAPLACE {input} {transfer}` only after canonical tests pass.
@@ -750,7 +758,7 @@ Place near [src/SpiceSharpParser.Tests/Parsers](../src/SpiceSharpParser.Tests/Pa
750758
- Missing transfer expression.
751759
- Unsupported `F`, `H`, and `B` forms.
752760
- Unsupported arbitrary input expression.
753-
- Unsupported delay and `M=` parameters.
761+
- Supported `M=`, `TD=`, and `DELAY=` parameters plus invalid duplicate / non-finite / negative option diagnostics.
754762

755763
### Compatibility Matrix Tests
756764

src/SpiceSharpParser.IntegrationTests/AnalogBehavioralModeling/LaplaceTests.cs

Lines changed: 155 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,14 @@ public static IEnumerable<object[]> RejectedLaplaceTransfers
3030
}
3131
}
3232

33-
public static IEnumerable<object[]> UnsupportedLaplaceOptions
33+
public static IEnumerable<object[]> InvalidLaplaceOptions
3434
{
3535
get
3636
{
37-
yield return new object[] { "M=2", "multiplier" };
38-
yield return new object[] { "TD=1n", "delay" };
39-
yield return new object[] { "DELAY=1n", "delay" };
37+
yield return new object[] { "M=2 M=3", "only once" };
38+
yield return new object[] { "TD=1n TD=2n", "only once" };
39+
yield return new object[] { "TD=1n DELAY=2n", "only once" };
40+
yield return new object[] { "TD=-1n", "non-negative" };
4041
}
4142
}
4243

@@ -104,6 +105,25 @@ public void When_ELaplaceNoEqualsSyntaxRunsOp_Expect_DcGainNearOne()
104105
Assert.True(EqualsWithTol(1.0, export), $"Expected OP gain near 1, got {export}.");
105106
}
106107

108+
[Fact]
109+
public void When_ELaplaceMultiplierRunsOp_Expect_DcGainScaled()
110+
{
111+
var model = GetSpiceSharpModel(
112+
"E LAPLACE multiplier OP",
113+
".PARAM tau=1u",
114+
"VIN in 0 1",
115+
"ELOW out 0 LAPLACE {V(in)} = {1/(1+s*tau)} M=2",
116+
"RLOAD out 0 1k",
117+
".OP",
118+
".SAVE V(out)",
119+
".END");
120+
121+
AssertNoValidationErrors(model);
122+
123+
double export = RunOpSimulation(model, "V(out)");
124+
Assert.True(EqualsWithTol(2.0, export), $"Expected OP gain near 2, got {export}.");
125+
}
126+
107127
[Fact]
108128
public void When_ELaplaceLowPassRunsAcAtCutoff_Expect_ExpectedMagnitudeAndPhase()
109129
{
@@ -225,6 +245,26 @@ public void When_GLaplaceEqualsAfterKeywordSyntaxRunsOp_Expect_LoadVoltageWithCu
225245
Assert.True(EqualsWithTol(-1.0, export), $"Expected OP load voltage near -1, got {export}.");
226246
}
227247

248+
[Fact]
249+
public void When_GLaplaceMultiplierRunsOp_Expect_LoadVoltageScaled()
250+
{
251+
var model = GetSpiceSharpModel(
252+
"G LAPLACE multiplier OP",
253+
".PARAM gm=1m",
254+
".PARAM tau=1u",
255+
"VIN in 0 1",
256+
"GLOW out 0 LAPLACE {V(in)} = {gm/(1+s*tau)} M=2",
257+
"RLOAD out 0 1k",
258+
".OP",
259+
".SAVE V(out)",
260+
".END");
261+
262+
AssertNoValidationErrors(model);
263+
264+
double export = RunOpSimulation(model, "V(out)");
265+
Assert.True(EqualsWithTol(-2.0, export), $"Expected OP load voltage near -2, got {export}.");
266+
}
267+
228268
[Fact]
229269
public void When_GLaplaceLowPassRunsAcAtCutoff_Expect_ExpectedMagnitudeAndPhase()
230270
{
@@ -305,6 +345,97 @@ public void When_ELaplaceIsMixedWithExistingAbmSources_Expect_AllOutputs()
305345
Assert.True(EqualsWithTol(exports, new[] { 3.0, 4.0, 5.0, 2.0 }));
306346
}
307347

348+
[Fact]
349+
public void When_ELaplaceDelayOptionsRunOp_Expect_ValidCircuit()
350+
{
351+
var tdModel = GetSpiceSharpModel(
352+
"E LAPLACE TD OP",
353+
"VIN in 0 1",
354+
"ETD out 0 LAPLACE {V(in)} = {1/(1+s*1u)} TD=1n",
355+
"RLOAD out 0 1k",
356+
".OP",
357+
".SAVE V(out)",
358+
".END");
359+
360+
var delayModel = GetSpiceSharpModel(
361+
"E LAPLACE DELAY OP",
362+
"VIN in 0 1",
363+
"EDELAY out 0 LAPLACE {V(in)} = {1/(1+s*1u)} DELAY=1n",
364+
"RLOAD out 0 1k",
365+
".OP",
366+
".SAVE V(out)",
367+
".END");
368+
369+
AssertNoValidationErrors(tdModel);
370+
AssertNoValidationErrors(delayModel);
371+
Assert.True(EqualsWithTol(1.0, RunOpSimulation(tdModel, "V(out)")));
372+
Assert.True(EqualsWithTol(1.0, RunOpSimulation(delayModel, "V(out)")));
373+
}
374+
375+
[Fact]
376+
public void When_ELaplaceLowPassRunsTransient_Expect_FirstOrderStepResponse()
377+
{
378+
const double tau = 1e-6;
379+
var model = GetSpiceSharpModel(
380+
"E LAPLACE low-pass TRAN",
381+
".PARAM tau=1u",
382+
"VIN in 0 PULSE(0 1 0 1n 1n 100u 200u)",
383+
"ELOW out 0 LAPLACE {V(in)} = {1/(1+s*tau)}",
384+
"RLOAD out 0 1k",
385+
".TRAN 50n 8u",
386+
".SAVE V(out)",
387+
".END");
388+
389+
AssertNoValidationErrors(model);
390+
391+
var exports = RunTransientSimulation(model, "V(out)");
392+
AssertTransientPoint(exports, 1.0 * tau, FirstOrderStep(1.0), 0.06);
393+
AssertTransientPoint(exports, 2.0 * tau, FirstOrderStep(2.0), 0.06);
394+
AssertTransientPoint(exports, 5.0 * tau, FirstOrderStep(5.0), 0.04);
395+
}
396+
397+
[Fact]
398+
public void When_GLaplaceLowPassRunsTransient_Expect_FirstOrderStepResponseWithCurrentSourceSign()
399+
{
400+
const double tau = 1e-6;
401+
var model = GetSpiceSharpModel(
402+
"G LAPLACE low-pass TRAN",
403+
".PARAM tau=1u",
404+
".PARAM gm=1m",
405+
"VIN in 0 PULSE(0 1 0 1n 1n 100u 200u)",
406+
"GLOW out 0 LAPLACE {V(in)} = {gm/(1+s*tau)}",
407+
"RLOAD out 0 1k",
408+
".TRAN 50n 8u",
409+
".SAVE V(out)",
410+
".END");
411+
412+
AssertNoValidationErrors(model);
413+
414+
var exports = RunTransientSimulation(model, "V(out)");
415+
AssertTransientPoint(exports, 1.0 * tau, -FirstOrderStep(1.0), 0.06);
416+
AssertTransientPoint(exports, 2.0 * tau, -FirstOrderStep(2.0), 0.06);
417+
AssertTransientPoint(exports, 5.0 * tau, -FirstOrderStep(5.0), 0.04);
418+
}
419+
420+
[Fact]
421+
public void When_ELaplaceDelayedLowPassRunsTransient_Expect_NoRuntimeException()
422+
{
423+
var model = GetSpiceSharpModel(
424+
"E LAPLACE delayed low-pass TRAN",
425+
".PARAM tau=1u",
426+
"VIN in 0 PULSE(0 1 0 1n 1n 100u 200u)",
427+
"ELOW out 0 LAPLACE {V(in)} = {1/(1+s*tau)} TD=2u",
428+
"RLOAD out 0 1k",
429+
".TRAN 50n 8u",
430+
".SAVE V(out)",
431+
".END");
432+
433+
AssertNoValidationErrors(model);
434+
435+
var exception = Record.Exception(() => RunTransientSimulation(model, "V(out)"));
436+
Assert.Null(exception);
437+
}
438+
308439
[Theory]
309440
[MemberData(nameof(InvalidLaplaceInputs))]
310441
public void When_ELaplaceInputIsInvalid_Expect_ReaderValidationError(
@@ -328,13 +459,13 @@ public void When_ELaplaceTransferIsRejected_Expect_ReaderValidationError(
328459
}
329460

330461
[Theory]
331-
[MemberData(nameof(UnsupportedLaplaceOptions))]
332-
public void When_ELaplaceUnsupportedOptionIsUsed_Expect_ReaderValidationError(
462+
[MemberData(nameof(InvalidLaplaceOptions))]
463+
public void When_ELaplaceInvalidOptionIsUsed_Expect_ReaderValidationError(
333464
string option,
334465
string expectedMessage)
335466
{
336467
var model = GetSpiceSharpModel(
337-
"E LAPLACE unsupported option",
468+
"E LAPLACE invalid option",
338469
"VIN in 0 1",
339470
$"EBAD out 0 LAPLACE {{V(in)}} = {{1/(1+s)}} {option}",
340471
"RLOAD out 0 1k",
@@ -345,21 +476,6 @@ public void When_ELaplaceUnsupportedOptionIsUsed_Expect_ReaderValidationError(
345476
AssertReaderErrorContains(model, expectedMessage);
346477
}
347478

348-
[Fact]
349-
public void When_GLaplaceUnsupportedMultiplierIsUsed_Expect_ReaderValidationError()
350-
{
351-
var model = GetSpiceSharpModel(
352-
"G LAPLACE unsupported multiplier",
353-
"VIN in 0 1",
354-
"GBAD out 0 LAPLACE {V(in)} = {1/(1+s)} M=2",
355-
"RLOAD out 0 1k",
356-
".OP",
357-
".SAVE V(out)",
358-
".END");
359-
360-
AssertReaderErrorContains(model, "multiplier");
361-
}
362-
363479
[Fact]
364480
public void When_ValueLaplaceFunctionIsUsed_Expect_ReaderValidationError()
365481
{
@@ -432,5 +548,22 @@ private static void AssertReaderErrorContains(SpiceSharpModel model, string expe
432548
var messages = string.Join(Environment.NewLine, model.ValidationResult.Errors.Select(error => error.Message));
433549
Assert.Contains(expectedMessage, messages, StringComparison.OrdinalIgnoreCase);
434550
}
551+
552+
private static void AssertTransientPoint(
553+
IReadOnlyList<Tuple<double, double>> exports,
554+
double time,
555+
double expected,
556+
double tolerance)
557+
{
558+
var nearest = exports.OrderBy(export => Math.Abs(export.Item1 - time)).First();
559+
Assert.True(
560+
Math.Abs(nearest.Item2 - expected) <= tolerance,
561+
$"Expected V(out) near {expected} at {time}, got {nearest.Item2} at {nearest.Item1}.");
562+
}
563+
564+
private static double FirstOrderStep(double normalizedTime)
565+
{
566+
return 1.0 - Math.Exp(-normalizedTime);
567+
}
435568
}
436569
}

0 commit comments

Comments
 (0)