优化: 增强高风速功率曲线清洗
- 自动估算每台风机平台功率和平台起始风速 - 增加高风速低功率、曲线残差和严格平台过滤 - 前端展示估算参数并更新接口与设计文档
This commit is contained in:
@@ -61,6 +61,18 @@ struct CalculationOptions {
|
||||
double generator_speed_k = 0.9;
|
||||
};
|
||||
|
||||
struct EstimatedParams {
|
||||
double rated_power = 0.0;
|
||||
double rated_wind_speed = 0.0;
|
||||
std::string source = "fallback";
|
||||
};
|
||||
|
||||
struct CurveBin {
|
||||
double wind_speed = 0.0;
|
||||
double median_power = 0.0;
|
||||
size_t sample_count = 0;
|
||||
};
|
||||
|
||||
std::string Trim(const std::string& value) {
|
||||
const auto begin = value.find_first_not_of(" \t\r\n");
|
||||
if (begin == std::string::npos) {
|
||||
@@ -230,6 +242,18 @@ double StdDev(const std::vector<double>& values, double mean) {
|
||||
return std::sqrt(sum / static_cast<double>(values.size()));
|
||||
}
|
||||
|
||||
double MedianAbsoluteDeviation(const std::vector<double>& values, double median) {
|
||||
if (values.empty()) {
|
||||
return 0.0;
|
||||
}
|
||||
std::vector<double> deviations;
|
||||
deviations.reserve(values.size());
|
||||
for (double value : values) {
|
||||
deviations.push_back(std::abs(value - median));
|
||||
}
|
||||
return Quantile(deviations, 0.5);
|
||||
}
|
||||
|
||||
CalculationOptions ParseOptions(const json& body) {
|
||||
CalculationOptions options;
|
||||
if (!body.contains("options") || !body["options"].is_object()) {
|
||||
@@ -359,7 +383,7 @@ std::vector<ValidRow> FilterByWindBinIqr(const std::vector<ValidRow>& rows,
|
||||
|
||||
std::vector<ValidRow> result;
|
||||
result.reserve(rows.size());
|
||||
for (double interval = min_wind; interval < max_wind;
|
||||
for (double interval = min_wind; interval <= max_wind;
|
||||
interval += options.cleaning_wind_speed_step) {
|
||||
std::vector<ValidRow> interval_rows;
|
||||
std::vector<double> values;
|
||||
@@ -399,6 +423,248 @@ std::vector<ValidRow> FilterByWindBinIqr(const std::vector<ValidRow>& rows,
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<CurveBin> BuildMedianCurveBins(const std::vector<ValidRow>& rows,
|
||||
double wind_speed_step) {
|
||||
if (rows.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const double curve_min = 1.0 - wind_speed_step * 0.5;
|
||||
const double curve_max = 25.0 + wind_speed_step * 0.5;
|
||||
std::vector<CurveBin> bins;
|
||||
for (double start = curve_min; start < curve_max; start += wind_speed_step) {
|
||||
const double end = start + wind_speed_step;
|
||||
std::vector<double> values;
|
||||
for (const auto& row : rows) {
|
||||
if (row.wind_speed > start && row.wind_speed <= end) {
|
||||
values.push_back(row.active_power);
|
||||
}
|
||||
}
|
||||
if (!values.empty()) {
|
||||
CurveBin bin;
|
||||
bin.wind_speed = (start + end) / 2.0;
|
||||
bin.median_power = Quantile(values, 0.5);
|
||||
bin.sample_count = values.size();
|
||||
bins.push_back(bin);
|
||||
}
|
||||
}
|
||||
return bins;
|
||||
}
|
||||
|
||||
double InterpolateMedianPower(const std::vector<CurveBin>& bins, double wind_speed) {
|
||||
if (bins.empty()) {
|
||||
return 0.0;
|
||||
}
|
||||
if (wind_speed <= bins.front().wind_speed) {
|
||||
return bins.front().median_power;
|
||||
}
|
||||
if (wind_speed >= bins.back().wind_speed) {
|
||||
return bins.back().median_power;
|
||||
}
|
||||
|
||||
for (size_t i = 1; i < bins.size(); ++i) {
|
||||
if (wind_speed <= bins[i].wind_speed) {
|
||||
const auto& left = bins[i - 1];
|
||||
const auto& right = bins[i];
|
||||
const double span = right.wind_speed - left.wind_speed;
|
||||
if (span <= 0.0) {
|
||||
return left.median_power;
|
||||
}
|
||||
const double ratio = (wind_speed - left.wind_speed) / span;
|
||||
return left.median_power + (right.median_power - left.median_power) * ratio;
|
||||
}
|
||||
}
|
||||
return bins.back().median_power;
|
||||
}
|
||||
|
||||
EstimatedParams EstimateRatedParams(const std::vector<ValidRow>& rows,
|
||||
const CalculationOptions& options) {
|
||||
EstimatedParams params;
|
||||
params.rated_power = options.rated_power;
|
||||
params.rated_wind_speed = options.rated_wind_speed;
|
||||
|
||||
if (rows.size() < 20) {
|
||||
return params;
|
||||
}
|
||||
|
||||
std::vector<double> powers;
|
||||
powers.reserve(rows.size());
|
||||
for (const auto& row : rows) {
|
||||
powers.push_back(row.active_power);
|
||||
}
|
||||
|
||||
const double p90 = Quantile(powers, 0.90);
|
||||
std::vector<double> platform_candidates;
|
||||
for (double power : powers) {
|
||||
if (power >= p90 * 0.85) {
|
||||
platform_candidates.push_back(power);
|
||||
}
|
||||
}
|
||||
|
||||
if (platform_candidates.size() >= 10) {
|
||||
params.rated_power = Quantile(platform_candidates, 0.5);
|
||||
params.source = "auto";
|
||||
}
|
||||
|
||||
const auto bins = BuildMedianCurveBins(rows, options.curve_wind_speed_step);
|
||||
if (params.source == "auto") {
|
||||
for (const auto& bin : bins) {
|
||||
if (bin.sample_count >= 4 && bin.median_power >= params.rated_power * 0.95) {
|
||||
params.rated_wind_speed = bin.wind_speed;
|
||||
return params;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
params.source = params.source == "auto" ? "auto_power_fallback_wind" : "fallback";
|
||||
return params;
|
||||
}
|
||||
|
||||
std::vector<ValidRow> FilterHighWindLowPower(const std::vector<ValidRow>& rows,
|
||||
const EstimatedParams& params,
|
||||
int& removed_count,
|
||||
std::vector<RemovedPoint>& removed_points) {
|
||||
removed_count = 0;
|
||||
if (rows.empty() || params.rated_power <= 0.0 || params.rated_wind_speed <= 0.0) {
|
||||
return rows;
|
||||
}
|
||||
|
||||
std::vector<double> platform_powers;
|
||||
for (const auto& row : rows) {
|
||||
if (row.wind_speed >= params.rated_wind_speed) {
|
||||
platform_powers.push_back(row.active_power);
|
||||
}
|
||||
}
|
||||
if (platform_powers.size() < 8) {
|
||||
return rows;
|
||||
}
|
||||
|
||||
const double median = Quantile(platform_powers, 0.5);
|
||||
const double mad_sigma = MedianAbsoluteDeviation(platform_powers, median) * 1.4826;
|
||||
const double robust_margin = std::max(4.0 * mad_sigma, median * 0.08);
|
||||
const double lower_limit = std::max(params.rated_power * 0.85, median - robust_margin);
|
||||
|
||||
std::vector<ValidRow> result;
|
||||
result.reserve(rows.size());
|
||||
for (const auto& row : rows) {
|
||||
if (row.wind_speed >= params.rated_wind_speed &&
|
||||
row.active_power < lower_limit) {
|
||||
++removed_count;
|
||||
removed_points.push_back(RemovedPoint{row, "high_wind_low_power"});
|
||||
} else {
|
||||
result.push_back(row);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<ValidRow> FilterCurveResidualOutliers(const std::vector<ValidRow>& rows,
|
||||
const CalculationOptions& options,
|
||||
const EstimatedParams& params,
|
||||
int& removed_count,
|
||||
std::vector<RemovedPoint>& removed_points) {
|
||||
removed_count = 0;
|
||||
if (rows.size() < 20) {
|
||||
return rows;
|
||||
}
|
||||
|
||||
const auto median_curve = BuildMedianCurveBins(rows, options.curve_wind_speed_step);
|
||||
if (median_curve.size() < 4) {
|
||||
return rows;
|
||||
}
|
||||
|
||||
std::vector<ValidRow> result;
|
||||
result.reserve(rows.size());
|
||||
double min_wind = rows.front().wind_speed;
|
||||
double max_wind = min_wind;
|
||||
for (const auto& row : rows) {
|
||||
min_wind = std::min(min_wind, row.wind_speed);
|
||||
max_wind = std::max(max_wind, row.wind_speed);
|
||||
}
|
||||
|
||||
for (double start = min_wind - options.curve_wind_speed_step;
|
||||
start <= max_wind;
|
||||
start += options.curve_wind_speed_step) {
|
||||
const double end = start + options.curve_wind_speed_step;
|
||||
std::vector<ValidRow> interval_rows;
|
||||
std::vector<double> residuals;
|
||||
for (const auto& row : rows) {
|
||||
if (row.wind_speed > start && row.wind_speed <= end) {
|
||||
const double expected = InterpolateMedianPower(median_curve, row.wind_speed);
|
||||
interval_rows.push_back(row);
|
||||
residuals.push_back(row.active_power - expected);
|
||||
}
|
||||
}
|
||||
|
||||
if (interval_rows.empty()) {
|
||||
continue;
|
||||
}
|
||||
if (interval_rows.size() < 8) {
|
||||
result.insert(result.end(), interval_rows.begin(), interval_rows.end());
|
||||
continue;
|
||||
}
|
||||
|
||||
const double median = Quantile(residuals, 0.5);
|
||||
const double mad_sigma = MedianAbsoluteDeviation(residuals, median) * 1.4826;
|
||||
const double rated_power = params.rated_power > 0.0 ? params.rated_power : options.rated_power;
|
||||
const double lower_margin = std::max(4.0 * mad_sigma, rated_power * 0.10);
|
||||
const double upper_margin = std::max(4.0 * mad_sigma, rated_power * 0.16);
|
||||
const double lower = median - lower_margin;
|
||||
const double upper = median + upper_margin;
|
||||
|
||||
for (size_t i = 0; i < interval_rows.size(); ++i) {
|
||||
if (residuals[i] >= lower && residuals[i] <= upper) {
|
||||
result.push_back(interval_rows[i]);
|
||||
} else {
|
||||
++removed_count;
|
||||
removed_points.push_back(RemovedPoint{interval_rows[i], "curve_residual_outlier"});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<ValidRow> FilterStrictRatedPlateau(const std::vector<ValidRow>& rows,
|
||||
const EstimatedParams& params,
|
||||
int& removed_count,
|
||||
std::vector<RemovedPoint>& removed_points) {
|
||||
removed_count = 0;
|
||||
if (rows.empty() || params.rated_power <= 0.0 || params.rated_wind_speed <= 0.0) {
|
||||
return rows;
|
||||
}
|
||||
|
||||
const double strict_start_wind_speed = params.rated_wind_speed + 0.5;
|
||||
std::vector<double> plateau_powers;
|
||||
for (const auto& row : rows) {
|
||||
if (row.wind_speed >= strict_start_wind_speed) {
|
||||
plateau_powers.push_back(row.active_power);
|
||||
}
|
||||
}
|
||||
if (plateau_powers.size() < 6) {
|
||||
return rows;
|
||||
}
|
||||
|
||||
const double median = Quantile(plateau_powers, 0.5);
|
||||
const double mad_sigma = MedianAbsoluteDeviation(plateau_powers, median) * 1.4826;
|
||||
const double lower_limit = std::max(
|
||||
params.rated_power * 0.92,
|
||||
median - std::max(2.5 * mad_sigma, median * 0.04));
|
||||
|
||||
std::vector<ValidRow> result;
|
||||
result.reserve(rows.size());
|
||||
for (const auto& row : rows) {
|
||||
if (row.wind_speed >= strict_start_wind_speed &&
|
||||
row.active_power < lower_limit) {
|
||||
++removed_count;
|
||||
removed_points.push_back(RemovedPoint{row, "rated_plateau_low_power"});
|
||||
} else {
|
||||
result.push_back(row);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string DatePart(const std::string& time_text) {
|
||||
if (time_text.size() >= 10) {
|
||||
return time_text.substr(0, 10);
|
||||
@@ -608,6 +874,7 @@ void WindPowerController::FinishJob(
|
||||
json bins = json::object();
|
||||
json scatter_points = json::object();
|
||||
json filtered_points = json::object();
|
||||
json estimated_params = json::object();
|
||||
|
||||
std::vector<std::string> fan_ids;
|
||||
fan_ids.reserve(rows_by_fan.size());
|
||||
@@ -619,6 +886,9 @@ void WindPowerController::FinishJob(
|
||||
int limit_power_count = 0;
|
||||
int tip_speed_ratio_outlier_count = 0;
|
||||
int speed_power_outlier_count = 0;
|
||||
int high_wind_low_power_count = 0;
|
||||
int curve_residual_outlier_count = 0;
|
||||
int rated_plateau_low_power_count = 0;
|
||||
int cleaned_rows_count = 0;
|
||||
for (const auto& fan_id : fan_ids) {
|
||||
fans.push_back(fan_id);
|
||||
@@ -651,6 +921,36 @@ void WindPowerController::FinishJob(
|
||||
fan_removed_points,
|
||||
"speed_power_outlier");
|
||||
speed_power_outlier_count += removed;
|
||||
|
||||
const auto fan_estimated_params = EstimateRatedParams(fan_rows, options);
|
||||
json params_json;
|
||||
params_json["rated_power"] = fan_estimated_params.rated_power;
|
||||
params_json["rated_wind_speed"] = fan_estimated_params.rated_wind_speed;
|
||||
params_json["source"] = fan_estimated_params.source;
|
||||
estimated_params[fan_id] = params_json;
|
||||
|
||||
fan_rows = FilterHighWindLowPower(
|
||||
fan_rows,
|
||||
fan_estimated_params,
|
||||
removed,
|
||||
fan_removed_points);
|
||||
high_wind_low_power_count += removed;
|
||||
|
||||
fan_rows = FilterCurveResidualOutliers(
|
||||
fan_rows,
|
||||
options,
|
||||
fan_estimated_params,
|
||||
removed,
|
||||
fan_removed_points);
|
||||
curve_residual_outlier_count += removed;
|
||||
|
||||
fan_rows = FilterStrictRatedPlateau(
|
||||
fan_rows,
|
||||
fan_estimated_params,
|
||||
removed,
|
||||
fan_removed_points);
|
||||
rated_plateau_low_power_count += removed;
|
||||
|
||||
cleaned_rows_count += static_cast<int>(fan_rows.size());
|
||||
|
||||
json fan_scatter = json::array();
|
||||
@@ -709,6 +1009,9 @@ void WindPowerController::FinishJob(
|
||||
invalid_reasons["limit_power"] = limit_power_count;
|
||||
invalid_reasons["tip_speed_ratio_outlier"] = tip_speed_ratio_outlier_count;
|
||||
invalid_reasons["speed_power_outlier"] = speed_power_outlier_count;
|
||||
invalid_reasons["high_wind_low_power"] = high_wind_low_power_count;
|
||||
invalid_reasons["curve_residual_outlier"] = curve_residual_outlier_count;
|
||||
invalid_reasons["rated_plateau_low_power"] = rated_plateau_low_power_count;
|
||||
|
||||
const int invalid_total = raw_rows - cleaned_rows_count;
|
||||
|
||||
@@ -720,6 +1023,9 @@ void WindPowerController::FinishJob(
|
||||
summary["limit_power_rows"] = limit_power_count;
|
||||
summary["tip_speed_ratio_outlier_rows"] = tip_speed_ratio_outlier_count;
|
||||
summary["speed_power_outlier_rows"] = speed_power_outlier_count;
|
||||
summary["high_wind_low_power_rows"] = high_wind_low_power_count;
|
||||
summary["curve_residual_outlier_rows"] = curve_residual_outlier_count;
|
||||
summary["rated_plateau_low_power_rows"] = rated_plateau_low_power_count;
|
||||
summary["invalid_reasons"] = CounterJson(invalid_reasons);
|
||||
summary["fan_count"] = fans.size();
|
||||
|
||||
@@ -730,6 +1036,7 @@ void WindPowerController::FinishJob(
|
||||
data["bins"] = bins;
|
||||
data["scatter_points"] = scatter_points;
|
||||
data["filtered_points"] = filtered_points;
|
||||
data["estimated_params"] = estimated_params;
|
||||
|
||||
try {
|
||||
fs::remove_all(JobDir(job_id.value()));
|
||||
|
||||
Reference in New Issue
Block a user