diff --git a/src/SpiceSharpParser.IntegrationTests/DotStatements/MeasTests.cs b/src/SpiceSharpParser.IntegrationTests/DotStatements/MeasTests.cs index c8198a5f..1c82f2b9 100644 --- a/src/SpiceSharpParser.IntegrationTests/DotStatements/MeasTests.cs +++ b/src/SpiceSharpParser.IntegrationTests/DotStatements/MeasTests.cs @@ -2636,5 +2636,195 @@ public void FifteenMeasurementsStressTest() model.Measurements["m5"][0].Value, model.Measurements["m14"][0].Value)); } + // ===================================================================== + // Group O: FIND ... AT= Measurements + // ===================================================================== + + [Fact] + public void FindAtConstantVoltage() + { + // Constant 5V source — FIND V(out) AT=any_time should return 5.0 + var model = GetSpiceSharpModel( + "MEAS FIND AT Constant Voltage", + "V1 OUT 0 5.0", + "R1 OUT 0 1e3", + ".TRAN 1e-4 10e-3", + ".MEAS TRAN res1 FIND V(OUT) AT=5m", + ".END"); + + RunSimulations(model); + + Assert.True(model.Measurements.ContainsKey("res1")); + var results = model.Measurements["res1"]; + Assert.Single(results); + Assert.True(results[0].Success); + Assert.True(EqualsWithTol(5.0, results[0].Value)); + } + + [Fact] + public void FindAtRCCharging() + { + // RC circuit: V(out) = 10*(1-exp(-t/RC)), RC=10ms + // At t=10ms: V(out) = 10*(1-exp(-1)) ≈ 6.3212 + double rc = 10e3 * 1e-6; // 10ms + double expected = 10.0 * (1.0 - Math.Exp(-10e-3 / rc)); + + var model = GetSpiceSharpModel( + "MEAS FIND AT RC Charging", + "V1 IN 0 10.0", + "R1 IN OUT 10e3", + "C1 OUT 0 1e-6", + ".IC V(OUT)=0.0", + ".TRAN 1e-5 50e-3", + ".MEAS TRAN res1 FIND V(OUT) AT=10m", + ".END"); + + RunSimulations(model); + + Assert.True(model.Measurements.ContainsKey("res1")); + var results = model.Measurements["res1"]; + Assert.Single(results); + Assert.True(results[0].Success); + Assert.True(EqualsWithTol(expected, results[0].Value)); + } + + // ===================================================================== + // Group P: TD on WHEN Measurements + // ===================================================================== + + [Fact] + public void WhenWithTD() + { + // Oscillating signal: crosses 0V many times. With TD, skip early crossings. + // V(out) = sin(2*pi*1000*t) crosses 0 at t=0, 0.5ms, 1ms, 1.5ms, ... + // With TD=0.8ms, the first crossing after 0.8ms is at t=1ms + var model = GetSpiceSharpModel( + "MEAS WHEN with TD", + "V1 OUT 0 SIN(0 1 1e3)", + "R1 OUT 0 1e3", + ".TRAN 1e-6 5e-3", + ".MEAS TRAN res_notd WHEN V(OUT)=0 CROSS=1", + ".MEAS TRAN res_td WHEN V(OUT)=0 CROSS=1 TD=0.8m", + ".END"); + + RunSimulations(model); + + Assert.True(model.Measurements.ContainsKey("res_notd")); + Assert.True(model.Measurements.ContainsKey("res_td")); + Assert.True(model.Measurements["res_notd"][0].Success); + Assert.True(model.Measurements["res_td"][0].Success); + // Without TD, first crossing is near t=0.5ms (first positive-to-negative) + // With TD=0.8ms, first crossing after 0.8ms should be at ~1ms + Assert.True(model.Measurements["res_td"][0].Value > 0.8e-3); + } + + // ===================================================================== + // Group Q: RISE/FALL/CROSS=LAST + // ===================================================================== + + [Fact] + public void WhenCrossLast() + { + // Oscillating signal: find the last crossing of 0V + var model = GetSpiceSharpModel( + "MEAS WHEN CROSS LAST", + "V1 OUT 0 SIN(0 1 1e3)", + "R1 OUT 0 1e3", + ".TRAN 1e-6 5e-3", + ".MEAS TRAN res_last WHEN V(OUT)=0 CROSS=LAST", + ".MEAS TRAN res_first WHEN V(OUT)=0 CROSS=1", + ".END"); + + RunSimulations(model); + + Assert.True(model.Measurements.ContainsKey("res_last")); + Assert.True(model.Measurements.ContainsKey("res_first")); + Assert.True(model.Measurements["res_last"][0].Success); + Assert.True(model.Measurements["res_first"][0].Success); + // Last crossing should be much later than first + Assert.True(model.Measurements["res_last"][0].Value > model.Measurements["res_first"][0].Value); + // Last crossing should be near end of simulation (5ms) + Assert.True(model.Measurements["res_last"][0].Value > 4e-3); + } + + [Fact] + public void FindWhenRiseLast() + { + // Find V(out) at the last rising crossing of 0V + var model = GetSpiceSharpModel( + "MEAS FIND WHEN RISE LAST", + "V1 OUT 0 SIN(0 1 1e3)", + "R1 OUT 0 1e3", + ".TRAN 1e-6 5e-3", + ".MEAS TRAN res1 FIND V(OUT) WHEN V(OUT)=0 RISE=LAST", + ".END"); + + RunSimulations(model); + + Assert.True(model.Measurements.ContainsKey("res1")); + Assert.True(model.Measurements["res1"][0].Success); + // At a rising zero crossing, V(OUT) ≈ 0 + Assert.True(Math.Abs(model.Measurements["res1"][0].Value) < 0.1); + } + + [Fact] + public void TrigTargCrossLast() + { + // Use CROSS=LAST on TARG for TRIG/TARG measurement + var model = GetSpiceSharpModel( + "MEAS TRIG/TARG CROSS LAST", + "V1 OUT 0 SIN(0 1 1e3)", + "R1 OUT 0 1e3", + ".TRAN 1e-6 5e-3", + ".MEAS TRAN res1 TRIG V(OUT) VAL=0 CROSS=1 TARG V(OUT) VAL=0 CROSS=LAST", + ".END"); + + RunSimulations(model); + + Assert.True(model.Measurements.ContainsKey("res1")); + Assert.True(model.Measurements["res1"][0].Success); + // Should be a large positive value (last crossing - first crossing) + Assert.True(model.Measurements["res1"][0].Value > 3e-3); + } + + // ===================================================================== + // Group R: DERIV with WHEN + // ===================================================================== + + [Fact] + public void DerivWithWhen() + { + // RC circuit: V(out) = 10*(1-exp(-t/RC)) + // dV/dt = (10/RC)*exp(-t/RC) + // Use DERIV WHEN V(OUT)=5 to find the derivative at the point where V=5V + // V=5 when t = -RC*ln(0.5) = RC*ln(2) ≈ 6.93ms + // dV/dt at that point = (10/RC)*exp(-ln(2)) = (10/RC)*0.5 = 500 V/s + double rc = 10e3 * 1e-6; // 10ms + + var model = GetSpiceSharpModel( + "MEAS DERIV with WHEN", + "V1 IN 0 10.0", + "R1 IN OUT 10e3", + "C1 OUT 0 1e-6", + ".IC V(OUT)=0.0", + ".TRAN 1e-5 50e-3", + ".MEAS TRAN res1 DERIV V(OUT) AT=6.93m", + ".MEAS TRAN res2 DERIV V(OUT) WHEN V(OUT)=5", + ".END"); + + RunSimulations(model); + + Assert.True(model.Measurements.ContainsKey("res1")); + Assert.True(model.Measurements.ContainsKey("res2")); + Assert.True(model.Measurements["res1"][0].Success); + Assert.True(model.Measurements["res2"][0].Success); + // Both should give approximately the same derivative (around 500 V/s) + // Use a 5% tolerance since discrete derivative is approximate + double expected = (10.0 / rc) * 0.5; // 500 V/s + Assert.True(Math.Abs(model.Measurements["res1"][0].Value - expected) / expected < 0.05, + $"res1 derivative {model.Measurements["res1"][0].Value} not within 5% of {expected}"); + Assert.True(Math.Abs(model.Measurements["res2"][0].Value - expected) / expected < 0.05, + $"res2 derivative {model.Measurements["res2"][0].Value} not within 5% of {expected}"); + } } } diff --git a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/Controls/MeasControl.cs b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/Controls/MeasControl.cs index 2ed0420e..957db796 100644 --- a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/Controls/MeasControl.cs +++ b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/Controls/MeasControl.cs @@ -122,6 +122,12 @@ private void SetupMeasurement(MeasurementDefinition definition, ISimulationWithE findExport = GenerateExport(definition.FindSignal, context, simulation); } + // For DERIV with WHEN, collect the derivative signal separately + if (definition.Type == MeasType.Deriv && definition.WhenSignal != null && definition.Signal != null) + { + findExport = GenerateExport(definition.Signal, context, simulation); + } + // For TRIG/TARG, we might need separate exports for trigger and target signals Export trigExport = null; Export targExport = null; @@ -222,13 +228,23 @@ private static Parameter GetPrimarySignalParameter(MeasurementDefinition definit return definition.WhenSignal; case MeasType.FindWhen: return definition.WhenSignal; + case MeasType.FindAt: + return definition.FindSignal; case MeasType.Min: case MeasType.Max: case MeasType.Avg: case MeasType.Rms: case MeasType.Pp: case MeasType.Integ: + return definition.Signal; case MeasType.Deriv: + // If DERIV has a WHEN condition, the primary signal is the WHEN signal + // and we collect the derivative signal separately via findExport + if (definition.WhenSignal != null) + { + return definition.WhenSignal; + } + return definition.Signal; default: return null; @@ -459,15 +475,15 @@ private void ParseThresholdSection( break; case "RISE": edge = EdgeType.Rise; - edgeNumber = Math.Max(1, (int)context.Evaluator.EvaluateDouble(ap.Value)); + edgeNumber = ParseEdgeNumber(ap.Value, context); break; case "FALL": edge = EdgeType.Fall; - edgeNumber = Math.Max(1, (int)context.Evaluator.EvaluateDouble(ap.Value)); + edgeNumber = ParseEdgeNumber(ap.Value, context); break; case "CROSS": edge = EdgeType.Cross; - edgeNumber = Math.Max(1, (int)context.Evaluator.EvaluateDouble(ap.Value)); + edgeNumber = ParseEdgeNumber(ap.Value, context); break; case "TD": td = context.Evaluator.EvaluateDouble(ap.Value); @@ -499,10 +515,11 @@ private MeasurementDefinition ParseWhen(MeasurementDefinition definition, List

specParams, IReadingContext context) { // .MEAS TRAN name FIND V(out) WHEN V(in)=0.5 [RISE|FALL|CROSS=] - definition.Type = MeasType.FindWhen; + // .MEAS TRAN name FIND V(out) AT=5m - // Find the WHEN keyword + // Find the WHEN keyword or AT= parameter int whenIndex = -1; + int atIndex = -1; for (int i = 1; i < specParams.Count; i++) { if (specParams[i].Value.ToUpper() == "WHEN") @@ -510,18 +527,26 @@ private MeasurementDefinition ParseFindWhen(MeasurementDefinition definition, Li whenIndex = i; break; } + + if (specParams[i] is AssignmentParameter ap && ap.Name.ToUpper() == "AT") + { + atIndex = i; + break; + } } - if (whenIndex < 0) + if (whenIndex < 0 && atIndex < 0) { context.Result.ValidationResult.AddError( ValidationEntrySource.Reader, - ".MEAS FIND: Missing WHEN keyword"); + ".MEAS FIND: Missing WHEN or AT= keyword"); return null; } - // The FIND signal is between FIND keyword and WHEN keyword - for (int i = 1; i < whenIndex; i++) + int signalEnd = whenIndex >= 0 ? whenIndex : atIndex; + + // The FIND signal is between FIND keyword and WHEN/AT keyword + for (int i = 1; i < signalEnd; i++) { if (specParams[i] is BracketParameter || specParams[i] is ReferenceParameter) { @@ -538,13 +563,24 @@ private MeasurementDefinition ParseFindWhen(MeasurementDefinition definition, Li { context.Result.ValidationResult.AddError( ValidationEntrySource.Reader, - ".MEAS FIND/WHEN: Missing signal between FIND and WHEN keywords"); + ".MEAS FIND: Missing signal after FIND keyword"); return null; } - // Parse the WHEN condition - var whenParams = specParams.GetRange(whenIndex, specParams.Count - whenIndex); - ParseWhenCondition(whenParams, 1, definition, context); + if (atIndex >= 0) + { + // FIND ... AT= form + definition.Type = MeasType.FindAt; + var atParam = (AssignmentParameter)specParams[atIndex]; + definition.At = context.Evaluator.EvaluateDouble(atParam.Value); + } + else + { + // FIND ... WHEN form + definition.Type = MeasType.FindWhen; + var whenParams = specParams.GetRange(whenIndex, specParams.Count - whenIndex); + ParseWhenCondition(whenParams, 1, definition, context); + } return definition; } @@ -607,15 +643,18 @@ private void ParseWhenCondition(List specParams, int startIndex, Meas break; case "RISE": definition.WhenEdge = EdgeType.Rise; - definition.WhenEdgeNumber = Math.Max(1, (int)context.Evaluator.EvaluateDouble(edgeAp.Value)); + definition.WhenEdgeNumber = ParseEdgeNumber(edgeAp.Value, context); break; case "FALL": definition.WhenEdge = EdgeType.Fall; - definition.WhenEdgeNumber = Math.Max(1, (int)context.Evaluator.EvaluateDouble(edgeAp.Value)); + definition.WhenEdgeNumber = ParseEdgeNumber(edgeAp.Value, context); break; case "CROSS": definition.WhenEdge = EdgeType.Cross; - definition.WhenEdgeNumber = Math.Max(1, (int)context.Evaluator.EvaluateDouble(edgeAp.Value)); + definition.WhenEdgeNumber = ParseEdgeNumber(edgeAp.Value, context); + break; + case "TD": + definition.WhenTd = context.Evaluator.EvaluateDouble(edgeAp.Value); break; case "FROM": definition.From = context.Evaluator.EvaluateDouble(edgeAp.Value); @@ -669,9 +708,23 @@ private MeasurementDefinition ParseStatistical(MeasurementDefinition definition, private MeasurementDefinition ParseDeriv(MeasurementDefinition definition, List specParams, IReadingContext context) { // .MEAS TRAN name DERIV V(out) AT= + // .MEAS TRAN name DERIV V(out) WHEN V(in)=0.5 definition.Type = MeasType.Deriv; + // Check for WHEN keyword + int whenIndex = -1; for (int i = 1; i < specParams.Count; i++) + { + if (specParams[i].Value.ToUpper() == "WHEN") + { + whenIndex = i; + break; + } + } + + int parseEnd = whenIndex >= 0 ? whenIndex : specParams.Count; + + for (int i = 1; i < parseEnd; i++) { var param = specParams[i]; if (param is AssignmentParameter ap) @@ -703,17 +756,33 @@ private MeasurementDefinition ParseDeriv(MeasurementDefinition definition, List< return null; } - if (!definition.At.HasValue) + if (whenIndex >= 0) + { + // DERIV ... WHEN form: find crossing point, then compute derivative there + var whenParams = specParams.GetRange(whenIndex, specParams.Count - whenIndex); + ParseWhenCondition(whenParams, 1, definition, context); + } + else if (!definition.At.HasValue) { context.Result.ValidationResult.AddError( ValidationEntrySource.Reader, - ".MEAS DERIV: Missing required AT= parameter"); + ".MEAS DERIV: Missing required AT= or WHEN parameter"); return null; } return definition; } + private static int ParseEdgeNumber(string value, IReadingContext context) + { + if (value.Equals("LAST", StringComparison.OrdinalIgnoreCase)) + { + return EdgeConstants.Last; + } + + return Math.Max(1, (int)context.Evaluator.EvaluateDouble(value)); + } + private MeasurementDefinition ParseParam(MeasurementDefinition definition, List specParams) { // .MEAS TRAN name PARAM='expression' or PARAM {expression} diff --git a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/Controls/Measurements/MeasurementDefinition.cs b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/Controls/Measurements/MeasurementDefinition.cs index 35f48b06..fd0f718d 100644 --- a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/Controls/Measurements/MeasurementDefinition.cs +++ b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/Controls/Measurements/MeasurementDefinition.cs @@ -37,10 +37,22 @@ public enum MeasType ///

Compute the derivative of a signal at a point. Deriv, + /// Find the value of a signal at a specific point (FIND ... AT=). + FindAt, + /// Compute a parameter expression from other measurement results. Param, } + /// + /// Sentinel value for RISE/FALL/CROSS=LAST. + /// + public static class EdgeConstants + { + /// Indicates the last matching edge should be used. + public const int Last = -1; + } + /// /// The type of edge to detect for threshold crossings. /// @@ -119,9 +131,12 @@ public class MeasurementDefinition /// Gets or sets the WHEN edge type. public EdgeType WhenEdge { get; set; } = EdgeType.Cross; - /// Gets or sets the WHEN edge number (1-based). + /// Gets or sets the WHEN edge number (1-based, or EdgeConstants.Last). public int WhenEdgeNumber { get; set; } = 1; + /// Gets or sets the WHEN time delay offset. + public double? WhenTd { get; set; } + // --- FIND/WHEN properties --- /// Gets or sets the FIND signal parameter (signal to evaluate at the WHEN crossing point). diff --git a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/Controls/Measurements/MeasurementEvaluator.cs b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/Controls/Measurements/MeasurementEvaluator.cs index fa8fa007..e07df009 100644 --- a/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/Controls/Measurements/MeasurementEvaluator.cs +++ b/src/SpiceSharpParser/ModelReaders/Netlist/Spice/Readers/Controls/Measurements/MeasurementEvaluator.cs @@ -62,6 +62,8 @@ public MeasurementResult ComputeResult(string simulationName) return ComputeWhen(simulationName); case MeasType.FindWhen: return ComputeFindWhen(simulationName); + case MeasType.FindAt: + return ComputeFindAt(simulationName); case MeasType.Min: return ComputeMin(simulationName); case MeasType.Max: @@ -93,7 +95,9 @@ public MeasurementResult ComputeResult(string simulationName) double? fromX, double? toX) { + bool findLast = edgeNumber == EdgeConstants.Last; int edgeCount = 0; + double? lastCrossing = null; for (int i = 1; i < data.Count; i++) { @@ -152,21 +156,31 @@ public MeasurementResult ComputeResult(string simulationName) } edgeCount++; - if (edgeCount == edgeNumber) - { - // Linear interpolation to find precise crossing point - double dy = data[i].Y - data[i - 1].Y; - if (Math.Abs(dy) < 1e-30) - { - return data[i].X; - } + // Linear interpolation to find precise crossing point + double crossX; + double dy = data[i].Y - data[i - 1].Y; + if (Math.Abs(dy) < 1e-30) + { + crossX = data[i].X; + } + else + { double fraction = (threshold - data[i - 1].Y) / dy; - return data[i - 1].X + fraction * (data[i].X - data[i - 1].X); + crossX = data[i - 1].X + fraction * (data[i].X - data[i - 1].X); + } + + if (findLast) + { + lastCrossing = crossX; + } + else if (edgeCount == edgeNumber) + { + return crossX; } } - return null; + return findLast ? lastCrossing : null; } /// @@ -238,7 +252,7 @@ internal static double InterpolateY(List<(double X, double Y)> data, double targ private MeasurementResult ComputeWhen(string simulationName) { - double? crossX = FindCrossing(_data, Definition.WhenVal, Definition.WhenEdge, Definition.WhenEdgeNumber, null, Definition.From, Definition.To); + double? crossX = FindCrossing(_data, Definition.WhenVal, Definition.WhenEdge, Definition.WhenEdgeNumber, Definition.WhenTd, Definition.From, Definition.To); if (!crossX.HasValue) { return new MeasurementResult(Definition.Name, double.NaN, false, "WHEN", simulationName); @@ -250,7 +264,7 @@ private MeasurementResult ComputeWhen(string simulationName) private MeasurementResult ComputeFindWhen(string simulationName) { // Use the primary data (_data) for the WHEN condition - double? crossX = FindCrossing(_data, Definition.WhenVal, Definition.WhenEdge, Definition.WhenEdgeNumber, null, Definition.From, Definition.To); + double? crossX = FindCrossing(_data, Definition.WhenVal, Definition.WhenEdge, Definition.WhenEdgeNumber, Definition.WhenTd, Definition.From, Definition.To); if (!crossX.HasValue) { return new MeasurementResult(Definition.Name, double.NaN, false, "FIND_WHEN", simulationName); @@ -262,6 +276,22 @@ private MeasurementResult ComputeFindWhen(string simulationName) return new MeasurementResult(Definition.Name, value, true, "FIND_WHEN", simulationName); } + private MeasurementResult ComputeFindAt(string simulationName) + { + if (!Definition.At.HasValue) + { + return new MeasurementResult(Definition.Name, double.NaN, false, "FIND_AT", simulationName); + } + + if (_data.Count == 0) + { + return new MeasurementResult(Definition.Name, double.NaN, false, "FIND_AT", simulationName); + } + + double value = InterpolateY(_data, Definition.At.Value); + return new MeasurementResult(Definition.Name, value, true, "FIND_AT", simulationName); + } + private MeasurementResult ComputeMin(string simulationName) { var windowed = GetWindowedData(_data); @@ -403,13 +433,31 @@ private MeasurementResult ComputeInteg(string simulationName) private MeasurementResult ComputeDeriv(string simulationName) { - if (!Definition.At.HasValue) + double targetX; + + if (Definition.At.HasValue) + { + targetX = Definition.At.Value; + } + else if (Definition.WhenSignal != null) + { + // DERIV ... WHEN: find the crossing point first using _data (the WHEN signal) + double? crossX = FindCrossing(_data, Definition.WhenVal, Definition.WhenEdge, Definition.WhenEdgeNumber, Definition.WhenTd, Definition.From, Definition.To); + if (!crossX.HasValue) + { + return new MeasurementResult(Definition.Name, double.NaN, false, "DERIV", simulationName); + } + + targetX = crossX.Value; + } + else { return new MeasurementResult(Definition.Name, double.NaN, false, "DERIV", simulationName); } - double targetX = Definition.At.Value; - var windowed = GetWindowedData(_data); + // Use _findData for the derivative signal when WHEN is used, otherwise _data + var derivData = _findData.Count > 0 ? _findData : _data; + var windowed = GetWindowedData(derivData); if (windowed.Count < 2) { diff --git a/src/SpiceSharpParser/SpiceSharpParser.csproj b/src/SpiceSharpParser/SpiceSharpParser.csproj index 4b0e5218..541b5630 100644 --- a/src/SpiceSharpParser/SpiceSharpParser.csproj +++ b/src/SpiceSharpParser/SpiceSharpParser.csproj @@ -22,18 +22,14 @@ MIT latest - 3.2.10 + 3.2.11 - + 1701;1702; - - - - diff --git a/src/docs/articles/meas.md b/src/docs/articles/meas.md index 9e03e944..dc665cab 100644 --- a/src/docs/articles/meas.md +++ b/src/docs/articles/meas.md @@ -13,7 +13,7 @@ The `.MEAS` (or `.MEASURE`) statement extracts a single scalar number from simul |-------|---------|--------| | `analysis_type` | Which simulation type to measure | `TRAN`, `AC`, `DC`, `OP`, `NOISE` | | `name` | A label you choose — this is the key used to look up the result in C# | Any identifier (e.g. `rise_time`, `vmax`) | -| `measurement_spec` | What to measure and how (described in detail below) | One of the 11 measurement types | +| `measurement_spec` | What to measure and how (described in detail below) | One of the 12 measurement types | ## Quick Reference — All Measurement Types @@ -22,6 +22,7 @@ The `.MEAS` (or `.MEASURE`) statement extracts a single scalar number from simul | `TRIG … TARG` | Time (or sweep) difference between two threshold crossings | [TRIG/TARG](#trigtarg--timing-measurements) | | `WHEN` | Time (or sweep value) at which a signal crosses a threshold | [WHEN](#when--threshold-crossing-time) | | `FIND … WHEN` | Value of signal A at the moment signal B crosses a threshold | [FIND/WHEN](#findwhen--value-at-a-threshold-crossing) | +| `FIND … AT` | Value of a signal at a specific point on the abscissa | [FIND/AT](#findat--value-at-a-specific-point) | | `MAX` | Maximum value of a signal | [MAX](#max--maximum-value) | | `MIN` | Minimum value of a signal | [MIN](#min--minimum-value) | | `AVG` | Time-weighted average (trapezoidal mean) | [AVG](#avg--average-value) | @@ -39,12 +40,15 @@ These qualifiers can be combined with most measurement types: |-----------|---------|---------| | `VAL=` | Threshold voltage/current value | `VAL=5.0` | | `RISE=` | Match the *n*th **rising** crossing | `RISE=1` (first rising edge) | +| `RISE=LAST` | Match the **last** rising crossing | `RISE=LAST` | | `FALL=` | Match the *n*th **falling** crossing | `FALL=2` (second falling edge) | +| `FALL=LAST` | Match the **last** falling crossing | `FALL=LAST` | | `CROSS=` | Match the *n*th crossing in **either** direction | `CROSS=3` | -| `TD=