feat(aggregation): 按 Threshold 分桶并优化 Qfod 聚合
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +9,7 @@ namespace Gpulse.WCT.DataAnalyzer.Core.Application;
|
||||
|
||||
/// <summary>
|
||||
/// 从本地解析库生成独立正式发布库。
|
||||
/// 当前业务规则:功率列使用 Ploss 的 Field7,重复结果取平均值;缺失值保留为空。
|
||||
/// 当前业务规则:功率列按 Threshold(Field9) 分桶、取值 Ploss(Field7),重复结果取平均值;缺失值保留为空。
|
||||
/// </summary>
|
||||
public class AggregationService
|
||||
{
|
||||
@@ -34,9 +34,10 @@ public class AggregationService
|
||||
public async Task<AggregationReport> AggregateAsync(bool rebuild = true, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var rows = await LoadRowsAsync(cancellationToken);
|
||||
var qfodAggregates = await LoadQfodAggregatesAsync(cancellationToken);
|
||||
var records = rows
|
||||
.GroupBy(r => BuildKey(r.Scenario))
|
||||
.Select(group => CreateRecord(group.Key, group.ToList()))
|
||||
.Select(group => CreateRecord(group.Key, group.ToList(), qfodAggregates))
|
||||
.ToList();
|
||||
|
||||
await using var transaction = await _releaseContext.Database.BeginTransactionAsync(cancellationToken);
|
||||
@@ -61,8 +62,11 @@ public class AggregationService
|
||||
|
||||
private async Task<List<PlossRow>> LoadRowsAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// 只加载 Ploss 及其引用维度。Qfod 明细不在此加载(避免集合 Include 产生笛卡尔积),
|
||||
// 仅在 LoadQfodAggregatesAsync 中按场景聚合出 Sum/Count。
|
||||
var records = await _localContext.PlossRecords
|
||||
.AsNoTracking()
|
||||
.AsSplitQuery()
|
||||
.Where(r => !r.IsDeleted)
|
||||
.Include(r => r.Scenario)
|
||||
.ThenInclude(s => s!.TxPanel)
|
||||
@@ -70,8 +74,6 @@ public class AggregationService
|
||||
.ThenInclude(s => s!.TxHardware)
|
||||
.Include(r => r.Scenario)
|
||||
.ThenInclude(s => s!.RxType)
|
||||
.Include(r => r.Scenario)
|
||||
.ThenInclude(s => s!.QfodRecords)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return records.Select(r => new PlossRow(
|
||||
@@ -82,6 +84,29 @@ public class AggregationService
|
||||
r.Scenario.RxType.Name)).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按场景聚合 Qfod 记录的 CurrentQ/RawQ 求和与计数(在数据库侧完成,只返回每场景 3 个数字)。
|
||||
/// </summary>
|
||||
private async Task<Dictionary<Guid, QfodAggregate>> LoadQfodAggregatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var groups = await _localContext.QfodRecords
|
||||
.AsNoTracking()
|
||||
.Where(r => !r.IsDeleted)
|
||||
.GroupBy(r => r.ScenarioId)
|
||||
.Select(g => new
|
||||
{
|
||||
ScenarioId = g.Key,
|
||||
QValueSum = g.Sum(r => (double)r.CurrentQ),
|
||||
QBaseSum = g.Sum(r => (double)r.RawQ),
|
||||
Count = g.Count()
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return groups.ToDictionary(
|
||||
g => g.ScenarioId,
|
||||
g => new QfodAggregate(g.QValueSum, g.QBaseSum, g.Count));
|
||||
}
|
||||
|
||||
private ChargingParameterKey BuildKey(TestScenario scenario)
|
||||
{
|
||||
var panelName = scenario.TxPanel.Name;
|
||||
@@ -102,12 +127,17 @@ public class AggregationService
|
||||
return ("Unknown", value);
|
||||
}
|
||||
|
||||
private static ChargingParameterRecord CreateRecord(ChargingParameterKey key, List<PlossRow> rows)
|
||||
private static ChargingParameterRecord CreateRecord(
|
||||
ChargingParameterKey key,
|
||||
List<PlossRow> rows,
|
||||
IReadOnlyDictionary<Guid, QfodAggregate> qfodAggregates)
|
||||
{
|
||||
var values = PowerLevels.Select(power => Average(rows
|
||||
.Where(row => IsPowerMatch(row.Record, power))
|
||||
.Select(row => (double?)row.Record.Field7))).ToArray();
|
||||
|
||||
var (qValueSum, qBaseSum, qCount) = AggregateQfod(rows, qfodAggregates);
|
||||
|
||||
return new ChargingParameterRecord
|
||||
{
|
||||
CarFactory = key.CarFactory,
|
||||
@@ -123,16 +153,43 @@ public class AggregationService
|
||||
Power1750mW = values[6],
|
||||
Power2000mW = values[7],
|
||||
Power2250mW = values[8],
|
||||
QValue = Average(rows.SelectMany(row => row.Scenario.QfodRecords).Select(q => (double?)q.CurrentQ)),
|
||||
QBaseValue = Average(rows.SelectMany(row => row.Scenario.QfodRecords).Select(q => (double?)q.RawQ)),
|
||||
QValue = qCount > 0 ? qValueSum / qCount : null,
|
||||
QBaseValue = qCount > 0 ? qBaseSum / qCount : null,
|
||||
PqCoefficient = Average(rows.Select(row => row.Record.DeltaP.HasValue ? (double?)row.Record.DeltaP.Value : null)),
|
||||
ResonanceFrequency = null
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 累加分组内各行的场景 Qfod 聚合值。
|
||||
/// 等价于原实现的 SelectMany(row => row.Scenario.QfodRecords):同一场景出现 k 次,其 Sum 与 Count 同倍放大,
|
||||
/// 商保持不变,因此多场景分组时加权语义与原来完全一致,但无需加载 Qfod 明细。
|
||||
/// </summary>
|
||||
private static (double Sum, double BaseSum, int Count) AggregateQfod(
|
||||
List<PlossRow> rows,
|
||||
IReadOnlyDictionary<Guid, QfodAggregate> qfodAggregates)
|
||||
{
|
||||
double sum = 0, baseSum = 0;
|
||||
int count = 0;
|
||||
|
||||
foreach (var row in rows)
|
||||
{
|
||||
if (qfodAggregates.TryGetValue(row.Scenario.Id, out var agg))
|
||||
{
|
||||
sum += agg.QValueSum;
|
||||
baseSum += agg.QBaseSum;
|
||||
count += agg.Count;
|
||||
}
|
||||
}
|
||||
|
||||
return (sum, baseSum, count);
|
||||
}
|
||||
|
||||
private static bool IsPowerMatch(PlossRecord record, int power)
|
||||
{
|
||||
return record.Field4 == power || record.Field5 == power || record.PowLoss == power;
|
||||
// 功率列按 Threshold(Field9) 分桶:记录的阈值等于哪个档位,其 Ploss 就进哪一列。
|
||||
// 注:需求文档中 Threshold 为逻辑编号 (8),但当前解析数据里阈值实际落在 field_9。
|
||||
return record.Field9 == power;
|
||||
}
|
||||
|
||||
private static double? Average(IEnumerable<double?> values)
|
||||
@@ -143,6 +200,7 @@ public class AggregationService
|
||||
|
||||
private record PlossRow(PlossRecord Record, TestScenario Scenario, string Panel, string Hardware, string RxType);
|
||||
private record ChargingParameterKey(string CarFactory, string CarModel, string PhoneBrand, string PhoneModel);
|
||||
private readonly record struct QfodAggregate(double QValueSum, double QBaseSum, int Count);
|
||||
}
|
||||
|
||||
public record AggregationReport(int ReleaseRecordCount, int LocalRecordCount);
|
||||
|
||||
Reference in New Issue
Block a user