173 lines
6.8 KiB
C#
173 lines
6.8 KiB
C#
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)
|
||
{
|
||
int idx = FirstReachIndex(curve.Voltage, vMax * thresholdRatio, rising: true);
|
||
if (idx < 0) return double.NaN;
|
||
double t = curve.TimeAt(idx) - curve.TriggerTimeSec;
|
||
return t * 1000.0;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 关闭时间(ms):控制信号撤销到电压降到 Vmax 的 thresholdRatio(如 10%)。
|
||
/// </summary>
|
||
public static double OffTimeMs(CurveData curve, double vMax, double thresholdRatio = 0.1)
|
||
{
|
||
int idx = FirstReachIndex(curve.Voltage, vMax * thresholdRatio, rising: false);
|
||
if (idx < 0) return double.NaN;
|
||
double t = curve.TimeAt(idx) - curve.TriggerTimeSec;
|
||
return t * 1000.0;
|
||
}
|
||
|
||
/// <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;
|
||
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>
|
||
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);
|
||
}
|