添加曲线标记功能
This commit is contained in:
parent
041bf693d1
commit
5d56137ce5
108
Docs/曲线算法说明文档.md
Normal file
108
Docs/曲线算法说明文档.md
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
# 算法说明文档
|
||||||
|
|
||||||
|
## 1. ThresholdV1 阈值算法
|
||||||
|
|
||||||
|
上升曲线和下降曲线都使用同一类阈值判断算法:根据名义高电平 `Vhigh` 计算 90% 阈值和 10% 阈值,再在曲线中查找对应的时间点。
|
||||||
|
|
||||||
|
核心阈值:
|
||||||
|
|
||||||
|
```text
|
||||||
|
90% 阈值 = Vhigh * R90
|
||||||
|
10% 阈值 = Vhigh * Rstart
|
||||||
|
```
|
||||||
|
|
||||||
|
在参数模板中:
|
||||||
|
|
||||||
|
```text
|
||||||
|
90% 阈值 = 3.0 * 0.9 = 2.7 V
|
||||||
|
10% 阈值 = 3.0 * 0.1 = 0.3 V
|
||||||
|
```
|
||||||
|
|
||||||
|
曲线图中显示的两条水平虚线分别对应 90% 阈值线和 10% 阈值线。
|
||||||
|
|
||||||
|
## 2. 曲线内标记时间线
|
||||||
|
|
||||||
|
### 开启曲线:开启 - 上升
|
||||||
|
|
||||||
|
开启曲线用于分析信号从低电平上升到高电平的过程。
|
||||||
|
|
||||||
|
```text
|
||||||
|
低电平 -> 开启起点 T0 -> 上升 -> T90 -> 高电平
|
||||||
|
```
|
||||||
|
|
||||||
|
判断逻辑:
|
||||||
|
|
||||||
|
1. 查找信号第一次达到 10% 阈值的位置,作为开启起点 `T0`。
|
||||||
|
2. 从 `T0` 之后继续查找信号第一次达到 90% 阈值的位置,作为 `T90`。
|
||||||
|
3. 计算开启上升时间:
|
||||||
|
|
||||||
|
```text
|
||||||
|
RiseTime = T90 - T0
|
||||||
|
```
|
||||||
|
|
||||||
|
| 标记 | 含义 |
|
||||||
|
| --- | --- |
|
||||||
|
| `T0` | 开启起点,信号达到 10% 阈值 |
|
||||||
|
| `T90` | 信号达到 90% 阈值 |
|
||||||
|
| `RiseTime` | 开启上升时间 |
|
||||||
|
|
||||||
|
### 关断曲线:关断 - 下降
|
||||||
|
|
||||||
|
关断曲线用于分析信号从高电平下降到低电平的过程。
|
||||||
|
|
||||||
|
```text
|
||||||
|
高电平 -> 关断起点 Toff -> 下降 -> T10 -> 低电平
|
||||||
|
```
|
||||||
|
|
||||||
|
判断逻辑:
|
||||||
|
|
||||||
|
1. 确认曲线中存在高电平区域。
|
||||||
|
2. 查找信号下降到 90% 阈值的位置,作为关断起点 `Toff`。
|
||||||
|
3. 从 `Toff` 之后继续查找信号下降到 10% 阈值的位置,作为 `T10`。
|
||||||
|
4. 计算关断下降时间:
|
||||||
|
|
||||||
|
```text
|
||||||
|
FallTime = T10 - Toff
|
||||||
|
```
|
||||||
|
|
||||||
|
| 标记 | 含义 |
|
||||||
|
| --- | --- |
|
||||||
|
| `Toff` | 关断起点,信号下降到 90% 阈值 |
|
||||||
|
| `T10` | 信号下降到 10% 阈值 |
|
||||||
|
| `FallTime` | 关断下降时间 |
|
||||||
|
|
||||||
|
## 3. 参数模板
|
||||||
|
|
||||||
|
参数固定在代码变量中,模板如下:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Vhigh: 3.0
|
||||||
|
R90: 0.9
|
||||||
|
Rstart: 0.1
|
||||||
|
StableN: 1
|
||||||
|
Win: 5
|
||||||
|
Sigma: 300
|
||||||
|
MedianFilterCheckBox: 5
|
||||||
|
```
|
||||||
|
|
||||||
|
### 参数含义
|
||||||
|
|
||||||
|
| 参数 | 模板值 | 含义 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `Vhigh` | `3.0` | 名义高电平电压 |
|
||||||
|
| `R90` | `0.9` | 90% 阈值比例 |
|
||||||
|
| `Rstart` | `0.1` | 10% 阈值比例 |
|
||||||
|
| `StableN` | `1` | 连续满足阈值的采样点数量 |
|
||||||
|
| `Win` | `5` | 搜索/判断窗口点数 |
|
||||||
|
| `Sigma` | `300` | 算法阈值或窗口相关参数,按代码变量固定 |
|
||||||
|
| `MedianFilterCheckBox` | `5` | 中值滤波窗口点数 |
|
||||||
|
|
||||||
|
## 4. 输出字段
|
||||||
|
|
||||||
|
| 字段 | 开启曲线含义 | 关断曲线含义 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `StartIndex` | `T0` 对应采样点索引 | `Toff` 对应采样点索引 |
|
||||||
|
| `EndIndex` | `T90` 对应采样点索引 | `T10` 对应采样点索引 |
|
||||||
|
| `T0` | 开启起点时间 | 关断起点时间 |
|
||||||
|
| `T90` | 上升到 90% 的时间 | 下降到 10% 的时间 |
|
||||||
|
| `RiseTime` | `T90 - T0` | `T10 - Toff` |
|
||||||
@ -47,11 +47,8 @@ public static class CurveMetrics
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static double RiseTimeMs(CurveData curve, double vMax, double lowRatio = 0.1, double highRatio = 0.9)
|
public static double RiseTimeMs(CurveData curve, double vMax, double lowRatio = 0.1, double highRatio = 0.9)
|
||||||
{
|
{
|
||||||
int iLow = FirstReachIndex(curve.Voltage, vMax * lowRatio, rising: true);
|
var mark = AnalyzeRising(curve, vMax, lowRatio, highRatio);
|
||||||
if (iLow < 0) return double.NaN;
|
return mark.IsValid ? mark.DurationMs : double.NaN;
|
||||||
int iHigh = FirstReachIndex(curve.Voltage, vMax * highRatio, rising: true, startIndex: iLow);
|
|
||||||
if (iHigh < 0) return double.NaN;
|
|
||||||
return (curve.TimeAt(iHigh) - curve.TimeAt(iLow)) * 1000.0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -59,11 +56,84 @@ public static class CurveMetrics
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static double FallTimeMs(CurveData curve, double vMax, double highRatio = 0.9, double lowRatio = 0.1)
|
public static double FallTimeMs(CurveData curve, double vMax, double highRatio = 0.9, double lowRatio = 0.1)
|
||||||
{
|
{
|
||||||
int iHigh = FirstReachIndex(curve.Voltage, vMax * highRatio, rising: false);
|
var mark = AnalyzeFalling(curve, vMax, highRatio, lowRatio);
|
||||||
if (iHigh < 0) return double.NaN;
|
return mark.IsValid ? mark.DurationMs : double.NaN;
|
||||||
int iLow = FirstReachIndex(curve.Voltage, vMax * lowRatio, rising: false, startIndex: iHigh);
|
}
|
||||||
if (iLow < 0) return double.NaN;
|
|
||||||
return (curve.TimeAt(iLow) - curve.TimeAt(iHigh)) * 1000.0;
|
/// <summary>Rising curve 10% - 90% transition mark.</summary>
|
||||||
|
public static CurveTransitionMark AnalyzeRising(CurveData curve, double vMax, double lowRatio = 0.1, double highRatio = 0.9)
|
||||||
|
{
|
||||||
|
double startThreshold = 0;
|
||||||
|
double endThreshold = vMax * highRatio;
|
||||||
|
int triggerIndex = TriggerIndex(curve);
|
||||||
|
int endIndex = FirstReachIndex(curve.Voltage, endThreshold, rising: true, startIndex: triggerIndex);
|
||||||
|
if (endIndex < 0) return CurveTransitionMark.Invalid(startThreshold, endThreshold);
|
||||||
|
|
||||||
|
int startIndex = LastReachIndex(curve.Voltage, startThreshold, rising: false, startIndex: triggerIndex, endIndex: endIndex);
|
||||||
|
if (startIndex < 0) startIndex = FirstReachIndex(curve.Voltage, startThreshold, rising: true, startIndex: triggerIndex);
|
||||||
|
if (startIndex < 0 || startIndex > endIndex) return CurveTransitionMark.Invalid(startThreshold, endThreshold);
|
||||||
|
|
||||||
|
return BuildTransitionMark(curve, startIndex, endIndex, startThreshold, endThreshold);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Falling curve 90% - 10% transition mark.</summary>
|
||||||
|
public static CurveTransitionMark AnalyzeFalling(CurveData curve, double vMax, double highRatio = 0.9, double lowRatio = 0.1)
|
||||||
|
{
|
||||||
|
double startThreshold = vMax;
|
||||||
|
double endThreshold = vMax * lowRatio;
|
||||||
|
int triggerIndex = TriggerIndex(curve);
|
||||||
|
int endIndex = FirstReachIndex(curve.Voltage, endThreshold, rising: false, startIndex: triggerIndex);
|
||||||
|
if (endIndex < 0) return CurveTransitionMark.Invalid(startThreshold, endThreshold);
|
||||||
|
|
||||||
|
int startIndex = LastReachIndex(curve.Voltage, vMax * 0.98, rising: true, startIndex: triggerIndex, endIndex: endIndex);
|
||||||
|
if (startIndex < 0) startIndex = triggerIndex;
|
||||||
|
if (startIndex > endIndex) return CurveTransitionMark.Invalid(startThreshold, endThreshold);
|
||||||
|
|
||||||
|
return BuildTransitionMark(curve, startIndex, endIndex, startThreshold, endThreshold);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int TriggerIndex(CurveData curve)
|
||||||
|
{
|
||||||
|
if (curve.TriggerSampleIndex > 0)
|
||||||
|
return Math.Min(curve.TriggerSampleIndex, Math.Max(curve.Voltage.Length - 1, 0));
|
||||||
|
|
||||||
|
if (curve.SampleRateHz <= 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
int index = (int)Math.Round(curve.TriggerTimeSec * curve.SampleRateHz);
|
||||||
|
return Math.Clamp(index, 0, Math.Max(curve.Voltage.Length - 1, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int LastReachIndex(double[] v, double vTarget, bool rising, int startIndex, int endIndex)
|
||||||
|
{
|
||||||
|
if (v == null || v.Length == 0) return -1;
|
||||||
|
startIndex = Math.Clamp(startIndex, 0, v.Length - 1);
|
||||||
|
endIndex = Math.Clamp(endIndex, 0, v.Length - 1);
|
||||||
|
for (int i = endIndex; i >= startIndex; i--)
|
||||||
|
{
|
||||||
|
if (rising ? v[i] >= vTarget : v[i] <= vTarget) return i;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CurveTransitionMark BuildTransitionMark(
|
||||||
|
CurveData curve,
|
||||||
|
int startIndex,
|
||||||
|
int endIndex,
|
||||||
|
double startThreshold,
|
||||||
|
double endThreshold)
|
||||||
|
{
|
||||||
|
double startTimeMs = (curve.TimeAt(startIndex) - curve.TriggerTimeSec) * 1000.0;
|
||||||
|
double endTimeMs = (curve.TimeAt(endIndex) - curve.TriggerTimeSec) * 1000.0;
|
||||||
|
return new CurveTransitionMark(
|
||||||
|
startIndex,
|
||||||
|
endIndex,
|
||||||
|
startTimeMs,
|
||||||
|
endTimeMs,
|
||||||
|
endTimeMs - startTimeMs,
|
||||||
|
startThreshold,
|
||||||
|
endThreshold,
|
||||||
|
true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>从波形最大值估算 Vmax(去掉前后噪声,取 95 百分位以上的均值)。</summary>
|
/// <summary>从波形最大值估算 Vmax(去掉前后噪声,取 95 百分位以上的均值)。</summary>
|
||||||
@ -78,3 +148,25 @@ public static class CurveMetrics
|
|||||||
return n > 0 ? sum / n : sorted[^1];
|
return n > 0 ? sum / n : sorted[^1];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public readonly record struct CurveTransitionMark(
|
||||||
|
int StartIndex,
|
||||||
|
int EndIndex,
|
||||||
|
double StartTimeMs,
|
||||||
|
double EndTimeMs,
|
||||||
|
double DurationMs,
|
||||||
|
double StartThresholdV,
|
||||||
|
double EndThresholdV,
|
||||||
|
bool IsValid)
|
||||||
|
{
|
||||||
|
public static CurveTransitionMark Invalid(double startThresholdV, double endThresholdV) =>
|
||||||
|
new(
|
||||||
|
-1,
|
||||||
|
-1,
|
||||||
|
double.NaN,
|
||||||
|
double.NaN,
|
||||||
|
double.NaN,
|
||||||
|
startThresholdV,
|
||||||
|
endThresholdV,
|
||||||
|
false);
|
||||||
|
}
|
||||||
|
|||||||
@ -18,6 +18,7 @@ public static class CurveImageRenderer
|
|||||||
curve,
|
curve,
|
||||||
"ON",
|
"ON",
|
||||||
Color.FromHex("#1A73C4"),
|
Color.FromHex("#1A73C4"),
|
||||||
|
true,
|
||||||
viewStartMs,
|
viewStartMs,
|
||||||
viewEndMs,
|
viewEndMs,
|
||||||
width,
|
width,
|
||||||
@ -35,6 +36,7 @@ public static class CurveImageRenderer
|
|||||||
curve,
|
curve,
|
||||||
"OFF",
|
"OFF",
|
||||||
Color.FromHex("#D23B3B"),
|
Color.FromHex("#D23B3B"),
|
||||||
|
false,
|
||||||
viewStartMs,
|
viewStartMs,
|
||||||
viewEndMs,
|
viewEndMs,
|
||||||
width,
|
width,
|
||||||
@ -68,6 +70,8 @@ public static class CurveImageRenderer
|
|||||||
double vMax = CurveMetrics.EstimateVMax(rising.Voltage);
|
double vMax = CurveMetrics.EstimateVMax(rising.Voltage);
|
||||||
AddThreshold(plot, vMax * 0.9, "90%");
|
AddThreshold(plot, vMax * 0.9, "90%");
|
||||||
AddThreshold(plot, vMax * 0.1, "10%");
|
AddThreshold(plot, vMax * 0.1, "10%");
|
||||||
|
AddTransitionMark(plot, rising, vMax, true, viewStartMs, viewEndMs);
|
||||||
|
AddTransitionMark(plot, falling, vMax, false, viewStartMs, viewEndMs);
|
||||||
|
|
||||||
if (!hasWindow || (start <= 0 && end >= 0))
|
if (!hasWindow || (start <= 0 && end >= 0))
|
||||||
{
|
{
|
||||||
@ -87,6 +91,7 @@ public static class CurveImageRenderer
|
|||||||
CurveData curve,
|
CurveData curve,
|
||||||
string phaseLabel,
|
string phaseLabel,
|
||||||
Color color,
|
Color color,
|
||||||
|
bool isRising,
|
||||||
double? viewStartMs,
|
double? viewStartMs,
|
||||||
double? viewEndMs,
|
double? viewEndMs,
|
||||||
int width,
|
int width,
|
||||||
@ -110,6 +115,7 @@ public static class CurveImageRenderer
|
|||||||
double vMax = CurveMetrics.EstimateVMax(curve.Voltage);
|
double vMax = CurveMetrics.EstimateVMax(curve.Voltage);
|
||||||
AddThreshold(plot, vMax * 0.9, "90%");
|
AddThreshold(plot, vMax * 0.9, "90%");
|
||||||
AddThreshold(plot, vMax * 0.1, "10%");
|
AddThreshold(plot, vMax * 0.1, "10%");
|
||||||
|
AddTransitionMark(plot, curve, vMax, isRising, viewStartMs, viewEndMs);
|
||||||
|
|
||||||
if (!hasWindow || (start <= 0 && end >= 0))
|
if (!hasWindow || (start <= 0 && end >= 0))
|
||||||
{
|
{
|
||||||
@ -176,4 +182,61 @@ public static class CurveImageRenderer
|
|||||||
line.LineWidth = 1;
|
line.LineWidth = 1;
|
||||||
line.LegendText = label;
|
line.LegendText = label;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void AddTransitionMark(
|
||||||
|
Plot plot,
|
||||||
|
CurveData curve,
|
||||||
|
double vMax,
|
||||||
|
bool isRising,
|
||||||
|
double? viewStartMs,
|
||||||
|
double? viewEndMs)
|
||||||
|
{
|
||||||
|
if (!double.IsFinite(vMax) || vMax <= 0) return;
|
||||||
|
|
||||||
|
var mark = isRising
|
||||||
|
? CurveMetrics.AnalyzeRising(curve, vMax)
|
||||||
|
: CurveMetrics.AnalyzeFalling(curve, vMax);
|
||||||
|
if (!mark.IsValid) return;
|
||||||
|
|
||||||
|
bool hasWindow = IsValidWindow(viewStartMs, viewEndMs);
|
||||||
|
double start = viewStartMs ?? 0;
|
||||||
|
double end = viewEndMs ?? 0;
|
||||||
|
|
||||||
|
string startLabel = isRising ? "TON" : "TOFF";
|
||||||
|
string endLabel = isRising ? "T90" : "T10";
|
||||||
|
|
||||||
|
if (IsVisibleInView(mark.StartTimeMs, hasWindow, start, end))
|
||||||
|
AddMarkerLine(plot, mark.StartTimeMs, mark.StartThresholdV, startLabel, Color.FromHex("#1C8A2E"));
|
||||||
|
|
||||||
|
if (IsVisibleInView(mark.EndTimeMs, hasWindow, start, end))
|
||||||
|
AddMarkerLine(plot, mark.EndTimeMs, mark.EndThresholdV, endLabel, Color.FromHex("#D93025"));
|
||||||
|
|
||||||
|
double middleTime = (mark.StartTimeMs + mark.EndTimeMs) / 2.0;
|
||||||
|
if (IsVisibleInView(middleTime, hasWindow, start, end))
|
||||||
|
{
|
||||||
|
var points = BuildViewPoints(curve, viewStartMs, viewEndMs);
|
||||||
|
if (points.Count == 0) return;
|
||||||
|
|
||||||
|
double minY = points.Min(x => x.Voltage);
|
||||||
|
double maxY = points.Max(x => x.Voltage);
|
||||||
|
double labelY = minY + (maxY - minY) * 0.86;
|
||||||
|
var duration = plot.Add.Text($"{startLabel}-{endLabel} {mark.DurationMs:F3} ms", middleTime, labelY);
|
||||||
|
duration.LabelFontColor = Color.FromHex("#2F3A45");
|
||||||
|
duration.LabelFontSize = 11;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddMarkerLine(Plot plot, double timeMs, double thresholdV, string text, Color color)
|
||||||
|
{
|
||||||
|
var line = plot.Add.VerticalLine(timeMs);
|
||||||
|
line.Color = color;
|
||||||
|
line.LineWidth = 2;
|
||||||
|
|
||||||
|
var label = plot.Add.Text(text, timeMs, thresholdV);
|
||||||
|
label.LabelFontColor = color;
|
||||||
|
label.LabelFontSize = 11;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsVisibleInView(double timeMs, bool hasWindow, double viewStart, double viewEnd) =>
|
||||||
|
double.IsFinite(timeMs) && (!hasWindow || (timeMs >= viewStart && timeMs <= viewEnd));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -41,12 +41,29 @@ public class CurveMetricsTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void RiseTimeMs_TenToNinety_AboutTwoPointTwo()
|
public void RiseTimeMs_TonToNinety_AboutNinetyPercentTime()
|
||||||
{
|
{
|
||||||
var c = MakeRising();
|
var c = MakeRising();
|
||||||
double rt = CurveMetrics.RiseTimeMs(c, 28);
|
double rt = CurveMetrics.RiseTimeMs(c, 28);
|
||||||
// ln(0.9/0.1) = ln 9 ≈ 2.197;rt = tau * 2.197 ≈ 1.099 ms
|
// ln(0.9/0.1) = ln 9 ≈ 2.197;rt = tau * 2.197 ≈ 1.099 ms
|
||||||
Assert.InRange(rt, 1.05, 1.15);
|
Assert.InRange(rt, 1.1, 1.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AnalyzeRising_ReturnsTonToNinetyIndexesAndDuration()
|
||||||
|
{
|
||||||
|
var c = MakeRising();
|
||||||
|
var mark = CurveMetrics.AnalyzeRising(c, 28);
|
||||||
|
int triggerIndex = (int)Math.Round(c.TriggerTimeSec * c.SampleRateHz);
|
||||||
|
int expectedEnd = CurveMetrics.FirstReachIndex(c.Voltage, 28 * 0.9, rising: true, triggerIndex);
|
||||||
|
int expectedStart = triggerIndex;
|
||||||
|
|
||||||
|
Assert.True(mark.IsValid);
|
||||||
|
Assert.Equal(expectedStart, mark.StartIndex);
|
||||||
|
Assert.Equal(expectedEnd, mark.EndIndex);
|
||||||
|
Assert.Equal(0, mark.StartThresholdV, 6);
|
||||||
|
Assert.Equal((c.TimeAt(expectedEnd) - c.TimeAt(expectedStart)) * 1000.0, mark.DurationMs, 6);
|
||||||
|
Assert.Equal(mark.DurationMs, CurveMetrics.RiseTimeMs(c, 28), 6);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@ -57,6 +74,55 @@ public class CurveMetricsTests
|
|||||||
Assert.InRange(off, 1.1, 1.2);
|
Assert.InRange(off, 1.1, 1.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AnalyzeFalling_ReturnsStableHighToTenIndexesAndDuration()
|
||||||
|
{
|
||||||
|
var c = MakeFalling();
|
||||||
|
var mark = CurveMetrics.AnalyzeFalling(c, 28);
|
||||||
|
int triggerIndex = (int)Math.Round(c.TriggerTimeSec * c.SampleRateHz);
|
||||||
|
int expectedEnd = CurveMetrics.FirstReachIndex(c.Voltage, 28 * 0.1, rising: false, triggerIndex);
|
||||||
|
int expectedStart = triggerIndex;
|
||||||
|
for (int i = triggerIndex; i <= expectedEnd; i++)
|
||||||
|
{
|
||||||
|
if (c.Voltage[i] >= 28 * 0.98)
|
||||||
|
expectedStart = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.True(mark.IsValid);
|
||||||
|
Assert.Equal(expectedStart, mark.StartIndex);
|
||||||
|
Assert.Equal(expectedEnd, mark.EndIndex);
|
||||||
|
Assert.Equal(28, mark.StartThresholdV, 6);
|
||||||
|
Assert.Equal((c.TimeAt(expectedEnd) - c.TimeAt(expectedStart)) * 1000.0, mark.DurationMs, 6);
|
||||||
|
Assert.Equal(mark.DurationMs, CurveMetrics.FallTimeMs(c, 28), 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AnalyzeTransition_ReturnsInvalidWhenThresholdsAreNotReached()
|
||||||
|
{
|
||||||
|
var risingCurve = new CurveData
|
||||||
|
{
|
||||||
|
SampleRateHz = 100_000,
|
||||||
|
TriggerTimeSec = 0,
|
||||||
|
Voltage = Enumerable.Repeat(0.0, 100).ToArray()
|
||||||
|
};
|
||||||
|
var fallingCurve = new CurveData
|
||||||
|
{
|
||||||
|
SampleRateHz = 100_000,
|
||||||
|
TriggerTimeSec = 0,
|
||||||
|
Voltage = Enumerable.Repeat(28.0, 100).ToArray()
|
||||||
|
};
|
||||||
|
|
||||||
|
var rising = CurveMetrics.AnalyzeRising(risingCurve, 28);
|
||||||
|
var falling = CurveMetrics.AnalyzeFalling(fallingCurve, 28);
|
||||||
|
|
||||||
|
Assert.False(rising.IsValid);
|
||||||
|
Assert.False(falling.IsValid);
|
||||||
|
Assert.Equal(-1, rising.StartIndex);
|
||||||
|
Assert.Equal(-1, falling.EndIndex);
|
||||||
|
Assert.True(double.IsNaN(CurveMetrics.RiseTimeMs(risingCurve, 28)));
|
||||||
|
Assert.True(double.IsNaN(CurveMetrics.FallTimeMs(fallingCurve, 28)));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void EstimateVMax_NearActualMax()
|
public void EstimateVMax_NearActualMax()
|
||||||
{
|
{
|
||||||
|
|||||||
@ -5,6 +5,7 @@ using System.Windows.Media;
|
|||||||
using ScottPlot;
|
using ScottPlot;
|
||||||
using ScottPlot.WPF;
|
using ScottPlot.WPF;
|
||||||
using SSPCTester.Devices.Interfaces;
|
using SSPCTester.Devices.Interfaces;
|
||||||
|
using SSPCTester.Logic.Calculation;
|
||||||
|
|
||||||
namespace SSPCTester.UI.Controls;
|
namespace SSPCTester.UI.Controls;
|
||||||
|
|
||||||
@ -103,14 +104,9 @@ public partial class CurvePlotControl : UserControl
|
|||||||
sig.MarkerSize = 0;
|
sig.MarkerSize = 0;
|
||||||
|
|
||||||
// 阈值线
|
// 阈值线
|
||||||
double threshold = IsRising ? VMax * 0.9 : VMax * 0.1;
|
AddThresholdLine(VMax * 0.9, "90%", xs[0]);
|
||||||
var hLine = _plot.Plot.Add.HorizontalLine(threshold);
|
AddThresholdLine(VMax * 0.1, "10%", xs[0]);
|
||||||
hLine.Color = ScottPlot.Color.FromHex("#98A1AD");
|
AddTransitionMark(c, xs, ys);
|
||||||
hLine.LinePattern = ScottPlot.LinePattern.Dashed;
|
|
||||||
hLine.LineWidth = 1;
|
|
||||||
var label = _plot.Plot.Add.Text($"{(IsRising ? 90 : 10)}%", xs[0], threshold);
|
|
||||||
label.LabelFontColor = ScottPlot.Color.FromHex("#98A1AD");
|
|
||||||
label.LabelFontSize = 10;
|
|
||||||
|
|
||||||
// 触发线 t=0
|
// 触发线 t=0
|
||||||
if (!HasViewWindow(out double start, out double end) || (start <= 0 && end >= 0))
|
if (!HasViewWindow(out double start, out double end) || (start <= 0 && end >= 0))
|
||||||
@ -125,6 +121,66 @@ public partial class CurvePlotControl : UserControl
|
|||||||
_plot.Refresh();
|
_plot.Refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void AddThresholdLine(double volts, string text, double labelX)
|
||||||
|
{
|
||||||
|
if (!double.IsFinite(volts)) return;
|
||||||
|
|
||||||
|
var hLine = _plot!.Plot.Add.HorizontalLine(volts);
|
||||||
|
hLine.Color = ScottPlot.Color.FromHex("#98A1AD");
|
||||||
|
hLine.LinePattern = ScottPlot.LinePattern.Dashed;
|
||||||
|
hLine.LineWidth = 1;
|
||||||
|
|
||||||
|
var label = _plot.Plot.Add.Text(text, labelX, volts);
|
||||||
|
label.LabelFontColor = ScottPlot.Color.FromHex("#98A1AD");
|
||||||
|
label.LabelFontSize = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddTransitionMark(CurveData curve, double[] xs, double[] ys)
|
||||||
|
{
|
||||||
|
if (!double.IsFinite(VMax) || VMax <= 0) return;
|
||||||
|
|
||||||
|
var mark = IsRising
|
||||||
|
? CurveMetrics.AnalyzeRising(curve, VMax)
|
||||||
|
: CurveMetrics.AnalyzeFalling(curve, VMax);
|
||||||
|
if (!mark.IsValid) return;
|
||||||
|
|
||||||
|
bool hasWindow = HasViewWindow(out double viewStart, out double viewEnd);
|
||||||
|
string startLabel = IsRising ? "TON" : "TOFF";
|
||||||
|
string endLabel = IsRising ? "T90" : "T10";
|
||||||
|
|
||||||
|
if (IsVisibleInView(mark.StartTimeMs, hasWindow, viewStart, viewEnd))
|
||||||
|
AddMarkerLine(mark.StartTimeMs, mark.StartThresholdV, startLabel, "#1C8A2E");
|
||||||
|
|
||||||
|
if (IsVisibleInView(mark.EndTimeMs, hasWindow, viewStart, viewEnd))
|
||||||
|
AddMarkerLine(mark.EndTimeMs, mark.EndThresholdV, endLabel, "#D93025");
|
||||||
|
|
||||||
|
double middleTime = (mark.StartTimeMs + mark.EndTimeMs) / 2.0;
|
||||||
|
if (IsVisibleInView(middleTime, hasWindow, viewStart, viewEnd))
|
||||||
|
{
|
||||||
|
double minY = ys.Min();
|
||||||
|
double maxY = ys.Max();
|
||||||
|
double labelY = minY + (maxY - minY) * 0.86;
|
||||||
|
var duration = _plot!.Plot.Add.Text($"{startLabel}-{endLabel} {mark.DurationMs:F3} ms", middleTime, labelY);
|
||||||
|
duration.LabelFontColor = ScottPlot.Color.FromHex("#2F3A45");
|
||||||
|
duration.LabelFontSize = 11;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddMarkerLine(double timeMs, double thresholdV, string text, string colorHex)
|
||||||
|
{
|
||||||
|
var color = ScottPlot.Color.FromHex(colorHex);
|
||||||
|
var line = _plot!.Plot.Add.VerticalLine(timeMs);
|
||||||
|
line.Color = color;
|
||||||
|
line.LineWidth = 2;
|
||||||
|
|
||||||
|
var label = _plot.Plot.Add.Text(text, timeMs, thresholdV);
|
||||||
|
label.LabelFontColor = color;
|
||||||
|
label.LabelFontSize = 11;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsVisibleInView(double timeMs, bool hasWindow, double viewStart, double viewEnd) =>
|
||||||
|
double.IsFinite(timeMs) && (!hasWindow || (timeMs >= viewStart && timeMs <= viewEnd));
|
||||||
|
|
||||||
private List<(double TimeMs, double Voltage)> BuildViewPoints(CurveData curve)
|
private List<(double TimeMs, double Voltage)> BuildViewPoints(CurveData curve)
|
||||||
{
|
{
|
||||||
var points = new List<(double TimeMs, double Voltage)>(curve.Voltage.Length);
|
var points = new List<(double TimeMs, double Voltage)>(curve.Voltage.Length);
|
||||||
|
|||||||
@ -146,6 +146,22 @@ public partial class CurveTestViewModel : ObservableObject
|
|||||||
SaveProjectQuietly();
|
SaveProjectQuietly();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void ZoomRisingCurveToMarks()
|
||||||
|
{
|
||||||
|
if (SelectedRow == null || RisingCurve == null) return;
|
||||||
|
|
||||||
|
double vMax = CurveMetrics.EstimateVMax(RisingCurve.Voltage);
|
||||||
|
if (!TryBuildMarkWindow(CurveMetrics.AnalyzeRising(RisingCurve, vMax), RisingCurve, out double start, out double end))
|
||||||
|
return;
|
||||||
|
|
||||||
|
RisingViewStartMs = start;
|
||||||
|
RisingViewEndMs = end;
|
||||||
|
SelectedRow.RisingViewStartMs = start;
|
||||||
|
SelectedRow.RisingViewEndMs = end;
|
||||||
|
SaveProjectQuietly();
|
||||||
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void ResetFallingCurveView()
|
private void ResetFallingCurveView()
|
||||||
{
|
{
|
||||||
@ -157,6 +173,41 @@ public partial class CurveTestViewModel : ObservableObject
|
|||||||
SaveProjectQuietly();
|
SaveProjectQuietly();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void ZoomFallingCurveToMarks()
|
||||||
|
{
|
||||||
|
if (SelectedRow == null || FallingCurve == null) return;
|
||||||
|
|
||||||
|
double vMax = RisingCurve != null
|
||||||
|
? CurveMetrics.EstimateVMax(RisingCurve.Voltage)
|
||||||
|
: CurveMetrics.EstimateVMax(FallingCurve.Voltage);
|
||||||
|
if (!TryBuildMarkWindow(CurveMetrics.AnalyzeFalling(FallingCurve, vMax), FallingCurve, out double start, out double end))
|
||||||
|
return;
|
||||||
|
|
||||||
|
FallingViewStartMs = start;
|
||||||
|
FallingViewEndMs = end;
|
||||||
|
SelectedRow.FallingViewStartMs = start;
|
||||||
|
SelectedRow.FallingViewEndMs = end;
|
||||||
|
SaveProjectQuietly();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryBuildMarkWindow(CurveTransitionMark mark, CurveData curve, out double start, out double end)
|
||||||
|
{
|
||||||
|
start = 0;
|
||||||
|
end = 0;
|
||||||
|
if (!mark.IsValid || !double.IsFinite(mark.StartTimeMs) || !double.IsFinite(mark.EndTimeMs))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
double span = Math.Abs(mark.EndTimeMs - mark.StartTimeMs);
|
||||||
|
double padding = Math.Max(span * 0.15, 0.05);
|
||||||
|
double minTimeMs = -curve.TriggerTimeSec * 1000.0;
|
||||||
|
double maxTimeMs = (curve.DurationSec - curve.TriggerTimeSec) * 1000.0;
|
||||||
|
|
||||||
|
start = Math.Max(minTimeMs, Math.Min(mark.StartTimeMs, mark.EndTimeMs) - padding);
|
||||||
|
end = Math.Min(maxTimeMs, Math.Max(mark.StartTimeMs, mark.EndTimeMs) + padding);
|
||||||
|
return double.IsFinite(start) && double.IsFinite(end) && end > start;
|
||||||
|
}
|
||||||
|
|
||||||
private void SaveProjectQuietly()
|
private void SaveProjectQuietly()
|
||||||
{
|
{
|
||||||
var project = Project;
|
var project = Project;
|
||||||
|
|||||||
@ -89,6 +89,8 @@
|
|||||||
FontFamily="{StaticResource FontMono}" />
|
FontFamily="{StaticResource FontMono}" />
|
||||||
<Button Content="默认" Width="54" Height="26" Padding="0" Margin="0,0,8,0"
|
<Button Content="默认" Width="54" Height="26" Padding="0" Margin="0,0,8,0"
|
||||||
Command="{Binding ResetRisingCurveViewCommand}" />
|
Command="{Binding ResetRisingCurveViewCommand}" />
|
||||||
|
<Button Content="缩放到点位" Width="86" Height="26" Padding="0" Margin="0,0,8,0"
|
||||||
|
Command="{Binding ZoomRisingCurveToMarksCommand}" />
|
||||||
<Button Content="放大" Width="64" Height="26" Padding="0"
|
<Button Content="放大" Width="64" Height="26" Padding="0"
|
||||||
Command="{Binding OpenPopupCommand}"
|
Command="{Binding OpenPopupCommand}"
|
||||||
CommandParameter="Rising" />
|
CommandParameter="Rising" />
|
||||||
@ -118,6 +120,8 @@
|
|||||||
FontFamily="{StaticResource FontMono}" />
|
FontFamily="{StaticResource FontMono}" />
|
||||||
<Button Content="默认" Width="54" Height="26" Padding="0" Margin="0,0,8,0"
|
<Button Content="默认" Width="54" Height="26" Padding="0" Margin="0,0,8,0"
|
||||||
Command="{Binding ResetFallingCurveViewCommand}" />
|
Command="{Binding ResetFallingCurveViewCommand}" />
|
||||||
|
<Button Content="缩放到点位" Width="86" Height="26" Padding="0" Margin="0,0,8,0"
|
||||||
|
Command="{Binding ZoomFallingCurveToMarksCommand}" />
|
||||||
<Button Content="放大" Width="64" Height="26" Padding="0"
|
<Button Content="放大" Width="64" Height="26" Padding="0"
|
||||||
Command="{Binding OpenPopupCommand}"
|
Command="{Binding OpenPopupCommand}"
|
||||||
CommandParameter="Falling" />
|
CommandParameter="Falling" />
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user