2026-06-30 12:10:29 +08:00
|
|
|
|
using SSPCTester.Devices.Interfaces;
|
|
|
|
|
|
|
|
|
|
|
|
namespace SSPCTester.Logic.Calculation;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 曲线指标计算(纯函数,可单测)。
|
|
|
|
|
|
/// 覆盖开发文档 §6.4 中 1.1-1.6 指标。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public static class CurveMetrics
|
|
|
|
|
|
{
|
|
|
|
|
|
/// <summary>查找第一个达到 vTarget 的样点索引;找不到返回 -1。</summary>
|
|
|
|
|
|
public static int FirstReachIndex(double[] v, double vTarget, bool rising = true, int startIndex = 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (v == null || v.Length == 0) return -1;
|
|
|
|
|
|
if (startIndex < 0) startIndex = 0;
|
|
|
|
|
|
for (int i = startIndex; i < v.Length; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (rising ? v[i] >= vTarget : v[i] <= vTarget) return i;
|
|
|
|
|
|
}
|
|
|
|
|
|
return -1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 开启时间(ms):控制信号发出(curve.TriggerTimeSec)到电压达到 Vmax 的 thresholdRatio。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public static double OnTimeMs(CurveData curve, double vMax, double thresholdRatio = 0.9)
|
|
|
|
|
|
{
|
2026-07-06 17:26:09 +08:00
|
|
|
|
var mark = AnalyzeRising(curve, vMax, 0.1, thresholdRatio);
|
|
|
|
|
|
return mark.IsValid ? mark.EndTimeMs : double.NaN;
|
2026-06-30 12:10:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 关闭时间(ms):控制信号撤销到电压降到 Vmax 的 thresholdRatio(如 10%)。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public static double OffTimeMs(CurveData curve, double vMax, double thresholdRatio = 0.1)
|
|
|
|
|
|
{
|
2026-07-06 17:26:09 +08:00
|
|
|
|
var mark = AnalyzeFalling(curve, vMax, 0.9, thresholdRatio);
|
|
|
|
|
|
return mark.IsValid ? mark.EndTimeMs : double.NaN;
|
2026-06-30 12:10:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 上升时间(ms):电压从 Vmax * low 到 Vmax * high 的时间(默认 10% → 90%)。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public static double RiseTimeMs(CurveData curve, double vMax, double lowRatio = 0.1, double highRatio = 0.9)
|
|
|
|
|
|
{
|
2026-07-05 13:33:55 +08:00
|
|
|
|
var mark = AnalyzeRising(curve, vMax, lowRatio, highRatio);
|
|
|
|
|
|
return mark.IsValid ? mark.DurationMs : double.NaN;
|
2026-06-30 12:10:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 下降时间(ms):电压从 Vmax * high 到 Vmax * low 的时间(默认 90% → 10%)。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public static double FallTimeMs(CurveData curve, double vMax, double highRatio = 0.9, double lowRatio = 0.1)
|
|
|
|
|
|
{
|
2026-07-05 13:33:55 +08:00
|
|
|
|
var mark = AnalyzeFalling(curve, vMax, highRatio, lowRatio);
|
|
|
|
|
|
return mark.IsValid ? mark.DurationMs : double.NaN;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <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;
|
2026-07-06 17:26:09 +08:00
|
|
|
|
if (!CanAnalyze(curve, vMax, startThreshold, endThreshold))
|
|
|
|
|
|
return CurveTransitionMark.Invalid(startThreshold, endThreshold);
|
|
|
|
|
|
|
2026-07-05 13:33:55 +08:00
|
|
|
|
int triggerIndex = TriggerIndex(curve);
|
2026-07-06 17:26:09 +08:00
|
|
|
|
double[] filtered = Smooth(curve.Voltage);
|
|
|
|
|
|
int preliminaryEndIndex = FindStableReachIndex(filtered, endThreshold, rising: true, triggerIndex, filtered.Length - 1);
|
|
|
|
|
|
if (preliminaryEndIndex < 0)
|
|
|
|
|
|
preliminaryEndIndex = FirstReachIndex(filtered, endThreshold, rising: true, startIndex: triggerIndex);
|
|
|
|
|
|
if (preliminaryEndIndex < 0) return CurveTransitionMark.Invalid(startThreshold, endThreshold);
|
2026-07-05 13:33:55 +08:00
|
|
|
|
|
2026-07-06 17:26:09 +08:00
|
|
|
|
int startIndex = FindDepartureStartIndex(filtered, curve.SampleRateHz, triggerIndex, preliminaryEndIndex, rising: true, vMax);
|
|
|
|
|
|
int endIndex = FirstReachIndex(curve.Voltage, endThreshold, rising: true, startIndex: startIndex);
|
|
|
|
|
|
if (endIndex < 0 || startIndex > endIndex) return CurveTransitionMark.Invalid(startThreshold, endThreshold);
|
2026-07-05 13:33:55 +08:00
|
|
|
|
|
2026-07-06 17:26:09 +08:00
|
|
|
|
return BuildTransitionMark(curve, startIndex, endIndex, curve.Voltage[startIndex], endThreshold);
|
2026-07-05 13:33:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <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;
|
2026-07-06 17:26:09 +08:00
|
|
|
|
if (!CanAnalyze(curve, vMax, startThreshold, endThreshold))
|
|
|
|
|
|
return CurveTransitionMark.Invalid(startThreshold, endThreshold);
|
|
|
|
|
|
|
2026-07-05 13:33:55 +08:00
|
|
|
|
int triggerIndex = TriggerIndex(curve);
|
2026-07-06 17:26:09 +08:00
|
|
|
|
double[] filtered = Smooth(curve.Voltage);
|
|
|
|
|
|
int preliminaryEndIndex = FindStableReachIndex(filtered, endThreshold, rising: false, triggerIndex, filtered.Length - 1);
|
|
|
|
|
|
if (preliminaryEndIndex < 0)
|
|
|
|
|
|
preliminaryEndIndex = FirstReachIndex(filtered, endThreshold, rising: false, startIndex: triggerIndex);
|
|
|
|
|
|
if (preliminaryEndIndex < 0) return CurveTransitionMark.Invalid(startThreshold, endThreshold);
|
2026-07-05 13:33:55 +08:00
|
|
|
|
|
2026-07-06 17:26:09 +08:00
|
|
|
|
int startIndex = FindDepartureStartIndex(filtered, curve.SampleRateHz, triggerIndex, preliminaryEndIndex, rising: false, vMax);
|
|
|
|
|
|
int endIndex = FirstReachIndex(curve.Voltage, endThreshold, rising: false, startIndex: startIndex);
|
|
|
|
|
|
if (endIndex < 0 || startIndex > endIndex) return CurveTransitionMark.Invalid(startThreshold, endThreshold);
|
2026-07-05 13:33:55 +08:00
|
|
|
|
|
2026-07-06 17:26:09 +08:00
|
|
|
|
return BuildTransitionMark(curve, startIndex, endIndex, curve.Voltage[startIndex], endThreshold);
|
2026-07-05 13:33:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-06 17:26:09 +08:00
|
|
|
|
private static bool CanAnalyze(CurveData curve, double vMax, double startThreshold, double endThreshold) =>
|
|
|
|
|
|
curve.Voltage.Length > 1
|
|
|
|
|
|
&& curve.SampleRateHz > 0
|
|
|
|
|
|
&& double.IsFinite(vMax)
|
|
|
|
|
|
&& vMax > 0
|
|
|
|
|
|
&& double.IsFinite(startThreshold)
|
|
|
|
|
|
&& double.IsFinite(endThreshold);
|
|
|
|
|
|
|
|
|
|
|
|
private static double[] Smooth(double[] values)
|
|
|
|
|
|
{
|
|
|
|
|
|
double[] median = MedianFilter(values, window: 5);
|
|
|
|
|
|
return MovingAverage(median, window: 5);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static double[] MedianFilter(double[] values, int window)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (values.Length == 0 || window <= 1)
|
|
|
|
|
|
return (double[])values.Clone();
|
|
|
|
|
|
|
|
|
|
|
|
int radius = window / 2;
|
|
|
|
|
|
var result = new double[values.Length];
|
|
|
|
|
|
var scratch = new double[window];
|
|
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < values.Length; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
int from = Math.Max(0, i - radius);
|
|
|
|
|
|
int to = Math.Min(values.Length - 1, i + radius);
|
|
|
|
|
|
int count = 0;
|
|
|
|
|
|
for (int j = from; j <= to; j++)
|
|
|
|
|
|
scratch[count++] = values[j];
|
|
|
|
|
|
|
|
|
|
|
|
Array.Sort(scratch, 0, count);
|
|
|
|
|
|
result[i] = scratch[count / 2];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static double[] MovingAverage(double[] values, int window)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (values.Length == 0 || window <= 1)
|
|
|
|
|
|
return (double[])values.Clone();
|
|
|
|
|
|
|
|
|
|
|
|
int radius = window / 2;
|
|
|
|
|
|
var result = new double[values.Length];
|
|
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < values.Length; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
int from = Math.Max(0, i - radius);
|
|
|
|
|
|
int to = Math.Min(values.Length - 1, i + radius);
|
|
|
|
|
|
double sum = 0;
|
|
|
|
|
|
int count = 0;
|
|
|
|
|
|
for (int j = from; j <= to; j++)
|
|
|
|
|
|
{
|
|
|
|
|
|
sum += values[j];
|
|
|
|
|
|
count++;
|
|
|
|
|
|
}
|
|
|
|
|
|
result[i] = count > 0 ? sum / count : values[i];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static int FindDepartureStartIndex(double[] values, int sampleRateHz, int triggerIndex, int endIndex, bool rising, double vMax)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (values.Length == 0) return -1;
|
|
|
|
|
|
|
|
|
|
|
|
triggerIndex = Math.Clamp(triggerIndex, 0, values.Length - 1);
|
|
|
|
|
|
endIndex = Math.Clamp(endIndex, triggerIndex, values.Length - 1);
|
|
|
|
|
|
|
|
|
|
|
|
double baseline = EstimateBaseline(values, triggerIndex);
|
|
|
|
|
|
double noise = EstimateAmplitudeNoise(values, triggerIndex);
|
|
|
|
|
|
double deadband = Math.Max(vMax * 0.005, noise * 6.0);
|
|
|
|
|
|
int stableCount = StableCount(sampleRateHz);
|
|
|
|
|
|
int lastStableBaselineEnd = triggerIndex;
|
|
|
|
|
|
|
|
|
|
|
|
for (int i = triggerIndex; i <= endIndex - stableCount + 1; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
bool isBaseline = true;
|
|
|
|
|
|
for (int j = i; j < i + stableCount; j++)
|
|
|
|
|
|
{
|
|
|
|
|
|
bool nearBaseline = rising
|
|
|
|
|
|
? values[j] <= baseline + deadband
|
|
|
|
|
|
: values[j] >= baseline - deadband;
|
|
|
|
|
|
if (!nearBaseline)
|
|
|
|
|
|
{
|
|
|
|
|
|
isBaseline = false;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (isBaseline)
|
|
|
|
|
|
lastStableBaselineEnd = i + stableCount - 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
int start = Math.Min(lastStableBaselineEnd + 1, endIndex);
|
|
|
|
|
|
return Math.Clamp(start, triggerIndex, values.Length - 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static int FindStableReachIndex(double[] values, double target, bool rising, int startIndex, int endIndex)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (values.Length == 0) return -1;
|
|
|
|
|
|
|
|
|
|
|
|
const int stableCount = 3;
|
|
|
|
|
|
startIndex = Math.Clamp(startIndex, 0, values.Length - 1);
|
|
|
|
|
|
endIndex = Math.Clamp(endIndex, 0, values.Length - 1);
|
|
|
|
|
|
|
|
|
|
|
|
for (int i = startIndex; i <= endIndex; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
bool ok = true;
|
|
|
|
|
|
int to = Math.Min(endIndex, i + stableCount - 1);
|
|
|
|
|
|
for (int j = i; j <= to; j++)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!(rising ? values[j] >= target : values[j] <= target))
|
|
|
|
|
|
{
|
|
|
|
|
|
ok = false;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (ok && to - i + 1 == stableCount)
|
|
|
|
|
|
return i;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return -1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static int StableCount(int sampleRateHz)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (sampleRateHz <= 0) return 3;
|
|
|
|
|
|
int count = (int)Math.Round(sampleRateHz * 0.00002);
|
|
|
|
|
|
return Math.Clamp(count, 3, 25);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static double EstimateBaseline(double[] values, int triggerIndex)
|
|
|
|
|
|
{
|
|
|
|
|
|
int end = Math.Clamp(triggerIndex, 0, values.Length - 1);
|
|
|
|
|
|
int count = Math.Min(Math.Max(end + 1, 1), 100);
|
|
|
|
|
|
int start = Math.Max(0, end - count + 1);
|
|
|
|
|
|
var samples = new double[end - start + 1];
|
|
|
|
|
|
for (int i = start; i <= end; i++)
|
|
|
|
|
|
samples[i - start] = values[i];
|
|
|
|
|
|
|
|
|
|
|
|
Array.Sort(samples);
|
|
|
|
|
|
return samples[samples.Length / 2];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static double EstimateAmplitudeNoise(double[] values, int triggerIndex)
|
|
|
|
|
|
{
|
|
|
|
|
|
int end = Math.Clamp(triggerIndex, 0, values.Length - 1);
|
|
|
|
|
|
int count = Math.Min(Math.Max(end + 1, 1), 100);
|
|
|
|
|
|
int start = Math.Max(0, end - count + 1);
|
|
|
|
|
|
if (end <= start) return 0;
|
|
|
|
|
|
|
|
|
|
|
|
double baseline = EstimateBaseline(values, triggerIndex);
|
|
|
|
|
|
var deviations = new double[end - start + 1];
|
|
|
|
|
|
for (int i = start; i <= end; i++)
|
|
|
|
|
|
deviations[i - start] = Math.Abs(values[i] - baseline);
|
|
|
|
|
|
|
|
|
|
|
|
Array.Sort(deviations);
|
|
|
|
|
|
return deviations[deviations.Length / 2];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-05 13:33:55 +08:00
|
|
|
|
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);
|
2026-06-30 12:10:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>从波形最大值估算 Vmax(去掉前后噪声,取 95 百分位以上的均值)。</summary>
|
|
|
|
|
|
public static double EstimateVMax(double[] v)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (v == null || v.Length == 0) return 0;
|
|
|
|
|
|
var sorted = (double[])v.Clone();
|
|
|
|
|
|
Array.Sort(sorted);
|
|
|
|
|
|
int from = (int)(sorted.Length * 0.95);
|
|
|
|
|
|
double sum = 0; int n = 0;
|
|
|
|
|
|
for (int i = from; i < sorted.Length; i++) { sum += sorted[i]; n++; }
|
|
|
|
|
|
return n > 0 ? sum / n : sorted[^1];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-05 13:33:55 +08:00
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|