using SSPCTester.Devices.Interfaces; namespace SSPCTester.Logic.Calculation; /// /// 曲线指标计算(纯函数,可单测)。 /// 覆盖开发文档 §6.4 中 1.1-1.6 指标。 /// public static class CurveMetrics { /// 查找第一个达到 vTarget 的样点索引;找不到返回 -1。 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; } /// /// 开启时间(ms):控制信号发出(curve.TriggerTimeSec)到电压达到 Vmax 的 thresholdRatio。 /// 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; } /// /// 关闭时间(ms):控制信号撤销到电压降到 Vmax 的 thresholdRatio(如 10%)。 /// 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; } /// /// 上升时间(ms):电压从 Vmax * low 到 Vmax * high 的时间(默认 10% → 90%)。 /// 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; } /// /// 下降时间(ms):电压从 Vmax * high 到 Vmax * low 的时间(默认 90% → 10%)。 /// 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; } /// Rising curve 10% - 90% transition mark. 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); } /// Falling curve 90% - 10% transition mark. 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); } /// 从波形最大值估算 Vmax(去掉前后噪声,取 95 百分位以上的均值)。 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); }