功能: 完善风电功率方案计算

- 新增计算方案保存与方案二筛选参数
- 支持方案二桨角规则和有效数据导出
- 优化功率曲线绘制和设计功率展示
This commit is contained in:
cloud
2026-07-17 16:01:58 +08:00
parent 3cb7684039
commit d5d381759e
23 changed files with 918 additions and 50 deletions
@@ -4,6 +4,7 @@
#include <chrono>
#include <cmath>
#include <ctime>
#include <deque>
#include <filesystem>
#include <fstream>
#include <iomanip>
@@ -23,6 +24,13 @@ namespace {
constexpr int kErrorInvalidRequest = 1001;
constexpr int kErrorJobNotFound = 1002;
constexpr int kErrorServer = 1003;
constexpr const char* kDefaultSchemeId = "scheme_one";
constexpr const char* kSchemeTwoId = "scheme_two";
constexpr const char* kSchemeOneName = "方案一";
constexpr const char* kSchemeTwoName = "方案二";
constexpr const char* kSchemeOneDefaultDescription = "通用方案";
constexpr const char* kSchemeTwoDefaultDescription =
"桨角筛选方案,使用三支叶片角度平均值、600s 平均风速和手填转速/功率参数";
struct RawRow {
std::string time;
@@ -40,6 +48,11 @@ struct ValidRow {
double wind_speed = 0.0;
double active_power = 0.0;
double generator_speed = 0.0;
double blade_pitch_1 = 0.0;
double blade_pitch_2 = 0.0;
double blade_pitch_3 = 0.0;
double pitch_angle_average = 0.0;
double wind_speed_600s_average = 0.0;
double tip_speed_ratio = 0.0;
};
@@ -49,6 +62,7 @@ struct RemovedPoint {
};
struct CalculationOptions {
std::string scheme_id = kDefaultSchemeId;
double rated_power = 4800.0;
double rated_wind_speed = 18.0;
double power_step = 5.0;
@@ -59,6 +73,11 @@ struct CalculationOptions {
double iqr_upper_multiplier = 2.0;
double minimum_generator_speed = 1.0;
double generator_speed_k = 0.9;
double grid_connected_speed = 0.0;
double rated_generator_speed = 0.0;
bool rated_power_provided = false;
bool grid_connected_speed_provided = false;
bool rated_generator_speed_provided = false;
};
struct EstimatedParams {
@@ -73,6 +92,15 @@ struct CurveBin {
size_t sample_count = 0;
};
struct SchemeInfo {
std::string id;
std::string name;
std::string description;
double grid_connected_speed = 1030.0;
double rated_generator_speed = 1755.0;
double rated_power = 2000.0;
};
std::string Trim(const std::string& value) {
const auto begin = value.find_first_not_of(" \t\r\n");
if (begin == std::string::npos) {
@@ -103,6 +131,10 @@ fs::path JobRowsPath(const std::string& job_id) {
return JobDir(job_id) / "rows.jsonl";
}
fs::path SchemeConfigPath() {
return fs::path("data") / "wind_schemes.json";
}
std::string GenerateJobId() {
const auto now = std::chrono::system_clock::now().time_since_epoch().count();
std::random_device rd;
@@ -148,6 +180,117 @@ std::optional<std::string> GetStringField(const json& body, const std::string& f
return std::nullopt;
}
std::optional<double> GetNumberField(const json& body, const std::string& field) {
if (!body.contains(field) || !body[field].is_number()) {
return std::nullopt;
}
return body[field].get<double>();
}
std::string NormalizeSchemeId(const std::string& scheme_id) {
return scheme_id == kSchemeTwoId ? kSchemeTwoId : kDefaultSchemeId;
}
std::vector<SchemeInfo> DefaultSchemes() {
return {
{kDefaultSchemeId, kSchemeOneName, kSchemeOneDefaultDescription},
{kSchemeTwoId, kSchemeTwoName, kSchemeTwoDefaultDescription, 1030.0, 1755.0, 2000.0},
};
}
std::optional<SchemeInfo> FindScheme(const std::vector<SchemeInfo>& schemes,
const std::string& scheme_id) {
const std::string normalized_id = NormalizeSchemeId(scheme_id);
for (const auto& scheme : schemes) {
if (scheme.id == normalized_id) {
return scheme;
}
}
return std::nullopt;
}
json SchemeToJson(const SchemeInfo& scheme) {
json data;
data["id"] = scheme.id;
data["name"] = scheme.name;
data["description"] = scheme.description;
if (scheme.id == kSchemeTwoId) {
data["parameters"]["grid_connected_speed"] = scheme.grid_connected_speed;
data["parameters"]["rated_generator_speed"] = scheme.rated_generator_speed;
data["parameters"]["rated_power"] = scheme.rated_power;
}
return data;
}
std::vector<SchemeInfo> LoadSchemes() {
auto schemes = DefaultSchemes();
const auto config_path = SchemeConfigPath();
if (!fs::exists(config_path)) {
return schemes;
}
try {
std::ifstream in(config_path);
const auto saved = json::parse(in);
if (!saved.contains("schemes") || !saved["schemes"].is_array()) {
return schemes;
}
for (auto& scheme : schemes) {
for (const auto& item : saved["schemes"]) {
const auto saved_id = GetStringField(item, "id");
if (!saved_id.has_value() || saved_id.value() != scheme.id) {
continue;
}
const auto description = GetStringField(item, "description");
if (description.has_value()) {
scheme.description = description.value();
}
if (scheme.id == kSchemeTwoId &&
item.contains("parameters") &&
item["parameters"].is_object()) {
const auto& params = item["parameters"];
if (const auto value = GetNumberField(params, "grid_connected_speed");
value.has_value() && value.value() > 0.0) {
scheme.grid_connected_speed = value.value();
}
if (const auto value = GetNumberField(params, "rated_generator_speed");
value.has_value() && value.value() > 0.0) {
scheme.rated_generator_speed = value.value();
}
if (const auto value = GetNumberField(params, "rated_power");
value.has_value() && value.value() > 0.0) {
scheme.rated_power = value.value();
}
}
}
}
} catch (const std::exception&) {
return schemes;
}
return schemes;
}
bool SaveSchemes(const std::vector<SchemeInfo>& schemes) {
try {
const auto config_path = SchemeConfigPath();
fs::create_directories(config_path.parent_path());
json data;
data["default_scheme_id"] = kDefaultSchemeId;
data["schemes"] = json::array();
for (const auto& scheme : schemes) {
data["schemes"].push_back(SchemeToJson(scheme));
}
std::ofstream out(config_path);
out << data.dump(2);
return true;
} catch (const std::exception&) {
return false;
}
}
std::optional<double> GetDoubleField(const json& body, const std::string& field) {
if (!body.contains(field)) {
return std::nullopt;
@@ -261,9 +404,13 @@ CalculationOptions ParseOptions(const json& body) {
}
const auto& opt = body["options"];
if (const auto value = GetStringField(opt, "scheme_id"); value.has_value()) {
options.scheme_id = NormalizeSchemeId(value.value());
}
if (const auto value = GetDoubleField(opt, "rated_power");
value.has_value() && value.value() > 0.0) {
options.rated_power = value.value();
options.rated_power_provided = true;
}
if (const auto value = GetDoubleField(opt, "rated_wind_speed");
value.has_value() && value.value() > 0.0) {
@@ -301,13 +448,43 @@ CalculationOptions ParseOptions(const json& body) {
value.has_value() && value.value() >= 0.0) {
options.generator_speed_k = value.value();
}
if (const auto value = GetDoubleField(opt, "grid_connected_speed");
value.has_value() && value.value() > 0.0) {
options.grid_connected_speed = value.value();
options.grid_connected_speed_provided = true;
}
if (const auto value = GetDoubleField(opt, "rated_generator_speed");
value.has_value() && value.value() > 0.0) {
options.rated_generator_speed = value.value();
options.rated_generator_speed_provided = true;
}
return options;
}
bool IsSchemeTwo(const CalculationOptions& options) {
return options.scheme_id == kSchemeTwoId;
}
void AddInvalid(std::unordered_map<std::string, int>& counters, const std::string& reason) {
counters[reason] += 1;
}
void ComputeWindSpeed600sAverage(std::vector<ValidRow>& rows) {
std::deque<ValidRow> window;
double wind_speed_sum = 0.0;
for (auto& row : rows) {
while (!window.empty() &&
std::difftime(row.timestamp, window.front().timestamp) > 600.0) {
wind_speed_sum -= window.front().wind_speed;
window.pop_front();
}
window.push_back(row);
wind_speed_sum += row.wind_speed;
row.wind_speed_600s_average = wind_speed_sum / static_cast<double>(window.size());
}
}
std::vector<ValidRow> FilterLimitPower(const std::vector<ValidRow>& rows,
const CalculationOptions& options,
int& removed_count,
@@ -362,6 +539,68 @@ std::vector<ValidRow> FilterLimitPower(const std::vector<ValidRow>& rows,
return result;
}
std::vector<ValidRow> FilterSchemeTwoGridSpeed(const std::vector<ValidRow>& rows,
const CalculationOptions& options,
int& removed_count,
std::vector<RemovedPoint>& removed_points) {
removed_count = 0;
if (!IsSchemeTwo(options) || rows.empty()) {
return rows;
}
std::vector<ValidRow> result;
result.reserve(rows.size());
for (const auto& row : rows) {
if (row.generator_speed <= options.grid_connected_speed) {
++removed_count;
removed_points.push_back(RemovedPoint{row, "scheme_two_grid_speed_low"});
} else {
result.push_back(row);
}
}
return result;
}
std::vector<ValidRow> FilterSchemeTwoPitchRules(
const std::vector<ValidRow>& rows,
const CalculationOptions& options,
int& low_power_pitch_wind_count,
int& low_speed_pitch_count,
int& low_power_pitch_count,
std::vector<RemovedPoint>& removed_points) {
low_power_pitch_wind_count = 0;
low_speed_pitch_count = 0;
low_power_pitch_count = 0;
if (!IsSchemeTwo(options) || rows.empty()) {
return rows;
}
std::vector<ValidRow> result;
result.reserve(rows.size());
for (const auto& row : rows) {
if (row.active_power < options.rated_power &&
row.pitch_angle_average > 5.0 &&
row.wind_speed_600s_average < 15.0) {
++low_power_pitch_wind_count;
removed_points.push_back(RemovedPoint{row, "scheme_two_low_power_pitch_wind"});
continue;
}
if (row.generator_speed < options.rated_generator_speed &&
row.pitch_angle_average > 5.0) {
++low_speed_pitch_count;
removed_points.push_back(RemovedPoint{row, "scheme_two_low_speed_pitch"});
continue;
}
if (row.active_power < 1980.0 && row.pitch_angle_average > 2.0) {
++low_power_pitch_count;
removed_points.push_back(RemovedPoint{row, "scheme_two_low_power_pitch"});
continue;
}
result.push_back(row);
}
return result;
}
template <typename ValueGetter>
std::vector<ValidRow> FilterByWindBinIqr(const std::vector<ValidRow>& rows,
const CalculationOptions& options,
@@ -674,6 +913,100 @@ std::string DatePart(const std::string& time_text) {
} // namespace
void WindPowerController::GetSchemes(
const HttpRequestPtr&,
std::function<void(const HttpResponsePtr&)>&& callback) {
json data;
data["default_scheme_id"] = kDefaultSchemeId;
data["schemes"] = json::array();
for (const auto& scheme : LoadSchemes()) {
data["schemes"].push_back(SchemeToJson(scheme));
}
SendSuccess(callback, data);
}
void WindPowerController::SaveSchemeDescription(
const HttpRequestPtr& req,
std::function<void(const HttpResponsePtr&)>&& callback,
const std::string& scheme_id) {
const std::string normalized_id = NormalizeSchemeId(scheme_id);
if (scheme_id != normalized_id) {
SendError(callback, kErrorInvalidRequest, "方案不存在");
return;
}
std::string error;
const auto body = ParseBody(req, error);
if (!body.has_value()) {
SendError(callback, kErrorInvalidRequest, error);
return;
}
const auto description = GetStringField(body.value(), "description");
if (!description.has_value()) {
SendError(callback, kErrorInvalidRequest, "方案描述不能为空");
return;
}
if (description.value().size() > 1000) {
SendError(callback, kErrorInvalidRequest, "方案描述不能超过 1000 个字符");
return;
}
std::optional<double> grid_connected_speed;
std::optional<double> rated_generator_speed;
std::optional<double> rated_power;
if (normalized_id == kSchemeTwoId && body.value().contains("parameters")) {
if (!body.value()["parameters"].is_object()) {
SendError(callback, kErrorInvalidRequest, "方案参数格式错误");
return;
}
const auto& params = body.value()["parameters"];
grid_connected_speed = GetDoubleField(params, "grid_connected_speed");
rated_generator_speed = GetDoubleField(params, "rated_generator_speed");
rated_power = GetDoubleField(params, "rated_power");
if (!grid_connected_speed.has_value() || grid_connected_speed.value() <= 0.0 ||
!rated_generator_speed.has_value() || rated_generator_speed.value() <= 0.0 ||
!rated_power.has_value() || rated_power.value() <= 0.0) {
SendError(callback, kErrorInvalidRequest, "方案二参数必须为正数");
return;
}
}
auto schemes = LoadSchemes();
bool updated = false;
for (auto& scheme : schemes) {
if (scheme.id == normalized_id) {
scheme.description = description.value();
if (scheme.id == kSchemeTwoId) {
if (grid_connected_speed.has_value()) {
scheme.grid_connected_speed = grid_connected_speed.value();
}
if (rated_generator_speed.has_value()) {
scheme.rated_generator_speed = rated_generator_speed.value();
}
if (rated_power.has_value()) {
scheme.rated_power = rated_power.value();
}
}
updated = true;
break;
}
}
if (!updated) {
SendError(callback, kErrorInvalidRequest, "方案不存在");
return;
}
if (!SaveSchemes(schemes)) {
SendError(callback, kErrorServer, "保存方案描述失败");
return;
}
const auto saved_scheme = FindScheme(schemes, normalized_id);
json data;
data["scheme"] = SchemeToJson(saved_scheme.value());
SendSuccess(callback, data);
}
void WindPowerController::StartJob(
const HttpRequestPtr& req,
std::function<void(const HttpResponsePtr&)>&& callback) {
@@ -769,6 +1102,14 @@ void WindPowerController::FinishJob(
}
const CalculationOptions options = ParseOptions(*body);
if (IsSchemeTwo(options) &&
(!options.rated_power_provided ||
!options.grid_connected_speed_provided ||
!options.rated_generator_speed_provided)) {
SendError(callback, kErrorInvalidRequest, "方案二需要填写并网转速、额定转速和额定功率");
return;
}
std::vector<ValidRow> parsed_rows;
std::unordered_map<std::string, int> invalid_reasons;
int raw_rows = 0;
@@ -795,6 +1136,9 @@ void WindPowerController::FinishJob(
const auto wind_speed = GetDoubleField(row, "wind_speed");
const auto active_power = GetDoubleField(row, "active_power");
const auto generator_speed = GetDoubleField(row, "generator_speed");
const auto blade_pitch_1 = GetDoubleField(row, "blade_pitch_1");
const auto blade_pitch_2 = GetDoubleField(row, "blade_pitch_2");
const auto blade_pitch_3 = GetDoubleField(row, "blade_pitch_3");
if (!active_power.has_value() || !std::isfinite(active_power.value()) ||
active_power.value() <= 0.0) {
@@ -807,6 +1151,13 @@ void WindPowerController::FinishJob(
AddInvalid(invalid_reasons, "invalid_generator_speed");
continue;
}
if (IsSchemeTwo(options) &&
(!blade_pitch_1.has_value() || !std::isfinite(blade_pitch_1.value()) ||
!blade_pitch_2.has_value() || !std::isfinite(blade_pitch_2.value()) ||
!blade_pitch_3.has_value() || !std::isfinite(blade_pitch_3.value()))) {
AddInvalid(invalid_reasons, "invalid_pitch_angle");
continue;
}
if (!time_text.has_value()) {
AddInvalid(invalid_reasons, "invalid_time");
continue;
@@ -834,6 +1185,15 @@ void WindPowerController::FinishJob(
valid_row.wind_speed = wind_speed.value();
valid_row.active_power = active_power.value();
valid_row.generator_speed = generator_speed.value();
if (IsSchemeTwo(options)) {
valid_row.blade_pitch_1 = blade_pitch_1.value();
valid_row.blade_pitch_2 = blade_pitch_2.value();
valid_row.blade_pitch_3 = blade_pitch_3.value();
valid_row.pitch_angle_average =
(valid_row.blade_pitch_1 +
valid_row.blade_pitch_2 +
valid_row.blade_pitch_3) / 3.0;
}
parsed_rows.push_back(valid_row);
}
} catch (const std::exception&) {
@@ -889,13 +1249,27 @@ void WindPowerController::FinishJob(
int high_wind_low_power_count = 0;
int curve_residual_outlier_count = 0;
int rated_plateau_low_power_count = 0;
int scheme_two_grid_speed_low_count = 0;
int scheme_two_low_power_pitch_wind_count = 0;
int scheme_two_low_speed_pitch_count = 0;
int scheme_two_low_power_pitch_count = 0;
int cleaned_rows_count = 0;
for (const auto& fan_id : fan_ids) {
fans.push_back(fan_id);
auto fan_rows = rows_by_fan[fan_id];
std::vector<RemovedPoint> fan_removed_points;
if (IsSchemeTwo(options)) {
ComputeWindSpeed600sAverage(fan_rows);
}
int removed = 0;
fan_rows = FilterSchemeTwoGridSpeed(
fan_rows,
options,
removed,
fan_removed_points);
scheme_two_grid_speed_low_count += removed;
fan_rows = FilterLimitPower(fan_rows, options, removed, fan_removed_points);
limit_power_count += removed;
@@ -951,13 +1325,31 @@ void WindPowerController::FinishJob(
fan_removed_points);
rated_plateau_low_power_count += removed;
int low_power_pitch_wind_removed = 0;
int low_speed_pitch_removed = 0;
int low_power_pitch_removed = 0;
fan_rows = FilterSchemeTwoPitchRules(
fan_rows,
options,
low_power_pitch_wind_removed,
low_speed_pitch_removed,
low_power_pitch_removed,
fan_removed_points);
scheme_two_low_power_pitch_wind_count += low_power_pitch_wind_removed;
scheme_two_low_speed_pitch_count += low_speed_pitch_removed;
scheme_two_low_power_pitch_count += low_power_pitch_removed;
cleaned_rows_count += static_cast<int>(fan_rows.size());
json fan_scatter = json::array();
for (const auto& row : fan_rows) {
json point;
point["fan_id"] = row.fan_id;
point["time"] = row.time;
point["wind_speed"] = row.wind_speed;
point["active_power"] = row.active_power;
point["generator_speed"] = row.generator_speed;
point["pitch_angle_average"] = row.pitch_angle_average;
fan_scatter.push_back(point);
}
scatter_points[fan_id] = fan_scatter;
@@ -1012,6 +1404,11 @@ void WindPowerController::FinishJob(
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;
invalid_reasons["scheme_two_grid_speed_low"] = scheme_two_grid_speed_low_count;
invalid_reasons["scheme_two_low_power_pitch_wind"] =
scheme_two_low_power_pitch_wind_count;
invalid_reasons["scheme_two_low_speed_pitch"] = scheme_two_low_speed_pitch_count;
invalid_reasons["scheme_two_low_power_pitch"] = scheme_two_low_power_pitch_count;
const int invalid_total = raw_rows - cleaned_rows_count;
@@ -1026,10 +1423,17 @@ void WindPowerController::FinishJob(
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["scheme_two_grid_speed_low_rows"] = scheme_two_grid_speed_low_count;
summary["scheme_two_low_power_pitch_wind_rows"] =
scheme_two_low_power_pitch_wind_count;
summary["scheme_two_low_speed_pitch_rows"] = scheme_two_low_speed_pitch_count;
summary["scheme_two_low_power_pitch_rows"] = scheme_two_low_power_pitch_count;
summary["invalid_reasons"] = CounterJson(invalid_reasons);
summary["fan_count"] = fans.size();
json data;
const auto selected_scheme = FindScheme(LoadSchemes(), options.scheme_id);
data["scheme"] = SchemeToJson(selected_scheme.value());
data["summary"] = summary;
data["fans"] = fans;
data["curves"] = curves;