From a5a7bf1e548c53543d53a19f395a5208a708939e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Go=C5=82=C4=99biowski?= Date: Tue, 20 Jan 2026 20:08:41 +0100 Subject: [PATCH 1/6] Prototype --- .../Examples/Circuits/MosfetExample3.cir | 5 + .../Examples/Extensions/Aggregate.cs | 48 +++++++ .../Extensions/ComplexMosfetModelGenerator.cs | 128 ++++++++++++++++++ .../Extensions/CustomMosfetModelTest.cs | 30 ++++ .../SpiceSharpParser.IntegrationTests.csproj | 3 + .../EntityGenerators/IModelGenerator.cs | 5 + .../StochasticModelsGenerator.cs | 14 +- 7 files changed, 231 insertions(+), 2 deletions(-) create mode 100644 src/SpiceSharpParser.IntegrationTests/Examples/Circuits/MosfetExample3.cir create mode 100644 src/SpiceSharpParser.IntegrationTests/Examples/Extensions/Aggregate.cs create mode 100644 src/SpiceSharpParser.IntegrationTests/Examples/Extensions/ComplexMosfetModelGenerator.cs diff --git a/src/SpiceSharpParser.IntegrationTests/Examples/Circuits/MosfetExample3.cir b/src/SpiceSharpParser.IntegrationTests/Examples/Circuits/MosfetExample3.cir new file mode 100644 index 00000000..cae39dda --- /dev/null +++ b/src/SpiceSharpParser.IntegrationTests/Examples/Circuits/MosfetExample3.cir @@ -0,0 +1,5 @@ +Mosfet circuit +Md 0 1 2 3 my-pmos +.model my-pmos.1 pmos(level = 49 lmin=0.18u) +.model my-pmos.2 pmos(level = 49 lmin=1.18u) +.END diff --git a/src/SpiceSharpParser.IntegrationTests/Examples/Extensions/Aggregate.cs b/src/SpiceSharpParser.IntegrationTests/Examples/Extensions/Aggregate.cs new file mode 100644 index 00000000..71dab453 --- /dev/null +++ b/src/SpiceSharpParser.IntegrationTests/Examples/Extensions/Aggregate.cs @@ -0,0 +1,48 @@ +using SpiceSharp.Components; +using SpiceSharp.Entities; +using SpiceSharp.Simulations; +using System.Collections.Generic; +using System.Xml.Linq; + +public class BSIM3AggregateModel : Entity +{ + /// + /// Gets the models in the aggregate model based on sizes. + /// + public HashSet Models { get; } = []; + + /// + /// Creates an aggregate model. + /// + /// The name. + public BSIM3AggregateModel(string name) + : base(name) + { + } + + /// + /// Creates an aggregate model based on sizes. + /// + /// + /// + public BSIM3AggregateModel(string name, IEnumerable models) + : base(name) + { + foreach (var model in models) + Models.Add(model); + } + + public override void CreateBehaviors(ISimulation simulation) + { + throw new System.NotImplementedException(); + } + + /// + public override IEntity Clone() + { + var n = new BSIM3AggregateModel(Name); + foreach (var model in Models) + n.Models.Add((BSIM3Model)model.Clone()); + return n; + } +} \ No newline at end of file diff --git a/src/SpiceSharpParser.IntegrationTests/Examples/Extensions/ComplexMosfetModelGenerator.cs b/src/SpiceSharpParser.IntegrationTests/Examples/Extensions/ComplexMosfetModelGenerator.cs new file mode 100644 index 00000000..5d97976b --- /dev/null +++ b/src/SpiceSharpParser.IntegrationTests/Examples/Extensions/ComplexMosfetModelGenerator.cs @@ -0,0 +1,128 @@ +using SpiceSharp.Components; +using SpiceSharpParser.Common.Validation; +using SpiceSharpParser.ModelReaders.Netlist.Spice.Context; +using SpiceSharpParser.ModelReaders.Netlist.Spice.Context.Models; +using SpiceSharpParser.Models.Netlist.Spice.Objects; +using SpiceSharpParser.Models.Netlist.Spice.Objects.Parameters; +using System; + +namespace SpiceSharpParser.ModelReaders.Netlist.Spice.Readers.EntityGenerators.Models +{ + public class ComplexMosfetModelGenerator : MosfetModelGenerator, ICustomModelGenerator + { + public ComplexMosfetModelGenerator() + { + } + + public Context.Models.Model Process(Context.Models.Model model, IModelsRegistry models) + { + + if (model.Entity is BSIM3Model bsim3Model) + { + + if (model.Name.Contains(".")) + { + var aggregateName = bsim3Model.Name.Substring(0, bsim3Model.Name.IndexOf('.')); + var aggregate = models.FindModel(aggregateName); + if (aggregate != null) + { + var aggrategeEntity = aggregate.Entity as BSIM3AggregateModel; + aggrategeEntity.Models.Add(bsim3Model); + + return aggregate; + } + else + { + var aggrategeEntity = new BSIM3AggregateModel(aggregateName); + aggrategeEntity.Models.Add(bsim3Model); + + return new Context.Models.Model(aggregateName, aggrategeEntity, null); + } + + } + + } + return model; + } + + public override void AddGenericLevel(int level) + { + base.AddGenericLevel(level); + } + + public override void AddLevel(int level) + { + Levels[level] = (name, type, _) => + { + var mosfet = (TModel)Activator.CreateInstance(typeof(TModel), name); + switch (type.ToLower()) + { + case "nmos": mosfet.SetParameter("nmos", true); break; + case "pmos": mosfet.SetParameter("pmos", true); break; + } + + return new Context.Models.Model(name, mosfet, mosfet.Parameters); + }; + } + + + public override Context.Models.Model Generate(string id, string type, ParameterCollection parameters, IReadingContext context) + { + var clonedParameters = (ParameterCollection)parameters.Clone(); + + int level = 1; + string version = null; + int lindex = -1, vindex = -1; + for (int i = 0; i < clonedParameters.Count; i++) + { + if (clonedParameters[i] is AssignmentParameter ap) + { + if (ap.Name.ToLower() == "level") + { + lindex = i; + level = (int)Math.Round(context.Evaluator.EvaluateDouble(ap.Value)); + } + + if (ap.Name.ToLower() == "version") + { + vindex = i; + version = ap.Value.ToLower(); + } + + if (vindex >= 0 && lindex >= 0) + { + break; + } + } + } + + if (lindex >= 0) + { + clonedParameters.RemoveAt(lindex); + } + + if (vindex >= 0) + { + clonedParameters.RemoveAt(vindex < lindex ? vindex : vindex - 1); + } + + // Generate the model + Context.Models.Model model; + if (Levels.ContainsKey(level)) + { + model = Levels[level].Invoke(id, type, version); + } + else + { + context.Result.ValidationResult.AddError(ValidationEntrySource.Reader, $"Unknown mosfet model level {level}", parameters.LineInfo); + return null; + } + + // Read all the parameters + SetParameters(context, model.Entity, clonedParameters); + + return model; + } + + } +} \ No newline at end of file diff --git a/src/SpiceSharpParser.IntegrationTests/Examples/Extensions/CustomMosfetModelTest.cs b/src/SpiceSharpParser.IntegrationTests/Examples/Extensions/CustomMosfetModelTest.cs index ed0a7c82..6204284c 100644 --- a/src/SpiceSharpParser.IntegrationTests/Examples/Extensions/CustomMosfetModelTest.cs +++ b/src/SpiceSharpParser.IntegrationTests/Examples/Extensions/CustomMosfetModelTest.cs @@ -80,6 +80,36 @@ public void When_BSIM1_Used_NoExceptions() spiceSharpReader.Settings.Mappings.Components.Map("M", mosfetGenerator); + var spiceSharpModel = spiceSharpReader.Read(parseResult.FinalModel); + + Assert.False(spiceSharpModel.ValidationResult.HasError); + Assert.False(spiceSharpModel.ValidationResult.HasWarning); + } + + [Fact] + public void When_BSIM3_Used_NoExceptions() + { + // Create a model from text file + string path = Path.Combine(Directory.GetCurrentDirectory(), "Examples/Circuits/MosfetExample3.cir"); + var netlistContent = File.ReadAllText(path); + var parser = new SpiceNetlistParser(); + parser.Settings.Lexing.HasTitle = true; + var parseResult = parser.ParseNetlist(netlistContent); + + // Convert to Spice# + var spiceSharpReader = new SpiceSharpReader(); + spiceSharpReader.Settings.CaseSensitivity.IsModelTypeCaseSensitive = false; + + // custom mosfet models + var modelGenerator = new ComplexMosfetModelGenerator(); + modelGenerator.AddGenericLevel(49); + + spiceSharpReader.Settings.Mappings.Models.Map(new[] { "PMOS", "NMOS" }, modelGenerator); + var mosfetGenerator = new MosfetGenerator(); + mosfetGenerator.AddMosfet(); + spiceSharpReader.Settings.Mappings.Components.Map("M", mosfetGenerator); + + var spiceSharpModel = spiceSharpReader.Read(parseResult.FinalModel); Assert.False(spiceSharpModel.ValidationResult.HasError); diff --git a/src/SpiceSharpParser.IntegrationTests/SpiceSharpParser.IntegrationTests.csproj b/src/SpiceSharpParser.IntegrationTests/SpiceSharpParser.IntegrationTests.csproj index 95097b93..2b1b40db 100644 --- a/src/SpiceSharpParser.IntegrationTests/SpiceSharpParser.IntegrationTests.csproj +++ b/src/SpiceSharpParser.IntegrationTests/SpiceSharpParser.IntegrationTests.csproj @@ -97,6 +97,9 @@ Always + + Always + Always diff --git a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/IModelGenerator.cs b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/IModelGenerator.cs index 37544c3f..4c4dcff9 100644 --- a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/IModelGenerator.cs +++ b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/IModelGenerator.cs @@ -7,4 +7,9 @@ public interface IModelGenerator { Model Generate(string id, string type, SpiceSharpParser.Models.Netlist.Spice.Objects.ParameterCollection parameters, IReadingContext context); } + + public interface ICustomModelGenerator : IModelGenerator + { + Context.Models.Model Process(Context.Models.Model model, IModelsRegistry models); + } } \ No newline at end of file diff --git a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/StochasticModelsGenerator.cs b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/StochasticModelsGenerator.cs index dbea6bfe..5d1af38a 100644 --- a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/StochasticModelsGenerator.cs +++ b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/StochasticModelsGenerator.cs @@ -82,8 +82,18 @@ public Context.Models.Model GenerateModel(IModelGenerator modelGenerator, string RegisterDevAndLotModels(parameters, stochasticModelRegistry, model, (modelId) => { var stochasticCandidate = modelGenerator.Generate(modelId, type, filteredParameters, context); - context.ModelsRegistry.RegisterModelInstance(stochasticCandidate); - return stochasticCandidate; + + if (modelGenerator is ICustomModelGenerator custom) + { + var model = custom.Process(stochasticCandidate, context.ModelsRegistry); + context.ModelsRegistry.RegisterModelInstance(model); + return model; + } + else + { + context.ModelsRegistry.RegisterModelInstance(stochasticCandidate); + return stochasticCandidate; + } }); return model; } From 54c20542a229f241bc35e3bf13b31c3f597f0aa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Go=C5=82=C4=99biowski?= Date: Tue, 20 Jan 2026 20:09:47 +0100 Subject: [PATCH 2/6] Fix --- .../Extensions/ComplexMosfetModelGenerator.cs | 85 ------------------- 1 file changed, 85 deletions(-) diff --git a/src/SpiceSharpParser.IntegrationTests/Examples/Extensions/ComplexMosfetModelGenerator.cs b/src/SpiceSharpParser.IntegrationTests/Examples/Extensions/ComplexMosfetModelGenerator.cs index 5d97976b..57ea5929 100644 --- a/src/SpiceSharpParser.IntegrationTests/Examples/Extensions/ComplexMosfetModelGenerator.cs +++ b/src/SpiceSharpParser.IntegrationTests/Examples/Extensions/ComplexMosfetModelGenerator.cs @@ -1,10 +1,5 @@ using SpiceSharp.Components; -using SpiceSharpParser.Common.Validation; -using SpiceSharpParser.ModelReaders.Netlist.Spice.Context; using SpiceSharpParser.ModelReaders.Netlist.Spice.Context.Models; -using SpiceSharpParser.Models.Netlist.Spice.Objects; -using SpiceSharpParser.Models.Netlist.Spice.Objects.Parameters; -using System; namespace SpiceSharpParser.ModelReaders.Netlist.Spice.Readers.EntityGenerators.Models { @@ -44,85 +39,5 @@ public Context.Models.Model Process(Context.Models.Model model, IModelsRegistry } return model; } - - public override void AddGenericLevel(int level) - { - base.AddGenericLevel(level); - } - - public override void AddLevel(int level) - { - Levels[level] = (name, type, _) => - { - var mosfet = (TModel)Activator.CreateInstance(typeof(TModel), name); - switch (type.ToLower()) - { - case "nmos": mosfet.SetParameter("nmos", true); break; - case "pmos": mosfet.SetParameter("pmos", true); break; - } - - return new Context.Models.Model(name, mosfet, mosfet.Parameters); - }; - } - - - public override Context.Models.Model Generate(string id, string type, ParameterCollection parameters, IReadingContext context) - { - var clonedParameters = (ParameterCollection)parameters.Clone(); - - int level = 1; - string version = null; - int lindex = -1, vindex = -1; - for (int i = 0; i < clonedParameters.Count; i++) - { - if (clonedParameters[i] is AssignmentParameter ap) - { - if (ap.Name.ToLower() == "level") - { - lindex = i; - level = (int)Math.Round(context.Evaluator.EvaluateDouble(ap.Value)); - } - - if (ap.Name.ToLower() == "version") - { - vindex = i; - version = ap.Value.ToLower(); - } - - if (vindex >= 0 && lindex >= 0) - { - break; - } - } - } - - if (lindex >= 0) - { - clonedParameters.RemoveAt(lindex); - } - - if (vindex >= 0) - { - clonedParameters.RemoveAt(vindex < lindex ? vindex : vindex - 1); - } - - // Generate the model - Context.Models.Model model; - if (Levels.ContainsKey(level)) - { - model = Levels[level].Invoke(id, type, version); - } - else - { - context.Result.ValidationResult.AddError(ValidationEntrySource.Reader, $"Unknown mosfet model level {level}", parameters.LineInfo); - return null; - } - - // Read all the parameters - SetParameters(context, model.Entity, clonedParameters); - - return model; - } - } } \ No newline at end of file From 500f5aabd0c4b04b7cd7cd2aef7fc7222071f1a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Go=C5=82=C4=99biowski?= Date: Sat, 31 Jan 2026 13:21:59 +0100 Subject: [PATCH 3/6] Prototype --- .../ModelDimensionSimulationTests.cs | 398 +++++++++++++ .../Components/ModelDimensionTests.cs | 543 ++++++++++++++++++ .../Examples/Extensions/Aggregate.cs | 48 -- .../Extensions/ComplexMosfetModelGenerator.cs | 43 -- .../Extensions/CustomMosfetModelTest.cs | 30 - .../Spice/Context/Models/IModelsRegistry.cs | 5 +- .../Netlist/Spice/Context/Models/Model.cs | 24 + .../Models/StochasticModelsRegistry.cs | 78 ++- .../Components/RLCKGenerator.cs | 39 +- .../BipolarJunctionTransistorGenerator.cs | 29 + .../Semiconductors/DiodeGenerator.cs | 31 + .../Semiconductors/JFETGenerator.cs | 30 + .../Semiconductors/MosfetGenerator.cs | 26 + .../Components/SwitchGenerator.cs | 4 + .../Models/BipolarModelGenerator.cs | 12 +- .../Models/DiodeModelGenerator.cs | 8 +- .../Models/JFETModelGenerator.cs | 12 +- .../EntityGenerators/Models/ModelGenerator.cs | 40 ++ .../Models/MosfetModelGenerator.cs | 3 + .../Models/RLCModelGenerator.cs | 16 +- .../Models/SwitchModelGenerator.cs | 25 +- .../StochasticModelsGenerator.cs | 14 +- 22 files changed, 1290 insertions(+), 168 deletions(-) create mode 100644 src/SpiceSharpParser.IntegrationTests/Components/ModelDimensionSimulationTests.cs create mode 100644 src/SpiceSharpParser.IntegrationTests/Components/ModelDimensionTests.cs delete mode 100644 src/SpiceSharpParser.IntegrationTests/Examples/Extensions/Aggregate.cs delete mode 100644 src/SpiceSharpParser.IntegrationTests/Examples/Extensions/ComplexMosfetModelGenerator.cs diff --git a/src/SpiceSharpParser.IntegrationTests/Components/ModelDimensionSimulationTests.cs b/src/SpiceSharpParser.IntegrationTests/Components/ModelDimensionSimulationTests.cs new file mode 100644 index 00000000..1a360cfe --- /dev/null +++ b/src/SpiceSharpParser.IntegrationTests/Components/ModelDimensionSimulationTests.cs @@ -0,0 +1,398 @@ +using System; +using Xunit; + +namespace SpiceSharpParser.IntegrationTests.Components +{ + /// + /// Integration tests with simulations for model selection based on L and W parameters. + /// These tests verify that the correct model is selected by running actual circuit simulations. + /// + public class ModelDimensionSimulationTests : BaseTests + { + #region Resistor Simulation Tests + + [Fact] + public void ResistorModelSelectionAffectsSimulationResults() + { + // Model with RSH=100 ohm/square, L/W range for small resistors + // Model with RSH=1000 ohm/square, L/W range for large resistors + // R = RSH * L / W + var netlist = GetSpiceSharpModel( + "Resistor model selection affects resistance value", + "V1 IN 0 10", + "R1 IN 0 RMOD L=1u W=1u", + ".model RMOD.0 R RSH=100 lmin=0.1u lmax=5u", + ".OP", + ".SAVE I(R1)", + ".END"); + + Assert.NotNull(netlist); + Assert.False(netlist.ValidationResult.HasError); + + // R1: L=1u, W=1u -> should use RMOD.0 (RSH=100) -> R = 100 * 1 / 1 = 100 ohms + // Expected current: 10V / 100 ohm = 0.1 A + var current1 = RunOpSimulation(netlist, "I(R1)"); + Assert.True(EqualsWithTol(0.1, Math.Abs(current1)), $"R1 current expected ~0.1A, got {current1}"); + } + + [Fact] + public void ResistorWidthParameterAffectsModelSelection() + { + var netlist = GetSpiceSharpModel( + "Resistor width affects model selection", + "V1 IN 0 5", + "R1 IN 0 RMOD L=2u W=2u", + ".model RMOD.0 R RSH=50 wmin=1u wmax=10u", + ".OP", + ".SAVE I(R1)", + ".END"); + + Assert.NotNull(netlist); + Assert.False(netlist.ValidationResult.HasError); + + // R1: L=2u, W=2u -> RMOD.0 (RSH=50) -> R = 50 * 2 / 2 = 50 ohms -> I = 5V / 50 = 0.1 A + var current1 = RunOpSimulation(netlist, "I(R1)"); + Assert.True(EqualsWithTol(0.1, Math.Abs(current1)), $"R1 current expected ~0.1A, got {current1}"); + } + + [Fact] + public void ResistorFallsBackToDefaultModelSimulation() + { + var netlist = GetSpiceSharpModel( + "Resistor falls back to default model when dimensions don't match", + "V1 IN 0 10", + "R1 IN 0 RMOD L=0.5u W=1u", + ".model RMOD.0 R RSH=100 lmin=1u lmax=10u", + ".model RMOD R RSH=500", + ".OP", + ".SAVE I(R1)", + ".END"); + + Assert.NotNull(netlist); + Assert.False(netlist.ValidationResult.HasError); + + // R1: L=0.5u (< lmin=1u) -> should use RMOD (default, RSH=500) -> R = 500 * 0.5 / 1 = 250 ohms + var current1 = RunOpSimulation(netlist, "I(R1)"); + Assert.True(EqualsWithTol(10.0 / 250.0, Math.Abs(current1)), $"R1 current expected ~0.04A, got {current1}"); + } + + [Fact] + public void ResistorWithBothLAndWConstraintsSimulation() + { + var netlist = GetSpiceSharpModel( + "Resistor with both L and W constraints", + "V1 IN 0 12", + "R1 IN 0 RMOD L=1u W=2u", + ".model RMOD.0 R RSH=60 lmin=0.5u lmax=5u wmin=1u wmax=10u", + ".OP", + ".SAVE I(R1)", + ".END"); + + Assert.NotNull(netlist); + Assert.False(netlist.ValidationResult.HasError); + + // R1: L=1u, W=2u -> RMOD.0 (RSH=60) -> R = 60 * 1 / 2 = 30 ohms -> I = 12V / 30 = 0.4 A + var current1 = RunOpSimulation(netlist, "I(R1)"); + Assert.True(EqualsWithTol(0.4, Math.Abs(current1)), $"R1 current expected ~0.4A, got {current1}"); + } + + #endregion + + #region Capacitor Simulation Tests + + [Fact] + public void CapacitorModelSelectionAffectsSimulationResults() + { + // Capacitance C = CJ * L * W (approximately for semiconductor capacitors) + var netlist = GetSpiceSharpModel( + "Capacitor model selection affects capacitance value", + "V1 IN 0 PULSE(0 5 0 1n 1n 10n 20n)", + "R1 IN OUT1 1k", + "C1 OUT1 0 CMOD L=2u W=2u", + ".model CMOD.0 C CJ=1e-6 lmin=0.5u lmax=5u wmin=0.5u wmax=5u", + ".TRAN 1n 30n", + ".SAVE V(OUT1)", + ".END"); + + Assert.NotNull(netlist); + Assert.False(netlist.ValidationResult.HasError); + + var exports1 = RunTransientSimulation(netlist, "V(OUT1)"); + Assert.NotNull(exports1); + Assert.True(exports1.Length > 0); + } + + [Fact] + public void CapacitorWidthParameterAffectsModelSelection() + { + var netlist = GetSpiceSharpModel( + "Capacitor width affects model selection", + "V1 IN 0 PULSE(0 10 0 1n 1n 20n 40n)", + "R1 IN OUT1 1k", + "C1 OUT1 0 CMOD L=1u W=3u", + ".model CMOD.0 C CJ=5e-7 wmin=1u wmax=10u", + ".TRAN 1n 40n", + ".SAVE V(OUT1)", + ".END"); + + Assert.NotNull(netlist); + Assert.False(netlist.ValidationResult.HasError); + + var exports1 = RunTransientSimulation(netlist, "V(OUT1)"); + Assert.NotNull(exports1); + Assert.True(exports1.Length > 0); + } + + [Fact] + public void CapacitorFallsBackToDefaultModelSimulation() + { + var netlist = GetSpiceSharpModel( + "Capacitor falls back to default model", + "V1 IN 0 PULSE(0 5 0 1n 1n 10n 20n)", + "R1 IN OUT1 1k", + "C1 OUT1 0 CMOD L=0.3u W=1u", + ".model CMOD.0 C CJ=1e-6 lmin=1u lmax=3u", + ".model CMOD C CJ=5e-7", + ".TRAN 1n 30n", + ".SAVE V(OUT1)", + ".END"); + + Assert.NotNull(netlist); + Assert.False(netlist.ValidationResult.HasError); + + var exports1 = RunTransientSimulation(netlist, "V(OUT1)"); + Assert.NotNull(exports1); + Assert.True(exports1.Length > 0); + } + + #endregion + + #region Inductor Simulation Tests + + [Fact] + public void InductorWithLengthAndWidthParameters() + { + // Basic test to verify inductors accept L and W parameters + var netlist = GetSpiceSharpModel( + "Inductor with L and W parameters", + "V1 IN 0 PULSE(0 1 0 1n 1n 10n 20n)", + "R1 IN OUT 10", + "L1 OUT 0 1u", + ".TRAN 1n 30n", + ".SAVE V(OUT) I(L1)", + ".END"); + + Assert.NotNull(netlist); + Assert.False(netlist.ValidationResult.HasError); + + var exports = RunTransientSimulation(netlist, "I(L1)"); + Assert.NotNull(exports); + Assert.True(exports.Length > 0); + } + + [Fact] + public void InductorBasicBehavior() + { + // Test basic RL circuit behavior + var netlist = GetSpiceSharpModel( + "RL circuit transient response", + "V1 IN 0 PULSE(0 10 0 1n 1n 50n 100n)", + "R1 IN OUT 100", + "L1 OUT 0 1u", + ".TRAN 0.5n 60n", + ".SAVE I(L1)", + ".END"); + + Assert.NotNull(netlist); + Assert.False(netlist.ValidationResult.HasError); + + var exports = RunTransientSimulation(netlist, "I(L1)"); + + // Current through inductor should gradually rise (not instantaneous) + // At t=0, current should be ~0 + Assert.True(Math.Abs(exports[0].Item2) < 0.01, $"Initial current should be ~0, got {exports[0].Item2}"); + + // Current should increase over time + var index30n = 60; // Approximate index for 30ns (0.5ns steps) + if (index30n < exports.Length) + { + Assert.True(exports[index30n].Item2 > exports[10].Item2, + $"Current should increase: I(t=5ns)={exports[10].Item2}, I(t=30ns)={exports[index30n].Item2}"); + } + } + + [Fact] + public void InductorComparisonDifferentValues() + { + // Compare two inductors with different inductance values + var netlist = GetSpiceSharpModel( + "Compare inductors with different values", + "V1 IN 0 PULSE(0 10 0 1n 1n 50n 100n)", + "R1 IN OUT1 100", + "L1 OUT1 0 1u", + "R2 IN OUT2 100", + "L2 OUT2 0 10u", + ".TRAN 0.5n 60n", + ".SAVE I(L1) I(L2)", + ".END"); + + Assert.NotNull(netlist); + Assert.False(netlist.ValidationResult.HasError); + + var exports1 = RunTransientSimulation(netlist, "I(L1)"); + var exports2 = RunTransientSimulation(netlist, "I(L2)"); + + // Smaller inductance (L1=1u) should reach steady state faster than larger (L2=10u) + var index20n = 40; // Approximate index for 20ns + if (index20n < exports1.Length && index20n < exports2.Length) + { + Assert.True(exports1[index20n].Item2 > exports2[index20n].Item2, + $"Smaller inductor should have higher current earlier: I(L1)={exports1[index20n].Item2}, I(L2)={exports2[index20n].Item2}"); + } + } + + #endregion + + #region Combined Component Tests + + [Fact] + public void RLCCircuitWithDimensionBasedModels() + { + var netlist = GetSpiceSharpModel( + "RLC circuit with dimension-based component models", + "V1 IN 0 PULSE(0 5 0 1n 1n 20n 40n)", + "R1 IN N1 RMOD L=2u W=1u", + "L1 N1 N2 10u", + "C1 N2 0 1p", + ".model RMOD.0 R RSH=50 lmin=1u lmax=10u", + ".TRAN 0.5n 50n", + ".SAVE V(N2)", + ".END"); + + Assert.NotNull(netlist); + Assert.False(netlist.ValidationResult.HasError); + + var exports = RunTransientSimulation(netlist, "V(N2)"); + + // Verify simulation runs and produces results + Assert.NotNull(exports); + Assert.True(exports.Length > 0); + + // Verify circuit responds to input (voltage should change from 0) + var maxVoltage = 0.0; + foreach (var v in exports) + { + if (Math.Abs(v.Item2) > maxVoltage) + maxVoltage = Math.Abs(v.Item2); + } + Assert.True(maxVoltage > 0.1, $"Circuit should respond to input, max voltage: {maxVoltage}"); + } + + [Fact] + public void MultipleResistorsWithDifferentModelSelection() + { + var netlist = GetSpiceSharpModel( + "Voltage divider with different resistor models", + "V1 IN 0 10", + "R1 IN MID RMOD L=1u W=1u", + "R2 MID 0 RMOD L=10u W=1u", + ".model RMOD.0 R RSH=100 lmin=0.5u lmax=5u", + ".model RMOD.1 R RSH=1000 lmin=5u lmax=50u", + ".OP", + ".SAVE V(MID)", + ".END"); + + Assert.NotNull(netlist); + Assert.False(netlist.ValidationResult.HasError); + + // R1: L=1u, W=1u -> RMOD.0 (RSH=100) -> R1 = 100 * 1 / 1 = 100 ohms + // R2: L=10u, W=1u -> RMOD.1 (RSH=1000) -> R2 = 1000 * 10 / 1 = 10000 ohms + // Voltage divider: V(MID) = 10V * R2 / (R1 + R2) = 10 * 10000 / 10100 ≈ 9.9 V + + var voltage = RunOpSimulation(netlist, "V(MID)"); + var expected = 9.9; + var tolerance = 0.1; + Assert.True(Math.Abs(expected - voltage) < tolerance, $"V(MID) expected ~{expected}V, got {voltage}"); + } + + [Fact] + public void CapacitorChargeDischargeWithModelSelection() + { + var netlist = GetSpiceSharpModel( + "Capacitor charge/discharge with model selection", + "V1 IN 0 PULSE(0 10 0 1n 1n 50n 100n)", + "R1 IN OUT 1k", + "C1 OUT 0 CMOD L=3u W=3u", + ".model CMOD.0 C CJ=1e-6 lmin=1u lmax=10u wmin=1u wmax=10u", + ".TRAN 1n 80n", + ".SAVE V(OUT)", + ".END"); + + Assert.NotNull(netlist); + Assert.False(netlist.ValidationResult.HasError); + + var exports = RunTransientSimulation(netlist, "V(OUT)"); + + // Verify charging behavior + Assert.True(exports[0].Item2 < 1.0, "Initial voltage should be low"); + + // Find peak during pulse + var peakVoltage = 0.0; + for (int i = 10; i < Math.Min(50, exports.Length); i++) + { + if (exports[i].Item2 > peakVoltage) + peakVoltage = exports[i].Item2; + } + + Assert.True(peakVoltage > 5.0, $"Capacitor should charge significantly, peak: {peakVoltage}V"); + } + + #endregion + + #region Edge Case Simulation Tests + + [Fact] + public void ResistorWithLminBoundaryCondition() + { + var netlist = GetSpiceSharpModel( + "Resistor at lmin boundary", + "V1 IN 0 10", + "R1 IN 0 RMOD L=1.01u W=1u", + ".model RMOD.0 R RSH=100 lmin=1u", + ".model RMOD R RSH=200", + ".OP", + ".SAVE I(R1)", + ".END"); + + Assert.NotNull(netlist); + Assert.False(netlist.ValidationResult.HasError); + + // R1: L=1.01u (>= lmin) -> should use RMOD.0 (RSH=100) -> R = 100 * 1.01 / 1 = 101 ohms + var current1 = RunOpSimulation(netlist, "I(R1)"); + Assert.True(EqualsWithTol(10.0 / 101.0, Math.Abs(current1)), $"R1 current expected ~0.099A, got {current1}"); + } + + [Fact] + public void ResistorWithLmaxBoundaryCondition() + { + var netlist = GetSpiceSharpModel( + "Resistor at lmax boundary", + "V1 IN 0 10", + "R1 IN 0 RMOD L=9.99u W=1u", + ".model RMOD.0 R RSH=100 lmax=10u", + ".model RMOD R RSH=200", + ".OP", + ".SAVE I(R1)", + ".END"); + + Assert.NotNull(netlist); + Assert.False(netlist.ValidationResult.HasError); + + // R1: L=9.99u (<= lmax) -> should use RMOD.0 (RSH=100) -> R = 100 * 9.99 / 1 = 999 ohms + var current1 = RunOpSimulation(netlist, "I(R1)"); + Assert.True(EqualsWithTol(10.0 / 999.0, Math.Abs(current1)), $"R1 current expected ~0.01A, got {current1}"); + } + + #endregion + } +} diff --git a/src/SpiceSharpParser.IntegrationTests/Components/ModelDimensionTests.cs b/src/SpiceSharpParser.IntegrationTests/Components/ModelDimensionTests.cs new file mode 100644 index 00000000..3f594227 --- /dev/null +++ b/src/SpiceSharpParser.IntegrationTests/Components/ModelDimensionTests.cs @@ -0,0 +1,543 @@ +using SpiceSharp.Components; +using Xunit; + +namespace SpiceSharpParser.IntegrationTests.Components +{ + /// + /// Integration tests for model selection based on L and W parameters with lmin, lmax, wmin, wmax constraints. + /// + public class ModelDimensionTests : BaseTests + { + #region MOSFET Tests + + [Fact] + public void MosfetWithLengthAndWidthParameters() + { + var netlist = GetSpiceSharpModel( + "MOSFET with L and W parameters", + "M1 D G S B NMOS1 L=1u W=10u", + ".model NMOS1 NMOS level=1", + ".END"); + + Assert.NotNull(netlist); + Assert.False(netlist.ValidationResult.HasError); + var mosfet = netlist.Circuit["m1"] as Mosfet1; + Assert.NotNull(mosfet); + } + + [Fact] + public void MosfetSelectsCorrectModelBasedOnLength() + { + var netlist = GetSpiceSharpModel( + "MOSFET model selection based on length", + "M1 D G S B NMOS L=0.5u W=10u", + "M2 D G S B NMOS L=5u W=10u", + ".model NMOS.0 NMOS level=1 lmin=0.1u lmax=1u", + ".model NMOS.1 NMOS level=1 lmin=1u lmax=10u", + ".END"); + + Assert.NotNull(netlist); + Assert.False(netlist.ValidationResult.HasError); + + // Both MOSFETs should be created + var mosfet1 = netlist.Circuit["m1"] as Mosfet1; + Assert.NotNull(mosfet1); + + var mosfet2 = netlist.Circuit["m2"] as Mosfet1; + Assert.NotNull(mosfet2); + + // Verify models exist in circuit + Assert.NotNull(netlist.Circuit["NMOS.0"]); + Assert.NotNull(netlist.Circuit["NMOS.1"]); + } + + [Fact] + public void MosfetSelectsCorrectModelBasedOnWidth() + { + var netlist = GetSpiceSharpModel( + "MOSFET model selection based on width", + "M1 D G S B PMOS L=1u W=2u", + "M2 D G S B PMOS L=1u W=20u", + ".model PMOS.0 PMOS level=1 wmin=1u wmax=10u", + ".model PMOS.1 PMOS level=1 wmin=10u wmax=100u", + ".END"); + + Assert.NotNull(netlist); + Assert.False(netlist.ValidationResult.HasError); + + var mosfet1 = netlist.Circuit["m1"] as Mosfet1; + Assert.NotNull(mosfet1); + + var mosfet2 = netlist.Circuit["m2"] as Mosfet1; + Assert.NotNull(mosfet2); + } + + [Fact] + public void MosfetSelectsCorrectModelBasedOnLengthAndWidth() + { + var netlist = GetSpiceSharpModel( + "MOSFET model selection based on L and W", + "M1 D G S B NMOS L=0.5u W=5u", + "M2 D G S B NMOS L=5u W=50u", + ".model NMOS.0 NMOS level=1 lmin=0.1u lmax=1u wmin=1u wmax=10u", + ".model NMOS.1 NMOS level=1 lmin=1u lmax=10u wmin=10u wmax=100u", + ".END"); + + Assert.NotNull(netlist); + Assert.False(netlist.ValidationResult.HasError); + + var mosfet1 = netlist.Circuit["m1"] as Mosfet1; + Assert.NotNull(mosfet1); + + var mosfet2 = netlist.Circuit["m2"] as Mosfet1; + Assert.NotNull(mosfet2); + } + + [Fact] + public void MosfetFallsBackToDefaultModelWhenNoMatch() + { + var netlist = GetSpiceSharpModel( + "MOSFET falls back to default model", + "M1 D G S B NMOS L=100u W=100u", + ".model NMOS.0 NMOS level=1 lmin=0.1u lmax=1u", + ".model NMOS NMOS level=1", + ".END"); + + Assert.NotNull(netlist); + Assert.False(netlist.ValidationResult.HasError); + + var mosfet1 = netlist.Circuit["m1"] as Mosfet1; + Assert.NotNull(mosfet1); + } + + #endregion + + #region Resistor Tests + + [Fact] + public void ResistorWithLengthAndWidthParameters() + { + var netlist = GetSpiceSharpModel( + "Resistor with L and W parameters", + "R1 1 0 RMOD L=1u W=10u", + ".model RMOD R RSH=1", + ".END"); + + Assert.NotNull(netlist); + Assert.False(netlist.ValidationResult.HasError); + var resistor = netlist.Circuit["r1"] as Resistor; + Assert.NotNull(resistor); + } + + [Fact] + public void ResistorSelectsCorrectModelBasedOnDimensions() + { + var netlist = GetSpiceSharpModel( + "Resistor model selection based on dimensions", + "R1 1 0 RMOD L=0.5u W=5u", + "R2 1 0 RMOD L=5u W=50u", + ".model RMOD.0 R RSH=1 lmin=0.1u lmax=1u wmin=1u wmax=10u", + ".model RMOD.1 R RSH=1 lmin=1u lmax=10u wmin=10u wmax=100u", + ".END"); + + Assert.NotNull(netlist); + Assert.False(netlist.ValidationResult.HasError); + + var resistor1 = netlist.Circuit["r1"] as Resistor; + Assert.NotNull(resistor1); + + var resistor2 = netlist.Circuit["r2"] as Resistor; + Assert.NotNull(resistor2); + } + + [Fact] + public void ResistorWithOnlyLengthParameter() + { + var netlist = GetSpiceSharpModel( + "Resistor with only L parameter", + "R1 1 0 RMOD L=0.5u W=1u", + "R2 1 0 RMOD L=5u W=1u", + ".model RMOD.0 R RSH=1 lmin=0.1u lmax=1u", + ".model RMOD.1 R RSH=1 lmin=1u lmax=10u", + ".END"); + + Assert.NotNull(netlist); + Assert.False(netlist.ValidationResult.HasError); + + var resistor1 = netlist.Circuit["r1"] as Resistor; + Assert.NotNull(resistor1); + + var resistor2 = netlist.Circuit["r2"] as Resistor; + Assert.NotNull(resistor2); + } + + [Fact] + public void ResistorWithOnlyWidthParameter() + { + var netlist = GetSpiceSharpModel( + "Resistor with only W parameter", + "R1 1 0 RMOD L=1u W=5u", + "R2 1 0 RMOD L=1u W=50u", + ".model RMOD.0 R RSH=1 wmin=1u wmax=10u", + ".model RMOD.1 R RSH=1 wmin=10u wmax=100u", + ".END"); + + Assert.NotNull(netlist); + Assert.False(netlist.ValidationResult.HasError); + + var resistor1 = netlist.Circuit["r1"] as Resistor; + Assert.NotNull(resistor1); + + var resistor2 = netlist.Circuit["r2"] as Resistor; + Assert.NotNull(resistor2); + } + + #endregion + + #region Capacitor Tests + + [Fact] + public void CapacitorWithLengthAndWidthParameters() + { + var netlist = GetSpiceSharpModel( + "Capacitor with L and W parameters", + "C1 1 0 CMOD L=1u W=10u", + ".model CMOD C CJ=1e-6", + ".END"); + + Assert.NotNull(netlist); + Assert.False(netlist.ValidationResult.HasError); + var capacitor = netlist.Circuit["c1"] as Capacitor; + Assert.NotNull(capacitor); + } + + [Fact] + public void CapacitorSelectsCorrectModelBasedOnDimensions() + { + var netlist = GetSpiceSharpModel( + "Capacitor model selection based on dimensions", + "C1 1 0 CMOD L=0.5u W=5u", + "C2 1 0 CMOD L=5u W=50u", + ".model CMOD.0 C CJ=1e-6 lmin=0.1u lmax=1u wmin=1u wmax=10u", + ".model CMOD.1 C CJ=1e-6 lmin=1u lmax=10u wmin=10u wmax=100u", + ".END"); + + Assert.NotNull(netlist); + Assert.False(netlist.ValidationResult.HasError); + + var capacitor1 = netlist.Circuit["c1"] as Capacitor; + Assert.NotNull(capacitor1); + + var capacitor2 = netlist.Circuit["c2"] as Capacitor; + Assert.NotNull(capacitor2); + } + + #endregion + + #region BJT Tests + + [Fact] + public void BJTWithLengthAndWidthParameters() + { + var netlist = GetSpiceSharpModel( + "BJT with L and W parameters", + "Q1 C B E QMOD L=1u W=10u", + ".model QMOD NPN", + ".END"); + + Assert.NotNull(netlist); + Assert.False(netlist.ValidationResult.HasError); + var bjt = netlist.Circuit["q1"] as BipolarJunctionTransistor; + Assert.NotNull(bjt); + } + + [Fact] + public void BJTSelectsCorrectModelBasedOnDimensions() + { + var netlist = GetSpiceSharpModel( + "BJT model selection based on dimensions", + "Q1 C B E QMOD L=0.5u W=5u", + "Q2 C B E QMOD L=5u W=50u", + ".model QMOD.0 NPN lmin=0.1u lmax=1u wmin=1u wmax=10u", + ".model QMOD.1 NPN lmin=1u lmax=10u wmin=10u wmax=100u", + ".END"); + + Assert.NotNull(netlist); + Assert.False(netlist.ValidationResult.HasError); + + var bjt1 = netlist.Circuit["q1"] as BipolarJunctionTransistor; + Assert.NotNull(bjt1); + + var bjt2 = netlist.Circuit["q2"] as BipolarJunctionTransistor; + Assert.NotNull(bjt2); + } + + [Fact] + public void BJTWithOnlyLengthParameter() + { + var netlist = GetSpiceSharpModel( + "BJT with only L parameter", + "Q1 C B E QMOD L=0.5u", + "Q2 C B E QMOD L=5u", + ".model QMOD.0 NPN lmin=0.1u lmax=1u", + ".model QMOD.1 NPN lmin=1u lmax=10u", + ".END"); + + Assert.NotNull(netlist); + Assert.False(netlist.ValidationResult.HasError); + + var bjt1 = netlist.Circuit["q1"] as BipolarJunctionTransistor; + Assert.NotNull(bjt1); + + var bjt2 = netlist.Circuit["q2"] as BipolarJunctionTransistor; + Assert.NotNull(bjt2); + } + + #endregion + + #region Diode Tests + + [Fact] + public void DiodeWithLengthAndWidthParameters() + { + var netlist = GetSpiceSharpModel( + "Diode with L and W parameters", + "D1 A K DMOD L=1u W=10u", + ".model DMOD D", + ".END"); + + Assert.NotNull(netlist); + Assert.False(netlist.ValidationResult.HasError); + var diode = netlist.Circuit["d1"] as Diode; + Assert.NotNull(diode); + } + + [Fact] + public void DiodeSelectsCorrectModelBasedOnDimensions() + { + var netlist = GetSpiceSharpModel( + "Diode model selection based on dimensions", + "D1 A K DMOD L=0.5u W=5u", + "D2 A K DMOD L=5u W=50u", + ".model DMOD.0 D lmin=0.1u lmax=1u wmin=1u wmax=10u", + ".model DMOD.1 D lmin=1u lmax=10u wmin=10u wmax=100u", + ".END"); + + Assert.NotNull(netlist); + Assert.False(netlist.ValidationResult.HasError); + + var diode1 = netlist.Circuit["d1"] as Diode; + Assert.NotNull(diode1); + + var diode2 = netlist.Circuit["d2"] as Diode; + Assert.NotNull(diode2); + } + + [Fact] + public void DiodeWithOnlyWidthParameter() + { + var netlist = GetSpiceSharpModel( + "Diode with only W parameter", + "D1 A K DMOD W=5u", + "D2 A K DMOD W=50u", + ".model DMOD.0 D wmin=1u wmax=10u", + ".model DMOD.1 D wmin=10u wmax=100u", + ".END"); + + Assert.NotNull(netlist); + Assert.False(netlist.ValidationResult.HasError); + + var diode1 = netlist.Circuit["d1"] as Diode; + Assert.NotNull(diode1); + + var diode2 = netlist.Circuit["d2"] as Diode; + Assert.NotNull(diode2); + } + + #endregion + + #region JFET Tests + + [Fact] + public void JFETWithLengthAndWidthParameters() + { + var netlist = GetSpiceSharpModel( + "JFET with L and W parameters", + "J1 D G S JMOD L=1u W=10u", + ".model JMOD NJF", + ".END"); + + Assert.NotNull(netlist); + Assert.False(netlist.ValidationResult.HasError); + var jfet = netlist.Circuit["j1"] as JFET; + Assert.NotNull(jfet); + } + + [Fact] + public void JFETSelectsCorrectModelBasedOnDimensions() + { + var netlist = GetSpiceSharpModel( + "JFET model selection based on dimensions", + "J1 D G S JMOD L=0.5u W=5u", + "J2 D G S JMOD L=5u W=50u", + ".model JMOD.0 NJF lmin=0.1u lmax=1u wmin=1u wmax=10u", + ".model JMOD.1 NJF lmin=1u lmax=10u wmin=10u wmax=100u", + ".END"); + + Assert.NotNull(netlist); + Assert.False(netlist.ValidationResult.HasError); + + var jfet1 = netlist.Circuit["j1"] as JFET; + Assert.NotNull(jfet1); + + var jfet2 = netlist.Circuit["j2"] as JFET; + Assert.NotNull(jfet2); + } + + [Fact] + public void JFETWithOnlyLengthParameter() + { + var netlist = GetSpiceSharpModel( + "JFET with only L parameter", + "J1 D G S JMOD L=0.5u", + "J2 D G S JMOD L=5u", + ".model JMOD.0 PJF lmin=0.1u lmax=1u", + ".model JMOD.1 PJF lmin=1u lmax=10u", + ".END"); + + Assert.NotNull(netlist); + Assert.False(netlist.ValidationResult.HasError); + + var jfet1 = netlist.Circuit["j1"] as JFET; + Assert.NotNull(jfet1); + + var jfet2 = netlist.Circuit["j2"] as JFET; + Assert.NotNull(jfet2); + } + + #endregion + + #region Edge Cases + + [Fact] + public void ComponentWithoutLWParametersUsesDefaultModel() + { + var netlist = GetSpiceSharpModel( + "Component without L/W uses default", + "M1 D G S B NMOS", + ".model NMOS.0 NMOS level=1 lmin=0.1u lmax=1u", + ".model NMOS NMOS level=1", + ".END"); + + Assert.NotNull(netlist); + Assert.False(netlist.ValidationResult.HasError); + + var mosfet = netlist.Circuit["m1"] as Mosfet1; + Assert.NotNull(mosfet); + } + + [Fact] + public void ModelWithOnlyLminConstraint() + { + var netlist = GetSpiceSharpModel( + "Model with only lmin constraint", + "R1 1 0 RMOD L=0.5u W=1u", + "R2 1 0 RMOD L=5u W=1u", + ".model RMOD.0 R RSH=1 lmin=1u", + ".model RMOD R RSH=1", + ".END"); + + Assert.NotNull(netlist); + Assert.False(netlist.ValidationResult.HasError); + + var resistor1 = netlist.Circuit["r1"] as Resistor; + Assert.NotNull(resistor1); + + var resistor2 = netlist.Circuit["r2"] as Resistor; + Assert.NotNull(resistor2); + } + + [Fact] + public void ModelWithOnlyLmaxConstraint() + { + var netlist = GetSpiceSharpModel( + "Model with only lmax constraint", + "C1 1 0 CMOD L=0.5u W=1u", + "C2 1 0 CMOD L=5u W=1u", + ".model CMOD.0 C CJ=1e-6 lmax=1u", + ".model CMOD C CJ=1e-6", + ".END"); + + Assert.NotNull(netlist); + Assert.False(netlist.ValidationResult.HasError); + + var capacitor1 = netlist.Circuit["c1"] as Capacitor; + Assert.NotNull(capacitor1); + + var capacitor2 = netlist.Circuit["c2"] as Capacitor; + Assert.NotNull(capacitor2); + } + + [Fact] + public void ModelWithOnlyWminConstraint() + { + var netlist = GetSpiceSharpModel( + "Model with only wmin constraint", + "M1 D G S B NMOS L=1u W=0.5u", + "M2 D G S B NMOS L=1u W=5u", + ".model NMOS.0 NMOS level=1 wmin=1u", + ".model NMOS NMOS level=1", + ".END"); + + Assert.NotNull(netlist); + Assert.False(netlist.ValidationResult.HasError); + + var mosfet1 = netlist.Circuit["m1"] as Mosfet1; + Assert.NotNull(mosfet1); + + var mosfet2 = netlist.Circuit["m2"] as Mosfet1; + Assert.NotNull(mosfet2); + } + + [Fact] + public void ModelWithOnlyWmaxConstraint() + { + var netlist = GetSpiceSharpModel( + "Model with only wmax constraint", + "Q1 C B E QMOD W=5u", + "Q2 C B E QMOD W=50u", + ".model QMOD.0 NPN wmax=10u", + ".model QMOD NPN", + ".END"); + + Assert.NotNull(netlist); + Assert.False(netlist.ValidationResult.HasError); + + var bjt1 = netlist.Circuit["q1"] as BipolarJunctionTransistor; + Assert.NotNull(bjt1); + + var bjt2 = netlist.Circuit["q2"] as BipolarJunctionTransistor; + Assert.NotNull(bjt2); + } + + [Fact] + public void MultipleModelsWithOverlappingRanges() + { + var netlist = GetSpiceSharpModel( + "Multiple models with overlapping ranges", + "D1 A K DMOD L=0.8u W=8u", + ".model DMOD.0 D lmin=0.1u lmax=1u wmin=1u wmax=10u", + ".model DMOD.1 D lmin=0.5u lmax=2u wmin=5u wmax=20u", + ".model DMOD D", + ".END"); + + Assert.NotNull(netlist); + Assert.False(netlist.ValidationResult.HasError); + + var diode1 = netlist.Circuit["d1"] as Diode; + Assert.NotNull(diode1); + } + + #endregion + } +} diff --git a/src/SpiceSharpParser.IntegrationTests/Examples/Extensions/Aggregate.cs b/src/SpiceSharpParser.IntegrationTests/Examples/Extensions/Aggregate.cs deleted file mode 100644 index 71dab453..00000000 --- a/src/SpiceSharpParser.IntegrationTests/Examples/Extensions/Aggregate.cs +++ /dev/null @@ -1,48 +0,0 @@ -using SpiceSharp.Components; -using SpiceSharp.Entities; -using SpiceSharp.Simulations; -using System.Collections.Generic; -using System.Xml.Linq; - -public class BSIM3AggregateModel : Entity -{ - /// - /// Gets the models in the aggregate model based on sizes. - /// - public HashSet Models { get; } = []; - - /// - /// Creates an aggregate model. - /// - /// The name. - public BSIM3AggregateModel(string name) - : base(name) - { - } - - /// - /// Creates an aggregate model based on sizes. - /// - /// - /// - public BSIM3AggregateModel(string name, IEnumerable models) - : base(name) - { - foreach (var model in models) - Models.Add(model); - } - - public override void CreateBehaviors(ISimulation simulation) - { - throw new System.NotImplementedException(); - } - - /// - public override IEntity Clone() - { - var n = new BSIM3AggregateModel(Name); - foreach (var model in Models) - n.Models.Add((BSIM3Model)model.Clone()); - return n; - } -} \ No newline at end of file diff --git a/src/SpiceSharpParser.IntegrationTests/Examples/Extensions/ComplexMosfetModelGenerator.cs b/src/SpiceSharpParser.IntegrationTests/Examples/Extensions/ComplexMosfetModelGenerator.cs deleted file mode 100644 index 57ea5929..00000000 --- a/src/SpiceSharpParser.IntegrationTests/Examples/Extensions/ComplexMosfetModelGenerator.cs +++ /dev/null @@ -1,43 +0,0 @@ -using SpiceSharp.Components; -using SpiceSharpParser.ModelReaders.Netlist.Spice.Context.Models; - -namespace SpiceSharpParser.ModelReaders.Netlist.Spice.Readers.EntityGenerators.Models -{ - public class ComplexMosfetModelGenerator : MosfetModelGenerator, ICustomModelGenerator - { - public ComplexMosfetModelGenerator() - { - } - - public Context.Models.Model Process(Context.Models.Model model, IModelsRegistry models) - { - - if (model.Entity is BSIM3Model bsim3Model) - { - - if (model.Name.Contains(".")) - { - var aggregateName = bsim3Model.Name.Substring(0, bsim3Model.Name.IndexOf('.')); - var aggregate = models.FindModel(aggregateName); - if (aggregate != null) - { - var aggrategeEntity = aggregate.Entity as BSIM3AggregateModel; - aggrategeEntity.Models.Add(bsim3Model); - - return aggregate; - } - else - { - var aggrategeEntity = new BSIM3AggregateModel(aggregateName); - aggrategeEntity.Models.Add(bsim3Model); - - return new Context.Models.Model(aggregateName, aggrategeEntity, null); - } - - } - - } - return model; - } - } -} \ No newline at end of file diff --git a/src/SpiceSharpParser.IntegrationTests/Examples/Extensions/CustomMosfetModelTest.cs b/src/SpiceSharpParser.IntegrationTests/Examples/Extensions/CustomMosfetModelTest.cs index 6204284c..ed0a7c82 100644 --- a/src/SpiceSharpParser.IntegrationTests/Examples/Extensions/CustomMosfetModelTest.cs +++ b/src/SpiceSharpParser.IntegrationTests/Examples/Extensions/CustomMosfetModelTest.cs @@ -80,36 +80,6 @@ public void When_BSIM1_Used_NoExceptions() spiceSharpReader.Settings.Mappings.Components.Map("M", mosfetGenerator); - var spiceSharpModel = spiceSharpReader.Read(parseResult.FinalModel); - - Assert.False(spiceSharpModel.ValidationResult.HasError); - Assert.False(spiceSharpModel.ValidationResult.HasWarning); - } - - [Fact] - public void When_BSIM3_Used_NoExceptions() - { - // Create a model from text file - string path = Path.Combine(Directory.GetCurrentDirectory(), "Examples/Circuits/MosfetExample3.cir"); - var netlistContent = File.ReadAllText(path); - var parser = new SpiceNetlistParser(); - parser.Settings.Lexing.HasTitle = true; - var parseResult = parser.ParseNetlist(netlistContent); - - // Convert to Spice# - var spiceSharpReader = new SpiceSharpReader(); - spiceSharpReader.Settings.CaseSensitivity.IsModelTypeCaseSensitive = false; - - // custom mosfet models - var modelGenerator = new ComplexMosfetModelGenerator(); - modelGenerator.AddGenericLevel(49); - - spiceSharpReader.Settings.Mappings.Models.Map(new[] { "PMOS", "NMOS" }, modelGenerator); - var mosfetGenerator = new MosfetGenerator(); - mosfetGenerator.AddMosfet(); - spiceSharpReader.Settings.Mappings.Components.Map("M", mosfetGenerator); - - var spiceSharpModel = spiceSharpReader.Read(parseResult.FinalModel); Assert.False(spiceSharpModel.ValidationResult.HasError); diff --git a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Context/Models/IModelsRegistry.cs b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Context/Models/IModelsRegistry.cs index c66a878b..e8d11bc4 100644 --- a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Context/Models/IModelsRegistry.cs +++ b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Context/Models/IModelsRegistry.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using SpiceSharp.Entities; -using SpiceSharp.Simulations; using SpiceSharpParser.Common; using SpiceSharpParser.Models.Netlist.Spice.Objects; @@ -9,11 +8,11 @@ namespace SpiceSharpParser.ModelReaders.Netlist.Spice.Context.Models { public interface IModelsRegistry { - void SetModel(Entity entity, ISimulationWithEvents simulation, Parameter modelNameParameter, string exceptionMessage, Action setModelAction, IReadingContext context); + void SetModel(Entity entity, double? l, double? w, ISimulationWithEvents simulation, Parameter modelNameParameter, string exceptionMessage, Action setModelAction, IReadingContext context); Model FindModel(string modelName); - IEntity FindModelEntity(string modelName); + IEntity FindModelEntity(string modelName, double? l, double? w); void RegisterModelInstance(Model model); diff --git a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Context/Models/Model.cs b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Context/Models/Model.cs index b78ad212..a03826bf 100644 --- a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Context/Models/Model.cs +++ b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Context/Models/Model.cs @@ -1,10 +1,13 @@ using SpiceSharp.Entities; using SpiceSharp.ParameterSets; +using System.Collections.Generic; namespace SpiceSharpParser.ModelReaders.Netlist.Spice.Context.Models { public class Model { + private readonly Dictionary _dimensionParameters = new Dictionary(System.StringComparer.OrdinalIgnoreCase); + public Model(string name, IEntity entity, IParameterSet parameters) { Name = name; @@ -17,5 +20,26 @@ public Model(string name, IEntity entity, IParameterSet parameters) public IEntity Entity { get; } public IParameterSet Parameters { get; } + + /// + /// Sets a dimension parameter (lmin, lmax, wmin, wmax) for model selection. + /// + /// The parameter name. + /// The parameter value. + public void SetDimensionParameter(string parameterName, double value) + { + _dimensionParameters[parameterName] = value; + } + + /// + /// Gets a dimension parameter (lmin, lmax, wmin, wmax) for model selection. + /// + /// The parameter name. + /// The parameter value. + /// True if the parameter exists. + public bool TryGetDimensionParameter(string parameterName, out double value) + { + return _dimensionParameters.TryGetValue(parameterName, out value); + } } } diff --git a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Context/Models/StochasticModelsRegistry.cs b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Context/Models/StochasticModelsRegistry.cs index 06c2543b..e985e232 100644 --- a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Context/Models/StochasticModelsRegistry.cs +++ b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Context/Models/StochasticModelsRegistry.cs @@ -1,6 +1,7 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.ExceptionServices; using SpiceSharp.Entities; using SpiceSharpParser.Common; using SpiceSharpParser.Common.Validation; @@ -231,9 +232,9 @@ public Dictionary GetStochasticModelLotParameter return null; } - public void SetModel(Entity entity, ISimulationWithEvents simulation, Parameter modelNameParameter, string exceptionMessage, Action setModelAction, IReadingContext context) + public void SetModel(Entity entity, double? l, double? w, ISimulationWithEvents simulation, Parameter modelNameParameter, string exceptionMessage, Action setModelAction, IReadingContext context) { - var model = FindModelEntity(modelNameParameter.Value); + var model = FindModelEntity(modelNameParameter.Value, l, w); if (model == null) { @@ -263,17 +264,49 @@ public Model FindModel(string modelName) { return model; } + + // Check for dimension-based model naming convention (e.g., MODELNAME.0, MODELNAME.1) + for (var i = 0; i < AllModels.Count; i++) + { + var key = AllModels.Keys.ElementAt(i); + + if (key == modelNameToSearch + "." + i) + { + if (AllModels.TryGetValue(key, out var modelInstance)) + { + return modelInstance; + } + } + } } return null; } - public IEntity FindModelEntity(string modelName) + public IEntity FindModelEntity(string modelName, double? l, double? w) { foreach (var generator in NamesGenerators) { var modelNameToSearch = generator.GenerateObjectName(modelName); + for (var i = 0; i < AllModels.Count; i++) + { + var key = AllModels.Keys.ElementAt(i); + + if (key == modelNameToSearch + "." + i) + { + if (AllModels.TryGetValue(key, out var modelInstance)) + { + if (HasGoodSize(modelInstance, l, w)) + { + return modelInstance.Entity; + } + } + } + } + + + if (AllModels.TryGetValue(modelNameToSearch, out var model)) { return model.Entity; @@ -283,6 +316,43 @@ public IEntity FindModelEntity(string modelName) return null; } + private bool HasGoodSize(Model model, double? l, double? w) + { + model.TryGetDimensionParameter("lmin", out double lminValue); + model.TryGetDimensionParameter("lmax", out double lmaxValue); + model.TryGetDimensionParameter("wmin", out double wminValue); + model.TryGetDimensionParameter("wmax", out double wmaxValue); + + // Check L dimension + if (l.HasValue) + { + if (lminValue > 0 && l.Value < lminValue) + { + return false; + } + + if (lmaxValue > 0 && l.Value > lmaxValue) + { + return false; + } + } + + // Check W dimension + if (w.HasValue) + { + if (wminValue > 0 && w.Value < wminValue) + { + return false; + } + if (wmaxValue > 0 && w.Value > wmaxValue) + { + return false; + } + } + + return true; + } + public IModelsRegistry CreateChildRegistry(List generators) { var result = new StochasticModelsRegistry(generators, IsModelNameCaseSensitive) diff --git a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Components/RLCKGenerator.cs b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Components/RLCKGenerator.cs index 6d20abdd..6ab4aa93 100644 --- a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Components/RLCKGenerator.cs +++ b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Components/RLCKGenerator.cs @@ -205,8 +205,13 @@ protected IComponent GenerateCap(string name, ParameterCollection parameters, IR { context.SimulationPreparations.ExecuteActionBeforeSetup((simulation) => { + double? l = GetLengthFromParameters(parameters, context); + double? w = GetWidthFromParameters(parameters, context); + context.ModelsRegistry.SetModel( capacitor, + l, + w, simulation, parameters.Get(2), $"Could not find model {parameters.Get(2)} for capacitor {name}", @@ -249,7 +254,9 @@ protected IComponent GenerateCap(string name, ParameterCollection parameters, IR if (modelBased) { - var model = context.ModelsRegistry.FindModelEntity(parameters.Get(2).Value); + double? l = GetLengthFromParameters(parameters, context); + double? w = GetWidthFromParameters(parameters, context); + var model = context.ModelsRegistry.FindModelEntity(parameters.Get(2).Value, l, w); if (tcParameterAssignment.Values.Count == 2) { @@ -417,8 +424,13 @@ protected IEntity GenerateRes(string name, ParameterCollection parameters, IRead { context.SimulationPreparations.ExecuteActionBeforeSetup((simulation) => { + double? l = GetLengthFromParameters(parameters, context); + double? w = GetWidthFromParameters(parameters, context); + context.ModelsRegistry.SetModel( res, + l, + w, simulation, something, $"Could not find model {something} for resistor {name}", @@ -477,8 +489,13 @@ protected IEntity GenerateRes(string name, ParameterCollection parameters, IRead // Ignore tc parameter on resistor ... context.SimulationPreparations.ExecuteActionBeforeSetup((simulation) => { + double? l = GetLengthFromParameters(parameters, context); + double? w = GetWidthFromParameters(parameters, context); + context.ModelsRegistry.SetModel( res, + l, + w, simulation, modelNameParameter, $"Could not find model {modelNameParameter} for resistor {name}", @@ -598,5 +615,25 @@ private string MultiplyIfNeeded(string expression, string mExpression, string nE return expression; } + + private double? GetLengthFromParameters(ParameterCollection parameters, IReadingContext context) + { + var lParameter = parameters.FirstOrDefault(p => p is AssignmentParameter ap && ap.Name.ToLower() == "l"); + if (lParameter != null && lParameter is AssignmentParameter lap) + { + return context.Evaluator.EvaluateDouble(lap.Value); + } + return null; + } + + private double? GetWidthFromParameters(ParameterCollection parameters, IReadingContext context) + { + var wParameter = parameters.FirstOrDefault(p => p is AssignmentParameter ap && ap.Name.ToLower() == "w"); + if (wParameter != null && wParameter is AssignmentParameter wap) + { + return context.Evaluator.EvaluateDouble(wap.Value); + } + return null; + } } } \ No newline at end of file diff --git a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Components/Semiconductors/BipolarJunctionTransistorGenerator.cs b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Components/Semiconductors/BipolarJunctionTransistorGenerator.cs index bf5d8bf9..c8c73d06 100644 --- a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Components/Semiconductors/BipolarJunctionTransistorGenerator.cs +++ b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Components/Semiconductors/BipolarJunctionTransistorGenerator.cs @@ -38,8 +38,13 @@ public IEntity Generate(string componentIdentifier, string originalName, string context.CreateNodes(bjt, parameters.Take(BipolarJunctionTransistor.PinCount)); context.SimulationPreparations.ExecuteActionBeforeSetup((simulation) => { + double? l = GetLengthFromParameters(parameters, context); + double? w = GetWidthFromParameters(parameters, context); + context.ModelsRegistry.SetModel( bjt, + l, + w, simulation, parameters.Get(4), $"Could not find model {parameters.Get(4)} for BJT {originalName}", @@ -93,6 +98,10 @@ public IEntity Generate(string componentIdentifier, string originalName, string context.SetParameter(bjt, "icvbe", asg.Values[0]); } } + else if (asg.Name.ToLower() == "l" || asg.Name.ToLower() == "w") + { + // Skip L and W parameters - they are used for model selection only + } else { context.SetParameter(bjt, asg.Name, asg); @@ -102,5 +111,25 @@ public IEntity Generate(string componentIdentifier, string originalName, string return bjt; } + + private double? GetLengthFromParameters(ParameterCollection parameters, IReadingContext context) + { + var lParameter = parameters.FirstOrDefault(p => p is AssignmentParameter ap && ap.Name.ToLower() == "l"); + if (lParameter != null && lParameter is AssignmentParameter lap) + { + return context.Evaluator.EvaluateDouble(lap.Value); + } + return null; + } + + private double? GetWidthFromParameters(ParameterCollection parameters, IReadingContext context) + { + var wParameter = parameters.FirstOrDefault(p => p is AssignmentParameter ap && ap.Name.ToLower() == "w"); + if (wParameter != null && wParameter is AssignmentParameter wap) + { + return context.Evaluator.EvaluateDouble(wap.Value); + } + return null; + } } } \ No newline at end of file diff --git a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Components/Semiconductors/DiodeGenerator.cs b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Components/Semiconductors/DiodeGenerator.cs index d90f53fb..ca200651 100644 --- a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Components/Semiconductors/DiodeGenerator.cs +++ b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Components/Semiconductors/DiodeGenerator.cs @@ -3,6 +3,7 @@ using SpiceSharpParser.ModelReaders.Netlist.Spice.Context; using SpiceSharpParser.Models.Netlist.Spice.Objects; using SpiceSharpParser.Models.Netlist.Spice.Objects.Parameters; +using System.Linq; namespace SpiceSharpParser.ModelReaders.Netlist.Spice.Readers.EntityGenerators.Components.Semiconductors { @@ -20,8 +21,13 @@ public IEntity Generate(string componentIdentifier, string originalName, string context.SimulationPreparations.ExecuteActionBeforeSetup((simulation) => { + double? l = GetLengthFromParameters(parameters, context); + double? w = GetWidthFromParameters(parameters, context); + context.ModelsRegistry.SetModel( diode, + l, + w, simulation, parameters.Get(2), $"Could not find model {parameters.Get(2)} for diode {originalName}", @@ -51,6 +57,11 @@ public IEntity Generate(string componentIdentifier, string originalName, string if (parameters[i] is AssignmentParameter asg) { + // Skip L and W parameters - they are used for model selection only + if (asg.Name.ToLower() == "l" || asg.Name.ToLower() == "w") + { + continue; + } context.SetParameter(diode, asg.Name, asg); } @@ -70,5 +81,25 @@ public IEntity Generate(string componentIdentifier, string originalName, string return diode; } + + private double? GetLengthFromParameters(ParameterCollection parameters, IReadingContext context) + { + var lParameter = parameters.FirstOrDefault(p => p is AssignmentParameter ap && ap.Name.ToLower() == "l"); + if (lParameter != null && lParameter is AssignmentParameter lap) + { + return context.Evaluator.EvaluateDouble(lap.Value); + } + return null; + } + + private double? GetWidthFromParameters(ParameterCollection parameters, IReadingContext context) + { + var wParameter = parameters.FirstOrDefault(p => p is AssignmentParameter ap && ap.Name.ToLower() == "w"); + if (wParameter != null && wParameter is AssignmentParameter wap) + { + return context.Evaluator.EvaluateDouble(wap.Value); + } + return null; + } } } \ No newline at end of file diff --git a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Components/Semiconductors/JFETGenerator.cs b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Components/Semiconductors/JFETGenerator.cs index 39ba9dd3..71939e1a 100644 --- a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Components/Semiconductors/JFETGenerator.cs +++ b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Components/Semiconductors/JFETGenerator.cs @@ -3,6 +3,7 @@ using SpiceSharpParser.ModelReaders.Netlist.Spice.Context; using SpiceSharpParser.Models.Netlist.Spice.Objects; using SpiceSharpParser.Models.Netlist.Spice.Objects.Parameters; +using System.Linq; namespace SpiceSharpParser.ModelReaders.Netlist.Spice.Readers.EntityGenerators.Components.Semiconductors { @@ -20,8 +21,13 @@ public IEntity Generate(string componentIdentifier, string originalName, string context.SimulationPreparations.ExecuteActionBeforeSetup((simulation) => { + double? l = GetLengthFromParameters(parameters, context); + double? w = GetWidthFromParameters(parameters, context); + context.ModelsRegistry.SetModel( jfet, + l, + w, simulation, parameters.Get(3), $"Could not find model {parameters.Get(3)} for JFET {originalName}", @@ -63,6 +69,10 @@ public IEntity Generate(string componentIdentifier, string originalName, string { context.SetParameter(jfet, "area", asg.Value); } + else if (asg.Name.ToLower() == "l" || asg.Name.ToLower() == "w") + { + // Skip L and W parameters - they are used for model selection only + } else { context.SetParameter(jfet, asg.Name, asg.Value); @@ -77,5 +87,25 @@ public IEntity Generate(string componentIdentifier, string originalName, string return jfet; } + + private double? GetLengthFromParameters(ParameterCollection parameters, IReadingContext context) + { + var lParameter = parameters.FirstOrDefault(p => p is AssignmentParameter ap && ap.Name.ToLower() == "l"); + if (lParameter != null && lParameter is AssignmentParameter lap) + { + return context.Evaluator.EvaluateDouble(lap.Value); + } + return null; + } + + private double? GetWidthFromParameters(ParameterCollection parameters, IReadingContext context) + { + var wParameter = parameters.FirstOrDefault(p => p is AssignmentParameter ap && ap.Name.ToLower() == "w"); + if (wParameter != null && wParameter is AssignmentParameter wap) + { + return context.Evaluator.EvaluateDouble(wap.Value); + } + return null; + } } } \ No newline at end of file diff --git a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Components/Semiconductors/MosfetGenerator.cs b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Components/Semiconductors/MosfetGenerator.cs index 3be4c2fc..aed9e34a 100644 --- a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Components/Semiconductors/MosfetGenerator.cs +++ b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Components/Semiconductors/MosfetGenerator.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using SpiceSharp.Components; using SpiceSharp.Entities; using SpiceSharpParser.Common.Validation; @@ -95,8 +96,13 @@ public override IEntity Generate(string componentIdentifier, string originalName context.SimulationPreparations.ExecuteActionBeforeSetup((simulation) => { + double? l = GetLengthFromParameters(parameters, context); + double? w = GetWidthFromParameters(parameters, context); + context.ModelsRegistry.SetModel( mosfetDetails.Mosfet, + l, + w, simulation, modelNameParameter, $"Could not find model {modelNameParameter} for mosfet {componentIdentifier}", @@ -161,6 +167,26 @@ public override IEntity Generate(string componentIdentifier, string originalName return mosfet; } + private double? GetLengthFromParameters(ParameterCollection parameters, IReadingContext context) + { + var lParameter = parameters.FirstOrDefault(p => p is AssignmentParameter ap && ap.Name.ToLower() == "l"); + if (lParameter != null && lParameter is AssignmentParameter lap) + { + return context.Evaluator.EvaluateDouble(lap.Value); + } + return null; + } + + private double? GetWidthFromParameters(ParameterCollection parameters, IReadingContext context) + { + var wParameter = parameters.FirstOrDefault(p => p is AssignmentParameter ap && ap.Name.ToLower() == "w"); + if (wParameter != null && wParameter is AssignmentParameter wap) + { + return context.Evaluator.EvaluateDouble(wap.Value); + } + return null; + } + protected class MosfetDetails { public SpiceSharp.Components.Component Mosfet { get; set; } diff --git a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Components/SwitchGenerator.cs b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Components/SwitchGenerator.cs index e68ced45..39c593a2 100644 --- a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Components/SwitchGenerator.cs +++ b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Components/SwitchGenerator.cs @@ -112,6 +112,8 @@ protected IEntity GenerateVoltageSwitch(string name, ParameterCollection paramet { context.ModelsRegistry.SetModel( vsw, + null, + null, simulation, parameters.Get(4), $"Could not find model {parameters.Get(4)} for voltage switch {name}", @@ -270,6 +272,8 @@ private IEntity GenerateCurrentSwitch(string name, ParameterCollection parameter { context.ModelsRegistry.SetModel( csw, + null, + null, simulation, parameters.Get(3), $"Could not find model {parameters.Get(3)} for current switch {name}", diff --git a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Models/BipolarModelGenerator.cs b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Models/BipolarModelGenerator.cs index 6e6e8c57..caebb899 100644 --- a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Models/BipolarModelGenerator.cs +++ b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Models/BipolarModelGenerator.cs @@ -8,20 +8,22 @@ public class BipolarModelGenerator : ModelGenerator { public override Model Generate(string id, string type, SpiceSharpParser.Models.Netlist.Spice.Objects.ParameterCollection parameters, IReadingContext context) { - BipolarJunctionTransistorModel model = new BipolarJunctionTransistorModel(id); + BipolarJunctionTransistorModel bjtModel = new BipolarJunctionTransistorModel(id); if (type.ToLower() == "npn") { - model.SetParameter("npn", true); + bjtModel.SetParameter("npn", true); } else if (type.ToLower() == "pnp") { - model.SetParameter("pnp", true); + bjtModel.SetParameter("pnp", true); } - SetParameters(context, model, parameters); + var contextModel = new Model(id, bjtModel, bjtModel.Parameters); + SetParameters(context, bjtModel, parameters); + SetDimensionParameters(context, contextModel, parameters); - return new Model(id, model, model.Parameters); + return contextModel; } } } \ No newline at end of file diff --git a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Models/DiodeModelGenerator.cs b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Models/DiodeModelGenerator.cs index 3e4b0d5b..30ffaad3 100644 --- a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Models/DiodeModelGenerator.cs +++ b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Models/DiodeModelGenerator.cs @@ -9,10 +9,12 @@ public class DiodeModelGenerator : ModelGenerator { public override Model Generate(string id, string type, ParameterCollection parameters, IReadingContext context) { - var model = new DiodeModel(id); - SetParameters(context, model, parameters); + var diodeModel = new DiodeModel(id); + var contextModel = new Model(id, diodeModel, diodeModel.Parameters); + SetParameters(context, diodeModel, parameters); + SetDimensionParameters(context, contextModel, parameters); - return new Model(id, model, model.Parameters); + return contextModel; } } } \ No newline at end of file diff --git a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Models/JFETModelGenerator.cs b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Models/JFETModelGenerator.cs index 9ba3f6f0..e018bcae 100644 --- a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Models/JFETModelGenerator.cs +++ b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Models/JFETModelGenerator.cs @@ -8,15 +8,17 @@ public class JFETModelGenerator : ModelGenerator { public override Context.Models.Model Generate(string id, string type, ParameterCollection parameters, IReadingContext context) { - var model = new JFETModel(id); + var jfetModel = new JFETModel(id); switch (type.ToLower()) { - case "pjf": model.SetParameter("pjf", true); break; - case "njf": model.SetParameter("njf", true); break; + case "pjf": jfetModel.SetParameter("pjf", true); break; + case "njf": jfetModel.SetParameter("njf", true); break; } - SetParameters(context, model, parameters); - return new Context.Models.Model(id, model, model.Parameters); + var contextModel = new Context.Models.Model(id, jfetModel, jfetModel.Parameters); + SetParameters(context, jfetModel, parameters); + SetDimensionParameters(context, contextModel, parameters); + return contextModel; } } } \ No newline at end of file diff --git a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Models/ModelGenerator.cs b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Models/ModelGenerator.cs index ff456d93..f1247b33 100644 --- a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Models/ModelGenerator.cs +++ b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Models/ModelGenerator.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using SpiceSharp.Entities; using SpiceSharpParser.Common.Validation; using SpiceSharpParser.ModelReaders.Netlist.Spice.Context; @@ -9,6 +10,14 @@ namespace SpiceSharpParser.ModelReaders.Netlist.Spice.Readers.EntityGenerators.M { public abstract class ModelGenerator : IModelGenerator { + /// + /// The dimension parameters used for model selection (not to be set on SpiceSharp entities). + /// + private static readonly HashSet DimensionParameterNames = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "lmin", "lmax", "wmin", "wmax" + }; + public abstract Context.Models.Model Generate(string id, string type, ParameterCollection parameters, IReadingContext context); protected void SetParameters(IReadingContext context, IEntity entity, ParameterCollection parameters, bool onload = true) @@ -17,6 +26,12 @@ protected void SetParameters(IReadingContext context, IEntity entity, ParameterC { if (parameter is AssignmentParameter ap) { + // Skip dimension parameters - they are handled separately + if (DimensionParameterNames.Contains(ap.Name)) + { + continue; + } + try { context.SetParameter(entity, ap.Name, ap.Value, onload); @@ -32,5 +47,30 @@ protected void SetParameters(IReadingContext context, IEntity entity, ParameterC } } } + + /// + /// Sets dimension parameters (lmin, lmax, wmin, wmax) on a model for model selection. + /// + /// The reading context. + /// The model to set dimension parameters on. + /// The parameters collection. + protected void SetDimensionParameters(IReadingContext context, Context.Models.Model model, ParameterCollection parameters) + { + foreach (Parameter parameter in parameters) + { + if (parameter is AssignmentParameter ap && DimensionParameterNames.Contains(ap.Name)) + { + try + { + var value = context.Evaluator.EvaluateDouble(ap.Value); + model.SetDimensionParameter(ap.Name, value); + } + catch (Exception ex) + { + context.Result.ValidationResult.AddError(ValidationEntrySource.Reader, $"Problem with setting dimension parameter: {parameter}", parameter.LineInfo, ex); + } + } + } + } } } \ No newline at end of file diff --git a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Models/MosfetModelGenerator.cs b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Models/MosfetModelGenerator.cs index 4bec0787..b4b198dc 100644 --- a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Models/MosfetModelGenerator.cs +++ b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Models/MosfetModelGenerator.cs @@ -149,6 +149,9 @@ public override Context.Models.Model Generate(string id, string type, ParameterC // Read all the parameters SetParameters(context, model.Entity, clonedParameters); + // Set dimension parameters for model selection (lmin, lmax, wmin, wmax) + SetDimensionParameters(context, model, clonedParameters); + return model; } } diff --git a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Models/RLCModelGenerator.cs b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Models/RLCModelGenerator.cs index 2294b63d..1b7f6566 100644 --- a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Models/RLCModelGenerator.cs +++ b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Models/RLCModelGenerator.cs @@ -12,14 +12,18 @@ public override Context.Models.Model Generate(string id, string type, ParameterC { case "res": case "r": - var model = new ResistorModel(id); - SetParameters(context, model, parameters); - return new Context.Models.Model(id, model, model.Parameters); + var resistorModel = new ResistorModel(id); + var resistorContextModel = new Context.Models.Model(id, resistorModel, resistorModel.Parameters); + SetParameters(context, resistorModel, parameters); + SetDimensionParameters(context, resistorContextModel, parameters); + return resistorContextModel; case "c": - var model2 = new CapacitorModel(id); - SetParameters(context, model2, parameters); - return new Context.Models.Model(id, model2, model2.Parameters); + var capacitorModel = new CapacitorModel(id); + var capacitorContextModel = new Context.Models.Model(id, capacitorModel, capacitorModel.Parameters); + SetParameters(context, capacitorModel, parameters); + SetDimensionParameters(context, capacitorContextModel, parameters); + return capacitorContextModel; } return null; diff --git a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Models/SwitchModelGenerator.cs b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Models/SwitchModelGenerator.cs index 1ce63cf1..e8f6e462 100644 --- a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Models/SwitchModelGenerator.cs +++ b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Models/SwitchModelGenerator.cs @@ -11,23 +11,32 @@ public override Context.Models.Model Generate(string id, string type, ParameterC { switch (type.ToLower()) { - case "sw": var model = new VoltageSwitchModel(id); - SetParameters(context, model, parameters); - return new Context.Models.Model(id, model, model.Parameters); + case "sw": + var vsModel = new VoltageSwitchModel(id); + var vsContextModel = new Context.Models.Model(id, vsModel, vsModel.Parameters); + SetParameters(context, vsModel, parameters); + SetDimensionParameters(context, vsContextModel, parameters); + return vsContextModel; case "csw": - var model2 = new CurrentSwitchModel(id); - SetParameters(context, model2, parameters); - return new Context.Models.Model(id, model2, model2.Parameters); + var csModel = new CurrentSwitchModel(id); + var csContextModel = new Context.Models.Model(id, csModel, csModel.Parameters); + SetParameters(context, csModel, parameters); + SetDimensionParameters(context, csContextModel, parameters); + return csContextModel; case "vswitch": var vSwitchModel = new VSwitchModel(id); + var vSwitchContextModel = new Context.Models.Model(id, vSwitchModel, vSwitchModel.Parameters); SetParameters(context, vSwitchModel, parameters); - return new Context.Models.Model(id, vSwitchModel, vSwitchModel.Parameters); + SetDimensionParameters(context, vSwitchContextModel, parameters); + return vSwitchContextModel; case "iswitch": var iSwitchModel = new ISwitchModel(id); + var iSwitchContextModel = new Context.Models.Model(id, iSwitchModel, iSwitchModel.Parameters); SetParameters(context, iSwitchModel, parameters); - return new Context.Models.Model(id, iSwitchModel, iSwitchModel.Parameters); + SetDimensionParameters(context, iSwitchContextModel, parameters); + return iSwitchContextModel; } return null; diff --git a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/StochasticModelsGenerator.cs b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/StochasticModelsGenerator.cs index 5d1af38a..dbea6bfe 100644 --- a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/StochasticModelsGenerator.cs +++ b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/StochasticModelsGenerator.cs @@ -82,18 +82,8 @@ public Context.Models.Model GenerateModel(IModelGenerator modelGenerator, string RegisterDevAndLotModels(parameters, stochasticModelRegistry, model, (modelId) => { var stochasticCandidate = modelGenerator.Generate(modelId, type, filteredParameters, context); - - if (modelGenerator is ICustomModelGenerator custom) - { - var model = custom.Process(stochasticCandidate, context.ModelsRegistry); - context.ModelsRegistry.RegisterModelInstance(model); - return model; - } - else - { - context.ModelsRegistry.RegisterModelInstance(stochasticCandidate); - return stochasticCandidate; - } + context.ModelsRegistry.RegisterModelInstance(stochasticCandidate); + return stochasticCandidate; }); return model; } From 5437acefa900e4f6e1e354a68477444a3533cfca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Go=C5=82=C4=99biowski?= Date: Sun, 1 Mar 2026 18:33:49 +0100 Subject: [PATCH 4/6] Make it more generic --- .../Components/ModelDimensionTests.cs | 130 ++++++++++++++++++ .../Spice/Context/Models/IModelsRegistry.cs | 4 +- .../Netlist/Spice/Context/Models/Model.cs | 21 +-- .../Models/StochasticModelsRegistry.cs | 98 +++---------- .../Components/ComponentGenerator.cs | 51 +++++++ .../Components/RLCKGenerator.cs | 46 ++----- .../BipolarJunctionTransistorGenerator.cs | 27 +--- .../Semiconductors/DiodeGenerator.cs | 27 +--- .../Semiconductors/JFETGenerator.cs | 27 +--- .../Semiconductors/MosfetGenerator.cs | 27 +--- .../Components/SwitchGenerator.cs | 2 - .../Models/BipolarModelGenerator.cs | 3 +- .../Models/DiodeModelGenerator.cs | 3 +- .../Models/JFETModelGenerator.cs | 3 +- .../EntityGenerators/Models/ModelGenerator.cs | 54 +++----- .../Models/MosfetModelGenerator.cs | 7 +- .../Models/RLCModelGenerator.cs | 6 +- .../Models/SwitchModelGenerator.cs | 12 +- 18 files changed, 275 insertions(+), 273 deletions(-) diff --git a/src/SpiceSharpParser.IntegrationTests/Components/ModelDimensionTests.cs b/src/SpiceSharpParser.IntegrationTests/Components/ModelDimensionTests.cs index 3f594227..cf1748b7 100644 --- a/src/SpiceSharpParser.IntegrationTests/Components/ModelDimensionTests.cs +++ b/src/SpiceSharpParser.IntegrationTests/Components/ModelDimensionTests.cs @@ -417,6 +417,136 @@ public void JFETWithOnlyLengthParameter() #endregion + #region Non-Numeric Model Suffix Tests + + [Fact] + public void MosfetSelectsModelWithNonNumericSuffix() + { + var netlist = GetSpiceSharpModel( + "MOSFET model selection with non-numeric suffix", + "M1 D G S B NMOS L=0.5u W=10u", + "M2 D G S B NMOS L=5u W=10u", + ".model NMOS.small NMOS level=1 lmin=0.1u lmax=1u", + ".model NMOS.large NMOS level=1 lmin=1u lmax=10u", + ".END"); + + Assert.NotNull(netlist); + Assert.False(netlist.ValidationResult.HasError); + + var mosfet1 = netlist.Circuit["m1"] as Mosfet1; + Assert.NotNull(mosfet1); + + var mosfet2 = netlist.Circuit["m2"] as Mosfet1; + Assert.NotNull(mosfet2); + + Assert.NotNull(netlist.Circuit["NMOS.small"]); + Assert.NotNull(netlist.Circuit["NMOS.large"]); + } + + [Fact] + public void MosfetSelectsModelWithArbitrarySuffix() + { + var netlist = GetSpiceSharpModel( + "MOSFET model selection with arbitrary suffix", + "M1 D G S B NMOS L=0.5u W=10u", + "M2 D G S B NMOS L=5u W=10u", + ".model NMOS.hp NMOS level=1 lmin=0.1u lmax=1u", + ".model NMOS.lp NMOS level=1 lmin=1u lmax=10u", + ".END"); + + Assert.NotNull(netlist); + Assert.False(netlist.ValidationResult.HasError); + + var mosfet1 = netlist.Circuit["m1"] as Mosfet1; + Assert.NotNull(mosfet1); + + var mosfet2 = netlist.Circuit["m2"] as Mosfet1; + Assert.NotNull(mosfet2); + } + + [Fact] + public void ModelWithNonSequentialNumericSuffixes() + { + var netlist = GetSpiceSharpModel( + "Model with non-sequential numeric suffixes", + "M1 D G S B NMOS L=0.5u W=10u", + "M2 D G S B NMOS L=5u W=10u", + ".model NMOS.3 NMOS level=1 lmin=0.1u lmax=1u", + ".model NMOS.7 NMOS level=1 lmin=1u lmax=10u", + ".END"); + + Assert.NotNull(netlist); + Assert.False(netlist.ValidationResult.HasError); + + var mosfet1 = netlist.Circuit["m1"] as Mosfet1; + Assert.NotNull(mosfet1); + + var mosfet2 = netlist.Circuit["m2"] as Mosfet1; + Assert.NotNull(mosfet2); + } + + [Fact] + public void FallsBackToBaseModelWhenNoSuffixedModelMatches() + { + var netlist = GetSpiceSharpModel( + "Falls back to base model when no suffixed model matches", + "M1 D G S B NMOS L=100u W=100u", + ".model NMOS.small NMOS level=1 lmin=0.1u lmax=1u", + ".model NMOS.large NMOS level=1 lmin=1u lmax=10u", + ".model NMOS NMOS level=1", + ".END"); + + Assert.NotNull(netlist); + Assert.False(netlist.ValidationResult.HasError); + + var mosfet = netlist.Circuit["m1"] as Mosfet1; + Assert.NotNull(mosfet); + } + + [Fact] + public void DiodeSelectsModelWithNonNumericSuffix() + { + var netlist = GetSpiceSharpModel( + "Diode model selection with non-numeric suffix", + "D1 A K DMOD L=0.5u W=5u", + "D2 A K DMOD L=5u W=50u", + ".model DMOD.fast D lmin=0.1u lmax=1u wmin=1u wmax=10u", + ".model DMOD.slow D lmin=1u lmax=10u wmin=10u wmax=100u", + ".END"); + + Assert.NotNull(netlist); + Assert.False(netlist.ValidationResult.HasError); + + var diode1 = netlist.Circuit["d1"] as Diode; + Assert.NotNull(diode1); + + var diode2 = netlist.Circuit["d2"] as Diode; + Assert.NotNull(diode2); + } + + [Fact] + public void ResistorSelectsModelWithNonNumericSuffix() + { + var netlist = GetSpiceSharpModel( + "Resistor model selection with non-numeric suffix", + "R1 1 0 RMOD L=0.5u W=5u", + "R2 1 0 RMOD L=5u W=50u", + ".model RMOD.thin R RSH=1 lmin=0.1u lmax=1u wmin=1u wmax=10u", + ".model RMOD.wide R RSH=1 lmin=1u lmax=10u wmin=10u wmax=100u", + ".END"); + + Assert.NotNull(netlist); + Assert.False(netlist.ValidationResult.HasError); + + var resistor1 = netlist.Circuit["r1"] as Resistor; + Assert.NotNull(resistor1); + + var resistor2 = netlist.Circuit["r2"] as Resistor; + Assert.NotNull(resistor2); + } + + #endregion + #region Edge Cases [Fact] diff --git a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Context/Models/IModelsRegistry.cs b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Context/Models/IModelsRegistry.cs index e8d11bc4..fd5fbd8a 100644 --- a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Context/Models/IModelsRegistry.cs +++ b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Context/Models/IModelsRegistry.cs @@ -8,11 +8,11 @@ namespace SpiceSharpParser.ModelReaders.Netlist.Spice.Context.Models { public interface IModelsRegistry { - void SetModel(Entity entity, double? l, double? w, ISimulationWithEvents simulation, Parameter modelNameParameter, string exceptionMessage, Action setModelAction, IReadingContext context); + void SetModel(Entity entity, Func predicate, ISimulationWithEvents simulation, Parameter modelNameParameter, string exceptionMessage, Action setModelAction, IReadingContext context); Model FindModel(string modelName); - IEntity FindModelEntity(string modelName, double? l, double? w); + IEntity FindModelEntity(string modelName, Func predicate); void RegisterModelInstance(Model model); diff --git a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Context/Models/Model.cs b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Context/Models/Model.cs index a03826bf..3841e6df 100644 --- a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Context/Models/Model.cs +++ b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Context/Models/Model.cs @@ -1,4 +1,4 @@ -using SpiceSharp.Entities; +using SpiceSharp.Entities; using SpiceSharp.ParameterSets; using System.Collections.Generic; @@ -6,7 +6,7 @@ namespace SpiceSharpParser.ModelReaders.Netlist.Spice.Context.Models { public class Model { - private readonly Dictionary _dimensionParameters = new Dictionary(System.StringComparer.OrdinalIgnoreCase); + private readonly Dictionary _selectionParameters = new Dictionary(System.StringComparer.OrdinalIgnoreCase); public Model(string name, IEntity entity, IParameterSet parameters) { @@ -22,24 +22,29 @@ public Model(string name, IEntity entity, IParameterSet parameters) public IParameterSet Parameters { get; } /// - /// Sets a dimension parameter (lmin, lmax, wmin, wmax) for model selection. + /// Gets all selection parameters as a read-only dictionary. + /// + public IReadOnlyDictionary SelectionParameters => _selectionParameters; + + /// + /// Sets a selection parameter for model matching. /// /// The parameter name. /// The parameter value. - public void SetDimensionParameter(string parameterName, double value) + public void SetSelectionParameter(string parameterName, double value) { - _dimensionParameters[parameterName] = value; + _selectionParameters[parameterName] = value; } /// - /// Gets a dimension parameter (lmin, lmax, wmin, wmax) for model selection. + /// Gets a selection parameter for model matching. /// /// The parameter name. /// The parameter value. /// True if the parameter exists. - public bool TryGetDimensionParameter(string parameterName, out double value) + public bool TryGetSelectionParameter(string parameterName, out double value) { - return _dimensionParameters.TryGetValue(parameterName, out value); + return _selectionParameters.TryGetValue(parameterName, out value); } } } diff --git a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Context/Models/StochasticModelsRegistry.cs b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Context/Models/StochasticModelsRegistry.cs index e985e232..b8b5cc91 100644 --- a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Context/Models/StochasticModelsRegistry.cs +++ b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Context/Models/StochasticModelsRegistry.cs @@ -232,9 +232,9 @@ public Dictionary GetStochasticModelLotParameter return null; } - public void SetModel(Entity entity, double? l, double? w, ISimulationWithEvents simulation, Parameter modelNameParameter, string exceptionMessage, Action setModelAction, IReadingContext context) + public void SetModel(Entity entity, Func predicate, ISimulationWithEvents simulation, Parameter modelNameParameter, string exceptionMessage, Action setModelAction, IReadingContext context) { - var model = FindModelEntity(modelNameParameter.Value, l, w); + var model = FindModelWithPredicate(modelNameParameter.Value, predicate); if (model == null) { @@ -242,7 +242,7 @@ public void SetModel(Entity entity, double? l, double? w, ISimulationWithEvents return; } - var stochasticModel = ProvideStochasticModel(entity.Name, simulation, new Model(model.Name, model, model.ParameterSets.First())); + var stochasticModel = ProvideStochasticModel(entity.Name, simulation, model); setModelAction(stochasticModel); if (stochasticModel != null) @@ -256,103 +256,45 @@ public void SetModel(Entity entity, double? l, double? w, ISimulationWithEvents public Model FindModel(string modelName) { - foreach (var generator in NamesGenerators) - { - var modelNameToSearch = generator.GenerateObjectName(modelName); - - if (AllModels.TryGetValue(modelNameToSearch, out var model)) - { - return model; - } - - // Check for dimension-based model naming convention (e.g., MODELNAME.0, MODELNAME.1) - for (var i = 0; i < AllModels.Count; i++) - { - var key = AllModels.Keys.ElementAt(i); - - if (key == modelNameToSearch + "." + i) - { - if (AllModels.TryGetValue(key, out var modelInstance)) - { - return modelInstance; - } - } - } - } + return FindModelWithPredicate(modelName, null); + } - return null; + public IEntity FindModelEntity(string modelName, Func predicate) + { + return FindModelWithPredicate(modelName, predicate)?.Entity; } - public IEntity FindModelEntity(string modelName, double? l, double? w) + private Model FindModelWithPredicate(string modelName, Func predicate) { + var comparison = IsModelNameCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase; + foreach (var generator in NamesGenerators) { var modelNameToSearch = generator.GenerateObjectName(modelName); + var prefix = modelNameToSearch + "."; - for (var i = 0; i < AllModels.Count; i++) + // Check suffixed models (e.g., MODELNAME.small, MODELNAME.0) + foreach (var kvp in AllModels) { - var key = AllModels.Keys.ElementAt(i); - - if (key == modelNameToSearch + "." + i) + if (kvp.Key.StartsWith(prefix, comparison) && kvp.Key.Length > prefix.Length) { - if (AllModels.TryGetValue(key, out var modelInstance)) + if (predicate == null || predicate(kvp.Value)) { - if (HasGoodSize(modelInstance, l, w)) - { - return modelInstance.Entity; - } + return kvp.Value; } } } - - + // Fall back to exact match (base model without suffix) if (AllModels.TryGetValue(modelNameToSearch, out var model)) { - return model.Entity; + return model; } } return null; } - private bool HasGoodSize(Model model, double? l, double? w) - { - model.TryGetDimensionParameter("lmin", out double lminValue); - model.TryGetDimensionParameter("lmax", out double lmaxValue); - model.TryGetDimensionParameter("wmin", out double wminValue); - model.TryGetDimensionParameter("wmax", out double wmaxValue); - - // Check L dimension - if (l.HasValue) - { - if (lminValue > 0 && l.Value < lminValue) - { - return false; - } - - if (lmaxValue > 0 && l.Value > lmaxValue) - { - return false; - } - } - - // Check W dimension - if (w.HasValue) - { - if (wminValue > 0 && w.Value < wminValue) - { - return false; - } - if (wmaxValue > 0 && w.Value > wmaxValue) - { - return false; - } - } - - return true; - } - public IModelsRegistry CreateChildRegistry(List generators) { var result = new StochasticModelsRegistry(generators, IsModelNameCaseSensitive) @@ -366,4 +308,4 @@ public IModelsRegistry CreateChildRegistry(List generators) return result; } } -} \ No newline at end of file +} diff --git a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Components/ComponentGenerator.cs b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Components/ComponentGenerator.cs index 815f2128..5bb5f925 100644 --- a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Components/ComponentGenerator.cs +++ b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Components/ComponentGenerator.cs @@ -1,7 +1,9 @@ using System; +using System.Linq; using SpiceSharp.Entities; using SpiceSharpParser.Common.Validation; using SpiceSharpParser.ModelReaders.Netlist.Spice.Context; +using SpiceSharpParser.ModelReaders.Netlist.Spice.Context.Models; using SpiceSharpParser.Models.Netlist.Spice.Objects; using SpiceSharpParser.Models.Netlist.Spice.Objects.Parameters; @@ -11,6 +13,55 @@ public abstract class ComponentGenerator : IComponentGenerator { public abstract IEntity Generate(string componentIdentifier, string originalName, string type, ParameterCollection parameters, IReadingContext context); + /// + /// Creates a predicate that checks whether a model's selection parameters + /// satisfy min/max range constraints for the given instance parameter values. + /// For each (name, value) pair, checks model's {name}min and {name}max. + /// + public static Func CreateRangePredicate(params (string name, double? value)[] parameters) + { + if (parameters.All(p => p.value == null)) + { + return null; + } + + return model => + { + foreach (var (name, value) in parameters) + { + if (!value.HasValue) + { + continue; + } + + if (model.TryGetSelectionParameter(name + "min", out double min) && min > 0 && value.Value < min) + { + return false; + } + + if (model.TryGetSelectionParameter(name + "max", out double max) && max > 0 && value.Value > max) + { + return false; + } + } + + return true; + }; + } + + /// + /// Gets the value of a named assignment parameter from the collection, or null if not found. + /// + public static double? GetAssignmentParameterValue(string name, ParameterCollection parameters, IReadingContext context) + { + var parameter = parameters.FirstOrDefault(p => p is AssignmentParameter ap && ap.Name.ToLower() == name); + if (parameter is AssignmentParameter ap) + { + return context.Evaluator.EvaluateDouble(ap.Value); + } + return null; + } + protected void SetParameters(IReadingContext context, IEntity entity, ParameterCollection parameters) { foreach (Parameter parameter in parameters) diff --git a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Components/RLCKGenerator.cs b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Components/RLCKGenerator.cs index 6ab4aa93..705aa089 100644 --- a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Components/RLCKGenerator.cs +++ b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Components/RLCKGenerator.cs @@ -205,13 +205,12 @@ protected IComponent GenerateCap(string name, ParameterCollection parameters, IR { context.SimulationPreparations.ExecuteActionBeforeSetup((simulation) => { - double? l = GetLengthFromParameters(parameters, context); - double? w = GetWidthFromParameters(parameters, context); + double? l = GetAssignmentParameterValue("l", parameters, context); + double? w = GetAssignmentParameterValue("w", parameters, context); context.ModelsRegistry.SetModel( capacitor, - l, - w, + CreateRangePredicate(("l", l), ("w", w)), simulation, parameters.Get(2), $"Could not find model {parameters.Get(2)} for capacitor {name}", @@ -254,9 +253,9 @@ protected IComponent GenerateCap(string name, ParameterCollection parameters, IR if (modelBased) { - double? l = GetLengthFromParameters(parameters, context); - double? w = GetWidthFromParameters(parameters, context); - var model = context.ModelsRegistry.FindModelEntity(parameters.Get(2).Value, l, w); + double? l = GetAssignmentParameterValue("l", parameters, context); + double? w = GetAssignmentParameterValue("w", parameters, context); + var model = context.ModelsRegistry.FindModelEntity(parameters.Get(2).Value, CreateRangePredicate(("l", l), ("w", w))); if (tcParameterAssignment.Values.Count == 2) { @@ -424,13 +423,12 @@ protected IEntity GenerateRes(string name, ParameterCollection parameters, IRead { context.SimulationPreparations.ExecuteActionBeforeSetup((simulation) => { - double? l = GetLengthFromParameters(parameters, context); - double? w = GetWidthFromParameters(parameters, context); + double? l = GetAssignmentParameterValue("l", parameters, context); + double? w = GetAssignmentParameterValue("w", parameters, context); context.ModelsRegistry.SetModel( res, - l, - w, + CreateRangePredicate(("l", l), ("w", w)), simulation, something, $"Could not find model {something} for resistor {name}", @@ -489,13 +487,12 @@ protected IEntity GenerateRes(string name, ParameterCollection parameters, IRead // Ignore tc parameter on resistor ... context.SimulationPreparations.ExecuteActionBeforeSetup((simulation) => { - double? l = GetLengthFromParameters(parameters, context); - double? w = GetWidthFromParameters(parameters, context); + double? l = GetAssignmentParameterValue("l", parameters, context); + double? w = GetAssignmentParameterValue("w", parameters, context); context.ModelsRegistry.SetModel( res, - l, - w, + CreateRangePredicate(("l", l), ("w", w)), simulation, modelNameParameter, $"Could not find model {modelNameParameter} for resistor {name}", @@ -616,24 +613,5 @@ private string MultiplyIfNeeded(string expression, string mExpression, string nE return expression; } - private double? GetLengthFromParameters(ParameterCollection parameters, IReadingContext context) - { - var lParameter = parameters.FirstOrDefault(p => p is AssignmentParameter ap && ap.Name.ToLower() == "l"); - if (lParameter != null && lParameter is AssignmentParameter lap) - { - return context.Evaluator.EvaluateDouble(lap.Value); - } - return null; - } - - private double? GetWidthFromParameters(ParameterCollection parameters, IReadingContext context) - { - var wParameter = parameters.FirstOrDefault(p => p is AssignmentParameter ap && ap.Name.ToLower() == "w"); - if (wParameter != null && wParameter is AssignmentParameter wap) - { - return context.Evaluator.EvaluateDouble(wap.Value); - } - return null; - } } } \ No newline at end of file diff --git a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Components/Semiconductors/BipolarJunctionTransistorGenerator.cs b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Components/Semiconductors/BipolarJunctionTransistorGenerator.cs index c8c73d06..c437fed5 100644 --- a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Components/Semiconductors/BipolarJunctionTransistorGenerator.cs +++ b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Components/Semiconductors/BipolarJunctionTransistorGenerator.cs @@ -2,6 +2,7 @@ using SpiceSharp.Entities; using SpiceSharpParser.Common.Validation; using SpiceSharpParser.ModelReaders.Netlist.Spice.Context; +using SpiceSharpParser.ModelReaders.Netlist.Spice.Readers.EntityGenerators.Components; using SpiceSharpParser.Models.Netlist.Spice.Objects; using SpiceSharpParser.Models.Netlist.Spice.Objects.Parameters; using System.Linq; @@ -38,13 +39,12 @@ public IEntity Generate(string componentIdentifier, string originalName, string context.CreateNodes(bjt, parameters.Take(BipolarJunctionTransistor.PinCount)); context.SimulationPreparations.ExecuteActionBeforeSetup((simulation) => { - double? l = GetLengthFromParameters(parameters, context); - double? w = GetWidthFromParameters(parameters, context); + double? l = ComponentGenerator.GetAssignmentParameterValue("l", parameters, context); + double? w = ComponentGenerator.GetAssignmentParameterValue("w", parameters, context); context.ModelsRegistry.SetModel( bjt, - l, - w, + ComponentGenerator.CreateRangePredicate(("l", l), ("w", w)), simulation, parameters.Get(4), $"Could not find model {parameters.Get(4)} for BJT {originalName}", @@ -112,24 +112,5 @@ public IEntity Generate(string componentIdentifier, string originalName, string return bjt; } - private double? GetLengthFromParameters(ParameterCollection parameters, IReadingContext context) - { - var lParameter = parameters.FirstOrDefault(p => p is AssignmentParameter ap && ap.Name.ToLower() == "l"); - if (lParameter != null && lParameter is AssignmentParameter lap) - { - return context.Evaluator.EvaluateDouble(lap.Value); - } - return null; - } - - private double? GetWidthFromParameters(ParameterCollection parameters, IReadingContext context) - { - var wParameter = parameters.FirstOrDefault(p => p is AssignmentParameter ap && ap.Name.ToLower() == "w"); - if (wParameter != null && wParameter is AssignmentParameter wap) - { - return context.Evaluator.EvaluateDouble(wap.Value); - } - return null; - } } } \ No newline at end of file diff --git a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Components/Semiconductors/DiodeGenerator.cs b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Components/Semiconductors/DiodeGenerator.cs index ca200651..f6e75b85 100644 --- a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Components/Semiconductors/DiodeGenerator.cs +++ b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Components/Semiconductors/DiodeGenerator.cs @@ -1,6 +1,7 @@ using SpiceSharp.Components; using SpiceSharp.Entities; using SpiceSharpParser.ModelReaders.Netlist.Spice.Context; +using SpiceSharpParser.ModelReaders.Netlist.Spice.Readers.EntityGenerators.Components; using SpiceSharpParser.Models.Netlist.Spice.Objects; using SpiceSharpParser.Models.Netlist.Spice.Objects.Parameters; using System.Linq; @@ -21,13 +22,12 @@ public IEntity Generate(string componentIdentifier, string originalName, string context.SimulationPreparations.ExecuteActionBeforeSetup((simulation) => { - double? l = GetLengthFromParameters(parameters, context); - double? w = GetWidthFromParameters(parameters, context); + double? l = ComponentGenerator.GetAssignmentParameterValue("l", parameters, context); + double? w = ComponentGenerator.GetAssignmentParameterValue("w", parameters, context); context.ModelsRegistry.SetModel( diode, - l, - w, + ComponentGenerator.CreateRangePredicate(("l", l), ("w", w)), simulation, parameters.Get(2), $"Could not find model {parameters.Get(2)} for diode {originalName}", @@ -82,24 +82,5 @@ public IEntity Generate(string componentIdentifier, string originalName, string return diode; } - private double? GetLengthFromParameters(ParameterCollection parameters, IReadingContext context) - { - var lParameter = parameters.FirstOrDefault(p => p is AssignmentParameter ap && ap.Name.ToLower() == "l"); - if (lParameter != null && lParameter is AssignmentParameter lap) - { - return context.Evaluator.EvaluateDouble(lap.Value); - } - return null; - } - - private double? GetWidthFromParameters(ParameterCollection parameters, IReadingContext context) - { - var wParameter = parameters.FirstOrDefault(p => p is AssignmentParameter ap && ap.Name.ToLower() == "w"); - if (wParameter != null && wParameter is AssignmentParameter wap) - { - return context.Evaluator.EvaluateDouble(wap.Value); - } - return null; - } } } \ No newline at end of file diff --git a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Components/Semiconductors/JFETGenerator.cs b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Components/Semiconductors/JFETGenerator.cs index 71939e1a..9b3b093e 100644 --- a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Components/Semiconductors/JFETGenerator.cs +++ b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Components/Semiconductors/JFETGenerator.cs @@ -1,6 +1,7 @@ using SpiceSharp.Components; using SpiceSharp.Entities; using SpiceSharpParser.ModelReaders.Netlist.Spice.Context; +using SpiceSharpParser.ModelReaders.Netlist.Spice.Readers.EntityGenerators.Components; using SpiceSharpParser.Models.Netlist.Spice.Objects; using SpiceSharpParser.Models.Netlist.Spice.Objects.Parameters; using System.Linq; @@ -21,13 +22,12 @@ public IEntity Generate(string componentIdentifier, string originalName, string context.SimulationPreparations.ExecuteActionBeforeSetup((simulation) => { - double? l = GetLengthFromParameters(parameters, context); - double? w = GetWidthFromParameters(parameters, context); + double? l = ComponentGenerator.GetAssignmentParameterValue("l", parameters, context); + double? w = ComponentGenerator.GetAssignmentParameterValue("w", parameters, context); context.ModelsRegistry.SetModel( jfet, - l, - w, + ComponentGenerator.CreateRangePredicate(("l", l), ("w", w)), simulation, parameters.Get(3), $"Could not find model {parameters.Get(3)} for JFET {originalName}", @@ -88,24 +88,5 @@ public IEntity Generate(string componentIdentifier, string originalName, string return jfet; } - private double? GetLengthFromParameters(ParameterCollection parameters, IReadingContext context) - { - var lParameter = parameters.FirstOrDefault(p => p is AssignmentParameter ap && ap.Name.ToLower() == "l"); - if (lParameter != null && lParameter is AssignmentParameter lap) - { - return context.Evaluator.EvaluateDouble(lap.Value); - } - return null; - } - - private double? GetWidthFromParameters(ParameterCollection parameters, IReadingContext context) - { - var wParameter = parameters.FirstOrDefault(p => p is AssignmentParameter ap && ap.Name.ToLower() == "w"); - if (wParameter != null && wParameter is AssignmentParameter wap) - { - return context.Evaluator.EvaluateDouble(wap.Value); - } - return null; - } } } \ No newline at end of file diff --git a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Components/Semiconductors/MosfetGenerator.cs b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Components/Semiconductors/MosfetGenerator.cs index aed9e34a..075e8252 100644 --- a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Components/Semiconductors/MosfetGenerator.cs +++ b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Components/Semiconductors/MosfetGenerator.cs @@ -96,13 +96,12 @@ public override IEntity Generate(string componentIdentifier, string originalName context.SimulationPreparations.ExecuteActionBeforeSetup((simulation) => { - double? l = GetLengthFromParameters(parameters, context); - double? w = GetWidthFromParameters(parameters, context); + double? l = GetAssignmentParameterValue("l", parameters, context); + double? w = GetAssignmentParameterValue("w", parameters, context); context.ModelsRegistry.SetModel( mosfetDetails.Mosfet, - l, - w, + CreateRangePredicate(("l", l), ("w", w)), simulation, modelNameParameter, $"Could not find model {modelNameParameter} for mosfet {componentIdentifier}", @@ -167,26 +166,6 @@ public override IEntity Generate(string componentIdentifier, string originalName return mosfet; } - private double? GetLengthFromParameters(ParameterCollection parameters, IReadingContext context) - { - var lParameter = parameters.FirstOrDefault(p => p is AssignmentParameter ap && ap.Name.ToLower() == "l"); - if (lParameter != null && lParameter is AssignmentParameter lap) - { - return context.Evaluator.EvaluateDouble(lap.Value); - } - return null; - } - - private double? GetWidthFromParameters(ParameterCollection parameters, IReadingContext context) - { - var wParameter = parameters.FirstOrDefault(p => p is AssignmentParameter ap && ap.Name.ToLower() == "w"); - if (wParameter != null && wParameter is AssignmentParameter wap) - { - return context.Evaluator.EvaluateDouble(wap.Value); - } - return null; - } - protected class MosfetDetails { public SpiceSharp.Components.Component Mosfet { get; set; } diff --git a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Components/SwitchGenerator.cs b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Components/SwitchGenerator.cs index 39c593a2..da0cd6a0 100644 --- a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Components/SwitchGenerator.cs +++ b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Components/SwitchGenerator.cs @@ -113,7 +113,6 @@ protected IEntity GenerateVoltageSwitch(string name, ParameterCollection paramet context.ModelsRegistry.SetModel( vsw, null, - null, simulation, parameters.Get(4), $"Could not find model {parameters.Get(4)} for voltage switch {name}", @@ -273,7 +272,6 @@ private IEntity GenerateCurrentSwitch(string name, ParameterCollection parameter context.ModelsRegistry.SetModel( csw, null, - null, simulation, parameters.Get(3), $"Could not find model {parameters.Get(3)} for current switch {name}", diff --git a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Models/BipolarModelGenerator.cs b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Models/BipolarModelGenerator.cs index caebb899..2cdde50a 100644 --- a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Models/BipolarModelGenerator.cs +++ b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Models/BipolarModelGenerator.cs @@ -20,8 +20,7 @@ public override Model Generate(string id, string type, SpiceSharpParser.Models.N } var contextModel = new Model(id, bjtModel, bjtModel.Parameters); - SetParameters(context, bjtModel, parameters); - SetDimensionParameters(context, contextModel, parameters); + SetParameters(context, bjtModel, contextModel, parameters); return contextModel; } diff --git a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Models/DiodeModelGenerator.cs b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Models/DiodeModelGenerator.cs index 30ffaad3..3052b06d 100644 --- a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Models/DiodeModelGenerator.cs +++ b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Models/DiodeModelGenerator.cs @@ -11,8 +11,7 @@ public override Model Generate(string id, string type, ParameterCollection param { var diodeModel = new DiodeModel(id); var contextModel = new Model(id, diodeModel, diodeModel.Parameters); - SetParameters(context, diodeModel, parameters); - SetDimensionParameters(context, contextModel, parameters); + SetParameters(context, diodeModel, contextModel, parameters); return contextModel; } diff --git a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Models/JFETModelGenerator.cs b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Models/JFETModelGenerator.cs index e018bcae..c869cafb 100644 --- a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Models/JFETModelGenerator.cs +++ b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Models/JFETModelGenerator.cs @@ -16,8 +16,7 @@ public override Context.Models.Model Generate(string id, string type, ParameterC } var contextModel = new Context.Models.Model(id, jfetModel, jfetModel.Parameters); - SetParameters(context, jfetModel, parameters); - SetDimensionParameters(context, contextModel, parameters); + SetParameters(context, jfetModel, contextModel, parameters); return contextModel; } } diff --git a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Models/ModelGenerator.cs b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Models/ModelGenerator.cs index f1247b33..7b66b09f 100644 --- a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Models/ModelGenerator.cs +++ b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Models/ModelGenerator.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using SpiceSharp.Entities; using SpiceSharpParser.Common.Validation; @@ -11,27 +11,40 @@ namespace SpiceSharpParser.ModelReaders.Netlist.Spice.Readers.EntityGenerators.M public abstract class ModelGenerator : IModelGenerator { /// - /// The dimension parameters used for model selection (not to be set on SpiceSharp entities). + /// Parameters to skip when setting on the SpiceSharp entity (because they are selection-only). + /// Subclasses can override to add custom selection parameter names. /// - private static readonly HashSet DimensionParameterNames = new HashSet(StringComparer.OrdinalIgnoreCase) + protected virtual ISet EntitySkipParameters { get; } = new HashSet(StringComparer.OrdinalIgnoreCase) { "lmin", "lmax", "wmin", "wmax" }; public abstract Context.Models.Model Generate(string id, string type, ParameterCollection parameters, IReadingContext context); - protected void SetParameters(IReadingContext context, IEntity entity, ParameterCollection parameters, bool onload = true) + protected void SetParameters(IReadingContext context, IEntity entity, Context.Models.Model model, ParameterCollection parameters, bool onload = true) { foreach (Parameter parameter in parameters) { if (parameter is AssignmentParameter ap) { - // Skip dimension parameters - they are handled separately - if (DimensionParameterNames.Contains(ap.Name)) + // Store ALL parameters on Model for predicate-based selection + try + { + var value = context.Evaluator.EvaluateDouble(ap.Value); + model.SetSelectionParameter(ap.Name, value); + } + catch + { + // Evaluation error — will be reported below when setting on entity + } + + // Skip entity-setting for params in the skip set + if (EntitySkipParameters.Contains(ap.Name)) { continue; } + // Set on SpiceSharp entity try { context.SetParameter(entity, ap.Name, ap.Value, onload); @@ -43,34 +56,9 @@ protected void SetParameters(IReadingContext context, IEntity entity, ParameterC } else { - context.Result.ValidationResult.AddError(ValidationEntrySource.Reader, $"Unsupported parameter: {parameter}", parameter.LineInfo); - } - } - } - - /// - /// Sets dimension parameters (lmin, lmax, wmin, wmax) on a model for model selection. - /// - /// The reading context. - /// The model to set dimension parameters on. - /// The parameters collection. - protected void SetDimensionParameters(IReadingContext context, Context.Models.Model model, ParameterCollection parameters) - { - foreach (Parameter parameter in parameters) - { - if (parameter is AssignmentParameter ap && DimensionParameterNames.Contains(ap.Name)) - { - try - { - var value = context.Evaluator.EvaluateDouble(ap.Value); - model.SetDimensionParameter(ap.Name, value); - } - catch (Exception ex) - { - context.Result.ValidationResult.AddError(ValidationEntrySource.Reader, $"Problem with setting dimension parameter: {parameter}", parameter.LineInfo, ex); - } + context.Result.ValidationResult.AddError(ValidationEntrySource.Reader, $"Unsupported parameter: {parameter}", parameter.LineInfo); } } } } -} \ No newline at end of file +} diff --git a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Models/MosfetModelGenerator.cs b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Models/MosfetModelGenerator.cs index b4b198dc..aaee2ea0 100644 --- a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Models/MosfetModelGenerator.cs +++ b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Models/MosfetModelGenerator.cs @@ -146,11 +146,8 @@ public override Context.Models.Model Generate(string id, string type, ParameterC return null; } - // Read all the parameters - SetParameters(context, model.Entity, clonedParameters); - - // Set dimension parameters for model selection (lmin, lmax, wmin, wmax) - SetDimensionParameters(context, model, clonedParameters); + // Read all the parameters and store on both entity and model + SetParameters(context, model.Entity, model, clonedParameters); return model; } diff --git a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Models/RLCModelGenerator.cs b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Models/RLCModelGenerator.cs index 1b7f6566..6b1fa6fe 100644 --- a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Models/RLCModelGenerator.cs +++ b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Models/RLCModelGenerator.cs @@ -14,15 +14,13 @@ public override Context.Models.Model Generate(string id, string type, ParameterC case "r": var resistorModel = new ResistorModel(id); var resistorContextModel = new Context.Models.Model(id, resistorModel, resistorModel.Parameters); - SetParameters(context, resistorModel, parameters); - SetDimensionParameters(context, resistorContextModel, parameters); + SetParameters(context, resistorModel, resistorContextModel, parameters); return resistorContextModel; case "c": var capacitorModel = new CapacitorModel(id); var capacitorContextModel = new Context.Models.Model(id, capacitorModel, capacitorModel.Parameters); - SetParameters(context, capacitorModel, parameters); - SetDimensionParameters(context, capacitorContextModel, parameters); + SetParameters(context, capacitorModel, capacitorContextModel, parameters); return capacitorContextModel; } diff --git a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Models/SwitchModelGenerator.cs b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Models/SwitchModelGenerator.cs index e8f6e462..3e2b78f7 100644 --- a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Models/SwitchModelGenerator.cs +++ b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/EntityGenerators/Models/SwitchModelGenerator.cs @@ -14,28 +14,24 @@ public override Context.Models.Model Generate(string id, string type, ParameterC case "sw": var vsModel = new VoltageSwitchModel(id); var vsContextModel = new Context.Models.Model(id, vsModel, vsModel.Parameters); - SetParameters(context, vsModel, parameters); - SetDimensionParameters(context, vsContextModel, parameters); + SetParameters(context, vsModel, vsContextModel, parameters); return vsContextModel; case "csw": var csModel = new CurrentSwitchModel(id); var csContextModel = new Context.Models.Model(id, csModel, csModel.Parameters); - SetParameters(context, csModel, parameters); - SetDimensionParameters(context, csContextModel, parameters); + SetParameters(context, csModel, csContextModel, parameters); return csContextModel; case "vswitch": var vSwitchModel = new VSwitchModel(id); var vSwitchContextModel = new Context.Models.Model(id, vSwitchModel, vSwitchModel.Parameters); - SetParameters(context, vSwitchModel, parameters); - SetDimensionParameters(context, vSwitchContextModel, parameters); + SetParameters(context, vSwitchModel, vSwitchContextModel, parameters); return vSwitchContextModel; case "iswitch": var iSwitchModel = new ISwitchModel(id); var iSwitchContextModel = new Context.Models.Model(id, iSwitchModel, iSwitchModel.Parameters); - SetParameters(context, iSwitchModel, parameters); - SetDimensionParameters(context, iSwitchContextModel, parameters); + SetParameters(context, iSwitchModel, iSwitchContextModel, parameters); return iSwitchContextModel; } From 6d687b66b7162f5192dab134b52acc3a2cd04d58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Go=C5=82=C4=99biowski?= Date: Sun, 1 Mar 2026 19:25:54 +0100 Subject: [PATCH 5/6] Improve comments --- .../Components/ModelDimensionTests.cs | 60 +++++++++++++------ 1 file changed, 41 insertions(+), 19 deletions(-) diff --git a/src/SpiceSharpParser.IntegrationTests/Components/ModelDimensionTests.cs b/src/SpiceSharpParser.IntegrationTests/Components/ModelDimensionTests.cs index cf1748b7..5fdf8a94 100644 --- a/src/SpiceSharpParser.IntegrationTests/Components/ModelDimensionTests.cs +++ b/src/SpiceSharpParser.IntegrationTests/Components/ModelDimensionTests.cs @@ -4,7 +4,8 @@ namespace SpiceSharpParser.IntegrationTests.Components { /// - /// Integration tests for model selection based on L and W parameters with lmin, lmax, wmin, wmax constraints. + /// Integration tests for model selection (binning) based on instance parameters + /// and model selection parameters (e.g. lmin, lmax, wmin, wmax). /// public class ModelDimensionTests : BaseTests { @@ -39,14 +40,14 @@ public void MosfetSelectsCorrectModelBasedOnLength() Assert.NotNull(netlist); Assert.False(netlist.ValidationResult.HasError); - // Both MOSFETs should be created + // M1 L=0.5u should match NMOS.0 (lmin=0.1u lmax=1u) var mosfet1 = netlist.Circuit["m1"] as Mosfet1; Assert.NotNull(mosfet1); + // M2 L=5u should match NMOS.1 (lmin=1u lmax=10u) var mosfet2 = netlist.Circuit["m2"] as Mosfet1; Assert.NotNull(mosfet2); - // Verify models exist in circuit Assert.NotNull(netlist.Circuit["NMOS.0"]); Assert.NotNull(netlist.Circuit["NMOS.1"]); } @@ -65,9 +66,11 @@ public void MosfetSelectsCorrectModelBasedOnWidth() Assert.NotNull(netlist); Assert.False(netlist.ValidationResult.HasError); + // M1 W=2u should match PMOS.0 (wmin=1u wmax=10u) var mosfet1 = netlist.Circuit["m1"] as Mosfet1; Assert.NotNull(mosfet1); + // M2 W=20u should match PMOS.1 (wmin=10u wmax=100u) var mosfet2 = netlist.Circuit["m2"] as Mosfet1; Assert.NotNull(mosfet2); } @@ -86,18 +89,21 @@ public void MosfetSelectsCorrectModelBasedOnLengthAndWidth() Assert.NotNull(netlist); Assert.False(netlist.ValidationResult.HasError); + // M1 L=0.5u W=5u should match NMOS.0 var mosfet1 = netlist.Circuit["m1"] as Mosfet1; Assert.NotNull(mosfet1); + // M2 L=5u W=50u should match NMOS.1 var mosfet2 = netlist.Circuit["m2"] as Mosfet1; Assert.NotNull(mosfet2); } [Fact] - public void MosfetFallsBackToDefaultModelWhenNoMatch() + public void MosfetFallsBackToBaseModelWhenNoMatch() { + // L=100u exceeds NMOS.0's lmax=1u, should fall back to base NMOS var netlist = GetSpiceSharpModel( - "MOSFET falls back to default model", + "MOSFET falls back to base model", "M1 D G S B NMOS L=100u W=100u", ".model NMOS.0 NMOS level=1 lmin=0.1u lmax=1u", ".model NMOS NMOS level=1", @@ -106,8 +112,8 @@ public void MosfetFallsBackToDefaultModelWhenNoMatch() Assert.NotNull(netlist); Assert.False(netlist.ValidationResult.HasError); - var mosfet1 = netlist.Circuit["m1"] as Mosfet1; - Assert.NotNull(mosfet1); + var mosfet = netlist.Circuit["m1"] as Mosfet1; + Assert.NotNull(mosfet); } #endregion @@ -151,10 +157,11 @@ public void ResistorSelectsCorrectModelBasedOnDimensions() } [Fact] - public void ResistorWithOnlyLengthParameter() + public void ResistorModelWithOnlyLengthConstraints() { + // Models only constrain L (no wmin/wmax), so W is irrelevant for selection var netlist = GetSpiceSharpModel( - "Resistor with only L parameter", + "Resistor model with only length constraints", "R1 1 0 RMOD L=0.5u W=1u", "R2 1 0 RMOD L=5u W=1u", ".model RMOD.0 R RSH=1 lmin=0.1u lmax=1u", @@ -172,10 +179,11 @@ public void ResistorWithOnlyLengthParameter() } [Fact] - public void ResistorWithOnlyWidthParameter() + public void ResistorModelWithOnlyWidthConstraints() { + // Models only constrain W (no lmin/lmax), so L is irrelevant for selection var netlist = GetSpiceSharpModel( - "Resistor with only W parameter", + "Resistor model with only width constraints", "R1 1 0 RMOD L=1u W=5u", "R2 1 0 RMOD L=1u W=50u", ".model RMOD.0 R RSH=1 wmin=1u wmax=10u", @@ -273,10 +281,11 @@ public void BJTSelectsCorrectModelBasedOnDimensions() } [Fact] - public void BJTWithOnlyLengthParameter() + public void BJTModelWithOnlyLengthConstraints() { + // Models only constrain L, component has only L var netlist = GetSpiceSharpModel( - "BJT with only L parameter", + "BJT model with only length constraints", "Q1 C B E QMOD L=0.5u", "Q2 C B E QMOD L=5u", ".model QMOD.0 NPN lmin=0.1u lmax=1u", @@ -334,10 +343,11 @@ public void DiodeSelectsCorrectModelBasedOnDimensions() } [Fact] - public void DiodeWithOnlyWidthParameter() + public void DiodeModelWithOnlyWidthConstraints() { + // Models only constrain W, component has only W var netlist = GetSpiceSharpModel( - "Diode with only W parameter", + "Diode model with only width constraints", "D1 A K DMOD W=5u", "D2 A K DMOD W=50u", ".model DMOD.0 D wmin=1u wmax=10u", @@ -395,10 +405,11 @@ public void JFETSelectsCorrectModelBasedOnDimensions() } [Fact] - public void JFETWithOnlyLengthParameter() + public void JFETModelWithOnlyLengthConstraints() { + // Models only constrain L, component has only L var netlist = GetSpiceSharpModel( - "JFET with only L parameter", + "JFET model with only length constraints", "J1 D G S JMOD L=0.5u", "J2 D G S JMOD L=5u", ".model JMOD.0 PJF lmin=0.1u lmax=1u", @@ -488,6 +499,7 @@ public void ModelWithNonSequentialNumericSuffixes() [Fact] public void FallsBackToBaseModelWhenNoSuffixedModelMatches() { + // L=100u exceeds both suffixed models' lmax, should fall back to base NMOS var netlist = GetSpiceSharpModel( "Falls back to base model when no suffixed model matches", "M1 D G S B NMOS L=100u W=100u", @@ -550,10 +562,11 @@ public void ResistorSelectsModelWithNonNumericSuffix() #region Edge Cases [Fact] - public void ComponentWithoutLWParametersUsesDefaultModel() + public void MosfetWithoutLWParametersMatchesFirstSuffixedModel() { + // Without L/W, predicate is null — first suffixed model is selected (not base model) var netlist = GetSpiceSharpModel( - "Component without L/W uses default", + "MOSFET without L/W matches first suffixed model", "M1 D G S B NMOS", ".model NMOS.0 NMOS level=1 lmin=0.1u lmax=1u", ".model NMOS NMOS level=1", @@ -569,6 +582,8 @@ public void ComponentWithoutLWParametersUsesDefaultModel() [Fact] public void ModelWithOnlyLminConstraint() { + // RMOD.0 has lmin=1u: R1 L=0.5u fails (below min), falls back to base RMOD + // R2 L=5u passes (above min), matches RMOD.0 var netlist = GetSpiceSharpModel( "Model with only lmin constraint", "R1 1 0 RMOD L=0.5u W=1u", @@ -590,6 +605,8 @@ public void ModelWithOnlyLminConstraint() [Fact] public void ModelWithOnlyLmaxConstraint() { + // CMOD.0 has lmax=1u: C1 L=0.5u passes (below max), matches CMOD.0 + // C2 L=5u fails (above max), falls back to base CMOD var netlist = GetSpiceSharpModel( "Model with only lmax constraint", "C1 1 0 CMOD L=0.5u W=1u", @@ -611,6 +628,8 @@ public void ModelWithOnlyLmaxConstraint() [Fact] public void ModelWithOnlyWminConstraint() { + // NMOS.0 has wmin=1u: M1 W=0.5u fails (below min), falls back to base NMOS + // M2 W=5u passes (above min), matches NMOS.0 var netlist = GetSpiceSharpModel( "Model with only wmin constraint", "M1 D G S B NMOS L=1u W=0.5u", @@ -632,6 +651,8 @@ public void ModelWithOnlyWminConstraint() [Fact] public void ModelWithOnlyWmaxConstraint() { + // QMOD.0 has wmax=10u: Q1 W=5u passes (below max), matches QMOD.0 + // Q2 W=50u fails (above max), falls back to base QMOD var netlist = GetSpiceSharpModel( "Model with only wmax constraint", "Q1 C B E QMOD W=5u", @@ -653,6 +674,7 @@ public void ModelWithOnlyWmaxConstraint() [Fact] public void MultipleModelsWithOverlappingRanges() { + // D1 matches both DMOD.0 and DMOD.1; first match wins var netlist = GetSpiceSharpModel( "Multiple models with overlapping ranges", "D1 A K DMOD L=0.8u W=8u", From 5b29c43e2a07528aeabe01bb33d9a3f9ebfb94c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Go=C5=82=C4=99biowski?= Date: Sun, 1 Mar 2026 19:33:29 +0100 Subject: [PATCH 6/6] ++ --- src/SpiceSharpParser/SpiceSharpParser.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/SpiceSharpParser/SpiceSharpParser.csproj b/src/SpiceSharpParser/SpiceSharpParser.csproj index fdbcaffb..9b852cfb 100644 --- a/src/SpiceSharpParser/SpiceSharpParser.csproj +++ b/src/SpiceSharpParser/SpiceSharpParser.csproj @@ -5,7 +5,7 @@ netstandard2.0;net8.0 SpiceSharp https://github.com/SpiceSharp/SpiceSharpParser - Copyright 2025 + Copyright 2026 true https://github.com/SpiceSharp/SpiceSharpParser @@ -22,7 +22,7 @@ MIT latest - 3.2.8 + 3.2.9