SSPC-Tester/SSPCTester.Logic/Calculation/CurveMetrics.cs
2026-07-06 17:26:09 +08:00

344 lines
13 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)
{
var mark = AnalyzeRising(curve, vMax, 0.1, thresholdRatio);
return mark.IsValid ? mark.EndTimeMs : double.NaN;
}
/// <summary>
/// 关闭时间ms控制信号撤销到电压降到 Vmax 的 thresholdRatio如 10%)。
/// </summary>
public static double OffTimeMs(CurveData curve, double vMax, double thresholdRatio = 0.1)
{
var mark = AnalyzeFalling(curve, vMax, 0.9, thresholdRatio);
return mark.IsValid ? mark.EndTimeMs : double.NaN;
}
/// <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)
{
var mark = AnalyzeRising(curve, vMax, lowRatio, highRatio);
return mark.IsValid ? mark.DurationMs : double.NaN;
}
/// <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)
{
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;
if (!CanAnalyze(curve, vMax, startThreshold, endThreshold))
return CurveTransitionMark.Invalid(startThreshold, endThreshold);
int triggerIndex = TriggerIndex(curve);
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);
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);
return BuildTransitionMark(curve, startIndex, endIndex, curve.Voltage[startIndex], 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;
if (!CanAnalyze(curve, vMax, startThreshold, endThreshold))
return CurveTransitionMark.Invalid(startThreshold, endThreshold);
int triggerIndex = TriggerIndex(curve);
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);
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);
return BuildTransitionMark(curve, startIndex, endIndex, curve.Voltage[startIndex], 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 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];
}
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>
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];
}
}
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);
}