|
|
|
|
@@ -116,17 +116,22 @@ public sealed class RunExportService
|
|
|
|
|
.OrderBy(sample => sample.SampleIndex)
|
|
|
|
|
.ToArray()
|
|
|
|
|
})
|
|
|
|
|
.Where(item => item.ValidSamples.Length > 0)
|
|
|
|
|
.Where(item => item.ValidSamples.Length > 0 || item.Data.ReciprocatingRecords.Any(record => record.HasData))
|
|
|
|
|
.Select((item, index) => new ExportCurveData(item.Data, item.ValidSamples, index + 1, GetCurveColorHex(index)))
|
|
|
|
|
.ToArray();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static ExportReportStatistics BuildExportStatistics(int sourceRunCount, IReadOnlyList<ExportCurveData> exportRuns)
|
|
|
|
|
{
|
|
|
|
|
var chartableRuns = exportRuns.Where(IsChartableRun).ToArray();
|
|
|
|
|
var chartImageCount = chartableRuns.Length == 0
|
|
|
|
|
? 0
|
|
|
|
|
: 1 + chartableRuns.Sum(CountReciprocatingCurveSegments);
|
|
|
|
|
|
|
|
|
|
return new ExportReportStatistics(
|
|
|
|
|
sourceRunCount,
|
|
|
|
|
exportRuns.Count,
|
|
|
|
|
exportRuns.Count(IsChartableRun),
|
|
|
|
|
chartImageCount,
|
|
|
|
|
Math.Max(0, sourceRunCount - exportRuns.Count),
|
|
|
|
|
exportRuns.Sum(item => item.ValidSamples.Count));
|
|
|
|
|
}
|
|
|
|
|
@@ -215,7 +220,7 @@ public sealed class RunExportService
|
|
|
|
|
{
|
|
|
|
|
var row = dataStartRow + index;
|
|
|
|
|
var item = runs[index];
|
|
|
|
|
var forceStats = CalculateForceStatistics(item.ValidSamples);
|
|
|
|
|
var metrics = BuildRunReportMetrics(item);
|
|
|
|
|
|
|
|
|
|
worksheet.Cell(row, 1).Value = BuildCurveKey(item);
|
|
|
|
|
worksheet.Cell(row, 2).Value = BuildCurveLabel(item);
|
|
|
|
|
@@ -228,19 +233,19 @@ public sealed class RunExportService
|
|
|
|
|
worksheet.Cell(row, 8).Value = item.Data.Recipe.ProductCode;
|
|
|
|
|
worksheet.Cell(row, 9).Value = item.Data.Recipe.TestMode;
|
|
|
|
|
worksheet.Cell(row, 10).Value = item.Data.Recipe.Direction;
|
|
|
|
|
worksheet.Cell(row, 11).Value = item.Data.Run.StaticCoefficient;
|
|
|
|
|
worksheet.Cell(row, 12).Value = item.Data.Run.KineticCoefficient;
|
|
|
|
|
worksheet.Cell(row, 13).Value = item.Data.Run.StandardDeviation;
|
|
|
|
|
worksheet.Cell(row, 14).Value = item.Data.Run.StandardDeviation1;
|
|
|
|
|
worksheet.Cell(row, 15).Value = item.Data.Run.StandardDeviation2;
|
|
|
|
|
worksheet.Cell(row, 16).Value = item.Data.Run.PeakForceN;
|
|
|
|
|
worksheet.Cell(row, 17).Value = item.Data.Run.AverageForceN;
|
|
|
|
|
worksheet.Cell(row, 11).Value = metrics.StaticCoefficient;
|
|
|
|
|
worksheet.Cell(row, 12).Value = metrics.KineticCoefficient;
|
|
|
|
|
worksheet.Cell(row, 13).Value = metrics.StandardDeviation;
|
|
|
|
|
worksheet.Cell(row, 14).Value = ResolveReportValue(item.Data.Run.StandardDeviation1, [metrics.StandardDeviation]);
|
|
|
|
|
worksheet.Cell(row, 15).Value = ResolveReportValue(item.Data.Run.StandardDeviation2, [metrics.StandardDeviation]);
|
|
|
|
|
worksheet.Cell(row, 16).Value = metrics.PeakForceN;
|
|
|
|
|
worksheet.Cell(row, 17).Value = metrics.AverageForceN;
|
|
|
|
|
worksheet.Cell(row, 18).Value = item.ValidSamples.Count;
|
|
|
|
|
worksheet.Cell(row, 19).Value = CountReciprocatingRecords(item);
|
|
|
|
|
worksheet.Cell(row, 20).Value = forceStats.Min;
|
|
|
|
|
worksheet.Cell(row, 21).Value = forceStats.Max;
|
|
|
|
|
worksheet.Cell(row, 22).Value = forceStats.Average;
|
|
|
|
|
worksheet.Cell(row, 23).Value = forceStats.StandardDeviation;
|
|
|
|
|
worksheet.Cell(row, 20).Value = metrics.ForceStats.Min;
|
|
|
|
|
worksheet.Cell(row, 21).Value = metrics.ForceStats.Max;
|
|
|
|
|
worksheet.Cell(row, 22).Value = metrics.ForceStats.Average;
|
|
|
|
|
worksheet.Cell(row, 23).Value = metrics.ForceStats.StandardDeviation;
|
|
|
|
|
|
|
|
|
|
worksheet.Cell(row, 4).Style.Fill.BackgroundColor = ToXlColor(item.CurveColorHex);
|
|
|
|
|
worksheet.Cell(row, 4).Style.Font.FontColor = XLColor.White;
|
|
|
|
|
@@ -284,12 +289,15 @@ public sealed class RunExportService
|
|
|
|
|
"产品编号",
|
|
|
|
|
"测试模式",
|
|
|
|
|
"方向",
|
|
|
|
|
"设置往复次数",
|
|
|
|
|
"当前往复序号",
|
|
|
|
|
"静摩擦系数 COFs",
|
|
|
|
|
"动摩擦系数 COFk",
|
|
|
|
|
"标准差",
|
|
|
|
|
"数据状态",
|
|
|
|
|
"采样序号",
|
|
|
|
|
"采样时间",
|
|
|
|
|
"时间(s)",
|
|
|
|
|
"位移(mm)",
|
|
|
|
|
"力值(N)",
|
|
|
|
|
"速度(mm/min)"
|
|
|
|
|
@@ -303,9 +311,17 @@ public sealed class RunExportService
|
|
|
|
|
var curveKey = BuildCurveKey(item);
|
|
|
|
|
var curveLabel = BuildCurveLabel(item);
|
|
|
|
|
var legendLabel = BuildLegendLabel(item);
|
|
|
|
|
var metrics = BuildRunReportMetrics(item);
|
|
|
|
|
var firstCapturedAt = item.ValidSamples.Count == 0 ? DateTime.MinValue : item.ValidSamples[0].CapturedAt;
|
|
|
|
|
var reciprocatingRecordByIndex = item.Data.ReciprocatingRecords
|
|
|
|
|
.Where(record => record.HasData)
|
|
|
|
|
.GroupBy(record => record.Index)
|
|
|
|
|
.ToDictionary(group => group.Key, group => group.Last());
|
|
|
|
|
|
|
|
|
|
foreach (var sample in item.ValidSamples)
|
|
|
|
|
{
|
|
|
|
|
var sampleReciprocatingIndex = ResolveSampleReciprocatingIndex(item, sample);
|
|
|
|
|
reciprocatingRecordByIndex.TryGetValue(sampleReciprocatingIndex, out var sampleReciprocatingRecord);
|
|
|
|
|
worksheet.Cell(rowIndex, 1).Value = curveKey;
|
|
|
|
|
worksheet.Cell(rowIndex, 2).Value = curveLabel;
|
|
|
|
|
worksheet.Cell(rowIndex, 3).Value = legendLabel;
|
|
|
|
|
@@ -317,18 +333,21 @@ public sealed class RunExportService
|
|
|
|
|
worksheet.Cell(rowIndex, 8).Value = item.Data.Recipe.ProductCode;
|
|
|
|
|
worksheet.Cell(rowIndex, 9).Value = item.Data.Recipe.TestMode;
|
|
|
|
|
worksheet.Cell(rowIndex, 10).Value = item.Data.Recipe.Direction;
|
|
|
|
|
worksheet.Cell(rowIndex, 11).Value = item.Data.Run.StaticCoefficient;
|
|
|
|
|
worksheet.Cell(rowIndex, 12).Value = item.Data.Run.KineticCoefficient;
|
|
|
|
|
worksheet.Cell(rowIndex, 13).Value = item.Data.Run.StandardDeviation;
|
|
|
|
|
worksheet.Cell(rowIndex, 14).Value = "有效";
|
|
|
|
|
worksheet.Cell(rowIndex, 15).Value = sample.SampleIndex;
|
|
|
|
|
worksheet.Cell(rowIndex, 16).Value = sample.CapturedAt;
|
|
|
|
|
worksheet.Cell(rowIndex, 16).Style.DateFormat.Format = "yyyy-mm-dd hh:mm:ss.000";
|
|
|
|
|
worksheet.Cell(rowIndex, 17).Value = sample.DisplacementMm;
|
|
|
|
|
worksheet.Cell(rowIndex, 18).Value = sample.ForceN;
|
|
|
|
|
worksheet.Cell(rowIndex, 11).Value = item.Data.Recipe.ReciprocatingCount;
|
|
|
|
|
worksheet.Cell(rowIndex, 12).Value = sampleReciprocatingIndex;
|
|
|
|
|
worksheet.Cell(rowIndex, 13).Value = ResolveNullableReportValue(sampleReciprocatingRecord?.StaticCoefficient, metrics.StaticCoefficient);
|
|
|
|
|
worksheet.Cell(rowIndex, 14).Value = ResolveNullableReportValue(sampleReciprocatingRecord?.KineticCoefficient, metrics.KineticCoefficient);
|
|
|
|
|
worksheet.Cell(rowIndex, 15).Value = metrics.StandardDeviation;
|
|
|
|
|
worksheet.Cell(rowIndex, 16).Value = "有效";
|
|
|
|
|
worksheet.Cell(rowIndex, 17).Value = sample.SampleIndex;
|
|
|
|
|
worksheet.Cell(rowIndex, 18).Value = sample.CapturedAt;
|
|
|
|
|
worksheet.Cell(rowIndex, 18).Style.DateFormat.Format = "yyyy-mm-dd hh:mm:ss.000";
|
|
|
|
|
worksheet.Cell(rowIndex, 19).Value = CalculateElapsedSeconds(firstCapturedAt, sample.CapturedAt);
|
|
|
|
|
worksheet.Cell(rowIndex, 20).Value = sample.DisplacementMm;
|
|
|
|
|
worksheet.Cell(rowIndex, 21).Value = sample.ForceN;
|
|
|
|
|
if (IsFinite(sample.SpeedMmPerMin))
|
|
|
|
|
{
|
|
|
|
|
worksheet.Cell(rowIndex, 19).Value = sample.SpeedMmPerMin;
|
|
|
|
|
worksheet.Cell(rowIndex, 22).Value = sample.SpeedMmPerMin;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
worksheet.Cell(rowIndex, 4).Style.Fill.BackgroundColor = ToXlColor(item.CurveColorHex);
|
|
|
|
|
@@ -344,7 +363,7 @@ public sealed class RunExportService
|
|
|
|
|
|
|
|
|
|
ApplyRawDataColumnWidths(worksheet);
|
|
|
|
|
ApplyRawDataFormatting(worksheet, headerRow + 1, dataLastRow);
|
|
|
|
|
ApplyPageLayout(worksheet, dataLastRow, "$A$1:$S$");
|
|
|
|
|
ApplyPageLayout(worksheet, dataLastRow, "$A$1:$V$");
|
|
|
|
|
worksheet.SheetView.FreezeRows(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -437,7 +456,7 @@ public sealed class RunExportService
|
|
|
|
|
worksheet.Cell("A1").Style.Font.FontColor = XLColor.FromHtml("#21313D");
|
|
|
|
|
|
|
|
|
|
worksheet.Cell("A3").Value = "图表说明";
|
|
|
|
|
worksheet.Cell("B3").Value = "总览曲线叠加本次导出的全部有效次数曲线;每次曲线仅显示对应单次试验,横轴按真实位移数值比例绘制。";
|
|
|
|
|
worksheet.Cell("B3").Value = "总览曲线按测试过程时间轴连续显示全部往复;每次曲线按往复序号拆分为独立图表,避免多次往复叠加在同一图中。";
|
|
|
|
|
worksheet.Range("B3:H3").Merge();
|
|
|
|
|
|
|
|
|
|
worksheet.Cell("A5").Value = "曲线清单";
|
|
|
|
|
@@ -456,7 +475,7 @@ public sealed class RunExportService
|
|
|
|
|
worksheet.Cell(row, 5).Value = runs[index].ValidSamples.Count;
|
|
|
|
|
worksheet.Cell(row, 6).Value = CountReciprocatingRecords(runs[index]);
|
|
|
|
|
worksheet.Cell(row, 7).Value = IsChartableRun(runs[index])
|
|
|
|
|
? "已记录总览曲线、每次曲线和原始数据"
|
|
|
|
|
? $"已记录总览曲线和 {CountReciprocatingCurveSegments(runs[index])} 张每次曲线"
|
|
|
|
|
: "采样点不足,保留原始数据和往复数据";
|
|
|
|
|
worksheet.Cell(row, 4).Style.Fill.BackgroundColor = ToXlColor(runs[index].CurveColorHex);
|
|
|
|
|
worksheet.Cell(row, 4).Style.Font.FontColor = XLColor.White;
|
|
|
|
|
@@ -472,16 +491,17 @@ public sealed class RunExportService
|
|
|
|
|
|
|
|
|
|
private static void BuildStatisticsPanel(IXLWorksheet worksheet, IReadOnlyList<ExportCurveData> runs)
|
|
|
|
|
{
|
|
|
|
|
var averageStatic = runs.Count == 0 ? 0 : runs.Average(item => item.Data.Run.StaticCoefficient);
|
|
|
|
|
var averageKinetic = runs.Count == 0 ? 0 : runs.Average(item => item.Data.Run.KineticCoefficient);
|
|
|
|
|
var maxStatic = runs.Count == 0 ? 0 : runs.Max(item => item.Data.Run.StaticCoefficient);
|
|
|
|
|
var minStatic = runs.Count == 0 ? 0 : runs.Min(item => item.Data.Run.StaticCoefficient);
|
|
|
|
|
var maxKinetic = runs.Count == 0 ? 0 : runs.Max(item => item.Data.Run.KineticCoefficient);
|
|
|
|
|
var minKinetic = runs.Count == 0 ? 0 : runs.Min(item => item.Data.Run.KineticCoefficient);
|
|
|
|
|
var standardDeviationStatic = CalculateStandardDeviation(runs.Select(item => item.Data.Run.StaticCoefficient));
|
|
|
|
|
var standardDeviationKinetic = CalculateStandardDeviation(runs.Select(item => item.Data.Run.KineticCoefficient));
|
|
|
|
|
var maxPeakForce = runs.Count == 0 ? 0 : runs.Max(item => item.Data.Run.PeakForceN);
|
|
|
|
|
var minPeakForce = runs.Count == 0 ? 0 : runs.Min(item => item.Data.Run.PeakForceN);
|
|
|
|
|
var metrics = runs.Select(BuildRunReportMetrics).ToArray();
|
|
|
|
|
var averageStatic = metrics.Length == 0 ? 0 : metrics.Average(item => item.StaticCoefficient);
|
|
|
|
|
var averageKinetic = metrics.Length == 0 ? 0 : metrics.Average(item => item.KineticCoefficient);
|
|
|
|
|
var maxStatic = metrics.Length == 0 ? 0 : metrics.Max(item => item.StaticCoefficient);
|
|
|
|
|
var minStatic = metrics.Length == 0 ? 0 : metrics.Min(item => item.StaticCoefficient);
|
|
|
|
|
var maxKinetic = metrics.Length == 0 ? 0 : metrics.Max(item => item.KineticCoefficient);
|
|
|
|
|
var minKinetic = metrics.Length == 0 ? 0 : metrics.Min(item => item.KineticCoefficient);
|
|
|
|
|
var standardDeviationStatic = CalculateStandardDeviation(metrics.Select(item => item.StaticCoefficient));
|
|
|
|
|
var standardDeviationKinetic = CalculateStandardDeviation(metrics.Select(item => item.KineticCoefficient));
|
|
|
|
|
var maxPeakForce = metrics.Length == 0 ? 0 : metrics.Max(item => item.PeakForceN);
|
|
|
|
|
var minPeakForce = metrics.Length == 0 ? 0 : metrics.Min(item => item.PeakForceN);
|
|
|
|
|
|
|
|
|
|
worksheet.Cell("H3").Value = "汇总统计";
|
|
|
|
|
worksheet.Cell("H3").Style.Font.Bold = true;
|
|
|
|
|
@@ -531,13 +551,14 @@ public sealed class RunExportService
|
|
|
|
|
currentColumn += 3;
|
|
|
|
|
|
|
|
|
|
worksheet.Cell(startRow, xColumn).Value = BuildCurveLabel(item);
|
|
|
|
|
worksheet.Cell(startRow + 1, xColumn).Value = "位移(mm)";
|
|
|
|
|
worksheet.Cell(startRow + 1, xColumn).Value = "时间(s)";
|
|
|
|
|
worksheet.Cell(startRow + 1, yColumn).Value = "力值(N)";
|
|
|
|
|
|
|
|
|
|
var currentRow = startRow + 2;
|
|
|
|
|
var firstCapturedAt = item.ValidSamples.Count == 0 ? DateTime.MinValue : item.ValidSamples[0].CapturedAt;
|
|
|
|
|
foreach (var sample in item.ValidSamples)
|
|
|
|
|
{
|
|
|
|
|
worksheet.Cell(currentRow, xColumn).Value = sample.DisplacementMm;
|
|
|
|
|
worksheet.Cell(currentRow, xColumn).Value = CalculateElapsedSeconds(firstCapturedAt, sample.CapturedAt);
|
|
|
|
|
worksheet.Cell(currentRow, yColumn).Value = sample.ForceN;
|
|
|
|
|
currentRow++;
|
|
|
|
|
}
|
|
|
|
|
@@ -548,7 +569,7 @@ public sealed class RunExportService
|
|
|
|
|
var titleRef = BuildCellReference(sheetName, startRow, xColumn);
|
|
|
|
|
var startDataRow = startRow + 2;
|
|
|
|
|
var endDataRow = Math.Max(startDataRow, currentRow - 1);
|
|
|
|
|
var xValues = item.ValidSamples.Select(sample => sample.DisplacementMm).ToArray();
|
|
|
|
|
var xValues = item.ValidSamples.Select(sample => CalculateElapsedSeconds(firstCapturedAt, sample.CapturedAt)).ToArray();
|
|
|
|
|
var yValues = item.ValidSamples.Select(sample => sample.ForceN).ToArray();
|
|
|
|
|
var layout = new SeriesLayout(
|
|
|
|
|
titleRef,
|
|
|
|
|
@@ -577,7 +598,7 @@ public sealed class RunExportService
|
|
|
|
|
{
|
|
|
|
|
charts.Add(new ChartDefinition(
|
|
|
|
|
"总览曲线(全部次数)",
|
|
|
|
|
"位移 (mm)",
|
|
|
|
|
"时间 (s)",
|
|
|
|
|
"力值 (N)",
|
|
|
|
|
true,
|
|
|
|
|
perRunSeries.Select(item => item.Series).ToArray(),
|
|
|
|
|
@@ -597,7 +618,7 @@ public sealed class RunExportService
|
|
|
|
|
|
|
|
|
|
charts.Add(new ChartDefinition(
|
|
|
|
|
BuildSingleRunCurveTitle(runSeries.Data),
|
|
|
|
|
"位移 (mm)",
|
|
|
|
|
"时间 (s)",
|
|
|
|
|
"力值 (N)",
|
|
|
|
|
false,
|
|
|
|
|
[runSeries.Series],
|
|
|
|
|
@@ -629,15 +650,24 @@ public sealed class RunExportService
|
|
|
|
|
row += 38;
|
|
|
|
|
foreach (var run in chartableRuns)
|
|
|
|
|
{
|
|
|
|
|
InsertCurveChartImage(
|
|
|
|
|
worksheet,
|
|
|
|
|
row,
|
|
|
|
|
BuildSingleRunCurveTitle(run),
|
|
|
|
|
BuildSingleRunCurveTitle(run),
|
|
|
|
|
$"Curve{run.Sequence:D2}",
|
|
|
|
|
[run],
|
|
|
|
|
showLegend: false);
|
|
|
|
|
row += 36;
|
|
|
|
|
foreach (var group in BuildReciprocatingSampleGroups(run).Where(group => group.Samples.Count >= 2))
|
|
|
|
|
{
|
|
|
|
|
var cycleRun = new ExportCurveData(
|
|
|
|
|
run.Data,
|
|
|
|
|
group.Samples,
|
|
|
|
|
run.Sequence,
|
|
|
|
|
GetCurveColorHex(group.Index - 1));
|
|
|
|
|
var title = BuildReciprocatingCurveTitle(run, group.Index);
|
|
|
|
|
InsertCurveChartImage(
|
|
|
|
|
worksheet,
|
|
|
|
|
row,
|
|
|
|
|
title,
|
|
|
|
|
title,
|
|
|
|
|
$"Curve{run.Sequence:D2}_{group.Index:D3}",
|
|
|
|
|
[cycleRun],
|
|
|
|
|
showLegend: false);
|
|
|
|
|
row += 36;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -677,8 +707,6 @@ public sealed class RunExportService
|
|
|
|
|
var gridPen = CreatePen("#D1DBE3", 1);
|
|
|
|
|
var minorGridPen = CreatePen("#E5ECF2", 1);
|
|
|
|
|
var axisPen = CreatePen("#4A5C6C", 1.4);
|
|
|
|
|
var bandBrush = new SolidColorBrush(Color.FromArgb(20, 0, 105, 180));
|
|
|
|
|
var bandPen = new Pen(new SolidColorBrush(Color.FromArgb(80, 0, 105, 180)), 1);
|
|
|
|
|
var pointBrush = CreateBrush("#0069B4");
|
|
|
|
|
var pointStrokePen = CreatePen("#F7F9FB", 2.5);
|
|
|
|
|
|
|
|
|
|
@@ -690,7 +718,6 @@ public sealed class RunExportService
|
|
|
|
|
|
|
|
|
|
DrawExportText(drawingContext, title, 32, 42, 28, titleBrush, bold: true);
|
|
|
|
|
DrawExportGridAndAxes(drawingContext, plot, xMax, yMax, labelBrush, axisTitleBrush, gridPen, minorGridPen, axisPen);
|
|
|
|
|
DrawExportKineticBand(drawingContext, plot, xMax, runs.Max(item => item.Data.Recipe.TravelMm), bandBrush, bandPen);
|
|
|
|
|
|
|
|
|
|
for (var index = 0; index < runs.Count; index++)
|
|
|
|
|
{
|
|
|
|
|
@@ -755,7 +782,7 @@ public sealed class RunExportService
|
|
|
|
|
labelBrush);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DrawCenteredExportText(drawingContext, "位移 / mm", plot.Left + plot.Width / 2, plot.Bottom + 58, 18, axisTitleBrush, bold: true);
|
|
|
|
|
DrawCenteredExportText(drawingContext, "时间 / s", plot.Left + plot.Width / 2, plot.Bottom + 58, 18, axisTitleBrush, bold: true);
|
|
|
|
|
DrawExportText(drawingContext, "力值 / N", 18, plot.Top - 30, 18, axisTitleBrush, bold: true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -794,10 +821,11 @@ public sealed class RunExportService
|
|
|
|
|
Brush pointBrush,
|
|
|
|
|
Pen pointStrokePen)
|
|
|
|
|
{
|
|
|
|
|
var firstCapturedAt = run.ValidSamples.Count == 0 ? DateTime.MinValue : run.ValidSamples[0].CapturedAt;
|
|
|
|
|
var points = run.ValidSamples
|
|
|
|
|
.Where(sample => IsFinite(sample.DisplacementMm) && IsFinite(sample.ForceN))
|
|
|
|
|
.Where(sample => IsFinite(sample.ForceN))
|
|
|
|
|
.Select(sample => new Point(
|
|
|
|
|
MapX(sample.DisplacementMm, plot, xMax),
|
|
|
|
|
MapX(CalculateElapsedSeconds(firstCapturedAt, sample.CapturedAt), plot, xMax),
|
|
|
|
|
MapY(sample.ForceN, plot, yMax)))
|
|
|
|
|
.ToArray();
|
|
|
|
|
|
|
|
|
|
@@ -843,9 +871,8 @@ public sealed class RunExportService
|
|
|
|
|
|
|
|
|
|
private static double ResolveExportChartXMax(IReadOnlyList<ExportCurveData> runs)
|
|
|
|
|
{
|
|
|
|
|
var travelMax = runs.Select(item => item.Data.Recipe.TravelMm).Where(IsFinite).DefaultIfEmpty(0).Max();
|
|
|
|
|
var sampleMax = runs.SelectMany(item => item.ValidSamples).Select(sample => sample.DisplacementMm).Where(IsFinite).DefaultIfEmpty(0).Max();
|
|
|
|
|
return Math.Max(Math.Max(travelMax, sampleMax) + 5, 1);
|
|
|
|
|
var sampleMax = runs.Select(item => ResolveSamplesDurationSeconds(item.ValidSamples)).Where(IsFinite).DefaultIfEmpty(0).Max();
|
|
|
|
|
return Math.Max(sampleMax + 1, 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static double ResolveExportChartYMax(IReadOnlyList<ExportCurveData> runs)
|
|
|
|
|
@@ -933,7 +960,7 @@ public sealed class RunExportService
|
|
|
|
|
|
|
|
|
|
private static int CalculateChartSheetLastRow(IReadOnlyList<ExportCurveData> runs)
|
|
|
|
|
{
|
|
|
|
|
var chartCount = runs.Count(IsChartableRun);
|
|
|
|
|
var chartCount = runs.Where(IsChartableRun).Sum(CountReciprocatingCurveSegments);
|
|
|
|
|
var renderedImageCount = chartCount == 0 ? 0 : chartCount + 1;
|
|
|
|
|
return Math.Max(40, runs.Count + 12 + renderedImageCount * 38);
|
|
|
|
|
}
|
|
|
|
|
@@ -999,15 +1026,18 @@ public sealed class RunExportService
|
|
|
|
|
worksheet.Column(8).Width = 16;
|
|
|
|
|
worksheet.Column(9).Width = 16;
|
|
|
|
|
worksheet.Column(10).Width = 14;
|
|
|
|
|
worksheet.Column(11).Width = 12;
|
|
|
|
|
worksheet.Column(12).Width = 12;
|
|
|
|
|
worksheet.Column(11).Width = 14;
|
|
|
|
|
worksheet.Column(12).Width = 14;
|
|
|
|
|
worksheet.Column(13).Width = 12;
|
|
|
|
|
worksheet.Column(14).Width = 10;
|
|
|
|
|
worksheet.Column(15).Width = 10;
|
|
|
|
|
worksheet.Column(16).Width = 24;
|
|
|
|
|
worksheet.Column(17).Width = 14;
|
|
|
|
|
worksheet.Column(18).Width = 14;
|
|
|
|
|
worksheet.Column(19).Width = 14;
|
|
|
|
|
worksheet.Column(14).Width = 12;
|
|
|
|
|
worksheet.Column(15).Width = 12;
|
|
|
|
|
worksheet.Column(16).Width = 10;
|
|
|
|
|
worksheet.Column(17).Width = 10;
|
|
|
|
|
worksheet.Column(18).Width = 24;
|
|
|
|
|
worksheet.Column(19).Width = 12;
|
|
|
|
|
worksheet.Column(20).Width = 14;
|
|
|
|
|
worksheet.Column(21).Width = 14;
|
|
|
|
|
worksheet.Column(22).Width = 14;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static void ApplyReciprocatingDataColumnWidths(IXLWorksheet worksheet)
|
|
|
|
|
@@ -1071,11 +1101,11 @@ public sealed class RunExportService
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
worksheet.Range(dataStartRow, 5, dataLastRow, 5).Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
|
|
|
|
|
worksheet.Range(dataStartRow, 10, dataLastRow, 10).Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
|
|
|
|
|
worksheet.Range(dataStartRow, 11, dataLastRow, 13).Style.NumberFormat.Format = "0.0000";
|
|
|
|
|
worksheet.Range(dataStartRow, 15, dataLastRow, 15).Style.NumberFormat.Format = "0";
|
|
|
|
|
worksheet.Range(dataStartRow, 17, dataLastRow, 18).Style.NumberFormat.Format = "0.0000";
|
|
|
|
|
worksheet.Range(dataStartRow, 19, dataLastRow, 19).Style.NumberFormat.Format = "0.000";
|
|
|
|
|
worksheet.Range(dataStartRow, 10, dataLastRow, 12).Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
|
|
|
|
|
worksheet.Range(dataStartRow, 13, dataLastRow, 15).Style.NumberFormat.Format = "0.0000";
|
|
|
|
|
worksheet.Range(dataStartRow, 17, dataLastRow, 17).Style.NumberFormat.Format = "0";
|
|
|
|
|
worksheet.Range(dataStartRow, 19, dataLastRow, 21).Style.NumberFormat.Format = "0.0000";
|
|
|
|
|
worksheet.Range(dataStartRow, 22, dataLastRow, 22).Style.NumberFormat.Format = "0.000";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static void ApplyReciprocatingDataFormatting(IXLWorksheet worksheet, int dataStartRow, int dataLastRow)
|
|
|
|
|
@@ -1348,6 +1378,11 @@ public sealed class RunExportService
|
|
|
|
|
return $"第 {item.Data.Run.RunIndex} 次曲线 - {item.Data.Recipe.BatchNumber}";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static string BuildReciprocatingCurveTitle(ExportCurveData item, int reciprocatingIndex)
|
|
|
|
|
{
|
|
|
|
|
return $"第 {item.Data.Run.RunIndex} 轮 - 第 {reciprocatingIndex} 次曲线 - {item.Data.Recipe.BatchNumber}";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static string BuildCurveSequenceCode(int sequence)
|
|
|
|
|
{
|
|
|
|
|
return $"曲线{sequence:D2}";
|
|
|
|
|
@@ -1396,6 +1431,58 @@ public sealed class RunExportService
|
|
|
|
|
CalculateStandardDeviation(forces));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static RunReportMetrics BuildRunReportMetrics(ExportCurveData item)
|
|
|
|
|
{
|
|
|
|
|
var forceStats = CalculateForceStatistics(item.ValidSamples);
|
|
|
|
|
var staticCoefficientValues = GetFiniteNullableValues(item.Data.ReciprocatingRecords.Select(record => record.StaticCoefficient));
|
|
|
|
|
var kineticCoefficientValues = GetFiniteNullableValues(item.Data.ReciprocatingRecords.Select(record => record.KineticCoefficient));
|
|
|
|
|
|
|
|
|
|
var staticCoefficient = ResolveReportValue(item.Data.Run.StaticCoefficient, staticCoefficientValues);
|
|
|
|
|
var kineticCoefficient = ResolveReportValue(item.Data.Run.KineticCoefficient, kineticCoefficientValues);
|
|
|
|
|
double[] standardDeviationFallback = staticCoefficientValues.Length > 1
|
|
|
|
|
? [CalculateStandardDeviation(staticCoefficientValues)]
|
|
|
|
|
: [];
|
|
|
|
|
|
|
|
|
|
return new RunReportMetrics(
|
|
|
|
|
staticCoefficient,
|
|
|
|
|
kineticCoefficient,
|
|
|
|
|
ResolveReportValue(item.Data.Run.StandardDeviation, standardDeviationFallback),
|
|
|
|
|
ResolveReportValue(item.Data.Run.PeakForceN, [forceStats.Max]),
|
|
|
|
|
ResolveReportValue(item.Data.Run.AverageForceN, [forceStats.Average]),
|
|
|
|
|
forceStats);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static double ResolveReportValue(double runValue, IEnumerable<double> fallbackValues)
|
|
|
|
|
{
|
|
|
|
|
if (IsUsableReportValue(runValue))
|
|
|
|
|
{
|
|
|
|
|
return runValue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var values = fallbackValues.Where(IsUsableReportValue).ToArray();
|
|
|
|
|
return values.Length == 0 ? 0 : values.Average();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static double ResolveNullableReportValue(double? value, double fallback)
|
|
|
|
|
{
|
|
|
|
|
return value is { } number && IsFinite(number)
|
|
|
|
|
? number
|
|
|
|
|
: fallback;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static bool IsUsableReportValue(double value)
|
|
|
|
|
{
|
|
|
|
|
return IsFinite(value) && Math.Abs(value) > 0.0000001;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static double[] GetFiniteNullableValues(IEnumerable<double?> values)
|
|
|
|
|
{
|
|
|
|
|
return values
|
|
|
|
|
.Where(value => value is { } number && IsFinite(number))
|
|
|
|
|
.Select(value => value!.Value)
|
|
|
|
|
.ToArray();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static double CalculateStandardDeviation(IEnumerable<double> values)
|
|
|
|
|
{
|
|
|
|
|
var samples = values.ToArray();
|
|
|
|
|
@@ -1414,6 +1501,115 @@ public sealed class RunExportService
|
|
|
|
|
return item.Data.ReciprocatingRecords.Count(record => record.HasData);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static int CountReciprocatingCurveSegments(ExportCurveData item)
|
|
|
|
|
{
|
|
|
|
|
return BuildReciprocatingSampleGroups(item).Count(group => group.Samples.Count >= 2);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static IReadOnlyList<ReciprocatingSampleGroup> BuildReciprocatingSampleGroups(ExportCurveData item)
|
|
|
|
|
{
|
|
|
|
|
var samples = item.ValidSamples
|
|
|
|
|
.OrderBy(sample => sample.SampleIndex)
|
|
|
|
|
.ToArray();
|
|
|
|
|
if (samples.Length == 0)
|
|
|
|
|
{
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (samples.Any(sample => sample.ReciprocatingIndex > 0))
|
|
|
|
|
{
|
|
|
|
|
return NormalizeReciprocatingGroups(samples
|
|
|
|
|
.GroupBy(sample => Math.Max(1, sample.ReciprocatingIndex))
|
|
|
|
|
.OrderBy(group => group.Key)
|
|
|
|
|
.Select(group => new ReciprocatingSampleGroup(group.Key, group.ToArray()))
|
|
|
|
|
.ToArray());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var expectedCount = ResolveExpectedReciprocatingCount(item);
|
|
|
|
|
if (expectedCount <= 1)
|
|
|
|
|
{
|
|
|
|
|
return [new ReciprocatingSampleGroup(1, samples)];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var groups = new List<ReciprocatingSampleGroup>();
|
|
|
|
|
for (var index = 1; index <= expectedCount; index++)
|
|
|
|
|
{
|
|
|
|
|
var start = (int)Math.Floor((index - 1) * samples.Length / (double)expectedCount);
|
|
|
|
|
var end = (int)Math.Floor(index * samples.Length / (double)expectedCount);
|
|
|
|
|
if (index == expectedCount)
|
|
|
|
|
{
|
|
|
|
|
end = samples.Length;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (end <= start)
|
|
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
groups.Add(new ReciprocatingSampleGroup(index, samples[start..end]));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return NormalizeReciprocatingGroups(groups);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static IReadOnlyList<ReciprocatingSampleGroup> NormalizeReciprocatingGroups(IReadOnlyList<ReciprocatingSampleGroup> groups)
|
|
|
|
|
{
|
|
|
|
|
if (groups.Any(group => group.Samples.Count >= 2))
|
|
|
|
|
{
|
|
|
|
|
return groups;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var samples = groups.SelectMany(group => group.Samples).OrderBy(sample => sample.SampleIndex).ToArray();
|
|
|
|
|
return samples.Length >= 2
|
|
|
|
|
? [new ReciprocatingSampleGroup(1, samples)]
|
|
|
|
|
: groups;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static int ResolveExpectedReciprocatingCount(ExportCurveData item)
|
|
|
|
|
{
|
|
|
|
|
var recordMax = item.Data.ReciprocatingRecords
|
|
|
|
|
.Where(record => record.HasData)
|
|
|
|
|
.Select(record => record.Index)
|
|
|
|
|
.DefaultIfEmpty(0)
|
|
|
|
|
.Max();
|
|
|
|
|
return Math.Max(1, Math.Max(item.Data.Recipe.ReciprocatingCount, recordMax));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static int ResolveSampleReciprocatingIndex(ExportCurveData item, RawSampleRecord sample)
|
|
|
|
|
{
|
|
|
|
|
if (sample.ReciprocatingIndex > 0)
|
|
|
|
|
{
|
|
|
|
|
return sample.ReciprocatingIndex;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var expectedCount = ResolveExpectedReciprocatingCount(item);
|
|
|
|
|
if (expectedCount <= 1 || item.ValidSamples.Count == 0)
|
|
|
|
|
{
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var firstSampleIndex = item.ValidSamples[0].SampleIndex;
|
|
|
|
|
var samplePosition = Math.Clamp(sample.SampleIndex - firstSampleIndex, 0, item.ValidSamples.Count - 1);
|
|
|
|
|
return Math.Clamp((int)Math.Floor(samplePosition * expectedCount / (double)item.ValidSamples.Count) + 1, 1, expectedCount);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static double ResolveSamplesDurationSeconds(IReadOnlyList<RawSampleRecord> samples)
|
|
|
|
|
{
|
|
|
|
|
return samples.Count == 0
|
|
|
|
|
? 0
|
|
|
|
|
: CalculateElapsedSeconds(samples[0].CapturedAt, samples[^1].CapturedAt);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static double CalculateElapsedSeconds(DateTime firstCapturedAt, DateTime capturedAt)
|
|
|
|
|
{
|
|
|
|
|
if (firstCapturedAt == DateTime.MinValue)
|
|
|
|
|
{
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Math.Max(0, (capturedAt - firstCapturedAt).TotalSeconds);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static void WriteReciprocatingMetadataCells(IXLWorksheet worksheet, int row, ExportCurveData item)
|
|
|
|
|
{
|
|
|
|
|
worksheet.Cell(row, 1).Value = BuildCurveKey(item);
|
|
|
|
|
@@ -1502,6 +1698,16 @@ public sealed class RunExportService
|
|
|
|
|
|
|
|
|
|
private sealed record ForceStatistics(double Min, double Max, double Average, double StandardDeviation);
|
|
|
|
|
|
|
|
|
|
private sealed record RunReportMetrics(
|
|
|
|
|
double StaticCoefficient,
|
|
|
|
|
double KineticCoefficient,
|
|
|
|
|
double StandardDeviation,
|
|
|
|
|
double PeakForceN,
|
|
|
|
|
double AverageForceN,
|
|
|
|
|
ForceStatistics ForceStats);
|
|
|
|
|
|
|
|
|
|
private sealed record ReciprocatingSampleGroup(int Index, IReadOnlyList<RawSampleRecord> Samples);
|
|
|
|
|
|
|
|
|
|
private sealed record ChartAnchor(int FromColumn, int FromRow, int ToColumn, int ToRow);
|
|
|
|
|
|
|
|
|
|
private sealed record ChartDefinition(
|
|
|
|
|
|