7927 lines
341 KiB
C++
7927 lines
341 KiB
C++
#include "pqdif_thread_processor.h"
|
||
|
||
#include <algorithm>
|
||
#include <chrono>
|
||
#include <thread>
|
||
#include <cctype>
|
||
#include <cmath>
|
||
#include <ctime>
|
||
#include <deque>
|
||
#include <iostream>
|
||
#include <mutex>
|
||
#include <sstream>
|
||
#include <string>
|
||
#include <vector>
|
||
#include <set>
|
||
#include <experimental/filesystem>
|
||
#include <fstream>
|
||
#include <iomanip>
|
||
#include <cstdio>
|
||
#include <cstdlib>
|
||
#include <limits>
|
||
#include <utility>
|
||
|
||
// PQDIF 解析库
|
||
#include "pqdif/PQDIF.h"
|
||
#include "pqdif/include/pqdif_ph.h"
|
||
#include "pqdif/include/pqdif_id.h"
|
||
#include "pqdif/include/pqdif_lg.h"
|
||
#include "pqdif_semantic_ids.h"
|
||
|
||
namespace fs = std::experimental::filesystem;
|
||
|
||
namespace {
|
||
|
||
constexpr int kScanIntervalSec = 60;
|
||
constexpr int kBackupLimit = 4800;
|
||
constexpr int kMaxPqdifFilesPerScan = 1;
|
||
constexpr size_t kParsedCacheLimit = 128;
|
||
|
||
const char* kPqdRootDir = "download";
|
||
const char* kDoneRootDir = "download_done";
|
||
const char* kFailRootDir = "download_fail";
|
||
|
||
std::deque<ParsedPqdifFile> g_parsed_cache;
|
||
std::mutex g_parsed_cache_mutex;
|
||
|
||
// PQDIF 统计桶 Base64 文件级“生成队列”。
|
||
// 对象用途:解析线程在完成一个 PQDIF 文件的桶数据组装后,会把一个
|
||
// PqdifStatBase64FileBatch 放入这个队列。
|
||
// 数据粒度:一个队列元素 = 一个 PQDIF 文件批次;批次内部再按
|
||
// “时间点 -> Max/Min/Avg/P95 子记录”保存,避免不同 PQDIF 文件的数据混合。
|
||
constexpr size_t kPqdifStatBase64QueueLimit = 128;
|
||
std::deque<PqdifStatBase64FileBatch> g_pqdif_stat_base64_queue;
|
||
std::mutex g_pqdif_stat_base64_mutex;
|
||
|
||
// PQDIF 统计桶 Base64 文件级“待后续处理队列”。
|
||
// 对象用途:RunPqdifScanLoop() 每轮循环末尾会从生成队列取出最多一个
|
||
// PqdifStatBase64FileBatch,并移动到这个队列中,后续入库/上传/推送逻辑
|
||
// 可以从这里取数据。
|
||
// 设计原因:避免在扫描解析队列上直接做耗时业务处理,同时保证取出的文件批次
|
||
// 不会因为局部变量析构而丢失。
|
||
std::deque<PqdifStatBase64FileBatch> g_pqdif_stat_base64_ready_queue;
|
||
std::mutex g_pqdif_stat_base64_ready_mutex;
|
||
|
||
std::string guid_to_string(const GUID& g)
|
||
{
|
||
char buf[64] = { 0 };
|
||
std::snprintf(buf, sizeof(buf),
|
||
"%08x-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x",
|
||
static_cast<unsigned int>(g.Data1),
|
||
static_cast<unsigned int>(g.Data2),
|
||
static_cast<unsigned int>(g.Data3),
|
||
static_cast<unsigned int>(g.Data4[0]),
|
||
static_cast<unsigned int>(g.Data4[1]),
|
||
static_cast<unsigned int>(g.Data4[2]),
|
||
static_cast<unsigned int>(g.Data4[3]),
|
||
static_cast<unsigned int>(g.Data4[4]),
|
||
static_cast<unsigned int>(g.Data4[5]),
|
||
static_cast<unsigned int>(g.Data4[6]),
|
||
static_cast<unsigned int>(g.Data4[7]));
|
||
return std::string(buf);
|
||
}
|
||
|
||
std::string safe_tag_name(const GUID& tag)
|
||
{
|
||
const char* name = theInfo.GetNameOfTag(tag);
|
||
if (name != nullptr && name[0] != '\0')
|
||
return std::string(name);
|
||
return guid_to_string(tag);
|
||
}
|
||
|
||
PqdifGuidValue make_guid_value(const GUID& guid)
|
||
{
|
||
PqdifGuidValue out;
|
||
out.value = guid;
|
||
out.symbolic_name = safe_tag_name(guid);
|
||
return out;
|
||
}
|
||
|
||
std::string format_time_text(time_t ts)
|
||
{
|
||
if (ts == 0)
|
||
return std::string();
|
||
|
||
std::tm tm_value{};
|
||
#if defined(_WIN32)
|
||
localtime_s(&tm_value, &ts);
|
||
#else
|
||
std::tm* ptm = std::localtime(&ts);
|
||
if (ptm == nullptr)
|
||
return std::string();
|
||
tm_value = *ptm;
|
||
#endif
|
||
char buf[32] = { 0 };
|
||
if (std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm_value) == 0)
|
||
return std::string();
|
||
return std::string(buf);
|
||
}
|
||
|
||
PqdifTimestampValue make_timestamp_value(CPQDIF& file_convert, const TIMESTAMPPQDIF& ts)
|
||
{
|
||
PqdifTimestampValue out;
|
||
out.day = ts.day;
|
||
out.second = ts.sec;
|
||
|
||
if (ts.day != 0 || std::fabs(ts.sec) > 1e-12)
|
||
{
|
||
DATE dt = static_cast<DATE>(ts.day) + (ts.sec / static_cast<double>(SECONDS_PER_DAY));
|
||
file_convert.GetTime(dt, &out.unix_time);
|
||
out.text = format_time_text(out.unix_time);
|
||
}
|
||
|
||
return out;
|
||
}
|
||
|
||
PqdifRecordHeaderInfo build_record_header_info(CPQDIFRecord* record, long record_index)
|
||
{
|
||
PqdifRecordHeaderInfo info;
|
||
info.record_index = record_index;
|
||
|
||
if (record == nullptr)
|
||
return info;
|
||
|
||
LINKABS4 pos = 0;
|
||
LINKABS4 next_pos = 0;
|
||
SIZE4 size_header = 0;
|
||
SIZE4 size_data = 0;
|
||
UINT checksum = 0;
|
||
GUID record_tag{};
|
||
|
||
record->HeaderGetPos(pos);
|
||
record->HeaderGetTag(record_tag);
|
||
record->HeaderGetSize(size_header, size_data);
|
||
record->HeaderGetPosNextRecord(next_pos);
|
||
record->HeaderGetChecksum(checksum);
|
||
|
||
info.file_position = static_cast<long>(pos);
|
||
info.record_type = make_guid_value(record_tag);
|
||
info.header_size = static_cast<long>(size_header);
|
||
info.data_size = static_cast<long>(size_data);
|
||
info.next_record_position = static_cast<long>(next_pos);
|
||
info.checksum = static_cast<unsigned int>(checksum);
|
||
return info;
|
||
}
|
||
|
||
std::string trim_trailing_nulls(const std::string& value)
|
||
{
|
||
std::string out = value;
|
||
while (!out.empty() && out.back() == '\0')
|
||
out.pop_back();
|
||
return out;
|
||
}
|
||
|
||
bool dump_file_basic_info(const std::string& file_path, std::string& err)
|
||
{
|
||
std::error_code ec;
|
||
fs::path p(file_path);
|
||
|
||
if (!fs::exists(p, ec))
|
||
{
|
||
err = "file not exists";
|
||
return false;
|
||
}
|
||
|
||
if (!fs::is_regular_file(p, ec))
|
||
{
|
||
err = "not a regular file";
|
||
return false;
|
||
}
|
||
|
||
auto file_size = fs::file_size(p, ec);
|
||
if (ec)
|
||
{
|
||
err = "cannot get file size";
|
||
return false;
|
||
}
|
||
|
||
std::ifstream ifs(file_path, std::ios::binary);
|
||
if (!ifs.is_open())
|
||
{
|
||
err = "ifstream open failed";
|
||
return false;
|
||
}
|
||
|
||
std::cout << "[PQDIF] file basic info: path=" << file_path
|
||
<< ", size=" << file_size << std::endl;
|
||
|
||
unsigned char buf[32] = { 0 };
|
||
ifs.read(reinterpret_cast<char*>(buf), sizeof(buf));
|
||
std::streamsize n = ifs.gcount();
|
||
|
||
std::ostringstream oss;
|
||
oss << "[PQDIF] first " << n << " bytes: ";
|
||
for (std::streamsize i = 0; i < n; ++i)
|
||
{
|
||
oss << std::hex << std::setw(2) << std::setfill('0')
|
||
<< static_cast<int>(buf[i]) << " ";
|
||
}
|
||
std::cout << oss.str() << std::endl;
|
||
|
||
return true;
|
||
}
|
||
|
||
std::string to_upper_copy(std::string s)
|
||
{
|
||
std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) {
|
||
return static_cast<char>(std::toupper(c));
|
||
});
|
||
return s;
|
||
}
|
||
|
||
std::string trim_copy(const std::string& s)
|
||
{
|
||
size_t beg = 0;
|
||
while (beg < s.size() && std::isspace(static_cast<unsigned char>(s[beg])))
|
||
++beg;
|
||
|
||
size_t end = s.size();
|
||
while (end > beg && std::isspace(static_cast<unsigned char>(s[end - 1])))
|
||
--end;
|
||
|
||
return s.substr(beg, end - beg);
|
||
}
|
||
|
||
std::string normalize_key(const std::string& src)
|
||
{
|
||
std::string out;
|
||
out.reserve(src.size());
|
||
|
||
for (char c : src)
|
||
{
|
||
unsigned char uc = static_cast<unsigned char>(c);
|
||
if (std::isalnum(uc) || c == '[' || c == ']')
|
||
out.push_back(static_cast<char>(std::toupper(uc)));
|
||
}
|
||
|
||
return out;
|
||
}
|
||
|
||
bool is_pqdif_file(const fs::path& path)
|
||
{
|
||
if (!fs::is_regular_file(path))
|
||
return false;
|
||
|
||
const std::string ext = to_upper_copy(path.extension().string());
|
||
return ext == ".PQD" || ext == ".PQDIF";
|
||
}
|
||
|
||
bool ensure_dir(const fs::path& dir)
|
||
{
|
||
std::error_code ec;
|
||
if (fs::exists(dir, ec))
|
||
return true;
|
||
|
||
return fs::create_directories(dir, ec) || fs::exists(dir, ec);
|
||
}
|
||
|
||
bool move_file_with_fallback(const fs::path& src, const fs::path& dst)
|
||
{
|
||
std::error_code ec;
|
||
ensure_dir(dst.parent_path());
|
||
|
||
fs::rename(src, dst, ec);
|
||
if (!ec)
|
||
return true;
|
||
|
||
ec.clear();
|
||
fs::copy_file(src, dst, fs::copy_options::overwrite_existing, ec);
|
||
if (ec)
|
||
return false;
|
||
|
||
ec.clear();
|
||
fs::remove(src, ec);
|
||
return !ec;
|
||
}
|
||
|
||
void cleanup_backup_dir(const fs::path& dir, int limit)
|
||
{
|
||
if (limit < 0)
|
||
return;
|
||
|
||
std::error_code ec;
|
||
if (!fs::exists(dir, ec) || !fs::is_directory(dir, ec))
|
||
return;
|
||
|
||
std::vector<fs::path> files;
|
||
for (fs::directory_iterator it(dir, ec), end; !ec && it != end; it.increment(ec))
|
||
{
|
||
if (!ec && is_pqdif_file(it->path()))
|
||
files.push_back(it->path());
|
||
}
|
||
|
||
if (files.size() <= static_cast<size_t>(limit))
|
||
return;
|
||
|
||
std::sort(files.begin(), files.end(), [](const fs::path& a, const fs::path& b) {
|
||
std::error_code ea, eb;
|
||
return fs::last_write_time(a, ea) < fs::last_write_time(b, eb);
|
||
});
|
||
|
||
const size_t remove_count = files.size() - static_cast<size_t>(limit);
|
||
for (size_t i = 0; i < remove_count; ++i)
|
||
{
|
||
std::error_code remove_ec;
|
||
fs::remove(files[i], remove_ec);
|
||
}
|
||
}
|
||
|
||
bool push_parsed_result_to_cache(ParsedPqdifFile&& parsed)
|
||
{
|
||
std::lock_guard<std::mutex> guard(g_parsed_cache_mutex);
|
||
|
||
if (g_parsed_cache.size() >= kParsedCacheLimit)
|
||
{
|
||
std::cout << "[PQDIF] cache full, drop oldest: "
|
||
<< g_parsed_cache.front().source_file << std::endl;
|
||
g_parsed_cache.pop_front();
|
||
}
|
||
|
||
g_parsed_cache.emplace_back(std::move(parsed));
|
||
return true;
|
||
}
|
||
|
||
bool read_scalar_raw(CPQDIF_E_Collection* collection, const GUID& tag, long& physical_type, PQDIFValue& value)
|
||
{
|
||
if (collection == nullptr)
|
||
return false;
|
||
|
||
CPQDIF_Element* element = collection->GetElement(tag, ID_ELEMENT_TYPE_SCALAR);
|
||
if (element == nullptr)
|
||
return false;
|
||
|
||
CPQDIF_E_Scalar* scalar = static_cast<CPQDIF_E_Scalar*>(element);
|
||
return scalar->GetValue(physical_type, value);
|
||
}
|
||
|
||
bool read_string_tag(CPQDIF_E_Collection* collection, const GUID& tag, std::string& out)
|
||
{
|
||
if (collection == nullptr)
|
||
return false;
|
||
|
||
CPQDIF_Element* element = collection->GetElement(tag, ID_ELEMENT_TYPE_VECTOR);
|
||
if (element == nullptr)
|
||
return false;
|
||
|
||
CPQDIF_E_Vector* vector = static_cast<CPQDIF_E_Vector*>(element);
|
||
std::string text;
|
||
if (!vector->GetValues(text))
|
||
return false;
|
||
|
||
out = trim_trailing_nulls(text);
|
||
return true;
|
||
}
|
||
|
||
bool read_uint_tag(CPQDIF_E_Collection* collection, const GUID& tag, unsigned int& out)
|
||
{
|
||
long physical_type = -1;
|
||
PQDIFValue value{};
|
||
if (!read_scalar_raw(collection, tag, physical_type, value))
|
||
return false;
|
||
|
||
switch (physical_type)
|
||
{
|
||
case ID_PHYS_TYPE_UNS_INTEGER1: out = static_cast<unsigned int>(value.uint1); return true;
|
||
case ID_PHYS_TYPE_UNS_INTEGER2: out = static_cast<unsigned int>(value.uint2); return true;
|
||
case ID_PHYS_TYPE_UNS_INTEGER4: out = static_cast<unsigned int>(value.uint4); return true;
|
||
case ID_PHYS_TYPE_INTEGER1: out = static_cast<unsigned int>(value.int1); return true;
|
||
case ID_PHYS_TYPE_INTEGER2: out = static_cast<unsigned int>(value.int2); return true;
|
||
case ID_PHYS_TYPE_INTEGER4: out = static_cast<unsigned int>(value.int4); return true;
|
||
default: return false;
|
||
}
|
||
}
|
||
|
||
bool read_int_tag(CPQDIF_E_Collection* collection, const GUID& tag, int& out)
|
||
{
|
||
long physical_type = -1;
|
||
PQDIFValue value{};
|
||
if (!read_scalar_raw(collection, tag, physical_type, value))
|
||
return false;
|
||
|
||
switch (physical_type)
|
||
{
|
||
case ID_PHYS_TYPE_INTEGER1: out = static_cast<int>(value.int1); return true;
|
||
case ID_PHYS_TYPE_INTEGER2: out = static_cast<int>(value.int2); return true;
|
||
case ID_PHYS_TYPE_INTEGER4: out = static_cast<int>(value.int4); return true;
|
||
case ID_PHYS_TYPE_UNS_INTEGER1: out = static_cast<int>(value.uint1); return true;
|
||
case ID_PHYS_TYPE_UNS_INTEGER2: out = static_cast<int>(value.uint2); return true;
|
||
case ID_PHYS_TYPE_UNS_INTEGER4: out = static_cast<int>(value.uint4); return true;
|
||
default: return false;
|
||
}
|
||
}
|
||
|
||
bool read_bool_tag(CPQDIF_E_Collection* collection, const GUID& tag, bool& out)
|
||
{
|
||
long physical_type = -1;
|
||
PQDIFValue value{};
|
||
if (!read_scalar_raw(collection, tag, physical_type, value))
|
||
return false;
|
||
|
||
switch (physical_type)
|
||
{
|
||
case ID_PHYS_TYPE_BOOLEAN1: out = (value.bool1 != 0); return true;
|
||
case ID_PHYS_TYPE_BOOLEAN2: out = (value.bool2 != 0); return true;
|
||
case ID_PHYS_TYPE_BOOLEAN4: out = (value.bool4 != 0); return true;
|
||
default: return false;
|
||
}
|
||
}
|
||
|
||
bool read_double_tag(CPQDIF_E_Collection* collection, const GUID& tag, double& out)
|
||
{
|
||
long physical_type = -1;
|
||
PQDIFValue value{};
|
||
if (!read_scalar_raw(collection, tag, physical_type, value))
|
||
return false;
|
||
|
||
switch (physical_type)
|
||
{
|
||
case ID_PHYS_TYPE_REAL4: out = static_cast<double>(value.real4); return true;
|
||
case ID_PHYS_TYPE_REAL8: out = value.real8; return true;
|
||
case ID_PHYS_TYPE_INTEGER1: out = static_cast<double>(value.int1); return true;
|
||
case ID_PHYS_TYPE_INTEGER2: out = static_cast<double>(value.int2); return true;
|
||
case ID_PHYS_TYPE_INTEGER4: out = static_cast<double>(value.int4); return true;
|
||
case ID_PHYS_TYPE_UNS_INTEGER1: out = static_cast<double>(value.uint1); return true;
|
||
case ID_PHYS_TYPE_UNS_INTEGER2: out = static_cast<double>(value.uint2); return true;
|
||
case ID_PHYS_TYPE_UNS_INTEGER4: out = static_cast<double>(value.uint4); return true;
|
||
default: return false;
|
||
}
|
||
}
|
||
|
||
bool read_guid_tag(CPQDIF_E_Collection* collection, const GUID& tag, PqdifGuidValue& out)
|
||
{
|
||
long physical_type = -1;
|
||
PQDIFValue value{};
|
||
if (!read_scalar_raw(collection, tag, physical_type, value))
|
||
return false;
|
||
if (physical_type != ID_PHYS_TYPE_GUID)
|
||
return false;
|
||
|
||
out = make_guid_value(value.guid);
|
||
return true;
|
||
}
|
||
|
||
bool read_timestamp_tag(CPQDIF_E_Collection* collection, const GUID& tag, CPQDIF& file_convert, PqdifTimestampValue& out)
|
||
{
|
||
long physical_type = -1;
|
||
PQDIFValue value{};
|
||
if (!read_scalar_raw(collection, tag, physical_type, value))
|
||
return false;
|
||
if (physical_type != ID_PHYS_TYPE_TIMESTAMPPQDIF)
|
||
return false;
|
||
|
||
out = make_timestamp_value(file_convert, value.ts);
|
||
return true;
|
||
}
|
||
|
||
std::vector<unsigned int> read_vector_uint_values(CPQDIF_E_Collection* collection, const GUID& tag)
|
||
{
|
||
std::vector<unsigned int> out;
|
||
if (collection == nullptr)
|
||
return out;
|
||
|
||
CPQDIF_Element* element = collection->GetElement(tag, ID_ELEMENT_TYPE_VECTOR);
|
||
if (element == nullptr)
|
||
return out;
|
||
|
||
CPQDIF_E_Vector* vector = static_cast<CPQDIF_E_Vector*>(element);
|
||
long count = 0;
|
||
vector->GetCount(count);
|
||
for (long i = 0; i < count; ++i)
|
||
{
|
||
PQDIFValue value{};
|
||
if (!vector->GetValue(i, value))
|
||
continue;
|
||
|
||
switch (vector->GetPhysicalType())
|
||
{
|
||
case ID_PHYS_TYPE_UNS_INTEGER1: out.push_back(static_cast<unsigned int>(value.uint1)); break;
|
||
case ID_PHYS_TYPE_UNS_INTEGER2: out.push_back(static_cast<unsigned int>(value.uint2)); break;
|
||
case ID_PHYS_TYPE_UNS_INTEGER4: out.push_back(static_cast<unsigned int>(value.uint4)); break;
|
||
case ID_PHYS_TYPE_INTEGER1: out.push_back(static_cast<unsigned int>(value.int1)); break;
|
||
case ID_PHYS_TYPE_INTEGER2: out.push_back(static_cast<unsigned int>(value.int2)); break;
|
||
case ID_PHYS_TYPE_INTEGER4: out.push_back(static_cast<unsigned int>(value.int4)); break;
|
||
default: break;
|
||
}
|
||
}
|
||
return out;
|
||
}
|
||
|
||
std::vector<int> read_vector_int_values(CPQDIF_E_Collection* collection, const GUID& tag)
|
||
{
|
||
std::vector<int> out;
|
||
if (collection == nullptr)
|
||
return out;
|
||
|
||
CPQDIF_Element* element = collection->GetElement(tag, ID_ELEMENT_TYPE_VECTOR);
|
||
if (element == nullptr)
|
||
return out;
|
||
|
||
CPQDIF_E_Vector* vector = static_cast<CPQDIF_E_Vector*>(element);
|
||
long count = 0;
|
||
vector->GetCount(count);
|
||
for (long i = 0; i < count; ++i)
|
||
{
|
||
PQDIFValue value{};
|
||
if (!vector->GetValue(i, value))
|
||
continue;
|
||
|
||
switch (vector->GetPhysicalType())
|
||
{
|
||
case ID_PHYS_TYPE_INTEGER1: out.push_back(static_cast<int>(value.int1)); break;
|
||
case ID_PHYS_TYPE_INTEGER2: out.push_back(static_cast<int>(value.int2)); break;
|
||
case ID_PHYS_TYPE_INTEGER4: out.push_back(static_cast<int>(value.int4)); break;
|
||
case ID_PHYS_TYPE_UNS_INTEGER1: out.push_back(static_cast<int>(value.uint1)); break;
|
||
case ID_PHYS_TYPE_UNS_INTEGER2: out.push_back(static_cast<int>(value.uint2)); break;
|
||
case ID_PHYS_TYPE_UNS_INTEGER4: out.push_back(static_cast<int>(value.uint4)); break;
|
||
default: break;
|
||
}
|
||
}
|
||
return out;
|
||
}
|
||
|
||
PqdifValueArray extract_vector_values(CPQDIF_E_Vector* vector, CPQDIF& file_convert)
|
||
{
|
||
PqdifValueArray out;
|
||
if (vector == nullptr)
|
||
return out;
|
||
|
||
out.physical_type = vector->GetPhysicalType();
|
||
vector->GetCount(out.count);
|
||
|
||
if (out.physical_type == ID_PHYS_TYPE_CHAR1 || out.physical_type == ID_PHYS_TYPE_CHAR2)
|
||
{
|
||
std::string text;
|
||
if (vector->GetValues(text))
|
||
out.text_values.push_back(trim_trailing_nulls(text));
|
||
return out;
|
||
}
|
||
|
||
for (long i = 0; i < out.count; ++i)
|
||
{
|
||
PQDIFValue value{};
|
||
if (!vector->GetValue(i, value))
|
||
continue;
|
||
|
||
switch (out.physical_type)
|
||
{
|
||
case ID_PHYS_TYPE_BOOLEAN1: out.bool_values.push_back(value.bool1 != 0); break;
|
||
case ID_PHYS_TYPE_BOOLEAN2: out.bool_values.push_back(value.bool2 != 0); break;
|
||
case ID_PHYS_TYPE_BOOLEAN4: out.bool_values.push_back(value.bool4 != 0); break;
|
||
case ID_PHYS_TYPE_INTEGER1: out.int_values.push_back(static_cast<long long>(value.int1)); break;
|
||
case ID_PHYS_TYPE_INTEGER2: out.int_values.push_back(static_cast<long long>(value.int2)); break;
|
||
case ID_PHYS_TYPE_INTEGER4: out.int_values.push_back(static_cast<long long>(value.int4)); break;
|
||
case ID_PHYS_TYPE_UNS_INTEGER1: out.uint_values.push_back(static_cast<unsigned long long>(value.uint1)); break;
|
||
case ID_PHYS_TYPE_UNS_INTEGER2: out.uint_values.push_back(static_cast<unsigned long long>(value.uint2)); break;
|
||
case ID_PHYS_TYPE_UNS_INTEGER4: out.uint_values.push_back(static_cast<unsigned long long>(value.uint4)); break;
|
||
case ID_PHYS_TYPE_REAL4: out.real_values.push_back(static_cast<double>(value.real4)); break;
|
||
case ID_PHYS_TYPE_REAL8: out.real_values.push_back(value.real8); break;
|
||
case ID_PHYS_TYPE_COMPLEX8: out.complex_values.push_back(std::complex<double>(value.complex8.real, value.complex8.image)); break;
|
||
case ID_PHYS_TYPE_COMPLEX16: out.complex_values.push_back(std::complex<double>(value.complex16.real, value.complex16.image)); break;
|
||
case ID_PHYS_TYPE_TIMESTAMPPQDIF: out.timestamp_values.push_back(make_timestamp_value(file_convert, value.ts)); break;
|
||
case ID_PHYS_TYPE_GUID: out.guid_values.push_back(make_guid_value(value.guid)); break;
|
||
default: break;
|
||
}
|
||
}
|
||
return out;
|
||
}
|
||
|
||
std::string scalar_value_to_text(long physical_type, const PQDIFValue& value, CPQDIF& file_convert)
|
||
{
|
||
std::ostringstream oss;
|
||
switch (physical_type)
|
||
{
|
||
case ID_PHYS_TYPE_BOOLEAN1: oss << (value.bool1 ? "true" : "false"); break;
|
||
case ID_PHYS_TYPE_BOOLEAN2: oss << (value.bool2 ? "true" : "false"); break;
|
||
case ID_PHYS_TYPE_BOOLEAN4: oss << (value.bool4 ? "true" : "false"); break;
|
||
case ID_PHYS_TYPE_INTEGER1: oss << static_cast<int>(value.int1); break;
|
||
case ID_PHYS_TYPE_INTEGER2: oss << value.int2; break;
|
||
case ID_PHYS_TYPE_INTEGER4: oss << value.int4; break;
|
||
case ID_PHYS_TYPE_UNS_INTEGER1: oss << static_cast<unsigned int>(value.uint1); break;
|
||
case ID_PHYS_TYPE_UNS_INTEGER2: oss << value.uint2; break;
|
||
case ID_PHYS_TYPE_UNS_INTEGER4: oss << value.uint4; break;
|
||
case ID_PHYS_TYPE_REAL4: oss << value.real4; break;
|
||
case ID_PHYS_TYPE_REAL8: oss << value.real8; break;
|
||
case ID_PHYS_TYPE_COMPLEX8: oss << value.complex8.real << "+" << value.complex8.image << "j"; break;
|
||
case ID_PHYS_TYPE_COMPLEX16: oss << value.complex16.real << "+" << value.complex16.image << "j"; break;
|
||
case ID_PHYS_TYPE_TIMESTAMPPQDIF:
|
||
{
|
||
PqdifTimestampValue ts = make_timestamp_value(file_convert, value.ts);
|
||
oss << ts.text;
|
||
break;
|
||
}
|
||
case ID_PHYS_TYPE_GUID: oss << safe_tag_name(value.guid) << "(" << guid_to_string(value.guid) << ")"; break;
|
||
default: oss << "physical_type=" << physical_type; break;
|
||
}
|
||
return oss.str();
|
||
}
|
||
|
||
std::string element_to_text(CPQDIF_Element* element, CPQDIF& file_convert)
|
||
{
|
||
if (element == nullptr)
|
||
return std::string();
|
||
|
||
std::ostringstream oss;
|
||
|
||
if (element->GetElementType() == ID_ELEMENT_TYPE_SCALAR)
|
||
{
|
||
CPQDIF_E_Scalar* scalar = static_cast<CPQDIF_E_Scalar*>(element);
|
||
long physical_type = -1;
|
||
PQDIFValue value{};
|
||
if (scalar->GetValue(physical_type, value))
|
||
return scalar_value_to_text(physical_type, value, file_convert);
|
||
return std::string();
|
||
}
|
||
|
||
if (element->GetElementType() == ID_ELEMENT_TYPE_VECTOR)
|
||
{
|
||
CPQDIF_E_Vector* vector = static_cast<CPQDIF_E_Vector*>(element);
|
||
PqdifValueArray values = extract_vector_values(vector, file_convert);
|
||
oss << "vector(type=" << values.physical_type << ", count=" << values.count << ")";
|
||
|
||
size_t shown = 0;
|
||
auto append_preview = [&](const std::string& text) {
|
||
if (shown == 0)
|
||
oss << " [";
|
||
else
|
||
oss << ", ";
|
||
oss << text;
|
||
++shown;
|
||
if (shown == 5)
|
||
return false;
|
||
return true;
|
||
};
|
||
|
||
for (size_t i = 0; i < values.real_values.size() && shown < 5; ++i)
|
||
append_preview(std::to_string(values.real_values[i]));
|
||
for (size_t i = 0; i < values.int_values.size() && shown < 5; ++i)
|
||
append_preview(std::to_string(values.int_values[i]));
|
||
for (size_t i = 0; i < values.uint_values.size() && shown < 5; ++i)
|
||
append_preview(std::to_string(values.uint_values[i]));
|
||
for (size_t i = 0; i < values.bool_values.size() && shown < 5; ++i)
|
||
append_preview(values.bool_values[i] ? "true" : "false");
|
||
for (size_t i = 0; i < values.text_values.size() && shown < 5; ++i)
|
||
append_preview(values.text_values[i]);
|
||
for (size_t i = 0; i < values.timestamp_values.size() && shown < 5; ++i)
|
||
append_preview(values.timestamp_values[i].text);
|
||
for (size_t i = 0; i < values.guid_values.size() && shown < 5; ++i)
|
||
append_preview(values.guid_values[i].symbolic_name);
|
||
if (shown > 0)
|
||
oss << (values.count > 5 ? ", ...]" : "]");
|
||
return oss.str();
|
||
}
|
||
|
||
if (element->GetElementType() == ID_ELEMENT_TYPE_COLLECTION)
|
||
{
|
||
CPQDIF_E_Collection* collection = static_cast<CPQDIF_E_Collection*>(element);
|
||
oss << "collection(count=" << collection->GetCount() << ")";
|
||
return oss.str();
|
||
}
|
||
|
||
return std::string();
|
||
}
|
||
|
||
void collect_extra_tags(CPQDIF_E_Collection* collection,
|
||
const std::set<std::string>& known_tag_names,
|
||
CPQDIF& file_convert,
|
||
PqdifExtraTagMap& out)
|
||
{
|
||
out.clear();
|
||
if (collection == nullptr)
|
||
return;
|
||
|
||
const long count = collection->GetCount();
|
||
for (long i = 0; i < count; ++i)
|
||
{
|
||
CPQDIF_Element* element = collection->GetElement(i);
|
||
if (element == nullptr)
|
||
continue;
|
||
|
||
const std::string tag_name = safe_tag_name(element->GetTag());
|
||
if (known_tag_names.find(tag_name) != known_tag_names.end())
|
||
continue;
|
||
|
||
out[tag_name] = element_to_text(element, file_convert);
|
||
}
|
||
}
|
||
|
||
void dump_logical_summary(const ParsedPqdifFile& parsed_file)
|
||
{
|
||
std::cout << "========== PQDIF LOGICAL SUMMARY ==========" << std::endl;
|
||
std::cout << "file=" << parsed_file.source_file
|
||
<< ", records=" << parsed_file.logical_file.record_headers.size()
|
||
<< ", containers=" << parsed_file.logical_file.containers.size()
|
||
<< ", data_sources=" << parsed_file.logical_file.data_sources.size()
|
||
<< ", settings=" << parsed_file.logical_file.monitor_settings.size()
|
||
<< ", observations=" << parsed_file.logical_file.observations.size()
|
||
<< std::endl;
|
||
|
||
if (!parsed_file.logical_file.observations.empty())
|
||
{
|
||
const PqdifObservationRecord& obs = parsed_file.logical_file.observations.front();
|
||
std::cout << " first_observation=" << obs.observation_name
|
||
<< ", start=" << obs.time_start.text
|
||
<< ", channels=" << obs.channel_instances.size()
|
||
<< std::endl;
|
||
}
|
||
|
||
std::cout << "==========================================" << std::endl;
|
||
}
|
||
|
||
/// @brief PQDIF 日志级别。
|
||
/// @details
|
||
/// 后续新增打印时请按级别输出:
|
||
/// - Core:默认核心摘要,必须短,适合平时测试;
|
||
/// - Info:处理流程、fallback 命中情况、文件移动等;
|
||
/// - Debug:observation 列表、疑似通道、指标来源排查;
|
||
/// - Trace:逐通道/逐序列/全量 bucket 明细,日志量很大。
|
||
enum class PqdifLogLevel
|
||
{
|
||
Core = 0,
|
||
Info = 1,
|
||
Debug = 2,
|
||
Trace = 3
|
||
};
|
||
|
||
bool pqdif_env_truthy(const char* v)
|
||
{
|
||
if (v == nullptr)
|
||
return false;
|
||
std::string s(v);
|
||
for (char& ch : s)
|
||
ch = static_cast<char>(std::toupper(static_cast<unsigned char>(ch)));
|
||
return s == "1" || s == "TRUE" || s == "YES" || s == "ON" ||
|
||
s == "DEBUG" || s == "DETAIL" || s == "TRACE" || s == "VERBOSE";
|
||
}
|
||
|
||
PqdifLogLevel pqdif_parse_log_level(const char* v)
|
||
{
|
||
if (v == nullptr || *v == '\0')
|
||
return PqdifLogLevel::Core;
|
||
|
||
std::string s(v);
|
||
for (char& ch : s)
|
||
ch = static_cast<char>(std::toupper(static_cast<unsigned char>(ch)));
|
||
|
||
if (s == "0" || s == "CORE" || s == "QUIET") return PqdifLogLevel::Core;
|
||
if (s == "1" || s == "INFO") return PqdifLogLevel::Info;
|
||
if (s == "2" || s == "DEBUG" || s == "DETAIL") return PqdifLogLevel::Debug;
|
||
if (s == "3" || s == "TRACE" || s == "VERBOSE") return PqdifLogLevel::Trace;
|
||
return pqdif_env_truthy(v) ? PqdifLogLevel::Debug : PqdifLogLevel::Core;
|
||
}
|
||
|
||
PqdifLogLevel pqdif_current_log_level()
|
||
{
|
||
static int cached = -1;
|
||
if (cached >= 0)
|
||
return static_cast<PqdifLogLevel>(cached);
|
||
|
||
PqdifLogLevel level = pqdif_parse_log_level(std::getenv("PQDIF_LOG_LEVEL"));
|
||
|
||
// 向后兼容旧开关。
|
||
if (level == PqdifLogLevel::Core)
|
||
{
|
||
if (pqdif_env_truthy(std::getenv("PQDIF_DETAIL_LOG")))
|
||
level = PqdifLogLevel::Debug;
|
||
if (pqdif_env_truthy(std::getenv("PQDIF_VERBOSE")))
|
||
level = PqdifLogLevel::Trace;
|
||
}
|
||
|
||
cached = static_cast<int>(level);
|
||
return level;
|
||
}
|
||
|
||
bool pqdif_log_enabled(PqdifLogLevel level)
|
||
{
|
||
return static_cast<int>(pqdif_current_log_level()) >= static_cast<int>(level);
|
||
}
|
||
|
||
/// @brief 是否开启详细调试日志。保留旧函数名,便于已有打印逻辑复用。
|
||
bool pqdif_is_detail_log_enabled()
|
||
{
|
||
return pqdif_log_enabled(PqdifLogLevel::Debug);
|
||
}
|
||
|
||
bool pqdif_is_trace_log_enabled()
|
||
{
|
||
return pqdif_log_enabled(PqdifLogLevel::Trace);
|
||
}
|
||
|
||
std::string short_guid_name(const PqdifGuidValue& g)
|
||
{
|
||
if (!g.symbolic_name.empty())
|
||
return g.symbolic_name;
|
||
return guid_to_string(g.value);
|
||
}
|
||
|
||
std::string preview_value_array(const PqdifValueArray& v)
|
||
{
|
||
std::ostringstream oss;
|
||
oss << "physical_type=" << v.physical_type
|
||
<< ", count=" << v.count;
|
||
|
||
auto append_real_preview = [&](const std::vector<double>& arr, const char* name)
|
||
{
|
||
if (arr.empty())
|
||
return;
|
||
oss << ", " << name << "=[";
|
||
for (size_t i = 0; i < arr.size() && i < 3; ++i)
|
||
{
|
||
if (i > 0) oss << ", ";
|
||
oss << arr[i];
|
||
}
|
||
if (arr.size() > 3) oss << ", ...";
|
||
oss << "]";
|
||
};
|
||
|
||
auto append_int_preview = [&](const std::vector<long long>& arr, const char* name)
|
||
{
|
||
if (arr.empty())
|
||
return;
|
||
oss << ", " << name << "=[";
|
||
for (size_t i = 0; i < arr.size() && i < 3; ++i)
|
||
{
|
||
if (i > 0) oss << ", ";
|
||
oss << arr[i];
|
||
}
|
||
if (arr.size() > 3) oss << ", ...";
|
||
oss << "]";
|
||
};
|
||
|
||
auto append_uint_preview = [&](const std::vector<unsigned long long>& arr, const char* name)
|
||
{
|
||
if (arr.empty())
|
||
return;
|
||
oss << ", " << name << "=[";
|
||
for (size_t i = 0; i < arr.size() && i < 3; ++i)
|
||
{
|
||
if (i > 0) oss << ", ";
|
||
oss << arr[i];
|
||
}
|
||
if (arr.size() > 3) oss << ", ...";
|
||
oss << "]";
|
||
};
|
||
|
||
auto append_time_preview = [&](const std::vector<PqdifTimestampValue>& arr, const char* name)
|
||
{
|
||
if (arr.empty())
|
||
return;
|
||
oss << ", " << name << "=[";
|
||
for (size_t i = 0; i < arr.size() && i < 3; ++i)
|
||
{
|
||
if (i > 0) oss << ", ";
|
||
oss << arr[i].text;
|
||
}
|
||
if (arr.size() > 3) oss << ", ...";
|
||
oss << "]";
|
||
};
|
||
|
||
append_real_preview(v.real_values, "real");
|
||
append_int_preview(v.int_values, "int");
|
||
append_uint_preview(v.uint_values, "uint");
|
||
append_time_preview(v.timestamp_values, "time");
|
||
|
||
if (!v.text_values.empty())
|
||
{
|
||
oss << ", text=[";
|
||
for (size_t i = 0; i < v.text_values.size() && i < 2; ++i)
|
||
{
|
||
if (i > 0) oss << ", ";
|
||
oss << "\"" << v.text_values[i] << "\"";
|
||
}
|
||
if (v.text_values.size() > 2) oss << ", ...";
|
||
oss << "]";
|
||
}
|
||
|
||
return oss.str();
|
||
}
|
||
|
||
void dump_semantic_probe(const ParsedPqdifFile& parsed_file)
|
||
{
|
||
const auto& lf = parsed_file.logical_file;
|
||
const auto& reg = pqdif_sem::GetGuidSemanticRegistry();
|
||
|
||
std::cout << "========== PQDIF SEMANTIC PROBE ==========" << std::endl;
|
||
std::cout << "file=" << parsed_file.source_file << std::endl;
|
||
std::cout << "data_sources=" << lf.data_sources.size()
|
||
<< ", observations=" << lf.observations.size()
|
||
<< std::endl;
|
||
|
||
// --------------------------------------------------------------------
|
||
// 1) 打印第一条数据源的前几个通道定义
|
||
// --------------------------------------------------------------------
|
||
if (!lf.data_sources.empty())
|
||
{
|
||
const auto& ds = lf.data_sources.front();
|
||
std::cout << "[DEF] data_source[0]"
|
||
<< " name=" << ds.name
|
||
<< ", model=" << ds.instrument_model_name
|
||
<< ", channels=" << ds.channel_definitions.size()
|
||
<< std::endl;
|
||
|
||
const size_t ch_limit = std::min<size_t>(ds.channel_definitions.size(), 3);
|
||
for (size_t i = 0; i < ch_limit; ++i)
|
||
{
|
||
const auto& ch = ds.channel_definitions[i];
|
||
|
||
std::cout << " [CH-DEF " << i << "]"
|
||
<< " name=" << ch.channel_name
|
||
<< ", phase=" << pqdif_sem::FindPhaseName(ch.phase_id)
|
||
<< ", measured=" << pqdif_sem::FindQuantityMeasuredName(ch.quantity_measured_id)
|
||
<< ", qty_type=" << reg.FindName(
|
||
pqdif_sem::GuidSemanticField::QuantityType,
|
||
ch.quantity_type_id.value,
|
||
short_guid_name(ch.quantity_type_id).c_str())
|
||
<< ", series_defs=" << ch.series_definitions.size()
|
||
<< std::endl;
|
||
|
||
const size_t sd_limit = std::min<size_t>(ch.series_definitions.size(), 5);
|
||
for (size_t j = 0; j < sd_limit; ++j)
|
||
{
|
||
const auto& sd = ch.series_definitions[j];
|
||
|
||
std::cout << " [SER-DEF " << j << "]"
|
||
<< " value_type=" << reg.FindName(
|
||
pqdif_sem::GuidSemanticField::ValueType,
|
||
sd.value_type_id.value,
|
||
short_guid_name(sd.value_type_id).c_str())
|
||
<< ", characteristic=" << reg.FindName(
|
||
pqdif_sem::GuidSemanticField::QuantityCharacteristic,
|
||
sd.quantity_characteristic_id.value,
|
||
short_guid_name(sd.quantity_characteristic_id).c_str())
|
||
<< ", unit=" << pqdif_sem::FindQuantityUnitsName(sd.quantity_units_id)
|
||
<< ", storage=" << pqdif_sem::FindStorageMethodFlagsName(sd.storage_method_id)
|
||
<< ", nominal=" << sd.nominal_quantity
|
||
<< std::endl;
|
||
}
|
||
}
|
||
}
|
||
|
||
// --------------------------------------------------------------------
|
||
// 2) 打印第一条 observation 的前几个通道实例
|
||
// --------------------------------------------------------------------
|
||
if (!lf.observations.empty())
|
||
{
|
||
const auto& obs = lf.observations.front();
|
||
|
||
std::cout << "[OBS] observation[0]"
|
||
<< " name=" << obs.observation_name
|
||
<< ", start=" << obs.time_start.text
|
||
<< ", trigger=" << pqdif_sem::FindTriggerMethodName(obs.trigger_method_id)
|
||
<< ", channels=" << obs.channel_instances.size()
|
||
<< std::endl;
|
||
|
||
const size_t ch_limit = std::min<size_t>(obs.channel_instances.size(), 3);
|
||
for (size_t i = 0; i < ch_limit; ++i)
|
||
{
|
||
const auto& ch = obs.channel_instances[i];
|
||
|
||
std::cout << " [CH-INS " << i << "]"
|
||
<< " name=" << ch.channel_name
|
||
<< ", phase=" << pqdif_sem::FindPhaseName(ch.phase_id)
|
||
<< ", measured=" << pqdif_sem::FindQuantityMeasuredName(ch.quantity_measured_id)
|
||
<< ", qty_type=" << reg.FindName(
|
||
pqdif_sem::GuidSemanticField::QuantityType,
|
||
ch.quantity_type_id.value,
|
||
short_guid_name(ch.quantity_type_id).c_str())
|
||
<< ", freq=" << ch.channel_frequency
|
||
<< ", group=" << ch.channel_group_id
|
||
<< ", series_instances=" << ch.series_instances.size()
|
||
<< std::endl;
|
||
|
||
const size_t si_limit = std::min<size_t>(ch.series_instances.size(), 5);
|
||
for (size_t j = 0; j < si_limit; ++j)
|
||
{
|
||
const auto& si = ch.series_instances[j];
|
||
|
||
std::cout << " [SER-INS " << j << "]"
|
||
<< " value_type=" << reg.FindName(
|
||
pqdif_sem::GuidSemanticField::ValueType,
|
||
si.value_type_id.value,
|
||
short_guid_name(si.value_type_id).c_str())
|
||
<< ", characteristic=" << reg.FindName(
|
||
pqdif_sem::GuidSemanticField::QuantityCharacteristic,
|
||
si.quantity_characteristic_id.value,
|
||
short_guid_name(si.quantity_characteristic_id).c_str())
|
||
<< ", unit=" << pqdif_sem::FindQuantityUnitsName(si.quantity_units_id)
|
||
<< ", storage=N/A"
|
||
<< ", " << preview_value_array(si.values)
|
||
<< ", share_ch=" << si.share_channel_index
|
||
<< ", share_ser=" << si.share_series_index
|
||
<< ", scale=" << si.scale
|
||
<< ", offset=" << si.offset
|
||
<< std::endl;
|
||
}
|
||
}
|
||
}
|
||
|
||
std::cout << "==========================================" << std::endl;
|
||
}
|
||
|
||
|
||
|
||
long max_series_value_count_in_channel(const PqdifChannelInstance& ch)
|
||
{
|
||
long max_count = 0;
|
||
for (const auto& si : ch.series_instances)
|
||
max_count = std::max<long>(max_count, si.values.count);
|
||
return max_count;
|
||
}
|
||
|
||
long max_series_value_count_in_observation(const PqdifObservationRecord& obs)
|
||
{
|
||
long max_count = 0;
|
||
for (const auto& ch : obs.channel_instances)
|
||
max_count = std::max<long>(max_count, max_series_value_count_in_channel(ch));
|
||
return max_count;
|
||
}
|
||
|
||
const PqdifChannelDefinition* find_channel_definition(
|
||
const PqdifLogicalFile& lf,
|
||
const PqdifObservationRecord& obs,
|
||
const PqdifChannelInstance& ch)
|
||
{
|
||
if (obs.related_data_source_index < 0 ||
|
||
obs.related_data_source_index >= static_cast<int>(lf.data_sources.size()))
|
||
return nullptr;
|
||
|
||
const auto& ds = lf.data_sources[static_cast<size_t>(obs.related_data_source_index)];
|
||
if (ch.channel_def_index < 0 ||
|
||
ch.channel_def_index >= static_cast<int>(ds.channel_definitions.size()))
|
||
return nullptr;
|
||
|
||
return &ds.channel_definitions[static_cast<size_t>(ch.channel_def_index)];
|
||
}
|
||
|
||
bool is_integer_in_range(double value, int min_value, int max_value)
|
||
{
|
||
if (!std::isfinite(value))
|
||
return false;
|
||
|
||
const double rounded = std::floor(value + 0.5);
|
||
if (std::fabs(value - rounded) > 1e-6)
|
||
return false;
|
||
|
||
const int n = static_cast<int>(rounded);
|
||
return n >= min_value && n <= max_value;
|
||
}
|
||
|
||
std::string harmonic_order_token(int order)
|
||
{
|
||
char buf[8] = { 0 };
|
||
std::snprintf(buf, sizeof(buf), "%02d", order);
|
||
return std::string(buf);
|
||
}
|
||
|
||
bool text_looks_like_harmonic(const std::string& text)
|
||
{
|
||
const std::string key = normalize_key(text);
|
||
if (key.empty())
|
||
return false;
|
||
|
||
if (key.find("HARM") != std::string::npos ||
|
||
key.find("HARMONIC") != std::string::npos ||
|
||
key.find("THD") != std::string::npos ||
|
||
key.find("IHD") != std::string::npos ||
|
||
key.find("VHD") != std::string::npos ||
|
||
key.find("HRMS") != std::string::npos)
|
||
return true;
|
||
|
||
for (int order = 2; order <= 50; ++order)
|
||
{
|
||
const std::string two = harmonic_order_token(order);
|
||
const std::string plain = std::to_string(order);
|
||
|
||
const std::string patterns[] = {
|
||
"H" + two, "H" + plain,
|
||
"HD" + two, "HD" + plain,
|
||
"VH" + two, "VH" + plain,
|
||
"UH" + two, "UH" + plain,
|
||
"IH" + two, "IH" + plain
|
||
};
|
||
|
||
for (const auto& pat : patterns)
|
||
{
|
||
if (key.find(pat) != std::string::npos)
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
bool extra_tags_look_like_harmonic(const PqdifExtraTagMap& tags)
|
||
{
|
||
for (const auto& kv : tags)
|
||
{
|
||
if (text_looks_like_harmonic(kv.first) || text_looks_like_harmonic(kv.second))
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
bool series_definition_looks_like_harmonic(const PqdifSeriesDefinition& sd)
|
||
{
|
||
if (is_integer_in_range(sd.nominal_quantity, 2, 50))
|
||
return true;
|
||
|
||
if (text_looks_like_harmonic(sd.value_type_name) ||
|
||
text_looks_like_harmonic(sd.value_type_id.symbolic_name) ||
|
||
text_looks_like_harmonic(sd.quantity_characteristic_id.symbolic_name) ||
|
||
extra_tags_look_like_harmonic(sd.extra_tags))
|
||
return true;
|
||
|
||
return false;
|
||
}
|
||
|
||
bool series_instance_looks_like_harmonic(const PqdifSeriesInstance& si)
|
||
{
|
||
if (is_integer_in_range(si.series_base_quantity, 2, 50) ||
|
||
is_integer_in_range(si.nominal_quantity, 2, 50))
|
||
return true;
|
||
|
||
if (text_looks_like_harmonic(si.value_type_id.symbolic_name) ||
|
||
text_looks_like_harmonic(si.quantity_characteristic_id.symbolic_name) ||
|
||
extra_tags_look_like_harmonic(si.extra_tags))
|
||
return true;
|
||
|
||
return false;
|
||
}
|
||
|
||
bool channel_definition_looks_like_harmonic(const PqdifChannelDefinition& ch)
|
||
{
|
||
if (text_looks_like_harmonic(ch.channel_name) ||
|
||
text_looks_like_harmonic(ch.quantity_name) ||
|
||
text_looks_like_harmonic(ch.group_name) ||
|
||
text_looks_like_harmonic(ch.other_channel_identifier) ||
|
||
text_looks_like_harmonic(ch.quantity_type_id.symbolic_name) ||
|
||
extra_tags_look_like_harmonic(ch.extra_tags))
|
||
return true;
|
||
|
||
for (const auto& sd : ch.series_definitions)
|
||
{
|
||
if (series_definition_looks_like_harmonic(sd))
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
bool channel_instance_looks_like_harmonic(
|
||
const PqdifChannelInstance& ch,
|
||
const PqdifChannelDefinition* def)
|
||
{
|
||
if (text_looks_like_harmonic(ch.channel_name) ||
|
||
text_looks_like_harmonic(ch.quantity_type_id.symbolic_name) ||
|
||
extra_tags_look_like_harmonic(ch.extra_tags))
|
||
return true;
|
||
|
||
if (def != nullptr && channel_definition_looks_like_harmonic(*def))
|
||
return true;
|
||
|
||
for (const auto& si : ch.series_instances)
|
||
{
|
||
if (series_instance_looks_like_harmonic(si))
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
void dump_observation_list(const ParsedPqdifFile& parsed_file)
|
||
{
|
||
const auto& lf = parsed_file.logical_file;
|
||
|
||
std::cout << "========== PQDIF OBSERVATION LIST ==========" << std::endl;
|
||
std::cout << "file=" << parsed_file.source_file
|
||
<< ", observation_count=" << lf.observations.size()
|
||
<< std::endl;
|
||
|
||
for (size_t i = 0; i < lf.observations.size(); ++i)
|
||
{
|
||
const auto& obs = lf.observations[i];
|
||
|
||
std::cout << " [OBS " << i << "]"
|
||
<< " index=" << obs.observation_index
|
||
<< ", record=" << obs.record_index
|
||
<< ", name=" << obs.observation_name
|
||
<< ", start=" << obs.time_start.text
|
||
<< ", create=" << obs.time_create.text
|
||
<< ", trigger=" << pqdif_sem::FindTriggerMethodName(obs.trigger_method_id)
|
||
<< ", triggered=" << obs.time_triggered.text
|
||
<< ", channels=" << obs.channel_instances.size()
|
||
<< ", max_series_count=" << max_series_value_count_in_observation(obs)
|
||
<< ", related_ds=" << obs.related_data_source_index
|
||
<< ", related_settings=" << obs.related_settings_index
|
||
<< std::endl;
|
||
|
||
const size_t preview_count = std::min<size_t>(obs.channel_instances.size(), 10);
|
||
if (preview_count > 0)
|
||
{
|
||
std::cout << " channel_preview=";
|
||
for (size_t j = 0; j < preview_count; ++j)
|
||
{
|
||
const auto& ch = obs.channel_instances[j];
|
||
if (j > 0)
|
||
std::cout << " | ";
|
||
std::cout << "#" << ch.channel_instance_index << ":" << ch.channel_name;
|
||
}
|
||
if (obs.channel_instances.size() > preview_count)
|
||
std::cout << " | ...";
|
||
std::cout << std::endl;
|
||
}
|
||
}
|
||
|
||
std::cout << "============================================" << std::endl;
|
||
}
|
||
|
||
void dump_harmonic_channel_probe(const ParsedPqdifFile& parsed_file)
|
||
{
|
||
const auto& lf = parsed_file.logical_file;
|
||
const auto& reg = pqdif_sem::GetGuidSemanticRegistry();
|
||
|
||
std::cout << "========== PQDIF HARMONIC CHANNEL PROBE ==========" << std::endl;
|
||
std::cout << "file=" << parsed_file.source_file << std::endl;
|
||
std::cout << "rule=print channel definitions/instances whose name, GUID name, extra tags, "
|
||
<< "series_base_quantity or nominal_quantity looks like harmonic order 2-50"
|
||
<< std::endl;
|
||
|
||
size_t def_hits = 0;
|
||
for (size_t ds_idx = 0; ds_idx < lf.data_sources.size(); ++ds_idx)
|
||
{
|
||
const auto& ds = lf.data_sources[ds_idx];
|
||
for (size_t ch_idx = 0; ch_idx < ds.channel_definitions.size(); ++ch_idx)
|
||
{
|
||
const auto& ch = ds.channel_definitions[ch_idx];
|
||
if (!channel_definition_looks_like_harmonic(ch))
|
||
continue;
|
||
|
||
++def_hits;
|
||
std::cout << " [HARM-DEF " << def_hits << "]"
|
||
<< " ds=" << ds_idx
|
||
<< ", ch_def=" << ch.channel_def_index
|
||
<< ", name=" << ch.channel_name
|
||
<< ", phase=" << pqdif_sem::FindPhaseName(ch.phase_id)
|
||
<< ", measured=" << pqdif_sem::FindQuantityMeasuredName(ch.quantity_measured_id)
|
||
<< ", qty_type=" << reg.FindName(
|
||
pqdif_sem::GuidSemanticField::QuantityType,
|
||
ch.quantity_type_id.value,
|
||
short_guid_name(ch.quantity_type_id).c_str())
|
||
<< ", physical_ch=" << ch.physical_channel
|
||
<< ", primary_series=" << ch.primary_series_index
|
||
<< ", series_defs=" << ch.series_definitions.size()
|
||
<< std::endl;
|
||
|
||
const size_t sd_limit = std::min<size_t>(ch.series_definitions.size(), 8);
|
||
for (size_t j = 0; j < sd_limit; ++j)
|
||
{
|
||
const auto& sd = ch.series_definitions[j];
|
||
std::cout << " [HARM-SER-DEF " << j << "]"
|
||
<< " value_type=" << reg.FindName(
|
||
pqdif_sem::GuidSemanticField::ValueType,
|
||
sd.value_type_id.value,
|
||
short_guid_name(sd.value_type_id).c_str())
|
||
<< ", characteristic=" << reg.FindName(
|
||
pqdif_sem::GuidSemanticField::QuantityCharacteristic,
|
||
sd.quantity_characteristic_id.value,
|
||
short_guid_name(sd.quantity_characteristic_id).c_str())
|
||
<< ", unit=" << pqdif_sem::FindQuantityUnitsName(sd.quantity_units_id)
|
||
<< ", storage=" << pqdif_sem::FindStorageMethodFlagsName(sd.storage_method_id)
|
||
<< ", nominal=" << sd.nominal_quantity
|
||
<< ", percentile=" << sd.prob_percentile
|
||
<< std::endl;
|
||
}
|
||
}
|
||
}
|
||
|
||
size_t inst_hits = 0;
|
||
for (size_t obs_idx = 0; obs_idx < lf.observations.size(); ++obs_idx)
|
||
{
|
||
const auto& obs = lf.observations[obs_idx];
|
||
for (size_t ch_idx = 0; ch_idx < obs.channel_instances.size(); ++ch_idx)
|
||
{
|
||
const auto& ch = obs.channel_instances[ch_idx];
|
||
const PqdifChannelDefinition* def = find_channel_definition(lf, obs, ch);
|
||
if (!channel_instance_looks_like_harmonic(ch, def))
|
||
continue;
|
||
|
||
++inst_hits;
|
||
std::cout << " [HARM-INS " << inst_hits << "]"
|
||
<< " obs=" << obs_idx
|
||
<< ", obs_name=" << obs.observation_name
|
||
<< ", obs_start=" << obs.time_start.text
|
||
<< ", ch=" << ch.channel_instance_index
|
||
<< ", ch_def=" << ch.channel_def_index
|
||
<< ", name=" << ch.channel_name
|
||
<< ", def_name=" << (def != nullptr ? def->channel_name : std::string())
|
||
<< ", phase=" << pqdif_sem::FindPhaseName(ch.phase_id)
|
||
<< ", measured=" << pqdif_sem::FindQuantityMeasuredName(ch.quantity_measured_id)
|
||
<< ", qty_type=" << reg.FindName(
|
||
pqdif_sem::GuidSemanticField::QuantityType,
|
||
ch.quantity_type_id.value,
|
||
short_guid_name(ch.quantity_type_id).c_str())
|
||
<< ", group=" << ch.channel_group_id
|
||
<< ", freq=" << ch.channel_frequency
|
||
<< ", primary_series=" << ch.primary_series_index
|
||
<< ", series_instances=" << ch.series_instances.size()
|
||
<< std::endl;
|
||
|
||
const size_t si_limit = std::min<size_t>(ch.series_instances.size(), 8);
|
||
for (size_t j = 0; j < si_limit; ++j)
|
||
{
|
||
const auto& si = ch.series_instances[j];
|
||
std::cout << " [HARM-SER-INS " << j << "]"
|
||
<< " value_type=" << reg.FindName(
|
||
pqdif_sem::GuidSemanticField::ValueType,
|
||
si.value_type_id.value,
|
||
short_guid_name(si.value_type_id).c_str())
|
||
<< ", characteristic=" << reg.FindName(
|
||
pqdif_sem::GuidSemanticField::QuantityCharacteristic,
|
||
si.quantity_characteristic_id.value,
|
||
short_guid_name(si.quantity_characteristic_id).c_str())
|
||
<< ", unit=" << pqdif_sem::FindQuantityUnitsName(si.quantity_units_id)
|
||
<< ", base_q=" << si.series_base_quantity
|
||
<< ", nominal=" << si.nominal_quantity
|
||
<< ", scale=" << si.scale
|
||
<< ", offset=" << si.offset
|
||
<< ", share_ch=" << si.share_channel_index
|
||
<< ", share_ser=" << si.share_series_index
|
||
<< ", " << preview_value_array(si.values)
|
||
<< std::endl;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (def_hits == 0 && inst_hits == 0)
|
||
{
|
||
std::cout << " [HARMONIC PROBE RESULT] no suspected harmonic channel found in definitions or observations"
|
||
<< std::endl;
|
||
}
|
||
else
|
||
{
|
||
std::cout << " [HARMONIC PROBE RESULT] definition_candidates=" << def_hits
|
||
<< ", observation_channel_candidates=" << inst_hits
|
||
<< std::endl;
|
||
}
|
||
|
||
std::cout << "=================================================" << std::endl;
|
||
}
|
||
|
||
|
||
|
||
int stat_make_channel_spectrum_order_hint(
|
||
const PqdifObservationRecord& obs,
|
||
const PqdifChannelInstance& ch,
|
||
int& block_offset,
|
||
int& block_size);
|
||
|
||
bool pqdif_probe_guid_is_harmonic_characteristic(const GUID& g)
|
||
{
|
||
return PQDIF_IsEqualGUID(g, ID_QC_HRMS) ||
|
||
PQDIF_IsEqualGUID(g, ID_QC_SPECTRA) ||
|
||
PQDIF_IsEqualGUID(g, ID_QC_SPECTRA_HGROUP);
|
||
}
|
||
|
||
bool pqdif_probe_guid_is_interharmonic_characteristic(const GUID& g)
|
||
{
|
||
return PQDIF_IsEqualGUID(g, ID_QC_SPECTRA_IGROUP);
|
||
}
|
||
|
||
bool pqdif_probe_guid_is_phase_angle_value_type(const GUID& g)
|
||
{
|
||
return PQDIF_IsEqualGUID(g, ID_SERIES_VALUE_TYPE_PHASEANGLE) ||
|
||
PQDIF_IsEqualGUID(g, ID_SERIES_VALUE_TYPE_PHASEANGLE_MIN) ||
|
||
PQDIF_IsEqualGUID(g, ID_SERIES_VALUE_TYPE_PHASEANGLE_MAX) ||
|
||
PQDIF_IsEqualGUID(g, ID_SERIES_VALUE_TYPE_PHASEANGLE_AVG);
|
||
}
|
||
|
||
bool pqdif_probe_text_looks_like_interharmonic(const std::string& text)
|
||
{
|
||
const std::string key = normalize_key(text);
|
||
return key.find("INTERHARM") != std::string::npos ||
|
||
key.find("INTERHARMONIC") != std::string::npos ||
|
||
key.find("IHARM") != std::string::npos ||
|
||
key.find("IGROUP") != std::string::npos ||
|
||
key.find("IHGROUP") != std::string::npos;
|
||
}
|
||
|
||
bool pqdif_probe_text_looks_like_angle(const std::string& text)
|
||
{
|
||
const std::string key = normalize_key(text);
|
||
return key.find("ANGLE") != std::string::npos ||
|
||
key.find("PHASEANGLE") != std::string::npos ||
|
||
key.find("PANGLE") != std::string::npos ||
|
||
key.find("HANGLE") != std::string::npos;
|
||
}
|
||
|
||
bool pqdif_probe_phase_is_line_voltage(unsigned int phase_id, const std::string& name)
|
||
{
|
||
if (phase_id == ID_PHASE_AB || phase_id == ID_PHASE_BC || phase_id == ID_PHASE_CA)
|
||
return true;
|
||
const std::string key = normalize_key(name);
|
||
return key.find("UAB") != std::string::npos || key.find("VAB") != std::string::npos ||
|
||
key.find("UBC") != std::string::npos || key.find("VBC") != std::string::npos ||
|
||
key.find("UCA") != std::string::npos || key.find("VCA") != std::string::npos ||
|
||
key.find("AB") != std::string::npos || key.find("BC") != std::string::npos || key.find("CA") != std::string::npos;
|
||
}
|
||
|
||
bool pqdif_probe_series_def_is_dynamic_candidate(const PqdifSeriesDefinition& sd)
|
||
{
|
||
return pqdif_probe_guid_is_harmonic_characteristic(sd.quantity_characteristic_id.value) ||
|
||
pqdif_probe_guid_is_interharmonic_characteristic(sd.quantity_characteristic_id.value) ||
|
||
pqdif_probe_guid_is_phase_angle_value_type(sd.value_type_id.value) ||
|
||
sd.quantity_units_id == ID_QU_DEGREES ||
|
||
text_looks_like_harmonic(sd.quantity_characteristic_id.symbolic_name) ||
|
||
pqdif_probe_text_looks_like_interharmonic(sd.quantity_characteristic_id.symbolic_name) ||
|
||
pqdif_probe_text_looks_like_angle(sd.value_type_id.symbolic_name);
|
||
}
|
||
|
||
bool pqdif_probe_series_ins_is_dynamic_candidate(const PqdifSeriesInstance& si)
|
||
{
|
||
return pqdif_probe_guid_is_harmonic_characteristic(si.quantity_characteristic_id.value) ||
|
||
pqdif_probe_guid_is_interharmonic_characteristic(si.quantity_characteristic_id.value) ||
|
||
pqdif_probe_guid_is_phase_angle_value_type(si.value_type_id.value) ||
|
||
si.quantity_units_id == ID_QU_DEGREES ||
|
||
is_integer_in_range(si.series_base_quantity, 2, 50) ||
|
||
is_integer_in_range(si.nominal_quantity, 2, 50) ||
|
||
text_looks_like_harmonic(si.quantity_characteristic_id.symbolic_name) ||
|
||
pqdif_probe_text_looks_like_interharmonic(si.quantity_characteristic_id.symbolic_name) ||
|
||
pqdif_probe_text_looks_like_angle(si.value_type_id.symbolic_name);
|
||
}
|
||
|
||
void dump_dynamic_spectrum_candidate_probe(const ParsedPqdifFile& parsed_file)
|
||
{
|
||
const auto& lf = parsed_file.logical_file;
|
||
const auto& reg = pqdif_sem::GetGuidSemanticRegistry();
|
||
|
||
std::cout << "========== PQDIF DYNAMIC SPECTRUM CANDIDATE PROBE ==========" << std::endl;
|
||
std::cout << "file=" << parsed_file.source_file << std::endl;
|
||
std::cout << "rule=DEBUG level: print all channel definitions/instances that look like harmonic, angle, interharmonic, spectra, HRMS, HGroup or IGroup" << std::endl;
|
||
|
||
size_t def_hits = 0;
|
||
size_t def_line_like = 0;
|
||
size_t def_inter_like = 0;
|
||
for (size_t ds_idx = 0; ds_idx < lf.data_sources.size(); ++ds_idx)
|
||
{
|
||
const auto& ds = lf.data_sources[ds_idx];
|
||
for (size_t ch_idx = 0; ch_idx < ds.channel_definitions.size(); ++ch_idx)
|
||
{
|
||
const auto& ch = ds.channel_definitions[ch_idx];
|
||
bool candidate = channel_definition_looks_like_harmonic(ch) ||
|
||
pqdif_probe_text_looks_like_interharmonic(ch.channel_name) ||
|
||
pqdif_probe_text_looks_like_angle(ch.channel_name);
|
||
for (const auto& sd : ch.series_definitions)
|
||
candidate = candidate || pqdif_probe_series_def_is_dynamic_candidate(sd);
|
||
if (!candidate)
|
||
continue;
|
||
|
||
const bool line_like = pqdif_probe_phase_is_line_voltage(ch.phase_id, ch.channel_name);
|
||
bool inter_like = pqdif_probe_text_looks_like_interharmonic(ch.channel_name);
|
||
bool angle_like = pqdif_probe_text_looks_like_angle(ch.channel_name);
|
||
bool harmonic_like = text_looks_like_harmonic(ch.channel_name);
|
||
for (const auto& sd : ch.series_definitions)
|
||
{
|
||
inter_like = inter_like || pqdif_probe_guid_is_interharmonic_characteristic(sd.quantity_characteristic_id.value) ||
|
||
pqdif_probe_text_looks_like_interharmonic(sd.quantity_characteristic_id.symbolic_name);
|
||
angle_like = angle_like || pqdif_probe_guid_is_phase_angle_value_type(sd.value_type_id.value) ||
|
||
sd.quantity_units_id == ID_QU_DEGREES || pqdif_probe_text_looks_like_angle(sd.value_type_id.symbolic_name);
|
||
harmonic_like = harmonic_like || pqdif_probe_guid_is_harmonic_characteristic(sd.quantity_characteristic_id.value) ||
|
||
text_looks_like_harmonic(sd.quantity_characteristic_id.symbolic_name);
|
||
}
|
||
|
||
++def_hits;
|
||
if (line_like) ++def_line_like;
|
||
if (inter_like) ++def_inter_like;
|
||
|
||
std::cout << " [DYN-DEF " << def_hits << "]"
|
||
<< " ds=" << ds_idx
|
||
<< ", ch_def=" << ch.channel_def_index
|
||
<< ", name=" << ch.channel_name
|
||
<< ", phase=" << pqdif_sem::FindPhaseName(ch.phase_id)
|
||
<< ", measured=" << pqdif_sem::FindQuantityMeasuredName(ch.quantity_measured_id)
|
||
<< ", line_like=" << (line_like ? "true" : "false")
|
||
<< ", harmonic_like=" << (harmonic_like ? "true" : "false")
|
||
<< ", interharmonic_like=" << (inter_like ? "true" : "false")
|
||
<< ", angle_like=" << (angle_like ? "true" : "false")
|
||
<< ", series_defs=" << ch.series_definitions.size()
|
||
<< std::endl;
|
||
|
||
for (size_t j = 0; j < ch.series_definitions.size(); ++j)
|
||
{
|
||
const auto& sd = ch.series_definitions[j];
|
||
if (!pqdif_probe_series_def_is_dynamic_candidate(sd) && !pqdif_is_trace_log_enabled())
|
||
continue;
|
||
std::cout << " [DYN-SER-DEF " << j << "]"
|
||
<< " value_type=" << reg.FindName(
|
||
pqdif_sem::GuidSemanticField::ValueType,
|
||
sd.value_type_id.value,
|
||
short_guid_name(sd.value_type_id).c_str())
|
||
<< ", characteristic=" << reg.FindName(
|
||
pqdif_sem::GuidSemanticField::QuantityCharacteristic,
|
||
sd.quantity_characteristic_id.value,
|
||
short_guid_name(sd.quantity_characteristic_id).c_str())
|
||
<< ", unit=" << pqdif_sem::FindQuantityUnitsName(sd.quantity_units_id)
|
||
<< ", nominal=" << sd.nominal_quantity
|
||
<< ", percentile=" << sd.prob_percentile
|
||
<< std::endl;
|
||
}
|
||
}
|
||
}
|
||
|
||
size_t inst_hits = 0;
|
||
size_t inst_line_like = 0;
|
||
size_t inst_inter_like = 0;
|
||
for (size_t obs_idx = 0; obs_idx < lf.observations.size(); ++obs_idx)
|
||
{
|
||
const auto& obs = lf.observations[obs_idx];
|
||
for (size_t ch_idx = 0; ch_idx < obs.channel_instances.size(); ++ch_idx)
|
||
{
|
||
const auto& ch = obs.channel_instances[ch_idx];
|
||
const PqdifChannelDefinition* def = find_channel_definition(lf, obs, ch);
|
||
bool candidate = channel_instance_looks_like_harmonic(ch, def) ||
|
||
pqdif_probe_text_looks_like_interharmonic(ch.channel_name) ||
|
||
pqdif_probe_text_looks_like_angle(ch.channel_name);
|
||
for (const auto& si : ch.series_instances)
|
||
candidate = candidate || pqdif_probe_series_ins_is_dynamic_candidate(si);
|
||
if (!candidate)
|
||
continue;
|
||
|
||
const bool line_like = pqdif_probe_phase_is_line_voltage(ch.phase_id, ch.channel_name);
|
||
bool inter_like = pqdif_probe_text_looks_like_interharmonic(ch.channel_name);
|
||
bool angle_like = pqdif_probe_text_looks_like_angle(ch.channel_name);
|
||
bool harmonic_like = text_looks_like_harmonic(ch.channel_name);
|
||
for (const auto& si : ch.series_instances)
|
||
{
|
||
inter_like = inter_like || pqdif_probe_guid_is_interharmonic_characteristic(si.quantity_characteristic_id.value) ||
|
||
pqdif_probe_text_looks_like_interharmonic(si.quantity_characteristic_id.symbolic_name);
|
||
angle_like = angle_like || pqdif_probe_guid_is_phase_angle_value_type(si.value_type_id.value) ||
|
||
si.quantity_units_id == ID_QU_DEGREES || pqdif_probe_text_looks_like_angle(si.value_type_id.symbolic_name);
|
||
harmonic_like = harmonic_like || pqdif_probe_guid_is_harmonic_characteristic(si.quantity_characteristic_id.value) ||
|
||
text_looks_like_harmonic(si.quantity_characteristic_id.symbolic_name);
|
||
}
|
||
|
||
int hint_offset = -1;
|
||
int hint_size = 0;
|
||
const int hint_order = stat_make_channel_spectrum_order_hint(obs, ch, hint_offset, hint_size);
|
||
|
||
++inst_hits;
|
||
if (line_like) ++inst_line_like;
|
||
if (inter_like) ++inst_inter_like;
|
||
|
||
std::cout << " [DYN-INS " << inst_hits << "]"
|
||
<< " obs=" << obs_idx
|
||
<< ", obs_name=" << obs.observation_name
|
||
<< ", ch=" << ch.channel_instance_index
|
||
<< ", ch_def=" << ch.channel_def_index
|
||
<< ", name=" << ch.channel_name
|
||
<< ", def_name=" << (def != nullptr ? def->channel_name : std::string())
|
||
<< ", phase=" << pqdif_sem::FindPhaseName(ch.phase_id)
|
||
<< ", measured=" << pqdif_sem::FindQuantityMeasuredName(ch.quantity_measured_id)
|
||
<< ", group=" << ch.channel_group_id
|
||
<< ", order_hint=" << hint_order
|
||
<< ", hint_offset=" << hint_offset
|
||
<< ", hint_block_size=" << hint_size
|
||
<< ", line_like=" << (line_like ? "true" : "false")
|
||
<< ", harmonic_like=" << (harmonic_like ? "true" : "false")
|
||
<< ", interharmonic_like=" << (inter_like ? "true" : "false")
|
||
<< ", angle_like=" << (angle_like ? "true" : "false")
|
||
<< ", series_instances=" << ch.series_instances.size()
|
||
<< std::endl;
|
||
|
||
for (size_t j = 0; j < ch.series_instances.size(); ++j)
|
||
{
|
||
const auto& si = ch.series_instances[j];
|
||
if (!pqdif_probe_series_ins_is_dynamic_candidate(si) && !pqdif_is_trace_log_enabled())
|
||
continue;
|
||
std::cout << " [DYN-SER-INS " << j << "]"
|
||
<< " value_type=" << reg.FindName(
|
||
pqdif_sem::GuidSemanticField::ValueType,
|
||
si.value_type_id.value,
|
||
short_guid_name(si.value_type_id).c_str())
|
||
<< ", characteristic=" << reg.FindName(
|
||
pqdif_sem::GuidSemanticField::QuantityCharacteristic,
|
||
si.quantity_characteristic_id.value,
|
||
short_guid_name(si.quantity_characteristic_id).c_str())
|
||
<< ", unit=" << pqdif_sem::FindQuantityUnitsName(si.quantity_units_id)
|
||
<< ", base_q=" << si.series_base_quantity
|
||
<< ", nominal=" << si.nominal_quantity
|
||
<< ", scale=" << si.scale
|
||
<< ", offset=" << si.offset
|
||
<< ", share_ch=" << si.share_channel_index
|
||
<< ", share_ser=" << si.share_series_index
|
||
<< ", " << preview_value_array(si.values)
|
||
<< std::endl;
|
||
}
|
||
}
|
||
}
|
||
|
||
std::cout << " [DYNAMIC SPECTRUM PROBE RESULT] definition_candidates=" << def_hits
|
||
<< ", definition_line_like=" << def_line_like
|
||
<< ", definition_interharmonic_like=" << def_inter_like
|
||
<< ", observation_channel_candidates=" << inst_hits
|
||
<< ", observation_line_like=" << inst_line_like
|
||
<< ", observation_interharmonic_like=" << inst_inter_like
|
||
<< std::endl;
|
||
std::cout << "==========================================================" << std::endl;
|
||
}
|
||
|
||
|
||
void dump_monitor_settings_probe(const ParsedPqdifFile& parsed_file)
|
||
{
|
||
const auto& lf = parsed_file.logical_file;
|
||
|
||
std::cout << "========== PQDIF MONITOR SETTINGS ==========" << std::endl;
|
||
std::cout << "settings_count=" << lf.monitor_settings.size() << std::endl;
|
||
|
||
for (size_t i = 0; i < lf.monitor_settings.size() && i < 3; ++i)
|
||
{
|
||
const auto& s = lf.monitor_settings[i];
|
||
std::cout << " [SET " << i << "]"
|
||
<< " effective=" << s.effective_time.text
|
||
<< ", nominal_freq=" << s.nominal_frequency
|
||
<< ", nominal_voltage=" << s.nominal_voltage
|
||
<< ", physical_connection=" << s.physical_connection
|
||
<< ", is_pcc=" << (s.is_pcc ? "true" : "false")
|
||
<< ", use_transducer=" << (s.use_transducer ? "true" : "false")
|
||
<< std::endl;
|
||
}
|
||
|
||
std::cout << "===========================================" << std::endl;
|
||
}
|
||
|
||
|
||
|
||
std::string stat_two_digit(int n);
|
||
|
||
int stat_dynamic_metric_base(StatMetricId id)
|
||
{
|
||
return static_cast<int>(id);
|
||
}
|
||
|
||
enum class StatDynamicMetricGroup
|
||
{
|
||
None = 0,
|
||
VoltageHarmonic,
|
||
LineVoltageHarmonic,
|
||
CurrentHarmonic,
|
||
VoltageHarmonicAngle,
|
||
LineVoltageHarmonicAngle,
|
||
CurrentHarmonicAngle,
|
||
|
||
HarmonicActivePower,
|
||
HarmonicReactivePower,
|
||
HarmonicApparentPower,
|
||
VoltageHarmonicRatio,
|
||
LineVoltageHarmonicRatio,
|
||
CurrentHarmonicRatio,
|
||
|
||
VoltageInterharmonic,
|
||
LineVoltageInterharmonic,
|
||
CurrentInterharmonic
|
||
};
|
||
|
||
struct StatDynamicMetricRange
|
||
{
|
||
StatDynamicMetricGroup group;
|
||
int base;
|
||
int phase_index;
|
||
const char* phase_label;
|
||
const char* prefix;
|
||
bool interharmonic;
|
||
};
|
||
|
||
const std::vector<StatDynamicMetricRange>& stat_dynamic_metric_ranges()
|
||
{
|
||
static const std::vector<StatDynamicMetricRange> ranges = {
|
||
{ StatDynamicMetricGroup::VoltageHarmonic, static_cast<int>(StatMetricId::VoltageHarmonicUaBase), 0, "a", "U", false },
|
||
{ StatDynamicMetricGroup::VoltageHarmonic, static_cast<int>(StatMetricId::VoltageHarmonicUbBase), 1, "b", "U", false },
|
||
{ StatDynamicMetricGroup::VoltageHarmonic, static_cast<int>(StatMetricId::VoltageHarmonicUcBase), 2, "c", "U", false },
|
||
{ StatDynamicMetricGroup::LineVoltageHarmonic, static_cast<int>(StatMetricId::LineVoltageHarmonicUabBase), 0, "ab", "U", false },
|
||
{ StatDynamicMetricGroup::LineVoltageHarmonic, static_cast<int>(StatMetricId::LineVoltageHarmonicUbcBase), 1, "bc", "U", false },
|
||
{ StatDynamicMetricGroup::LineVoltageHarmonic, static_cast<int>(StatMetricId::LineVoltageHarmonicUcaBase), 2, "ca", "U", false },
|
||
{ StatDynamicMetricGroup::CurrentHarmonic, static_cast<int>(StatMetricId::CurrentHarmonicIaBase), 0, "a", "I", false },
|
||
{ StatDynamicMetricGroup::CurrentHarmonic, static_cast<int>(StatMetricId::CurrentHarmonicIbBase), 1, "b", "I", false },
|
||
{ StatDynamicMetricGroup::CurrentHarmonic, static_cast<int>(StatMetricId::CurrentHarmonicIcBase), 2, "c", "I", false },
|
||
|
||
{ StatDynamicMetricGroup::VoltageHarmonicAngle, static_cast<int>(StatMetricId::VoltageHarmonicAngleUaBase), 0, "a", "U", false },
|
||
{ StatDynamicMetricGroup::VoltageHarmonicAngle, static_cast<int>(StatMetricId::VoltageHarmonicAngleUbBase), 1, "b", "U", false },
|
||
{ StatDynamicMetricGroup::VoltageHarmonicAngle, static_cast<int>(StatMetricId::VoltageHarmonicAngleUcBase), 2, "c", "U", false },
|
||
{ StatDynamicMetricGroup::LineVoltageHarmonicAngle, static_cast<int>(StatMetricId::LineVoltageHarmonicAngleUabBase), 0, "ab", "U", false },
|
||
{ StatDynamicMetricGroup::LineVoltageHarmonicAngle, static_cast<int>(StatMetricId::LineVoltageHarmonicAngleUbcBase), 1, "bc", "U", false },
|
||
{ StatDynamicMetricGroup::LineVoltageHarmonicAngle, static_cast<int>(StatMetricId::LineVoltageHarmonicAngleUcaBase), 2, "ca", "U", false },
|
||
{ StatDynamicMetricGroup::CurrentHarmonicAngle, static_cast<int>(StatMetricId::CurrentHarmonicAngleIaBase), 0, "a", "I", false },
|
||
{ StatDynamicMetricGroup::CurrentHarmonicAngle, static_cast<int>(StatMetricId::CurrentHarmonicAngleIbBase), 1, "b", "I", false },
|
||
{ StatDynamicMetricGroup::CurrentHarmonicAngle, static_cast<int>(StatMetricId::CurrentHarmonicAngleIcBase), 2, "c", "I", false },
|
||
|
||
{ StatDynamicMetricGroup::HarmonicActivePower, static_cast<int>(StatMetricId::HarmonicActivePowerPaBase), 0, "a", "P", false },
|
||
{ StatDynamicMetricGroup::HarmonicActivePower, static_cast<int>(StatMetricId::HarmonicActivePowerPbBase), 1, "b", "P", false },
|
||
{ StatDynamicMetricGroup::HarmonicActivePower, static_cast<int>(StatMetricId::HarmonicActivePowerPcBase), 2, "c", "P", false },
|
||
{ StatDynamicMetricGroup::HarmonicActivePower, static_cast<int>(StatMetricId::HarmonicActivePowerTotalBase), 3, "total", "P", false },
|
||
{ StatDynamicMetricGroup::HarmonicReactivePower, static_cast<int>(StatMetricId::HarmonicReactivePowerQaBase), 0, "a", "Q", false },
|
||
{ StatDynamicMetricGroup::HarmonicReactivePower, static_cast<int>(StatMetricId::HarmonicReactivePowerQbBase), 1, "b", "Q", false },
|
||
{ StatDynamicMetricGroup::HarmonicReactivePower, static_cast<int>(StatMetricId::HarmonicReactivePowerQcBase), 2, "c", "Q", false },
|
||
{ StatDynamicMetricGroup::HarmonicReactivePower, static_cast<int>(StatMetricId::HarmonicReactivePowerTotalBase), 3, "total", "Q", false },
|
||
{ StatDynamicMetricGroup::HarmonicApparentPower, static_cast<int>(StatMetricId::HarmonicApparentPowerSaBase), 0, "a", "S", false },
|
||
{ StatDynamicMetricGroup::HarmonicApparentPower, static_cast<int>(StatMetricId::HarmonicApparentPowerSbBase), 1, "b", "S", false },
|
||
{ StatDynamicMetricGroup::HarmonicApparentPower, static_cast<int>(StatMetricId::HarmonicApparentPowerScBase), 2, "c", "S", false },
|
||
{ StatDynamicMetricGroup::HarmonicApparentPower, static_cast<int>(StatMetricId::HarmonicApparentPowerTotalBase), 3, "total", "S", false },
|
||
|
||
{ StatDynamicMetricGroup::VoltageHarmonicRatio, static_cast<int>(StatMetricId::VoltageHarmonicRatioUaBase), 0, "a", "U", false },
|
||
{ StatDynamicMetricGroup::VoltageHarmonicRatio, static_cast<int>(StatMetricId::VoltageHarmonicRatioUbBase), 1, "b", "U", false },
|
||
{ StatDynamicMetricGroup::VoltageHarmonicRatio, static_cast<int>(StatMetricId::VoltageHarmonicRatioUcBase), 2, "c", "U", false },
|
||
{ StatDynamicMetricGroup::CurrentHarmonicRatio, static_cast<int>(StatMetricId::CurrentHarmonicRatioIaBase), 0, "a", "I", false },
|
||
{ StatDynamicMetricGroup::CurrentHarmonicRatio, static_cast<int>(StatMetricId::CurrentHarmonicRatioIbBase), 1, "b", "I", false },
|
||
{ StatDynamicMetricGroup::CurrentHarmonicRatio, static_cast<int>(StatMetricId::CurrentHarmonicRatioIcBase), 2, "c", "I", false },
|
||
{ StatDynamicMetricGroup::LineVoltageHarmonicRatio, static_cast<int>(StatMetricId::LineVoltageHarmonicRatioUabBase), 0, "ab", "U", false },
|
||
{ StatDynamicMetricGroup::LineVoltageHarmonicRatio, static_cast<int>(StatMetricId::LineVoltageHarmonicRatioUbcBase), 1, "bc", "U", false },
|
||
{ StatDynamicMetricGroup::LineVoltageHarmonicRatio, static_cast<int>(StatMetricId::LineVoltageHarmonicRatioUcaBase), 2, "ca", "U", false },
|
||
|
||
{ StatDynamicMetricGroup::VoltageInterharmonic, static_cast<int>(StatMetricId::VoltageInterharmonicUaBase), 0, "a", "U", true },
|
||
{ StatDynamicMetricGroup::VoltageInterharmonic, static_cast<int>(StatMetricId::VoltageInterharmonicUbBase), 1, "b", "U", true },
|
||
{ StatDynamicMetricGroup::VoltageInterharmonic, static_cast<int>(StatMetricId::VoltageInterharmonicUcBase), 2, "c", "U", true },
|
||
{ StatDynamicMetricGroup::LineVoltageInterharmonic, static_cast<int>(StatMetricId::LineVoltageInterharmonicUabBase), 0, "ab", "U", true },
|
||
{ StatDynamicMetricGroup::LineVoltageInterharmonic, static_cast<int>(StatMetricId::LineVoltageInterharmonicUbcBase), 1, "bc", "U", true },
|
||
{ StatDynamicMetricGroup::LineVoltageInterharmonic, static_cast<int>(StatMetricId::LineVoltageInterharmonicUcaBase), 2, "ca", "U", true },
|
||
{ StatDynamicMetricGroup::CurrentInterharmonic, static_cast<int>(StatMetricId::CurrentInterharmonicIaBase), 0, "a", "I", true },
|
||
{ StatDynamicMetricGroup::CurrentInterharmonic, static_cast<int>(StatMetricId::CurrentInterharmonicIbBase), 1, "b", "I", true },
|
||
{ StatDynamicMetricGroup::CurrentInterharmonic, static_cast<int>(StatMetricId::CurrentInterharmonicIcBase), 2, "c", "I", true }
|
||
};
|
||
return ranges;
|
||
}
|
||
|
||
int stat_dynamic_metric_min_offset(bool interharmonic)
|
||
{
|
||
return interharmonic ? 0 : 2;
|
||
}
|
||
|
||
int stat_dynamic_metric_max_offset(bool interharmonic)
|
||
{
|
||
return interharmonic ? 49 : 50;
|
||
}
|
||
|
||
const StatDynamicMetricRange* stat_find_dynamic_metric_range(StatMetricId id)
|
||
{
|
||
const int v = static_cast<int>(id);
|
||
for (const auto& r : stat_dynamic_metric_ranges())
|
||
{
|
||
const int min_offset = stat_dynamic_metric_min_offset(r.interharmonic);
|
||
const int max_offset = stat_dynamic_metric_max_offset(r.interharmonic);
|
||
if (v >= r.base + min_offset && v <= r.base + max_offset)
|
||
return &r;
|
||
}
|
||
return nullptr;
|
||
}
|
||
|
||
bool stat_is_dynamic_metric(StatMetricId id)
|
||
{
|
||
return stat_find_dynamic_metric_range(id) != nullptr;
|
||
}
|
||
|
||
bool stat_is_dynamic_metric_group(StatMetricId id, StatDynamicMetricGroup group)
|
||
{
|
||
const StatDynamicMetricRange* r = stat_find_dynamic_metric_range(id);
|
||
return r != nullptr && r->group == group;
|
||
}
|
||
|
||
int stat_dynamic_metric_order_or_slot(StatMetricId id)
|
||
{
|
||
const StatDynamicMetricRange* r = stat_find_dynamic_metric_range(id);
|
||
if (r == nullptr)
|
||
return -1;
|
||
return static_cast<int>(id) - r->base;
|
||
}
|
||
|
||
double stat_dynamic_metric_order_value(StatMetricId id)
|
||
{
|
||
const StatDynamicMetricRange* r = stat_find_dynamic_metric_range(id);
|
||
if (r == nullptr)
|
||
return -1.0;
|
||
const int offset = stat_dynamic_metric_order_or_slot(id);
|
||
return r->interharmonic ? (static_cast<double>(offset) + 0.5) : static_cast<double>(offset);
|
||
}
|
||
|
||
StatMetricId stat_dynamic_metric_id(StatDynamicMetricGroup group, int phase_index, int offset)
|
||
{
|
||
for (const auto& r : stat_dynamic_metric_ranges())
|
||
{
|
||
if (r.group != group || r.phase_index != phase_index)
|
||
continue;
|
||
const int min_offset = stat_dynamic_metric_min_offset(r.interharmonic);
|
||
const int max_offset = stat_dynamic_metric_max_offset(r.interharmonic);
|
||
if (offset < min_offset || offset > max_offset)
|
||
return StatMetricId::Unknown;
|
||
return static_cast<StatMetricId>(r.base + offset);
|
||
}
|
||
return StatMetricId::Unknown;
|
||
}
|
||
|
||
std::string stat_format_interharmonic_slot(int slot)
|
||
{
|
||
char buf[16] = { 0 };
|
||
std::snprintf(buf, sizeof(buf), "%02dp5", slot);
|
||
return std::string(buf);
|
||
}
|
||
|
||
std::string stat_dynamic_metric_name(StatMetricId id)
|
||
{
|
||
const StatDynamicMetricRange* r = stat_find_dynamic_metric_range(id);
|
||
if (r == nullptr)
|
||
return "Unknown";
|
||
|
||
const int offset = stat_dynamic_metric_order_or_slot(id);
|
||
std::string phase(r->phase_label);
|
||
if (!phase.empty())
|
||
phase[0] = static_cast<char>(std::toupper(static_cast<unsigned char>(phase[0])));
|
||
|
||
std::ostringstream os;
|
||
os << r->prefix << phase;
|
||
switch (r->group)
|
||
{
|
||
case StatDynamicMetricGroup::VoltageHarmonic:
|
||
case StatDynamicMetricGroup::LineVoltageHarmonic:
|
||
case StatDynamicMetricGroup::CurrentHarmonic:
|
||
os << "Harm" << stat_two_digit(offset);
|
||
break;
|
||
case StatDynamicMetricGroup::VoltageHarmonicAngle:
|
||
case StatDynamicMetricGroup::LineVoltageHarmonicAngle:
|
||
case StatDynamicMetricGroup::CurrentHarmonicAngle:
|
||
os << "HarmAng" << stat_two_digit(offset);
|
||
break;
|
||
case StatDynamicMetricGroup::HarmonicActivePower:
|
||
case StatDynamicMetricGroup::HarmonicReactivePower:
|
||
case StatDynamicMetricGroup::HarmonicApparentPower:
|
||
os << "Harm" << stat_two_digit(offset);
|
||
break;
|
||
case StatDynamicMetricGroup::VoltageHarmonicRatio:
|
||
case StatDynamicMetricGroup::LineVoltageHarmonicRatio:
|
||
case StatDynamicMetricGroup::CurrentHarmonicRatio:
|
||
os << "HarmRatio" << stat_two_digit(offset);
|
||
break;
|
||
case StatDynamicMetricGroup::VoltageInterharmonic:
|
||
case StatDynamicMetricGroup::LineVoltageInterharmonic:
|
||
case StatDynamicMetricGroup::CurrentInterharmonic:
|
||
os << "InterHarm" << stat_format_interharmonic_slot(offset);
|
||
break;
|
||
default:
|
||
os << "Dynamic" << offset;
|
||
break;
|
||
}
|
||
return os.str();
|
||
}
|
||
|
||
std::vector<StatMetricId> stat_dynamic_metric_order_for_group(StatDynamicMetricGroup group)
|
||
{
|
||
std::vector<StatMetricId> out;
|
||
for (const auto& r : stat_dynamic_metric_ranges())
|
||
{
|
||
if (r.group != group)
|
||
continue;
|
||
const int min_offset = stat_dynamic_metric_min_offset(r.interharmonic);
|
||
const int max_offset = stat_dynamic_metric_max_offset(r.interharmonic);
|
||
for (int offset = min_offset; offset <= max_offset; ++offset)
|
||
out.push_back(static_cast<StatMetricId>(r.base + offset));
|
||
}
|
||
return out;
|
||
}
|
||
|
||
std::vector<StatMetricId> stat_all_dynamic_metric_order()
|
||
{
|
||
std::vector<StatMetricId> out;
|
||
const std::vector<StatDynamicMetricGroup> groups = {
|
||
StatDynamicMetricGroup::VoltageHarmonic,
|
||
StatDynamicMetricGroup::LineVoltageHarmonic,
|
||
StatDynamicMetricGroup::CurrentHarmonic,
|
||
StatDynamicMetricGroup::VoltageHarmonicAngle,
|
||
StatDynamicMetricGroup::LineVoltageHarmonicAngle,
|
||
StatDynamicMetricGroup::CurrentHarmonicAngle,
|
||
StatDynamicMetricGroup::HarmonicActivePower,
|
||
StatDynamicMetricGroup::HarmonicReactivePower,
|
||
StatDynamicMetricGroup::HarmonicApparentPower,
|
||
StatDynamicMetricGroup::VoltageHarmonicRatio,
|
||
StatDynamicMetricGroup::LineVoltageHarmonicRatio,
|
||
StatDynamicMetricGroup::CurrentHarmonicRatio,
|
||
StatDynamicMetricGroup::VoltageInterharmonic,
|
||
StatDynamicMetricGroup::LineVoltageInterharmonic,
|
||
StatDynamicMetricGroup::CurrentInterharmonic
|
||
};
|
||
for (auto g : groups)
|
||
{
|
||
auto part = stat_dynamic_metric_order_for_group(g);
|
||
out.insert(out.end(), part.begin(), part.end());
|
||
}
|
||
return out;
|
||
}
|
||
|
||
/// @brief 兼容旧调用:判断三相电压 2-50 次谐波 RMS。
|
||
bool stat_is_voltage_harmonic_metric(StatMetricId id)
|
||
{
|
||
return stat_is_dynamic_metric_group(id, StatDynamicMetricGroup::VoltageHarmonic);
|
||
}
|
||
|
||
int stat_voltage_harmonic_order(StatMetricId id)
|
||
{
|
||
if (!stat_is_voltage_harmonic_metric(id))
|
||
return -1;
|
||
return stat_dynamic_metric_order_or_slot(id);
|
||
}
|
||
|
||
int stat_voltage_harmonic_phase_index(StatMetricId id)
|
||
{
|
||
const StatDynamicMetricRange* r = stat_find_dynamic_metric_range(id);
|
||
if (r == nullptr || r->group != StatDynamicMetricGroup::VoltageHarmonic)
|
||
return -1;
|
||
return r->phase_index;
|
||
}
|
||
|
||
StatMetricId stat_voltage_harmonic_metric_id(int phase_index, int order)
|
||
{
|
||
return stat_dynamic_metric_id(StatDynamicMetricGroup::VoltageHarmonic, phase_index, order);
|
||
}
|
||
|
||
std::string stat_voltage_harmonic_metric_name(StatMetricId id)
|
||
{
|
||
return stat_dynamic_metric_name(id);
|
||
}
|
||
|
||
/// @brief 将业务指标 ID 转为可读字符串。
|
||
/// @param id 业务指标枚举。
|
||
/// @return 指标名称字符串,便于调试日志输出。
|
||
std::string stat_metric_name(StatMetricId id)
|
||
{
|
||
if (stat_is_dynamic_metric(id))
|
||
return stat_dynamic_metric_name(id);
|
||
|
||
switch (id)
|
||
{
|
||
case StatMetricId::UaRms: return "UaRms";
|
||
case StatMetricId::UbRms: return "UbRms";
|
||
case StatMetricId::UcRms: return "UcRms";
|
||
case StatMetricId::IaRms: return "IaRms";
|
||
case StatMetricId::IbRms: return "IbRms";
|
||
case StatMetricId::IcRms: return "IcRms";
|
||
case StatMetricId::UabRms: return "UabRms";
|
||
case StatMetricId::UbcRms: return "UbcRms";
|
||
case StatMetricId::UcaRms: return "UcaRms";
|
||
case StatMetricId::UaDeviation: return "UaDeviation";
|
||
case StatMetricId::UbDeviation: return "UbDeviation";
|
||
case StatMetricId::UcDeviation: return "UcDeviation";
|
||
case StatMetricId::UabDeviation: return "UabDeviation";
|
||
case StatMetricId::UbcDeviation: return "UbcDeviation";
|
||
case StatMetricId::UcaDeviation: return "UcaDeviation";
|
||
case StatMetricId::Frequency: return "Frequency";
|
||
case StatMetricId::FrequencyDeviation: return "FrequencyDeviation";
|
||
case StatMetricId::UZeroSeq: return "UZeroSeq";
|
||
case StatMetricId::UNegSeq: return "UNegSeq";
|
||
case StatMetricId::UPosSeq: return "UPosSeq";
|
||
case StatMetricId::UNegSeqUnbalance: return "UNegSeqUnbalance";
|
||
case StatMetricId::IZeroSeq: return "IZeroSeq";
|
||
case StatMetricId::INegSeq: return "INegSeq";
|
||
case StatMetricId::IPosSeq: return "IPosSeq";
|
||
case StatMetricId::INegSeqUnbalance: return "INegSeqUnbalance";
|
||
case StatMetricId::PaPower: return "PaPower";
|
||
case StatMetricId::PbPower: return "PbPower";
|
||
case StatMetricId::PcPower: return "PcPower";
|
||
case StatMetricId::PTotalPower: return "PTotalPower";
|
||
case StatMetricId::QaPower: return "QaPower";
|
||
case StatMetricId::QbPower: return "QbPower";
|
||
case StatMetricId::QcPower: return "QcPower";
|
||
case StatMetricId::QTotalPower: return "QTotalPower";
|
||
case StatMetricId::SaPower: return "SaPower";
|
||
case StatMetricId::SbPower: return "SbPower";
|
||
case StatMetricId::ScPower: return "ScPower";
|
||
case StatMetricId::STotalPower: return "STotalPower";
|
||
case StatMetricId::PFa: return "PFa";
|
||
case StatMetricId::PFb: return "PFb";
|
||
case StatMetricId::PFc: return "PFc";
|
||
case StatMetricId::PFTotal: return "PFTotal";
|
||
case StatMetricId::FundPFa: return "FundPFa";
|
||
case StatMetricId::FundPFb: return "FundPFb";
|
||
case StatMetricId::FundPFc: return "FundPFc";
|
||
case StatMetricId::FundPFTotal: return "FundPFTotal";
|
||
case StatMetricId::UaDvc: return "UaDvc";
|
||
case StatMetricId::UbDvc: return "UbDvc";
|
||
case StatMetricId::UcDvc: return "UcDvc";
|
||
case StatMetricId::UabDvc: return "UabDvc";
|
||
case StatMetricId::UbcDvc: return "UbcDvc";
|
||
case StatMetricId::UcaDvc: return "UcaDvc";
|
||
case StatMetricId::UaPst: return "UaPst";
|
||
case StatMetricId::UbPst: return "UbPst";
|
||
case StatMetricId::UcPst: return "UcPst";
|
||
case StatMetricId::UabPst: return "UabPst";
|
||
case StatMetricId::UbcPst: return "UbcPst";
|
||
case StatMetricId::UcaPst: return "UcaPst";
|
||
case StatMetricId::UaPlt: return "UaPlt";
|
||
case StatMetricId::UbPlt: return "UbPlt";
|
||
case StatMetricId::UcPlt: return "UcPlt";
|
||
case StatMetricId::UabPlt: return "UabPlt";
|
||
case StatMetricId::UbcPlt: return "UbcPlt";
|
||
case StatMetricId::UcaPlt: return "UcaPlt";
|
||
case StatMetricId::PaFundPower: return "PaFundPower";
|
||
case StatMetricId::PbFundPower: return "PbFundPower";
|
||
case StatMetricId::PcFundPower: return "PcFundPower";
|
||
case StatMetricId::PTotalFundPower: return "PTotalFundPower";
|
||
case StatMetricId::QaFundPower: return "QaFundPower";
|
||
case StatMetricId::QbFundPower: return "QbFundPower";
|
||
case StatMetricId::QcFundPower: return "QcFundPower";
|
||
case StatMetricId::QTotalFundPower: return "QTotalFundPower";
|
||
case StatMetricId::SaFundPower: return "SaFundPower";
|
||
case StatMetricId::SbFundPower: return "SbFundPower";
|
||
case StatMetricId::ScFundPower: return "ScFundPower";
|
||
case StatMetricId::STotalFundPower: return "STotalFundPower";
|
||
case StatMetricId::UaFundRms: return "UaFundRms";
|
||
case StatMetricId::UbFundRms: return "UbFundRms";
|
||
case StatMetricId::UcFundRms: return "UcFundRms";
|
||
case StatMetricId::UaFundAngle: return "UaFundAngle";
|
||
case StatMetricId::UbFundAngle: return "UbFundAngle";
|
||
case StatMetricId::UcFundAngle: return "UcFundAngle";
|
||
case StatMetricId::IaFundRms: return "IaFundRms";
|
||
case StatMetricId::IbFundRms: return "IbFundRms";
|
||
case StatMetricId::IcFundRms: return "IcFundRms";
|
||
case StatMetricId::IaFundAngle: return "IaFundAngle";
|
||
case StatMetricId::IbFundAngle: return "IbFundAngle";
|
||
case StatMetricId::IcFundAngle: return "IcFundAngle";
|
||
case StatMetricId::UabFundRms: return "UabFundRms";
|
||
case StatMetricId::UbcFundRms: return "UbcFundRms";
|
||
case StatMetricId::UcaFundRms: return "UcaFundRms";
|
||
case StatMetricId::UabFundAngle: return "UabFundAngle";
|
||
case StatMetricId::UbcFundAngle: return "UbcFundAngle";
|
||
case StatMetricId::UcaFundAngle: return "UcaFundAngle";
|
||
case StatMetricId::UaThd: return "UaThd";
|
||
case StatMetricId::UbThd: return "UbThd";
|
||
case StatMetricId::UcThd: return "UcThd";
|
||
case StatMetricId::IaThd: return "IaThd";
|
||
case StatMetricId::IbThd: return "IbThd";
|
||
case StatMetricId::IcThd: return "IcThd";
|
||
case StatMetricId::UabThd: return "UabThd";
|
||
case StatMetricId::UbcThd: return "UbcThd";
|
||
case StatMetricId::UcaThd: return "UcaThd";
|
||
default: return "Unknown";
|
||
}
|
||
}
|
||
|
||
/// @brief 将统计值类型转为可读字符串。
|
||
/// @param kind 统计值类型。
|
||
/// @return 统计值类型名称。
|
||
std::string stat_value_kind_name(StatValueKind kind)
|
||
{
|
||
switch (kind)
|
||
{
|
||
case StatValueKind::Min: return "Min";
|
||
case StatValueKind::Max: return "Max";
|
||
case StatValueKind::Avg: return "Avg";
|
||
case StatValueKind::P95: return "P95";
|
||
default: return "Unknown";
|
||
}
|
||
}
|
||
|
||
/// @brief 将接线方式枚举转为可读字符串。
|
||
/// @param kind 接线方式枚举。
|
||
/// @return 可读字符串。
|
||
std::string stat_connection_kind_name(ParsedConnectionKind kind)
|
||
{
|
||
switch (kind)
|
||
{
|
||
case ParsedConnectionKind::Wye: return "Wye";
|
||
case ParsedConnectionKind::Delta: return "Delta";
|
||
default: return "Unknown";
|
||
}
|
||
}
|
||
|
||
/// @brief 将指标质量状态转为可读字符串。
|
||
/// @param q 指标质量状态。
|
||
/// @return 可读质量状态名称。
|
||
std::string stat_metric_quality_name(StatMetricQuality q)
|
||
{
|
||
switch (q)
|
||
{
|
||
case StatMetricQuality::Normal: return "OK";
|
||
case StatMetricQuality::AllZero: return "ALL_ZERO";
|
||
case StatMetricQuality::DuplicateSource: return "DUPLICATE_SOURCE";
|
||
case StatMetricQuality::SuspiciousRange: return "SUSPICIOUS_RANGE";
|
||
case StatMetricQuality::Missing: return "MISSING";
|
||
default: return "UNKNOWN";
|
||
}
|
||
}
|
||
|
||
/// @brief 非谐波核心指标打印顺序。
|
||
/// @details 平时核心日志只打印这些指标,避免 2-50 次谐波全部展开导致日志过大。
|
||
const std::vector<StatMetricId>& stat_core_metric_print_order()
|
||
{
|
||
static const std::vector<StatMetricId> order = {
|
||
StatMetricId::UaRms,
|
||
StatMetricId::UbRms,
|
||
StatMetricId::UcRms,
|
||
StatMetricId::IaRms,
|
||
StatMetricId::IbRms,
|
||
StatMetricId::IcRms,
|
||
StatMetricId::UabRms,
|
||
StatMetricId::UbcRms,
|
||
StatMetricId::UcaRms,
|
||
StatMetricId::UaDeviation,
|
||
StatMetricId::UbDeviation,
|
||
StatMetricId::UcDeviation,
|
||
StatMetricId::Frequency,
|
||
StatMetricId::FrequencyDeviation,
|
||
StatMetricId::UZeroSeq,
|
||
StatMetricId::UNegSeq,
|
||
StatMetricId::UPosSeq,
|
||
StatMetricId::UNegSeqUnbalance,
|
||
StatMetricId::IZeroSeq,
|
||
StatMetricId::INegSeq,
|
||
StatMetricId::IPosSeq,
|
||
StatMetricId::INegSeqUnbalance,
|
||
|
||
StatMetricId::PaPower,
|
||
StatMetricId::PbPower,
|
||
StatMetricId::PcPower,
|
||
StatMetricId::PTotalPower,
|
||
StatMetricId::QaPower,
|
||
StatMetricId::QbPower,
|
||
StatMetricId::QcPower,
|
||
StatMetricId::QTotalPower,
|
||
StatMetricId::SaPower,
|
||
StatMetricId::SbPower,
|
||
StatMetricId::ScPower,
|
||
StatMetricId::STotalPower,
|
||
|
||
StatMetricId::PFa,
|
||
StatMetricId::PFb,
|
||
StatMetricId::PFc,
|
||
StatMetricId::PFTotal,
|
||
StatMetricId::FundPFa,
|
||
StatMetricId::FundPFb,
|
||
StatMetricId::FundPFc,
|
||
StatMetricId::FundPFTotal,
|
||
|
||
StatMetricId::UaDvc,
|
||
StatMetricId::UbDvc,
|
||
StatMetricId::UcDvc,
|
||
StatMetricId::UabDvc,
|
||
StatMetricId::UbcDvc,
|
||
StatMetricId::UcaDvc,
|
||
|
||
StatMetricId::UaPst,
|
||
StatMetricId::UbPst,
|
||
StatMetricId::UcPst,
|
||
StatMetricId::UabPst,
|
||
StatMetricId::UbcPst,
|
||
StatMetricId::UcaPst,
|
||
StatMetricId::UaPlt,
|
||
StatMetricId::UbPlt,
|
||
StatMetricId::UcPlt,
|
||
StatMetricId::UabPlt,
|
||
StatMetricId::UbcPlt,
|
||
StatMetricId::UcaPlt,
|
||
|
||
StatMetricId::PaFundPower,
|
||
StatMetricId::PbFundPower,
|
||
StatMetricId::PcFundPower,
|
||
StatMetricId::PTotalFundPower,
|
||
StatMetricId::QaFundPower,
|
||
StatMetricId::QbFundPower,
|
||
StatMetricId::QcFundPower,
|
||
StatMetricId::QTotalFundPower,
|
||
StatMetricId::SaFundPower,
|
||
StatMetricId::SbFundPower,
|
||
StatMetricId::ScFundPower,
|
||
StatMetricId::STotalFundPower,
|
||
|
||
StatMetricId::UaFundRms,
|
||
StatMetricId::UbFundRms,
|
||
StatMetricId::UcFundRms,
|
||
StatMetricId::UaFundAngle,
|
||
StatMetricId::UbFundAngle,
|
||
StatMetricId::UcFundAngle,
|
||
StatMetricId::IaFundRms,
|
||
StatMetricId::IbFundRms,
|
||
StatMetricId::IcFundRms,
|
||
StatMetricId::IaFundAngle,
|
||
StatMetricId::IbFundAngle,
|
||
StatMetricId::IcFundAngle,
|
||
StatMetricId::UabFundRms,
|
||
StatMetricId::UbcFundRms,
|
||
StatMetricId::UcaFundRms,
|
||
StatMetricId::UabFundAngle,
|
||
StatMetricId::UbcFundAngle,
|
||
StatMetricId::UcaFundAngle,
|
||
|
||
StatMetricId::UaThd,
|
||
StatMetricId::UbThd,
|
||
StatMetricId::UcThd,
|
||
StatMetricId::IaThd,
|
||
StatMetricId::IbThd,
|
||
StatMetricId::IcThd,
|
||
StatMetricId::UabThd,
|
||
StatMetricId::UbcThd,
|
||
StatMetricId::UcaThd
|
||
};
|
||
return order;
|
||
}
|
||
|
||
/// @brief 动态生成三相电压 2-50 次谐波指标顺序。
|
||
/// @details 兼容旧调用;新的谐波/间谐波族统一走 stat_dynamic_metric_order_for_group / stat_all_dynamic_metric_order。
|
||
std::vector<StatMetricId> stat_voltage_harmonic_metric_order()
|
||
{
|
||
return stat_dynamic_metric_order_for_group(StatDynamicMetricGroup::VoltageHarmonic);
|
||
}
|
||
|
||
/// @brief 当前全部主要指标打印顺序。
|
||
/// @details 核心 22 项 + 所有动态谐波/间谐波指标。动态指标不逐项写 enum,统一按族和区间生成。
|
||
const std::vector<StatMetricId>& stat_primary_metric_print_order()
|
||
{
|
||
static const std::vector<StatMetricId> order = []() {
|
||
std::vector<StatMetricId> out = stat_core_metric_print_order();
|
||
const std::vector<StatMetricId> dynamic_metrics = stat_all_dynamic_metric_order();
|
||
out.insert(out.end(), dynamic_metrics.begin(), dynamic_metrics.end());
|
||
return out;
|
||
}();
|
||
return order;
|
||
}
|
||
|
||
/// @brief 已声明但当前 14 项之外的扩展指标打印顺序。
|
||
/// @details 这些指标不会计入“14 项”,但如果后续规则命中,也会在日志末尾显示,便于扩展核查。
|
||
const std::vector<StatMetricId>& stat_extra_metric_print_order()
|
||
{
|
||
static const std::vector<StatMetricId> order = {
|
||
StatMetricId::UabDeviation,
|
||
StatMetricId::UbcDeviation,
|
||
StatMetricId::UcaDeviation
|
||
};
|
||
return order;
|
||
}
|
||
|
||
bool stat_is_line_voltage_metric(StatMetricId id)
|
||
{
|
||
return id == StatMetricId::UabRms ||
|
||
id == StatMetricId::UbcRms ||
|
||
id == StatMetricId::UcaRms;
|
||
}
|
||
|
||
bool stat_is_phase_voltage_metric(StatMetricId id)
|
||
{
|
||
return id == StatMetricId::UaRms ||
|
||
id == StatMetricId::UbRms ||
|
||
id == StatMetricId::UcRms;
|
||
}
|
||
|
||
bool stat_is_current_metric(StatMetricId id)
|
||
{
|
||
return id == StatMetricId::IaRms ||
|
||
id == StatMetricId::IbRms ||
|
||
id == StatMetricId::IcRms;
|
||
}
|
||
|
||
bool stat_is_frequency_metric(StatMetricId id)
|
||
{
|
||
return id == StatMetricId::Frequency || id == StatMetricId::FrequencyDeviation;
|
||
}
|
||
|
||
bool stat_is_voltage_sequence_component_metric(StatMetricId id)
|
||
{
|
||
return id == StatMetricId::UZeroSeq ||
|
||
id == StatMetricId::UNegSeq ||
|
||
id == StatMetricId::UPosSeq;
|
||
}
|
||
|
||
bool stat_is_voltage_sequence_unbalance_metric(StatMetricId id)
|
||
{
|
||
return id == StatMetricId::UNegSeqUnbalance;
|
||
}
|
||
|
||
bool stat_is_current_sequence_component_metric(StatMetricId id)
|
||
{
|
||
return id == StatMetricId::IZeroSeq ||
|
||
id == StatMetricId::INegSeq ||
|
||
id == StatMetricId::IPosSeq;
|
||
}
|
||
|
||
bool stat_is_current_sequence_unbalance_metric(StatMetricId id)
|
||
{
|
||
return id == StatMetricId::INegSeqUnbalance;
|
||
}
|
||
|
||
bool stat_allow_all_zero_without_score_penalty(StatMetricId id)
|
||
{
|
||
return stat_is_current_metric(id) ||
|
||
stat_is_dynamic_metric(id) ||
|
||
id == StatMetricId::UZeroSeq ||
|
||
id == StatMetricId::UNegSeq ||
|
||
id == StatMetricId::UNegSeqUnbalance ||
|
||
stat_is_current_sequence_component_metric(id) ||
|
||
stat_is_current_sequence_unbalance_metric(id);
|
||
}
|
||
|
||
bool stat_has_value_kind(const AggregatedStatValues& agg, StatValueKind kind)
|
||
{
|
||
switch (kind)
|
||
{
|
||
case StatValueKind::Min: return agg.has_min;
|
||
case StatValueKind::Max: return agg.has_max;
|
||
case StatValueKind::Avg: return agg.has_avg;
|
||
case StatValueKind::P95: return agg.has_p95;
|
||
default: return false;
|
||
}
|
||
}
|
||
|
||
double stat_get_value_by_kind(const AggregatedStatValues& agg, StatValueKind kind)
|
||
{
|
||
switch (kind)
|
||
{
|
||
case StatValueKind::Min: return agg.min_value;
|
||
case StatValueKind::Max: return agg.max_value;
|
||
case StatValueKind::Avg: return agg.avg_value;
|
||
case StatValueKind::P95: return agg.p95_value;
|
||
default: return 0.0;
|
||
}
|
||
}
|
||
|
||
/// @brief 根据 observation 找到其关联的数据源定义。
|
||
/// @param lf 已解析完成的完整 PQDIF 逻辑对象。
|
||
/// @param obs 当前 observation。
|
||
/// @return 关联到的数据源定义指针;找不到时返回 nullptr。
|
||
const PqdifDataSourceRecord* stat_find_related_data_source(
|
||
const PqdifLogicalFile& lf,
|
||
const PqdifObservationRecord& obs)
|
||
{
|
||
if (obs.related_data_source_index >= 0 &&
|
||
static_cast<size_t>(obs.related_data_source_index) < lf.data_sources.size())
|
||
{
|
||
return &lf.data_sources[static_cast<size_t>(obs.related_data_source_index)];
|
||
}
|
||
|
||
if (!lf.data_sources.empty())
|
||
return &lf.data_sources.front();
|
||
|
||
return nullptr;
|
||
}
|
||
|
||
/// @brief 根据通道定义索引查找通道定义。
|
||
/// @param ds 数据源定义指针。
|
||
/// @param channel_def_index 通道定义索引。
|
||
/// @return 通道定义指针;找不到时返回 nullptr。
|
||
const PqdifChannelDefinition* stat_find_channel_definition(
|
||
const PqdifDataSourceRecord* ds,
|
||
int channel_def_index)
|
||
{
|
||
if (ds == nullptr || channel_def_index < 0)
|
||
return nullptr;
|
||
|
||
const size_t idx = static_cast<size_t>(channel_def_index);
|
||
if (idx >= ds->channel_definitions.size())
|
||
return nullptr;
|
||
|
||
return &ds->channel_definitions[idx];
|
||
}
|
||
|
||
/// @brief 根据序列定义索引查找序列定义。
|
||
/// @param ch_def 通道定义指针。
|
||
/// @param series_def_index 序列定义索引。
|
||
/// @return 序列定义指针;找不到时返回 nullptr。
|
||
const PqdifSeriesDefinition* stat_find_series_definition(
|
||
const PqdifChannelDefinition* ch_def,
|
||
int series_def_index)
|
||
{
|
||
if (ch_def == nullptr || series_def_index < 0)
|
||
return nullptr;
|
||
|
||
const size_t idx = static_cast<size_t>(series_def_index);
|
||
if (idx >= ch_def->series_definitions.size())
|
||
return nullptr;
|
||
|
||
return &ch_def->series_definitions[idx];
|
||
}
|
||
|
||
/// @brief 根据 monitor settings 与相别分布识别接线方式。
|
||
/// @details
|
||
/// 优先使用正式配置字段 physical_connection;若不可用,则回退到相别启发式判断。
|
||
/// @param lf 已解析完成的完整 PQDIF 逻辑对象。
|
||
/// @return 识别出的接线方式。
|
||
ParsedConnectionKind stat_classify_connection_kind(const PqdifLogicalFile& lf)
|
||
{
|
||
if (!lf.monitor_settings.empty())
|
||
{
|
||
const unsigned int pc = lf.monitor_settings.front().physical_connection;
|
||
|
||
switch (pc)
|
||
{
|
||
case ID_2_5ELEMENT_WYE:
|
||
case ID_3ELMENT_WYE:
|
||
return ParsedConnectionKind::Wye;
|
||
|
||
case ID_2ELEMENT_DELTA:
|
||
case ID_3ELEMENT_DELTA:
|
||
return ParsedConnectionKind::Delta;
|
||
|
||
default:
|
||
break;
|
||
}
|
||
}
|
||
|
||
bool has_phase_to_neutral = false;
|
||
bool has_line_to_line = false;
|
||
for (const auto& ds : lf.data_sources)
|
||
{
|
||
for (const auto& ch : ds.channel_definitions)
|
||
{
|
||
if (ch.phase_id == ID_PHASE_AN || ch.phase_id == ID_PHASE_BN || ch.phase_id == ID_PHASE_CN)
|
||
has_phase_to_neutral = true;
|
||
if (ch.phase_id == ID_PHASE_AB || ch.phase_id == ID_PHASE_BC || ch.phase_id == ID_PHASE_CA)
|
||
has_line_to_line = true;
|
||
}
|
||
}
|
||
|
||
if (has_phase_to_neutral)
|
||
return ParsedConnectionKind::Wye;
|
||
if (has_line_to_line)
|
||
return ParsedConnectionKind::Delta;
|
||
return ParsedConnectionKind::Unknown;
|
||
}
|
||
|
||
/// @brief 判断 observation 是否为当前阶段需要处理的“统计类 observation”。
|
||
/// @details
|
||
/// 当前阶段不处理全部 observation,而是先筛出统计类 observation。判断顺序:
|
||
/// 1) 触发方式为 ID_TRIGGER_METH_PERIODIC_STATS;
|
||
/// 2) 名称中包含 TREND / STAT / STATISTIC;
|
||
/// 3) 只要 observation 下有 VALUELOG 型通道,也认为它是统计 observation。
|
||
/// @param obs 当前 observation。
|
||
/// @param lf 完整 PQDIF 逻辑对象。
|
||
/// @return 若是统计类 observation,则返回 true。
|
||
bool stat_is_statistical_observation(
|
||
const PqdifLogicalFile& lf,
|
||
const PqdifObservationRecord& obs)
|
||
{
|
||
if (obs.trigger_method_id == ID_TRIGGER_METH_PERIODIC_STATS)
|
||
return true;
|
||
|
||
auto compact = [&](const std::string& s) {
|
||
std::string out;
|
||
for (unsigned char ch : s)
|
||
{
|
||
if (std::isalnum(ch))
|
||
out.push_back(static_cast<char>(std::toupper(ch)));
|
||
}
|
||
return out;
|
||
};
|
||
|
||
const std::string obs_name = compact(obs.observation_name);
|
||
if (obs_name.find("TREND") != std::string::npos ||
|
||
obs_name.find("STAT") != std::string::npos ||
|
||
obs_name.find("STATISTIC") != std::string::npos)
|
||
{
|
||
return true;
|
||
}
|
||
|
||
for (const auto& ch : obs.channel_instances)
|
||
{
|
||
if (pqdif_sem::IsQuantityTypeValueLog(ch.quantity_type_id.value))
|
||
return true;
|
||
}
|
||
|
||
(void)lf;
|
||
return false;
|
||
}
|
||
|
||
/// @brief 选择当前阶段要处理的“主统计 observation”。
|
||
/// @details
|
||
/// 当前阶段为了避免不同 observation 在同一 timestamp 上相互覆盖,
|
||
/// 只选择第一条命中的统计类 observation 参与识别与聚合。
|
||
/// @param lf 完整 PQDIF 逻辑对象。
|
||
/// @return 命中的 observation 指针;若没有则返回 nullptr。
|
||
const PqdifObservationRecord* stat_select_primary_statistical_observation(const PqdifLogicalFile& lf)
|
||
{
|
||
for (const auto& obs : lf.observations)
|
||
{
|
||
if (stat_is_statistical_observation(lf, obs))
|
||
return &obs;
|
||
}
|
||
return nullptr;
|
||
}
|
||
|
||
/// @brief 解析共享序列。
|
||
/// @details
|
||
/// 某些序列自身没有 values,而是通过 share_channel_index/share_series_index
|
||
/// 共享另一个序列的数组值,例如 B/C 相共享 A 相时间轴。
|
||
/// @param obs 当前 observation。
|
||
/// @param si 当前序列实例。
|
||
/// @return 最终应使用的“真实值序列实例”指针。
|
||
const PqdifSeriesInstance* stat_resolve_shared_series(
|
||
const PqdifObservationRecord& obs,
|
||
const PqdifSeriesInstance& si)
|
||
{
|
||
const PqdifSeriesInstance* current = &si;
|
||
|
||
for (int depth = 0; depth < 8; ++depth)
|
||
{
|
||
if (current->values.count > 0)
|
||
return current;
|
||
|
||
if (current->share_channel_index < 0 || current->share_series_index < 0)
|
||
return current;
|
||
|
||
const size_t ch_idx = static_cast<size_t>(current->share_channel_index);
|
||
const size_t ser_idx = static_cast<size_t>(current->share_series_index);
|
||
|
||
if (ch_idx >= obs.channel_instances.size())
|
||
return current;
|
||
|
||
const auto& shared_ch = obs.channel_instances[ch_idx];
|
||
if (ser_idx >= shared_ch.series_instances.size())
|
||
return current;
|
||
|
||
const PqdifSeriesInstance* next = &shared_ch.series_instances[ser_idx];
|
||
if (next == current)
|
||
return current;
|
||
|
||
current = next;
|
||
}
|
||
|
||
return current;
|
||
}
|
||
|
||
/// @brief 从数组容器中读取指定下标的原始数值。
|
||
/// @details 支持 real/int/uint/bool。
|
||
/// @param arr 数组容器。
|
||
/// @param idx 样本下标。
|
||
/// @param out_raw 返回原始数值。
|
||
/// @return 成功读取返回 true,否则返回 false。
|
||
bool stat_try_get_raw_numeric_at(const PqdifValueArray& arr, size_t idx, double& out_raw)
|
||
{
|
||
if (idx < arr.real_values.size())
|
||
{
|
||
out_raw = arr.real_values[idx];
|
||
return true;
|
||
}
|
||
|
||
if (idx < arr.int_values.size())
|
||
{
|
||
out_raw = static_cast<double>(arr.int_values[idx]);
|
||
return true;
|
||
}
|
||
|
||
if (idx < arr.uint_values.size())
|
||
{
|
||
out_raw = static_cast<double>(arr.uint_values[idx]);
|
||
return true;
|
||
}
|
||
|
||
if (idx < arr.bool_values.size())
|
||
{
|
||
out_raw = arr.bool_values[idx] ? 1.0 : 0.0;
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/// @brief 将原始值还原为工程值。
|
||
/// @details
|
||
/// 当序列存储方式包含 SCALED 且底层值是整型时,使用 raw*scale+offset;
|
||
/// 其他情况默认认为原始值即为工程值。
|
||
/// @param raw_value 原始数值。
|
||
/// @param si 序列实例,用于读取 scale/offset。
|
||
/// @param storage_method_id 序列定义层的存储方式。
|
||
/// @param physical_type 当前数组的物理类型。
|
||
/// @return 工程值。
|
||
double stat_decode_engineering_value(
|
||
double raw_value,
|
||
const PqdifSeriesInstance& si,
|
||
unsigned int storage_method_id,
|
||
int physical_type)
|
||
{
|
||
const bool integer_like =
|
||
physical_type == ID_PHYS_TYPE_INTEGER1 ||
|
||
physical_type == ID_PHYS_TYPE_INTEGER2 ||
|
||
physical_type == ID_PHYS_TYPE_INTEGER4 ||
|
||
physical_type == ID_PHYS_TYPE_UNS_INTEGER1 ||
|
||
physical_type == ID_PHYS_TYPE_UNS_INTEGER2 ||
|
||
physical_type == ID_PHYS_TYPE_UNS_INTEGER4;
|
||
|
||
if ((storage_method_id & ID_SERIES_METHOD_SCALED) && integer_like)
|
||
return raw_value * si.scale + si.offset;
|
||
|
||
return raw_value;
|
||
}
|
||
|
||
/// @brief 解析某一个样本点对应的绝对时刻。
|
||
/// @details
|
||
/// 优先使用 timestamp_values;若时间序列是数值型,则按
|
||
/// observation.time_start + 偏移秒 计算。
|
||
/// @param obs 当前 observation。
|
||
/// @param time_series 已解析出的时间序列实例。
|
||
/// @param idx 样本下标。
|
||
/// @param out_ts 返回绝对时刻。
|
||
/// @return 成功返回 true,否则返回 false。
|
||
bool stat_resolve_timestamp_at(
|
||
const PqdifObservationRecord& obs,
|
||
const PqdifSeriesInstance& time_series,
|
||
size_t idx,
|
||
time_t& out_ts)
|
||
{
|
||
if (idx < time_series.values.timestamp_values.size())
|
||
{
|
||
out_ts = time_series.values.timestamp_values[idx].unix_time;
|
||
return true;
|
||
}
|
||
|
||
double raw_time = 0.0;
|
||
if (stat_try_get_raw_numeric_at(time_series.values, idx, raw_time))
|
||
{
|
||
if (obs.time_start.unix_time != 0)
|
||
{
|
||
out_ts = obs.time_start.unix_time + static_cast<time_t>(std::llround(raw_time));
|
||
return true;
|
||
}
|
||
|
||
out_ts = static_cast<time_t>(std::llround(raw_time));
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/// @brief 判断 GUID 是否相等。
|
||
/// @param a GUID A。
|
||
/// @param b GUID B。
|
||
/// @return 相等返回 true。
|
||
bool stat_guid_equals(const GUID& a, const GUID& b)
|
||
{
|
||
return PQDIF_IsEqualGUID(a, b);
|
||
}
|
||
|
||
/// @brief 判断是否为旧式 P95 值类型。
|
||
/// @param value_type_id 值类型 GUID。
|
||
/// @return 若是旧式 P95 值类型则返回 true。
|
||
bool stat_is_p95_value_type(const GUID& value_type_id)
|
||
{
|
||
return stat_guid_equals(value_type_id, ID_SERIES_VALUE_TYPE_P95);
|
||
}
|
||
|
||
/// @brief 识别序列属于哪一种统计值类型。
|
||
/// @details 支持 MIN / MAX / AVG / P95。
|
||
/// @param si 序列实例。
|
||
/// @param sd 关联的序列定义,可为空。
|
||
/// @return 识别出的统计值类型。
|
||
StatValueKind stat_identify_value_kind(
|
||
const PqdifSeriesInstance& si,
|
||
const PqdifSeriesDefinition* sd)
|
||
{
|
||
if (pqdif_sem::IsValueTypeMin(si.value_type_id.value) ||
|
||
stat_guid_equals(si.value_type_id.value, ID_SERIES_VALUE_TYPE_PHASEANGLE_MIN))
|
||
return StatValueKind::Min;
|
||
if (pqdif_sem::IsValueTypeMax(si.value_type_id.value) ||
|
||
stat_guid_equals(si.value_type_id.value, ID_SERIES_VALUE_TYPE_PHASEANGLE_MAX))
|
||
return StatValueKind::Max;
|
||
if (pqdif_sem::IsValueTypeAvg(si.value_type_id.value) ||
|
||
stat_guid_equals(si.value_type_id.value, ID_SERIES_VALUE_TYPE_PHASEANGLE_AVG))
|
||
return StatValueKind::Avg;
|
||
if (stat_is_p95_value_type(si.value_type_id.value))
|
||
return StatValueKind::P95;
|
||
if (sd != nullptr && std::fabs(sd->prob_percentile - 95.0) < 1e-6)
|
||
return StatValueKind::P95;
|
||
return StatValueKind::Unknown;
|
||
}
|
||
|
||
/// @brief 判断是否为可以用“单值序列”兜底填充 Min/Max/Avg/P95 的指标。
|
||
/// @details 部分 PQDIF 厂家会把 Pst/Plt/DVC 这类已经是统计窗口结果的量,
|
||
/// 存成 Value/Instantaneous 单序列,而不是四条 Min/Max/Avg/P95 序列。
|
||
bool stat_metric_can_use_scalar_series_as_all_kinds(StatMetricId id)
|
||
{
|
||
switch (id)
|
||
{
|
||
case StatMetricId::UaDvc:
|
||
case StatMetricId::UbDvc:
|
||
case StatMetricId::UcDvc:
|
||
case StatMetricId::UabDvc:
|
||
case StatMetricId::UbcDvc:
|
||
case StatMetricId::UcaDvc:
|
||
case StatMetricId::UaPst:
|
||
case StatMetricId::UbPst:
|
||
case StatMetricId::UcPst:
|
||
case StatMetricId::UabPst:
|
||
case StatMetricId::UbcPst:
|
||
case StatMetricId::UcaPst:
|
||
case StatMetricId::UaPlt:
|
||
case StatMetricId::UbPlt:
|
||
case StatMetricId::UcPlt:
|
||
case StatMetricId::UabPlt:
|
||
case StatMetricId::UbcPlt:
|
||
case StatMetricId::UcaPlt:
|
||
return true;
|
||
default:
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// @brief 判断未知值类型序列是否可作为 Pst/Plt/DVC 的单值数据序列。
|
||
/// @details 排除时间轴、状态、计数、区间等非物理量序列,避免把质量标志误写成指标值。
|
||
bool stat_series_can_be_scalar_stat_value(
|
||
const PqdifSeriesInstance& si,
|
||
const PqdifSeriesDefinition* sd)
|
||
{
|
||
const GUID& vt = si.value_type_id.value;
|
||
if (pqdif_sem::IsValueTypeTime(vt) ||
|
||
stat_guid_equals(vt, ID_SERIES_VALUE_TYPE_TIME) ||
|
||
stat_guid_equals(vt, ID_SERIES_VALUE_TYPE_STATUS) ||
|
||
stat_guid_equals(vt, ID_SERIES_VALUE_TYPE_COUNT) ||
|
||
stat_guid_equals(vt, ID_SERIES_VALUE_TYPE_INTERVAL) ||
|
||
stat_guid_equals(vt, ID_SERIES_VALUE_TYPE_DURATION))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (sd != nullptr)
|
||
{
|
||
const GUID& def_vt = sd->value_type_id.value;
|
||
if (pqdif_sem::IsValueTypeTime(def_vt) ||
|
||
stat_guid_equals(def_vt, ID_SERIES_VALUE_TYPE_TIME) ||
|
||
stat_guid_equals(def_vt, ID_SERIES_VALUE_TYPE_STATUS) ||
|
||
stat_guid_equals(def_vt, ID_SERIES_VALUE_TYPE_COUNT) ||
|
||
stat_guid_equals(def_vt, ID_SERIES_VALUE_TYPE_INTERVAL) ||
|
||
stat_guid_equals(def_vt, ID_SERIES_VALUE_TYPE_DURATION))
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/// @brief 指标对象族。
|
||
/// @details 先按 C.2 表语义识别对象族,再根据相别拆成最终 metric_id。
|
||
enum class StatFamily
|
||
{
|
||
Unknown = 0,
|
||
VoltageRms,
|
||
CurrentRms,
|
||
VoltageDeviation,
|
||
Frequency,
|
||
FrequencyDeviation,
|
||
VoltageZeroSequence,
|
||
VoltageNegativeSequence,
|
||
VoltagePositiveSequence,
|
||
VoltageNegativeSequenceUnbalance,
|
||
CurrentZeroSequence,
|
||
CurrentNegativeSequence,
|
||
CurrentPositiveSequence,
|
||
CurrentNegativeSequenceUnbalance
|
||
};
|
||
|
||
/// @brief 将名称归一化为“仅保留字母数字并转大写”的紧凑形式。
|
||
/// @param s 原始名称。
|
||
/// @return 归一化后的字符串。
|
||
std::string stat_compact_upper(const std::string& s)
|
||
{
|
||
std::string out;
|
||
out.reserve(s.size());
|
||
for (unsigned char ch : s)
|
||
{
|
||
if (std::isalnum(ch))
|
||
out.push_back(static_cast<char>(std::toupper(ch)));
|
||
}
|
||
return out;
|
||
}
|
||
|
||
/// @brief 判断归一化后的文本中是否包含任一别名。
|
||
/// @param compact_text 归一化文本。
|
||
/// @param aliases 别名集合。
|
||
/// @return 命中任一别名则返回 true。
|
||
bool stat_contains_any_alias(
|
||
const std::string& compact_text,
|
||
const std::vector<std::string>& aliases)
|
||
{
|
||
for (const auto& alias : aliases)
|
||
{
|
||
if (!alias.empty() && compact_text.find(alias) != std::string::npos)
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
|
||
|
||
bool stat_ends_with(const std::string& text, const std::string& suffix)
|
||
{
|
||
return text.size() >= suffix.size() &&
|
||
text.compare(text.size() - suffix.size(), suffix.size(), suffix) == 0;
|
||
}
|
||
|
||
std::string stat_two_digit(int n)
|
||
{
|
||
char buf[8] = { 0 };
|
||
std::snprintf(buf, sizeof(buf), "%02d", n);
|
||
return std::string(buf);
|
||
}
|
||
|
||
int stat_round_order_if_valid(double value)
|
||
{
|
||
const int rounded = static_cast<int>(std::llround(value));
|
||
if (rounded < 2 || rounded > 50)
|
||
return -1;
|
||
if (std::fabs(value - static_cast<double>(rounded)) > 1e-6)
|
||
return -1;
|
||
return rounded;
|
||
}
|
||
|
||
/// @brief 动态谱类次数解析的特殊返回值:标准 group=1 表示基波,应跳过,不能兜底映射为 2 次。
|
||
constexpr int STAT_DYNAMIC_ORDER_SKIP_FUNDAMENTAL = -100000;
|
||
|
||
bool stat_is_voltage_harmonic_characteristic(const ExpandedStatPoint& p)
|
||
{
|
||
return stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_HRMS) ||
|
||
stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_SPECTRA) ||
|
||
stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_SPECTRA_HGROUP);
|
||
}
|
||
|
||
bool stat_is_voltage_harmonic_unit(unsigned int unit_id)
|
||
{
|
||
// UaHarmXX/UbHarmXX/UcHarmXX 表示谐波电压 RMS 值,单位应为 V。
|
||
// OBS 中常见的 V HR A/B/C 是谐波含有率/百分比,后续应单独建 HarmRatio 指标,
|
||
// 这里不能混入同一个谐波电压指标。
|
||
return unit_id == ID_QU_VOLTS;
|
||
}
|
||
|
||
/// @brief C.2 标准字段优先识别:电压谐波 RMS / 按谐波组频谱。
|
||
/// @details
|
||
/// 这里优先使用三个标准字段,而不是名称:
|
||
/// tagQuantityMeasuredID = ID_QM_VOLTAGE
|
||
/// tagQuantityCharacteristicID = ID_QC_HRMS / ID_QC_SPECTRA / ID_QC_SPECTRA_HGROUP
|
||
/// tagQuantityUnitsID = ID_QU_VOLTS
|
||
/// 谐波次数不在这三个 ID 中,当前文件通过 tagChannelGroupID 表达 group=2..50。
|
||
bool stat_match_c2_voltage_harmonic_rms(const ExpandedStatPoint& p)
|
||
{
|
||
return p.quantity_measured_id == ID_QM_VOLTAGE &&
|
||
stat_is_voltage_harmonic_unit(p.quantity_units_id) &&
|
||
stat_is_voltage_harmonic_characteristic(p);
|
||
}
|
||
|
||
int stat_extract_harmonic_order_from_compact_text(const std::string& compact_text)
|
||
{
|
||
if (compact_text.empty())
|
||
return -1;
|
||
|
||
// 优先匹配带有明确 HARM/HRMS/H 前缀或后缀的写法,避免误把普通数字当作谐波次数。
|
||
for (int order = 2; order <= 50; ++order)
|
||
{
|
||
const std::string n = std::to_string(order);
|
||
const std::string nn = stat_two_digit(order);
|
||
const std::vector<std::string> aliases = {
|
||
"HARMONIC" + n,
|
||
"HARMONIC" + nn,
|
||
"HARM" + n,
|
||
"HARM" + nn,
|
||
"HRMS" + n,
|
||
"HRMS" + nn,
|
||
"HGROUP" + n,
|
||
"HGROUP" + nn,
|
||
"H" + n,
|
||
"H" + nn,
|
||
n + "HARMONIC",
|
||
nn + "HARMONIC",
|
||
n + "HARM",
|
||
nn + "HARM"
|
||
};
|
||
if (stat_contains_any_alias(compact_text, aliases))
|
||
return order;
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
int stat_extract_order_from_standard_fields(const ExpandedStatPoint& p)
|
||
{
|
||
// 当前厂家文件把谐波次数写在 channel_group_id 中:group=1 为基波,group=2..50 为 2-50 次谐波。
|
||
// 修复点:一旦明确出现 group=1,必须视为基波并跳过,不能继续用 order_hint/name 兜底成 2 次。
|
||
if (p.channel_group_id == 1)
|
||
return STAT_DYNAMIC_ORDER_SKIP_FUNDAMENTAL;
|
||
|
||
const int order_by_group = stat_round_order_if_valid(static_cast<double>(p.channel_group_id));
|
||
if (order_by_group >= 2 && order_by_group <= 50)
|
||
return order_by_group;
|
||
|
||
// 其他厂家可能放在 series_base_quantity 或 nominal_quantity,作为标准字段不足时的补充。
|
||
// 仅当 channel_group_id 没有给出明确次数时才使用这些字段。
|
||
const int order_by_base = stat_round_order_if_valid(p.series_base_quantity);
|
||
if (order_by_base >= 2 && order_by_base <= 50)
|
||
return order_by_base;
|
||
|
||
const int order_by_nominal = stat_round_order_if_valid(p.nominal_quantity);
|
||
if (order_by_nominal >= 2 && order_by_nominal <= 50)
|
||
return order_by_nominal;
|
||
|
||
// 有些厂家把谐波/相角谱线拆成多个 channel instance,但每条通道名称完全相同,
|
||
// 且没有写 channel_group_id/base/nominal。此时在展开通道时会按同类通道实例顺序
|
||
// 生成 order_hint,避免所有通道都被名称里的 “HARM2” 误识别为 2 次。
|
||
// 注意:如果上面已检测到 group=1,这里不会执行。
|
||
if (p.channel_spectrum_order_hint >= 2 && p.channel_spectrum_order_hint <= 50)
|
||
return p.channel_spectrum_order_hint;
|
||
|
||
return -1;
|
||
}
|
||
|
||
int stat_extract_voltage_harmonic_order(const ExpandedStatPoint& p)
|
||
{
|
||
if (p.quantity_measured_id != ID_QM_VOLTAGE)
|
||
return -1;
|
||
if (!stat_is_voltage_harmonic_unit(p.quantity_units_id))
|
||
return -1;
|
||
|
||
// 第一优先级:C.2 标准字段组合。
|
||
// 只要 measured + characteristic + unit 命中,就直接从 group/base/nominal 读取次数,
|
||
// 不要求通道名包含 HRMS/HARM/HGROUP。
|
||
if (stat_match_c2_voltage_harmonic_rms(p))
|
||
{
|
||
const int order_by_standard = stat_extract_order_from_standard_fields(p);
|
||
if (order_by_standard == STAT_DYNAMIC_ORDER_SKIP_FUNDAMENTAL)
|
||
return -1;
|
||
if (order_by_standard >= 2 && order_by_standard <= 50)
|
||
return order_by_standard;
|
||
}
|
||
|
||
// 第二优先级:厂家名称兜底。仅在标准 characteristic 缺失或不规范时使用。
|
||
const std::string compact_text =
|
||
stat_compact_upper(p.channel_name) + stat_compact_upper(p.quantity_name);
|
||
const bool text_has_harmonic_hint = stat_contains_any_alias(compact_text, {
|
||
"HARM", "HARMONIC", "HRMS", "HGROUP", "SPECTRA"
|
||
});
|
||
if (!text_has_harmonic_hint)
|
||
return -1;
|
||
|
||
const int order_by_name = stat_extract_harmonic_order_from_compact_text(compact_text);
|
||
if (order_by_name >= 2 && order_by_name <= 50)
|
||
return order_by_name;
|
||
|
||
{
|
||
const int order_by_standard = stat_extract_order_from_standard_fields(p);
|
||
if (order_by_standard == STAT_DYNAMIC_ORDER_SKIP_FUNDAMENTAL)
|
||
return -1;
|
||
return order_by_standard;
|
||
}
|
||
}
|
||
|
||
int stat_extract_voltage_harmonic_phase_index(const ExpandedStatPoint& p)
|
||
{
|
||
if (p.phase_id == ID_PHASE_AN) return 0;
|
||
if (p.phase_id == ID_PHASE_BN) return 1;
|
||
if (p.phase_id == ID_PHASE_CN) return 2;
|
||
|
||
const std::string compact_text =
|
||
stat_compact_upper(p.channel_name) + stat_compact_upper(p.quantity_name);
|
||
|
||
if (stat_contains_any_alias(compact_text, { "PHASEA", "PHA", "VHA", "UHARMA", "VHARMONICA", "UHARMONICA", "HRMSA" }))
|
||
return 0;
|
||
if (stat_contains_any_alias(compact_text, { "PHASEB", "PHB", "VHB", "UHARMB", "VHARMONICB", "UHARMONICB", "HRMSB" }))
|
||
return 1;
|
||
if (stat_contains_any_alias(compact_text, { "PHASEC", "PHC", "VHC", "UHARMC", "VHARMONICC", "UHARMONICC", "HRMSC" }))
|
||
return 2;
|
||
|
||
// 常见格式如 VH02A / V2HA / U50B:优先使用末尾相别,但避免把单独的 HARMONIC 误判成 C 相。
|
||
if (stat_ends_with(compact_text, "A") && !stat_ends_with(compact_text, "HARMONICA"))
|
||
return 0;
|
||
if (stat_ends_with(compact_text, "B") && !stat_ends_with(compact_text, "HARMONICB"))
|
||
return 1;
|
||
if (stat_ends_with(compact_text, "C") && !stat_ends_with(compact_text, "HARMONIC"))
|
||
return 2;
|
||
|
||
return -1;
|
||
}
|
||
|
||
|
||
bool stat_is_harmonic_group_characteristic(const ExpandedStatPoint& p)
|
||
{
|
||
return stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_HRMS) ||
|
||
stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_SPECTRA) ||
|
||
stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_SPECTRA_HGROUP);
|
||
}
|
||
|
||
bool stat_is_interharmonic_group_characteristic(const ExpandedStatPoint& p)
|
||
{
|
||
return stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_SPECTRA_IGROUP);
|
||
}
|
||
|
||
bool stat_is_phase_angle_value_type(const GUID& value_type_id)
|
||
{
|
||
return stat_guid_equals(value_type_id, ID_SERIES_VALUE_TYPE_PHASEANGLE) ||
|
||
stat_guid_equals(value_type_id, ID_SERIES_VALUE_TYPE_PHASEANGLE_MIN) ||
|
||
stat_guid_equals(value_type_id, ID_SERIES_VALUE_TYPE_PHASEANGLE_MAX) ||
|
||
stat_guid_equals(value_type_id, ID_SERIES_VALUE_TYPE_PHASEANGLE_AVG);
|
||
}
|
||
|
||
bool stat_is_angle_like_point(const ExpandedStatPoint& p)
|
||
{
|
||
if (stat_is_phase_angle_value_type(p.value_type_id.value))
|
||
return true;
|
||
if (p.quantity_units_id == ID_QU_DEGREES)
|
||
return true;
|
||
|
||
const std::string compact = stat_compact_upper(p.channel_name) +
|
||
stat_compact_upper(p.quantity_name) +
|
||
stat_compact_upper(p.value_type_id.symbolic_name) +
|
||
stat_compact_upper(p.quantity_characteristic_id.symbolic_name);
|
||
return stat_contains_any_alias(compact, { "PHASEANGLE", "PHASANGLE", "ANGLE", "PANGLE", "HANGLE" });
|
||
}
|
||
|
||
bool stat_is_magnitude_unit_for_measured(unsigned int measured_id, unsigned int unit_id)
|
||
{
|
||
if (measured_id == ID_QM_VOLTAGE)
|
||
return unit_id == ID_QU_VOLTS;
|
||
if (measured_id == ID_QM_CURRENT)
|
||
return unit_id == ID_QU_AMPS;
|
||
return false;
|
||
}
|
||
|
||
int stat_extract_line_voltage_phase_index(const ExpandedStatPoint& p)
|
||
{
|
||
if (p.phase_id == ID_PHASE_AB) return 0;
|
||
if (p.phase_id == ID_PHASE_BC) return 1;
|
||
if (p.phase_id == ID_PHASE_CA) return 2;
|
||
|
||
const std::string compact = stat_compact_upper(p.channel_name) + stat_compact_upper(p.quantity_name);
|
||
if (stat_contains_any_alias(compact, { "PHASEAB", "PHAB", "UAB", "VAB", "HRMSAB", "HARMAB" })) return 0;
|
||
if (stat_contains_any_alias(compact, { "PHASEBC", "PHBC", "UBC", "VBC", "HRMSBC", "HARMBC" })) return 1;
|
||
if (stat_contains_any_alias(compact, { "PHASECA", "PHCA", "UCA", "VCA", "HRMSCA", "HARMCA" })) return 2;
|
||
if (stat_ends_with(compact, "AB")) return 0;
|
||
if (stat_ends_with(compact, "BC")) return 1;
|
||
if (stat_ends_with(compact, "CA")) return 2;
|
||
return -1;
|
||
}
|
||
|
||
int stat_extract_phase_current_phase_index(const ExpandedStatPoint& p)
|
||
{
|
||
if (p.phase_id == ID_PHASE_AN) return 0;
|
||
if (p.phase_id == ID_PHASE_BN) return 1;
|
||
if (p.phase_id == ID_PHASE_CN) return 2;
|
||
|
||
const std::string compact = stat_compact_upper(p.channel_name) + stat_compact_upper(p.quantity_name);
|
||
if (stat_contains_any_alias(compact, { "PHASEA", "PHA", "IHA", "IHARMA", "IHARMONICA", "HRMSA" })) return 0;
|
||
if (stat_contains_any_alias(compact, { "PHASEB", "PHB", "IHB", "IHARMB", "IHARMONICB", "HRMSB" })) return 1;
|
||
if (stat_contains_any_alias(compact, { "PHASEC", "PHC", "IHC", "IHARMC", "IHARMONICC", "HRMSC" })) return 2;
|
||
if (stat_ends_with(compact, "A")) return 0;
|
||
if (stat_ends_with(compact, "B")) return 1;
|
||
if (stat_ends_with(compact, "C")) return 2;
|
||
return -1;
|
||
}
|
||
|
||
int stat_round_interharmonic_slot_if_valid(double value)
|
||
{
|
||
if (value < -1e-6 || value > 49.5 + 1e-6)
|
||
return -1;
|
||
const double slot_value = value - 0.5;
|
||
const int slot = static_cast<int>(std::llround(slot_value));
|
||
if (slot < 0 || slot > 49)
|
||
return -1;
|
||
if (std::fabs(slot_value - static_cast<double>(slot)) > 1e-6)
|
||
return -1;
|
||
return slot;
|
||
}
|
||
|
||
int stat_extract_interharmonic_slot_from_standard_fields(const ExpandedStatPoint& p)
|
||
{
|
||
// 间谐波优先按标准 group 解析:group=1..50 -> 0.5..49.5。
|
||
// 即 slot=0..49。不要再兼容 group=0..49,否则会把缺失/无效 group 错映射为 0.5。
|
||
if (p.channel_group_id >= 1 && p.channel_group_id <= 50)
|
||
return p.channel_group_id - 1;
|
||
|
||
int slot = stat_round_interharmonic_slot_if_valid(p.series_base_quantity);
|
||
if (slot >= 0) return slot;
|
||
|
||
slot = stat_round_interharmonic_slot_if_valid(p.nominal_quantity);
|
||
if (slot >= 0) return slot;
|
||
|
||
// 仅当标准字段没有给出 group/base/nominal 时,才使用展开阶段生成的顺序提示。
|
||
if (p.channel_spectrum_order_hint >= 1 && p.channel_spectrum_order_hint <= 50)
|
||
return p.channel_spectrum_order_hint - 1;
|
||
|
||
return -1;
|
||
}
|
||
|
||
bool stat_text_has_harmonic_hint(const std::string& compact)
|
||
{
|
||
return stat_contains_any_alias(compact, { "HARM", "HARMONIC", "HRMS", "HGROUP", "SPECTRA" });
|
||
}
|
||
|
||
bool stat_text_has_interharmonic_hint(const std::string& compact)
|
||
{
|
||
return stat_contains_any_alias(compact, { "INTERHARM", "INTERHARMONIC", "IHARM", "IGROUP", "IHGROUP" });
|
||
}
|
||
|
||
int stat_extract_interharmonic_slot_from_text(const std::string& compact)
|
||
{
|
||
if (!stat_text_has_interharmonic_hint(compact))
|
||
return -1;
|
||
for (int slot = 0; slot <= 49; ++slot)
|
||
{
|
||
const int n = slot;
|
||
const std::string nn = stat_two_digit(n);
|
||
const std::vector<std::string> aliases = {
|
||
"IH" + nn + "P5", "IH" + std::to_string(n) + "P5",
|
||
"INTERHARM" + nn + "P5", "INTERHARM" + std::to_string(n) + "P5",
|
||
"INTERHARMONIC" + nn + "P5", "INTERHARMONIC" + std::to_string(n) + "P5"
|
||
};
|
||
if (stat_contains_any_alias(compact, aliases))
|
||
return slot;
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
|
||
bool stat_guid_in(const GUID& value, const std::vector<const GUID*>& candidates)
|
||
{
|
||
for (const GUID* g : candidates)
|
||
{
|
||
if (g != nullptr && stat_guid_equals(value, *g))
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
bool stat_unit_is_ratio_like(unsigned int unit_id)
|
||
{
|
||
return unit_id == ID_QU_PERCENT || unit_id == ID_QU_PERUNIT || unit_id == ID_QU_NONE;
|
||
}
|
||
|
||
bool stat_unit_is_power_unit(unsigned int unit_id)
|
||
{
|
||
return unit_id == ID_QU_WATTS || unit_id == ID_QU_VARS || unit_id == ID_QU_VA;
|
||
}
|
||
|
||
bool stat_text_has_total_hint(const std::string& compact)
|
||
{
|
||
return stat_contains_any_alias(compact, {
|
||
"TOTAL", "TOT", "SUM", "THREEPHASE", "3PHASE", "ALLPHASE", "NET",
|
||
"PTOTAL", "QTOTAL", "STOTAL", "PALL", "QALL", "SALL"
|
||
});
|
||
}
|
||
|
||
int stat_extract_phase_or_total_index(const ExpandedStatPoint& p)
|
||
{
|
||
if (p.phase_id == ID_PHASE_AN) return 0;
|
||
if (p.phase_id == ID_PHASE_BN) return 1;
|
||
if (p.phase_id == ID_PHASE_CN) return 2;
|
||
if (p.phase_id == ID_PHASE_TOTAL || p.phase_id == ID_PHASE_NET) return 3;
|
||
|
||
const std::string compact = stat_compact_upper(p.channel_name) + stat_compact_upper(p.quantity_name);
|
||
if (stat_text_has_total_hint(compact)) return 3;
|
||
if (stat_contains_any_alias(compact, { "PHASEA", "PHA", " PA", "PA", "A相" }) || stat_ends_with(compact, "A")) return 0;
|
||
if (stat_contains_any_alias(compact, { "PHASEB", "PHB", " PB", "PB", "B相" }) || stat_ends_with(compact, "B")) return 1;
|
||
if (stat_contains_any_alias(compact, { "PHASEC", "PHC", " PC", "PC", "C相" }) || stat_ends_with(compact, "C")) return 2;
|
||
return -1;
|
||
}
|
||
|
||
StatMetricId stat_metric_from_phase4(int phase, StatMetricId a, StatMetricId b, StatMetricId c, StatMetricId total)
|
||
{
|
||
if (phase == 0) return a;
|
||
if (phase == 1) return b;
|
||
if (phase == 2) return c;
|
||
if (phase == 3) return total;
|
||
return StatMetricId::Unknown;
|
||
}
|
||
|
||
StatMetricId stat_metric_from_phase3(int phase, StatMetricId a, StatMetricId b, StatMetricId c)
|
||
{
|
||
if (phase == 0) return a;
|
||
if (phase == 1) return b;
|
||
if (phase == 2) return c;
|
||
return StatMetricId::Unknown;
|
||
}
|
||
|
||
bool stat_is_power_factor_characteristic(const ExpandedStatPoint& p)
|
||
{
|
||
return stat_guid_in(p.quantity_characteristic_id.value, {
|
||
&ID_QC_PF, &ID_QC_PF_VECTOR, &ID_QC_PF_ARITH
|
||
});
|
||
}
|
||
|
||
bool stat_is_fund_power_factor_characteristic(const ExpandedStatPoint& p)
|
||
{
|
||
return stat_guid_in(p.quantity_characteristic_id.value, {
|
||
&ID_QC_DF, &ID_QC_DF_VECTOR, &ID_QC_DF_ARITH
|
||
});
|
||
}
|
||
|
||
bool stat_is_active_power_characteristic(const ExpandedStatPoint& p)
|
||
{
|
||
return stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_P);
|
||
}
|
||
|
||
bool stat_is_reactive_power_characteristic(const ExpandedStatPoint& p)
|
||
{
|
||
return stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_Q);
|
||
}
|
||
|
||
bool stat_is_apparent_power_characteristic(const ExpandedStatPoint& p)
|
||
{
|
||
return stat_guid_in(p.quantity_characteristic_id.value, {
|
||
&ID_QC_S, &ID_QC_S_VECTOR, &ID_QC_S_ARITH
|
||
});
|
||
}
|
||
|
||
bool stat_is_active_fund_power_characteristic(const ExpandedStatPoint& p)
|
||
{
|
||
return stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_P_FUND);
|
||
}
|
||
|
||
bool stat_is_reactive_fund_power_characteristic(const ExpandedStatPoint& p)
|
||
{
|
||
return stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_Q_FUND);
|
||
}
|
||
|
||
bool stat_is_apparent_fund_power_characteristic(const ExpandedStatPoint& p)
|
||
{
|
||
return stat_guid_in(p.quantity_characteristic_id.value, {
|
||
&ID_QC_S_FUND, &ID_QC_S_VECTOR_FUND, &ID_QC_S_ARITH_FUND
|
||
});
|
||
}
|
||
|
||
bool stat_text_has_fund_hint(const ExpandedStatPoint& p)
|
||
{
|
||
const std::string compact = stat_compact_upper(p.channel_name) +
|
||
stat_compact_upper(p.quantity_name) +
|
||
stat_compact_upper(p.quantity_characteristic_id.symbolic_name);
|
||
return stat_contains_any_alias(compact, {
|
||
"FUND", "FUNDAMENTAL", "BASEFREQ", "FREQFUND", "1STHARM", "HARM1"
|
||
});
|
||
}
|
||
|
||
bool stat_text_has_dvc_hint(const ExpandedStatPoint& p)
|
||
{
|
||
const std::string compact = stat_compact_upper(p.channel_name) +
|
||
stat_compact_upper(p.quantity_name) +
|
||
stat_compact_upper(p.quantity_characteristic_id.symbolic_name);
|
||
return stat_contains_any_alias(compact, { "DVC", "DVV", "DELTAV", "VOLTAGECHANGE", "VCHANGE", "FLKRMAXDVV" });
|
||
}
|
||
|
||
bool stat_text_has_pst_hint(const ExpandedStatPoint& p)
|
||
{
|
||
const std::string compact = stat_compact_upper(p.channel_name) +
|
||
stat_compact_upper(p.quantity_name) +
|
||
stat_compact_upper(p.quantity_characteristic_id.symbolic_name);
|
||
return stat_contains_any_alias(compact, { "PST", "SHORTTERMFLICKER", "SHORTFLICKER", "FLKRPST" });
|
||
}
|
||
|
||
bool stat_text_has_plt_hint(const ExpandedStatPoint& p)
|
||
{
|
||
const std::string compact = stat_compact_upper(p.channel_name) +
|
||
stat_compact_upper(p.quantity_name) +
|
||
stat_compact_upper(p.quantity_characteristic_id.symbolic_name);
|
||
return stat_contains_any_alias(compact, { "PLT", "LONGTERMFLICKER", "LONGFLICKER", "FLKRPLT" });
|
||
}
|
||
|
||
bool stat_text_has_thd_hint(const ExpandedStatPoint& p)
|
||
{
|
||
const std::string compact = stat_compact_upper(p.channel_name) +
|
||
stat_compact_upper(p.quantity_name) +
|
||
stat_compact_upper(p.quantity_characteristic_id.symbolic_name);
|
||
return stat_contains_any_alias(compact, { "THD", "TOTALHARMONICDISTORTION", "TOTALTHD" }) &&
|
||
!stat_contains_any_alias(compact, { "OTHD", "ETHD", "ODDTHD", "EVENTHD" });
|
||
}
|
||
|
||
bool stat_is_total_thd_characteristic(const ExpandedStatPoint& p)
|
||
{
|
||
return stat_guid_in(p.quantity_characteristic_id.value, {
|
||
&ID_QC_TOTAL_THD, &ID_QC_TOTAL_THD_RMS
|
||
});
|
||
}
|
||
|
||
StatMetricId stat_identify_scalar_extension_metric(const ExpandedStatPoint& p, bool& matched_by_name_fallback)
|
||
{
|
||
matched_by_name_fallback = false;
|
||
|
||
const std::string compact = stat_compact_upper(p.channel_name) +
|
||
stat_compact_upper(p.quantity_name) +
|
||
stat_compact_upper(p.quantity_characteristic_id.symbolic_name);
|
||
|
||
// 三相/总有功、无功、视在功率;以及基波功率。
|
||
if (p.quantity_measured_id == ID_QM_POWER)
|
||
{
|
||
const int phase = stat_extract_phase_or_total_index(p);
|
||
if (phase >= 0)
|
||
{
|
||
if (stat_is_active_power_characteristic(p) && p.quantity_units_id == ID_QU_WATTS)
|
||
return stat_metric_from_phase4(phase, StatMetricId::PaPower, StatMetricId::PbPower, StatMetricId::PcPower, StatMetricId::PTotalPower);
|
||
if (stat_is_reactive_power_characteristic(p) && p.quantity_units_id == ID_QU_VARS)
|
||
return stat_metric_from_phase4(phase, StatMetricId::QaPower, StatMetricId::QbPower, StatMetricId::QcPower, StatMetricId::QTotalPower);
|
||
if (stat_is_apparent_power_characteristic(p) && p.quantity_units_id == ID_QU_VA)
|
||
return stat_metric_from_phase4(phase, StatMetricId::SaPower, StatMetricId::SbPower, StatMetricId::ScPower, StatMetricId::STotalPower);
|
||
|
||
if (stat_is_active_fund_power_characteristic(p) && p.quantity_units_id == ID_QU_WATTS)
|
||
return stat_metric_from_phase4(phase, StatMetricId::PaFundPower, StatMetricId::PbFundPower, StatMetricId::PcFundPower, StatMetricId::PTotalFundPower);
|
||
if (stat_is_reactive_fund_power_characteristic(p) && p.quantity_units_id == ID_QU_VARS)
|
||
return stat_metric_from_phase4(phase, StatMetricId::QaFundPower, StatMetricId::QbFundPower, StatMetricId::QcFundPower, StatMetricId::QTotalFundPower);
|
||
if (stat_is_apparent_fund_power_characteristic(p) && p.quantity_units_id == ID_QU_VA)
|
||
return stat_metric_from_phase4(phase, StatMetricId::SaFundPower, StatMetricId::SbFundPower, StatMetricId::ScFundPower, StatMetricId::STotalFundPower);
|
||
|
||
if (stat_is_power_factor_characteristic(p) && stat_unit_is_ratio_like(p.quantity_units_id))
|
||
return stat_metric_from_phase4(phase, StatMetricId::PFa, StatMetricId::PFb, StatMetricId::PFc, StatMetricId::PFTotal);
|
||
if (stat_is_fund_power_factor_characteristic(p) && stat_unit_is_ratio_like(p.quantity_units_id))
|
||
return stat_metric_from_phase4(phase, StatMetricId::FundPFa, StatMetricId::FundPFb, StatMetricId::FundPFc, StatMetricId::FundPFTotal);
|
||
}
|
||
|
||
// 名称兜底:厂家可能只填 NONE/INSTANTANEOUS,但通道名中含 P/Q/S/PF/FUND。
|
||
if (phase >= 0)
|
||
{
|
||
if (p.quantity_units_id == ID_QU_WATTS && stat_contains_any_alias(compact, { "PFUND", "FUNDAMENTALP", "FUNDPOWERP" }))
|
||
{
|
||
matched_by_name_fallback = true;
|
||
return stat_metric_from_phase4(phase, StatMetricId::PaFundPower, StatMetricId::PbFundPower, StatMetricId::PcFundPower, StatMetricId::PTotalFundPower);
|
||
}
|
||
if (p.quantity_units_id == ID_QU_VARS && stat_contains_any_alias(compact, { "QFUND", "FUNDAMENTALQ", "FUNDPOWERQ" }))
|
||
{
|
||
matched_by_name_fallback = true;
|
||
return stat_metric_from_phase4(phase, StatMetricId::QaFundPower, StatMetricId::QbFundPower, StatMetricId::QcFundPower, StatMetricId::QTotalFundPower);
|
||
}
|
||
if (p.quantity_units_id == ID_QU_VA && stat_contains_any_alias(compact, { "SFUND", "FUNDAMENTALS", "FUNDPOWERS" }))
|
||
{
|
||
matched_by_name_fallback = true;
|
||
return stat_metric_from_phase4(phase, StatMetricId::SaFundPower, StatMetricId::SbFundPower, StatMetricId::ScFundPower, StatMetricId::STotalFundPower);
|
||
}
|
||
if (stat_unit_is_ratio_like(p.quantity_units_id) && stat_contains_any_alias(compact, { "FUNDPF", "FUNDAMENTALPF", "DISPLACEMENTFACTOR", "DF" }))
|
||
{
|
||
matched_by_name_fallback = true;
|
||
return stat_metric_from_phase4(phase, StatMetricId::FundPFa, StatMetricId::FundPFb, StatMetricId::FundPFc, StatMetricId::FundPFTotal);
|
||
}
|
||
if (stat_unit_is_ratio_like(p.quantity_units_id) && stat_contains_any_alias(compact, { "POWERFACTOR", "PF" }))
|
||
{
|
||
matched_by_name_fallback = true;
|
||
return stat_metric_from_phase4(phase, StatMetricId::PFa, StatMetricId::PFb, StatMetricId::PFc, StatMetricId::PFTotal);
|
||
}
|
||
if (p.quantity_units_id == ID_QU_WATTS && stat_contains_any_alias(compact, { "ACTIVEPOWER", "REALPOWER", "PW", "PPOWER" }))
|
||
{
|
||
matched_by_name_fallback = true;
|
||
return stat_metric_from_phase4(phase, StatMetricId::PaPower, StatMetricId::PbPower, StatMetricId::PcPower, StatMetricId::PTotalPower);
|
||
}
|
||
if (p.quantity_units_id == ID_QU_VARS && stat_contains_any_alias(compact, { "REACTIVEPOWER", "QPOWER", "VAR" }))
|
||
{
|
||
matched_by_name_fallback = true;
|
||
return stat_metric_from_phase4(phase, StatMetricId::QaPower, StatMetricId::QbPower, StatMetricId::QcPower, StatMetricId::QTotalPower);
|
||
}
|
||
if (p.quantity_units_id == ID_QU_VA && stat_contains_any_alias(compact, { "APPARENTPOWER", "SPOWER", "VA" }))
|
||
{
|
||
matched_by_name_fallback = true;
|
||
return stat_metric_from_phase4(phase, StatMetricId::SaPower, StatMetricId::SbPower, StatMetricId::ScPower, StatMetricId::STotalPower);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 总谐波畸变率 THD:三相电压/电流/线电压。
|
||
if ((p.quantity_measured_id == ID_QM_VOLTAGE || p.quantity_measured_id == ID_QM_CURRENT) &&
|
||
stat_unit_is_ratio_like(p.quantity_units_id) &&
|
||
(stat_is_total_thd_characteristic(p) || stat_text_has_thd_hint(p)))
|
||
{
|
||
if (!stat_is_total_thd_characteristic(p))
|
||
matched_by_name_fallback = true;
|
||
|
||
if (p.quantity_measured_id == ID_QM_CURRENT)
|
||
{
|
||
const int phase = stat_extract_phase_current_phase_index(p);
|
||
return stat_metric_from_phase3(phase, StatMetricId::IaThd, StatMetricId::IbThd, StatMetricId::IcThd);
|
||
}
|
||
|
||
int phase = stat_extract_voltage_harmonic_phase_index(p);
|
||
if (phase >= 0)
|
||
return stat_metric_from_phase3(phase, StatMetricId::UaThd, StatMetricId::UbThd, StatMetricId::UcThd);
|
||
|
||
phase = stat_extract_line_voltage_phase_index(p);
|
||
if (phase >= 0)
|
||
return stat_metric_from_phase3(phase, StatMetricId::UabThd, StatMetricId::UbcThd, StatMetricId::UcaThd);
|
||
}
|
||
|
||
// 电压变动幅值 DVC/dV/V。
|
||
// 先按 C.2 ID 组合识别;若厂家未规范填写 measured,只要名称明确包含电压变动提示,也允许名称兜底。
|
||
const bool measured_is_voltage_or_voltage_name =
|
||
p.quantity_measured_id == ID_QM_VOLTAGE ||
|
||
stat_contains_any_alias(compact, { "VOLTAGE", "VFLICKER", "VFLKR", "VDVC", "UDVC", "DVV" });
|
||
|
||
if (measured_is_voltage_or_voltage_name &&
|
||
stat_unit_is_ratio_like(p.quantity_units_id) &&
|
||
(stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_FLKR_MAX_DVV) || stat_text_has_dvc_hint(p)))
|
||
{
|
||
if (p.quantity_measured_id != ID_QM_VOLTAGE ||
|
||
!stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_FLKR_MAX_DVV))
|
||
{
|
||
matched_by_name_fallback = true;
|
||
}
|
||
int phase = stat_extract_voltage_harmonic_phase_index(p);
|
||
if (phase >= 0)
|
||
return stat_metric_from_phase3(phase, StatMetricId::UaDvc, StatMetricId::UbDvc, StatMetricId::UcDvc);
|
||
phase = stat_extract_line_voltage_phase_index(p);
|
||
if (phase >= 0)
|
||
return stat_metric_from_phase3(phase, StatMetricId::UabDvc, StatMetricId::UbcDvc, StatMetricId::UcaDvc);
|
||
}
|
||
|
||
// 闪变 Pst/Plt。
|
||
// 修复点:
|
||
// 1) ID_QC_FLKR_PST / ID_QC_FLKR_PLT 仍然是第一优先级;
|
||
// 2) 对 V Flicker Plt A/B/C、PLT VA/VB/VC 等厂家名称进行兜底;
|
||
// 3) 不强依赖 measured 必须填成 Voltage,避免厂家只靠通道名表达对象。
|
||
const bool id_is_pst = stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_FLKR_PST);
|
||
const bool id_is_plt = stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_FLKR_PLT);
|
||
const bool is_pst = id_is_pst || stat_text_has_pst_hint(p);
|
||
const bool is_plt = id_is_plt || stat_text_has_plt_hint(p);
|
||
const bool measured_is_voltage_or_flicker_name =
|
||
measured_is_voltage_or_voltage_name || is_pst || is_plt;
|
||
|
||
// Plt/Pst 修复:厂家有时会把 Plt 序列 unit 写成 Unknown/Unrecognized,而不是 None/pu/%。
|
||
// 只要 C.2 characteristic ID 或通道名称已经明确是 Pst/Plt,就不再用 unit 作为硬性门槛。
|
||
// unit 仅用于辅助判断,不作为清晰 Flicker 通道的排除条件。
|
||
if (measured_is_voltage_or_flicker_name && (is_pst || is_plt))
|
||
{
|
||
if (p.quantity_measured_id != ID_QM_VOLTAGE || !(id_is_pst || id_is_plt))
|
||
matched_by_name_fallback = true;
|
||
|
||
// 若同时命中 Pst 和 Plt,以标准 ID 为准;没有标准 ID 时 Plt 优先,
|
||
// 避免 “V Flicker Plt A/B/C” 因 unit 非标准而被跳过。
|
||
const bool choose_plt = id_is_plt || (!id_is_pst && is_plt);
|
||
|
||
int phase = stat_extract_voltage_harmonic_phase_index(p);
|
||
if (phase >= 0)
|
||
return choose_plt ?
|
||
stat_metric_from_phase3(phase, StatMetricId::UaPlt, StatMetricId::UbPlt, StatMetricId::UcPlt) :
|
||
stat_metric_from_phase3(phase, StatMetricId::UaPst, StatMetricId::UbPst, StatMetricId::UcPst);
|
||
|
||
phase = stat_extract_line_voltage_phase_index(p);
|
||
if (phase >= 0)
|
||
return choose_plt ?
|
||
stat_metric_from_phase3(phase, StatMetricId::UabPlt, StatMetricId::UbcPlt, StatMetricId::UcaPlt) :
|
||
stat_metric_from_phase3(phase, StatMetricId::UabPst, StatMetricId::UbcPst, StatMetricId::UcaPst);
|
||
}
|
||
|
||
// 基波有效值和基波相角。
|
||
if ((p.quantity_measured_id == ID_QM_VOLTAGE || p.quantity_measured_id == ID_QM_CURRENT) && stat_text_has_fund_hint(p))
|
||
{
|
||
const bool is_angle = stat_is_angle_like_point(p) || stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_ANGLE_FUND);
|
||
const bool is_rms = stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_RMS) ||
|
||
stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_INSTANTANEOUS) ||
|
||
stat_contains_any_alias(compact, { "RMS", "FUND" });
|
||
|
||
if (p.quantity_measured_id == ID_QM_CURRENT)
|
||
{
|
||
const int phase = stat_extract_phase_current_phase_index(p);
|
||
if (is_angle && p.quantity_units_id == ID_QU_DEGREES)
|
||
{
|
||
matched_by_name_fallback = !stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_ANGLE_FUND);
|
||
return stat_metric_from_phase3(phase, StatMetricId::IaFundAngle, StatMetricId::IbFundAngle, StatMetricId::IcFundAngle);
|
||
}
|
||
if (is_rms && p.quantity_units_id == ID_QU_AMPS)
|
||
{
|
||
matched_by_name_fallback = !stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_RMS);
|
||
return stat_metric_from_phase3(phase, StatMetricId::IaFundRms, StatMetricId::IbFundRms, StatMetricId::IcFundRms);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
int phase = stat_extract_voltage_harmonic_phase_index(p);
|
||
if (phase >= 0)
|
||
{
|
||
if (is_angle && p.quantity_units_id == ID_QU_DEGREES)
|
||
{
|
||
matched_by_name_fallback = !stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_ANGLE_FUND);
|
||
return stat_metric_from_phase3(phase, StatMetricId::UaFundAngle, StatMetricId::UbFundAngle, StatMetricId::UcFundAngle);
|
||
}
|
||
if (is_rms && p.quantity_units_id == ID_QU_VOLTS)
|
||
{
|
||
matched_by_name_fallback = !stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_RMS);
|
||
return stat_metric_from_phase3(phase, StatMetricId::UaFundRms, StatMetricId::UbFundRms, StatMetricId::UcFundRms);
|
||
}
|
||
}
|
||
|
||
phase = stat_extract_line_voltage_phase_index(p);
|
||
if (phase >= 0)
|
||
{
|
||
if (is_angle && p.quantity_units_id == ID_QU_DEGREES)
|
||
{
|
||
matched_by_name_fallback = !stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_ANGLE_FUND);
|
||
return stat_metric_from_phase3(phase, StatMetricId::UabFundAngle, StatMetricId::UbcFundAngle, StatMetricId::UcaFundAngle);
|
||
}
|
||
if (is_rms && p.quantity_units_id == ID_QU_VOLTS)
|
||
{
|
||
matched_by_name_fallback = !stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_RMS);
|
||
return stat_metric_from_phase3(phase, StatMetricId::UabFundRms, StatMetricId::UbcFundRms, StatMetricId::UcaFundRms);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return StatMetricId::Unknown;
|
||
}
|
||
|
||
StatMetricId stat_identify_dynamic_extension_metric(const ExpandedStatPoint& p, bool& matched_by_name_fallback)
|
||
{
|
||
matched_by_name_fallback = false;
|
||
|
||
const std::string compact = stat_compact_upper(p.channel_name) +
|
||
stat_compact_upper(p.quantity_name) +
|
||
stat_compact_upper(p.quantity_characteristic_id.symbolic_name) +
|
||
stat_compact_upper(p.value_type_id.symbolic_name);
|
||
|
||
const bool standard_harmonic =
|
||
stat_is_harmonic_group_characteristic(p) ||
|
||
stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_P_HARMONIC) ||
|
||
stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_P_HARMONIC_UNSIGNED);
|
||
|
||
const bool fallback_harmonic = !standard_harmonic && stat_text_has_harmonic_hint(compact) && !stat_text_has_interharmonic_hint(compact);
|
||
if (!standard_harmonic && !fallback_harmonic)
|
||
return StatMetricId::Unknown;
|
||
|
||
if (stat_is_angle_like_point(p))
|
||
return StatMetricId::Unknown;
|
||
|
||
int order = -1;
|
||
if (standard_harmonic)
|
||
order = stat_extract_order_from_standard_fields(p);
|
||
if (order == STAT_DYNAMIC_ORDER_SKIP_FUNDAMENTAL)
|
||
return StatMetricId::Unknown;
|
||
|
||
// 名称兜底场景下,仍然优先尝试 group/base/nominal/order_hint;
|
||
// 这样 P HARM A 这类“通道名不含具体次数,但通道序列成组排列”的文件也能识别 2-50 次。
|
||
if (order < 2 || order > 50)
|
||
{
|
||
const int order_by_standard_or_hint = stat_extract_order_from_standard_fields(p);
|
||
if (order_by_standard_or_hint == STAT_DYNAMIC_ORDER_SKIP_FUNDAMENTAL)
|
||
return StatMetricId::Unknown;
|
||
if (order_by_standard_or_hint >= 2 && order_by_standard_or_hint <= 50)
|
||
order = order_by_standard_or_hint;
|
||
}
|
||
|
||
if (order < 2 || order > 50)
|
||
order = stat_extract_harmonic_order_from_compact_text(compact);
|
||
if (order < 2 || order > 50)
|
||
return StatMetricId::Unknown;
|
||
|
||
// 谐波功率:按 POWER + W/var/VA 识别,支持三相和总。
|
||
if (p.quantity_measured_id == ID_QM_POWER && stat_unit_is_power_unit(p.quantity_units_id))
|
||
{
|
||
const int phase = stat_extract_phase_or_total_index(p);
|
||
if (phase < 0)
|
||
return StatMetricId::Unknown;
|
||
|
||
matched_by_name_fallback = !standard_harmonic;
|
||
if (p.quantity_units_id == ID_QU_WATTS)
|
||
return stat_dynamic_metric_id(StatDynamicMetricGroup::HarmonicActivePower, phase, order);
|
||
if (p.quantity_units_id == ID_QU_VARS)
|
||
return stat_dynamic_metric_id(StatDynamicMetricGroup::HarmonicReactivePower, phase, order);
|
||
if (p.quantity_units_id == ID_QU_VA)
|
||
return stat_dynamic_metric_id(StatDynamicMetricGroup::HarmonicApparentPower, phase, order);
|
||
}
|
||
|
||
// 谐波含有率:按 %/pu/无量纲 识别,和 V/A RMS 谐波分离。
|
||
if ((p.quantity_measured_id == ID_QM_VOLTAGE || p.quantity_measured_id == ID_QM_CURRENT) &&
|
||
stat_unit_is_ratio_like(p.quantity_units_id))
|
||
{
|
||
matched_by_name_fallback = !standard_harmonic;
|
||
|
||
if (p.quantity_measured_id == ID_QM_CURRENT)
|
||
{
|
||
const int phase = stat_extract_phase_current_phase_index(p);
|
||
if (phase >= 0)
|
||
return stat_dynamic_metric_id(StatDynamicMetricGroup::CurrentHarmonicRatio, phase, order);
|
||
}
|
||
else
|
||
{
|
||
int phase = stat_extract_voltage_harmonic_phase_index(p);
|
||
if (phase >= 0)
|
||
return stat_dynamic_metric_id(StatDynamicMetricGroup::VoltageHarmonicRatio, phase, order);
|
||
|
||
phase = stat_extract_line_voltage_phase_index(p);
|
||
if (phase >= 0)
|
||
return stat_dynamic_metric_id(StatDynamicMetricGroup::LineVoltageHarmonicRatio, phase, order);
|
||
}
|
||
}
|
||
|
||
return StatMetricId::Unknown;
|
||
}
|
||
|
||
|
||
StatMetricId stat_identify_dynamic_spectrum_metric(const ExpandedStatPoint& p, bool& matched_by_name_fallback)
|
||
{
|
||
matched_by_name_fallback = false;
|
||
if (p.quantity_measured_id != ID_QM_VOLTAGE && p.quantity_measured_id != ID_QM_CURRENT)
|
||
return StatMetricId::Unknown;
|
||
|
||
const std::string compact = stat_compact_upper(p.channel_name) + stat_compact_upper(p.quantity_name) +
|
||
stat_compact_upper(p.quantity_characteristic_id.symbolic_name) + stat_compact_upper(p.value_type_id.symbolic_name);
|
||
|
||
const bool standard_harmonic = stat_is_harmonic_group_characteristic(p);
|
||
const bool standard_interharmonic = stat_is_interharmonic_group_characteristic(p);
|
||
const bool has_interharmonic_text = stat_text_has_interharmonic_hint(compact);
|
||
const bool fallback_interharmonic = !standard_interharmonic && has_interharmonic_text;
|
||
const bool fallback_harmonic = !standard_harmonic && stat_text_has_harmonic_hint(compact) && !has_interharmonic_text;
|
||
|
||
const bool is_angle = stat_is_angle_like_point(p);
|
||
const bool magnitude_unit_ok = stat_is_magnitude_unit_for_measured(p.quantity_measured_id, p.quantity_units_id);
|
||
const bool angle_unit_ok = is_angle || p.quantity_units_id == ID_QU_DEGREES;
|
||
|
||
if (!is_angle && !magnitude_unit_ok)
|
||
return StatMetricId::Unknown;
|
||
if (is_angle && !angle_unit_ok)
|
||
return StatMetricId::Unknown;
|
||
|
||
// 间谐波必须优先于普通谐波识别,避免 IHRMS/INTERHARMONIC 被 HRMS/HARMONIC 兜底误吃掉。
|
||
if ((standard_interharmonic || fallback_interharmonic) && !is_angle)
|
||
{
|
||
int slot = -1;
|
||
if (standard_interharmonic)
|
||
slot = stat_extract_interharmonic_slot_from_standard_fields(p);
|
||
if (slot < 0)
|
||
slot = stat_extract_interharmonic_slot_from_text(compact);
|
||
if (slot < 0 || slot > 49)
|
||
return StatMetricId::Unknown;
|
||
|
||
if (p.quantity_measured_id == ID_QM_VOLTAGE)
|
||
{
|
||
int phase = stat_extract_voltage_harmonic_phase_index(p);
|
||
if (phase >= 0)
|
||
{
|
||
matched_by_name_fallback = !standard_interharmonic;
|
||
return stat_dynamic_metric_id(StatDynamicMetricGroup::VoltageInterharmonic, phase, slot);
|
||
}
|
||
phase = stat_extract_line_voltage_phase_index(p);
|
||
if (phase >= 0)
|
||
{
|
||
matched_by_name_fallback = !standard_interharmonic;
|
||
return stat_dynamic_metric_id(StatDynamicMetricGroup::LineVoltageInterharmonic, phase, slot);
|
||
}
|
||
}
|
||
else if (p.quantity_measured_id == ID_QM_CURRENT)
|
||
{
|
||
const int phase = stat_extract_phase_current_phase_index(p);
|
||
if (phase >= 0)
|
||
{
|
||
matched_by_name_fallback = !standard_interharmonic;
|
||
return stat_dynamic_metric_id(StatDynamicMetricGroup::CurrentInterharmonic, phase, slot);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (standard_harmonic || fallback_harmonic)
|
||
{
|
||
int order = -1;
|
||
if (standard_harmonic)
|
||
order = stat_extract_order_from_standard_fields(p);
|
||
|
||
// group=1 明确是基波,必须跳过。不能继续通过 order_hint/name fallback 变成 2 次谐波。
|
||
if (order == STAT_DYNAMIC_ORDER_SKIP_FUNDAMENTAL)
|
||
return StatMetricId::Unknown;
|
||
|
||
if (order < 2 || order > 50)
|
||
order = stat_extract_harmonic_order_from_compact_text(compact);
|
||
if (order < 2 || order > 50)
|
||
return StatMetricId::Unknown;
|
||
|
||
if (p.quantity_measured_id == ID_QM_VOLTAGE)
|
||
{
|
||
int phase = stat_extract_voltage_harmonic_phase_index(p);
|
||
if (phase >= 0)
|
||
{
|
||
matched_by_name_fallback = !standard_harmonic;
|
||
return stat_dynamic_metric_id(is_angle ? StatDynamicMetricGroup::VoltageHarmonicAngle : StatDynamicMetricGroup::VoltageHarmonic, phase, order);
|
||
}
|
||
phase = stat_extract_line_voltage_phase_index(p);
|
||
if (phase >= 0)
|
||
{
|
||
matched_by_name_fallback = !standard_harmonic;
|
||
return stat_dynamic_metric_id(is_angle ? StatDynamicMetricGroup::LineVoltageHarmonicAngle : StatDynamicMetricGroup::LineVoltageHarmonic, phase, order);
|
||
}
|
||
}
|
||
else if (p.quantity_measured_id == ID_QM_CURRENT)
|
||
{
|
||
const int phase = stat_extract_phase_current_phase_index(p);
|
||
if (phase >= 0)
|
||
{
|
||
matched_by_name_fallback = !standard_harmonic;
|
||
return stat_dynamic_metric_id(is_angle ? StatDynamicMetricGroup::CurrentHarmonicAngle : StatDynamicMetricGroup::CurrentHarmonic, phase, order);
|
||
}
|
||
}
|
||
}
|
||
|
||
return StatMetricId::Unknown;
|
||
}
|
||
|
||
/// @brief 判断样本点是否为 C.2 表中的“频率”对象族。
|
||
/// @details
|
||
/// 根据附录 C 表 C.2:
|
||
/// Frequency -> ID_QM_VOLTAGE + ID_QC_FREQUENCY + ID_QU_HERTZ。
|
||
/// @param p 样本点。
|
||
/// @return 若命中 Frequency 对象族则返回 true。
|
||
bool stat_match_c2_frequency(const ExpandedStatPoint& p)
|
||
{
|
||
return p.quantity_measured_id == ID_QM_VOLTAGE &&
|
||
stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_FREQUENCY) &&
|
||
p.quantity_units_id == ID_QU_HERTZ;
|
||
}
|
||
|
||
/// @brief 判断样本点是否为 C.2 表中的“电压有效值”对象族。
|
||
/// @details
|
||
/// 根据附录 C 表 C.2:
|
||
/// V RMS -> ID_QM_VOLTAGE + ID_QC_RMS + ID_QU_VOLTS。
|
||
/// @param p 样本点。
|
||
/// @return 若命中 VoltageRms 对象族则返回 true。
|
||
bool stat_match_c2_voltage_rms(const ExpandedStatPoint& p)
|
||
{
|
||
return p.quantity_measured_id == ID_QM_VOLTAGE &&
|
||
stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_RMS) &&
|
||
p.quantity_units_id == ID_QU_VOLTS;
|
||
}
|
||
|
||
/// @brief 判断样本点是否为 C.2 表中的“电流有效值”对象族。
|
||
/// @details
|
||
/// 根据附录 C 表 C.2:
|
||
/// I RMS -> ID_QM_CURRENT + ID_QC_RMS + ID_QU_AMPS。
|
||
/// @param p 样本点。
|
||
/// @return 若命中 CurrentRms 对象族则返回 true。
|
||
bool stat_match_c2_current_rms(const ExpandedStatPoint& p)
|
||
{
|
||
return p.quantity_measured_id == ID_QM_CURRENT &&
|
||
stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_RMS) &&
|
||
p.quantity_units_id == ID_QU_AMPS;
|
||
}
|
||
|
||
/// @brief 判断样本点是否为 C.2 表中的“电压偏差”对象族。
|
||
/// @details
|
||
/// 根据附录 C 表 C.2:
|
||
/// V RMS Deviation -> ID_QM_VOLTAGE + ID_QC_RMS + ID_QU_PERCENT。
|
||
/// 为兼容部分设备文件,也允许单位为 PU 作为辅助兼容。
|
||
/// @param p 样本点。
|
||
/// @return 若命中 VoltageDeviation 对象族则返回 true。
|
||
bool stat_match_c2_voltage_deviation(const ExpandedStatPoint& p)
|
||
{
|
||
const bool unit_match =
|
||
p.quantity_units_id == ID_QU_PERCENT ||
|
||
p.quantity_units_id == ID_QU_PERUNIT;
|
||
|
||
return p.quantity_measured_id == ID_QM_VOLTAGE &&
|
||
stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_RMS) &&
|
||
unit_match;
|
||
}
|
||
|
||
/// @brief 判断样本点是否为 C.2 表中的“电压零序分量”对象族。
|
||
/// @details Zero sequence component -> ID_QM_VOLTAGE + ID_QC_SZERO + ID_QU_VOLTS。
|
||
bool stat_match_c2_voltage_zero_sequence(const ExpandedStatPoint& p)
|
||
{
|
||
return p.quantity_measured_id == ID_QM_VOLTAGE &&
|
||
stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_SZERO) &&
|
||
p.quantity_units_id == ID_QU_VOLTS;
|
||
}
|
||
|
||
/// @brief 判断样本点是否为 C.2 表中的“电压负序分量”对象族。
|
||
/// @details Negative sequence component -> ID_QM_VOLTAGE + ID_QC_SNEG + ID_QU_VOLTS。
|
||
bool stat_match_c2_voltage_negative_sequence(const ExpandedStatPoint& p)
|
||
{
|
||
return p.quantity_measured_id == ID_QM_VOLTAGE &&
|
||
stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_SNEG) &&
|
||
p.quantity_units_id == ID_QU_VOLTS;
|
||
}
|
||
|
||
/// @brief 判断样本点是否为 C.2 表中的“电压正序分量”对象族。
|
||
/// @details Positive sequence component -> ID_QM_VOLTAGE + ID_QC_SPOS + ID_QU_VOLTS。
|
||
bool stat_match_c2_voltage_positive_sequence(const ExpandedStatPoint& p)
|
||
{
|
||
return p.quantity_measured_id == ID_QM_VOLTAGE &&
|
||
stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_SPOS) &&
|
||
p.quantity_units_id == ID_QU_VOLTS;
|
||
}
|
||
|
||
/// @brief 判断样本点是否为 C.2 表中的“电压负序不平衡”对象族。
|
||
/// @details Negative sequence component unbalance -> ID_QM_VOLTAGE + ID_QC_S2S1 + ID_QU_PERCENT/ID_QU_PERUNIT。
|
||
bool stat_match_c2_voltage_negative_sequence_unbalance(const ExpandedStatPoint& p)
|
||
{
|
||
const bool unit_match =
|
||
p.quantity_units_id == ID_QU_PERCENT ||
|
||
p.quantity_units_id == ID_QU_PERUNIT;
|
||
|
||
return p.quantity_measured_id == ID_QM_VOLTAGE &&
|
||
stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_S2S1) &&
|
||
unit_match;
|
||
}
|
||
|
||
/// @brief 判断样本点是否为 C.2 表中的“电流零序分量”对象族。
|
||
/// @details Zero sequence component -> ID_QM_CURRENT + ID_QC_SZERO + ID_QU_AMPS。
|
||
bool stat_match_c2_current_zero_sequence(const ExpandedStatPoint& p)
|
||
{
|
||
return p.quantity_measured_id == ID_QM_CURRENT &&
|
||
stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_SZERO) &&
|
||
p.quantity_units_id == ID_QU_AMPS;
|
||
}
|
||
|
||
/// @brief 判断样本点是否为 C.2 表中的“电流负序分量”对象族。
|
||
/// @details Negative sequence component -> ID_QM_CURRENT + ID_QC_SNEG + ID_QU_AMPS。
|
||
bool stat_match_c2_current_negative_sequence(const ExpandedStatPoint& p)
|
||
{
|
||
return p.quantity_measured_id == ID_QM_CURRENT &&
|
||
stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_SNEG) &&
|
||
p.quantity_units_id == ID_QU_AMPS;
|
||
}
|
||
|
||
/// @brief 判断样本点是否为 C.2 表中的“电流正序分量”对象族。
|
||
/// @details Positive sequence component -> ID_QM_CURRENT + ID_QC_SPOS + ID_QU_AMPS。
|
||
bool stat_match_c2_current_positive_sequence(const ExpandedStatPoint& p)
|
||
{
|
||
return p.quantity_measured_id == ID_QM_CURRENT &&
|
||
stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_SPOS) &&
|
||
p.quantity_units_id == ID_QU_AMPS;
|
||
}
|
||
|
||
/// @brief 判断样本点是否为 C.2 表中的“电流负序不平衡”对象族。
|
||
/// @details Negative sequence component unbalance -> ID_QM_CURRENT + ID_QC_S2S1 + ID_QU_PERCENT/ID_QU_PERUNIT。
|
||
bool stat_match_c2_current_negative_sequence_unbalance(const ExpandedStatPoint& p)
|
||
{
|
||
const bool unit_match =
|
||
p.quantity_units_id == ID_QU_PERCENT ||
|
||
p.quantity_units_id == ID_QU_PERUNIT;
|
||
|
||
return p.quantity_measured_id == ID_QM_CURRENT &&
|
||
stat_guid_equals(p.quantity_characteristic_id.value, ID_QC_S2S1) &&
|
||
unit_match;
|
||
}
|
||
|
||
/// @brief 判断样本点是否为“频率偏差”对象族。
|
||
/// @details
|
||
/// 频率偏差和频率在 C.2 表里共用同一组核心语义:
|
||
/// ID_QM_VOLTAGE + ID_QC_FREQUENCY + ID_QU_HERTZ,差别主要体现在对象名称。
|
||
/// 因此这里采用“C.2 语义 + 名称辅助”的方式识别。
|
||
/// @param p 样本点。
|
||
/// @return 若命中 FrequencyDeviation 对象族则返回 true。
|
||
bool stat_match_c2_frequency_deviation(const ExpandedStatPoint& p)
|
||
{
|
||
if (!stat_match_c2_frequency(p))
|
||
return false;
|
||
|
||
const std::string compact_channel = stat_compact_upper(p.channel_name);
|
||
const std::string compact_quantity = stat_compact_upper(p.quantity_name);
|
||
const std::vector<std::string> aliases = {
|
||
"FREQDEV", "FREQUENCYDEV", "DELTAF", "FDEV", "DEVIATION"
|
||
};
|
||
|
||
return stat_contains_any_alias(compact_channel, aliases) ||
|
||
stat_contains_any_alias(compact_quantity, aliases);
|
||
}
|
||
|
||
/// @brief 基于 C.2 表的核心语义识别对象族。
|
||
/// @param p 已展开样本点。
|
||
/// @return 命中的对象族。
|
||
StatFamily stat_identify_family_by_c2(const ExpandedStatPoint& p)
|
||
{
|
||
if (stat_match_c2_frequency_deviation(p))
|
||
return StatFamily::FrequencyDeviation;
|
||
if (stat_match_c2_frequency(p))
|
||
return StatFamily::Frequency;
|
||
if (stat_match_c2_voltage_negative_sequence_unbalance(p))
|
||
return StatFamily::VoltageNegativeSequenceUnbalance;
|
||
if (stat_match_c2_voltage_zero_sequence(p))
|
||
return StatFamily::VoltageZeroSequence;
|
||
if (stat_match_c2_voltage_negative_sequence(p))
|
||
return StatFamily::VoltageNegativeSequence;
|
||
if (stat_match_c2_voltage_positive_sequence(p))
|
||
return StatFamily::VoltagePositiveSequence;
|
||
if (stat_match_c2_current_negative_sequence_unbalance(p))
|
||
return StatFamily::CurrentNegativeSequenceUnbalance;
|
||
if (stat_match_c2_current_zero_sequence(p))
|
||
return StatFamily::CurrentZeroSequence;
|
||
if (stat_match_c2_current_negative_sequence(p))
|
||
return StatFamily::CurrentNegativeSequence;
|
||
if (stat_match_c2_current_positive_sequence(p))
|
||
return StatFamily::CurrentPositiveSequence;
|
||
if (stat_match_c2_voltage_deviation(p))
|
||
return StatFamily::VoltageDeviation;
|
||
if (stat_match_c2_voltage_rms(p))
|
||
return StatFamily::VoltageRms;
|
||
if (stat_match_c2_current_rms(p))
|
||
return StatFamily::CurrentRms;
|
||
return StatFamily::Unknown;
|
||
}
|
||
|
||
/// @brief 根据接线方式、相别与 C.2 对象族,把样本点映射成最终业务指标。
|
||
/// @param connection_kind 当前文件接线方式。
|
||
/// @param p 已展开样本点。
|
||
/// @param matched_by_name_fallback 返回是否通过名称辅助识别。
|
||
/// @return 识别出的业务指标 ID。
|
||
StatMetricId stat_identify_metric_id(
|
||
ParsedConnectionKind connection_kind,
|
||
const ExpandedStatPoint& p,
|
||
bool& matched_by_name_fallback)
|
||
{
|
||
matched_by_name_fallback = false;
|
||
|
||
// 扩展动态指标入口:谐波功率、谐波含有率。
|
||
// 仍然保持“ID 组合优先,名称兜底”策略;名称只在标准字段不足时参与。
|
||
{
|
||
bool dynamic_name_fallback = false;
|
||
const StatMetricId dynamic_metric = stat_identify_dynamic_extension_metric(p, dynamic_name_fallback);
|
||
if (dynamic_metric != StatMetricId::Unknown)
|
||
{
|
||
matched_by_name_fallback = dynamic_name_fallback;
|
||
return dynamic_metric;
|
||
}
|
||
}
|
||
|
||
// 谐波/间谐波类指标统一入口:
|
||
// 先按 C.2 标准 ID 组合识别 measured/characteristic/unit/value_type,再从 group/base/nominal 取次数;
|
||
// 标准字段不足时才使用通道名兜底。覆盖电压/电流/线电压、幅值/相角、谐波/间谐波。
|
||
{
|
||
bool dynamic_name_fallback = false;
|
||
const StatMetricId dynamic_metric = stat_identify_dynamic_spectrum_metric(p, dynamic_name_fallback);
|
||
if (dynamic_metric != StatMetricId::Unknown)
|
||
{
|
||
matched_by_name_fallback = dynamic_name_fallback;
|
||
return dynamic_metric;
|
||
}
|
||
}
|
||
|
||
// 扩展静态指标入口:功率、功率因数、DVC、闪变、THD、基波 RMS/相角。
|
||
{
|
||
bool scalar_name_fallback = false;
|
||
const StatMetricId scalar_metric = stat_identify_scalar_extension_metric(p, scalar_name_fallback);
|
||
if (scalar_metric != StatMetricId::Unknown)
|
||
{
|
||
matched_by_name_fallback = scalar_name_fallback;
|
||
return scalar_metric;
|
||
}
|
||
}
|
||
|
||
const StatFamily family = stat_identify_family_by_c2(p);
|
||
switch (family)
|
||
{
|
||
case StatFamily::VoltageRms:
|
||
if (p.phase_id == ID_PHASE_AN) return StatMetricId::UaRms;
|
||
if (p.phase_id == ID_PHASE_BN) return StatMetricId::UbRms;
|
||
if (p.phase_id == ID_PHASE_CN) return StatMetricId::UcRms;
|
||
if (p.phase_id == ID_PHASE_AB) return StatMetricId::UabRms;
|
||
if (p.phase_id == ID_PHASE_BC) return StatMetricId::UbcRms;
|
||
if (p.phase_id == ID_PHASE_CA) return StatMetricId::UcaRms;
|
||
break;
|
||
|
||
case StatFamily::CurrentRms:
|
||
if (p.phase_id == ID_PHASE_AN) return StatMetricId::IaRms;
|
||
if (p.phase_id == ID_PHASE_BN) return StatMetricId::IbRms;
|
||
if (p.phase_id == ID_PHASE_CN) return StatMetricId::IcRms;
|
||
break;
|
||
|
||
case StatFamily::VoltageDeviation:
|
||
if (connection_kind == ParsedConnectionKind::Wye || connection_kind == ParsedConnectionKind::Unknown)
|
||
{
|
||
if (p.phase_id == ID_PHASE_AN) return StatMetricId::UaDeviation;
|
||
if (p.phase_id == ID_PHASE_BN) return StatMetricId::UbDeviation;
|
||
if (p.phase_id == ID_PHASE_CN) return StatMetricId::UcDeviation;
|
||
}
|
||
if (connection_kind == ParsedConnectionKind::Delta || connection_kind == ParsedConnectionKind::Unknown)
|
||
{
|
||
if (p.phase_id == ID_PHASE_AB) return StatMetricId::UabDeviation;
|
||
if (p.phase_id == ID_PHASE_BC) return StatMetricId::UbcDeviation;
|
||
if (p.phase_id == ID_PHASE_CA) return StatMetricId::UcaDeviation;
|
||
}
|
||
break;
|
||
|
||
case StatFamily::VoltageZeroSequence:
|
||
return StatMetricId::UZeroSeq;
|
||
|
||
case StatFamily::VoltageNegativeSequence:
|
||
return StatMetricId::UNegSeq;
|
||
|
||
case StatFamily::VoltagePositiveSequence:
|
||
return StatMetricId::UPosSeq;
|
||
|
||
case StatFamily::VoltageNegativeSequenceUnbalance:
|
||
return StatMetricId::UNegSeqUnbalance;
|
||
|
||
case StatFamily::CurrentZeroSequence:
|
||
return StatMetricId::IZeroSeq;
|
||
|
||
case StatFamily::CurrentNegativeSequence:
|
||
return StatMetricId::INegSeq;
|
||
|
||
case StatFamily::CurrentPositiveSequence:
|
||
return StatMetricId::IPosSeq;
|
||
|
||
case StatFamily::CurrentNegativeSequenceUnbalance:
|
||
return StatMetricId::INegSeqUnbalance;
|
||
|
||
case StatFamily::Frequency:
|
||
return StatMetricId::Frequency;
|
||
|
||
case StatFamily::FrequencyDeviation:
|
||
matched_by_name_fallback = true;
|
||
return StatMetricId::FrequencyDeviation;
|
||
|
||
default:
|
||
break;
|
||
}
|
||
|
||
// 部分厂家文件可能没有正确填写序分量的 QuantityCharacteristicID,
|
||
// 但通道名/量名会包含 Pos/Neg/Zero/Sequence/S2S1 等信息;
|
||
// 这里只作为补充兜底,优先级低于上面的 C.2 GUID 语义识别。
|
||
const std::string compact_text =
|
||
stat_compact_upper(p.channel_name) + stat_compact_upper(p.quantity_name);
|
||
if (p.quantity_measured_id == ID_QM_VOLTAGE)
|
||
{
|
||
if (stat_contains_any_alias(compact_text, {
|
||
"S2S1", "NEGATIVESEQUENCEUNBALANCE", "NEGSEQUNBALANCE",
|
||
"NEGATIVEUNBALANCE", "NEGUNBALANCE", "VOLTAGEUNBALANCE",
|
||
"VUNBALANCE", "VOLTAGEIMBALANCE"
|
||
}))
|
||
{
|
||
matched_by_name_fallback = true;
|
||
return StatMetricId::UNegSeqUnbalance;
|
||
}
|
||
|
||
if (stat_contains_any_alias(compact_text, {
|
||
"ZEROSEQUENCE", "ZEROSEQ", "ZEROSQ", "SZERO",
|
||
"VZEROSEQ", "UZEROSEQ", "V0", "U0"
|
||
}))
|
||
{
|
||
matched_by_name_fallback = true;
|
||
return StatMetricId::UZeroSeq;
|
||
}
|
||
|
||
if (stat_contains_any_alias(compact_text, {
|
||
"NEGATIVESEQUENCE", "NEGSEQ", "NEGSQ", "SNEG",
|
||
"VNEGSEQ", "UNEGSEQ", "V2", "U2"
|
||
}))
|
||
{
|
||
matched_by_name_fallback = true;
|
||
return StatMetricId::UNegSeq;
|
||
}
|
||
|
||
if (stat_contains_any_alias(compact_text, {
|
||
"POSITIVESEQUENCE", "POSSEQ", "POSSQ", "SPOS",
|
||
"VPOSSEQ", "UPOSSEQ", "V1", "U1"
|
||
}))
|
||
{
|
||
matched_by_name_fallback = true;
|
||
return StatMetricId::UPosSeq;
|
||
}
|
||
}
|
||
|
||
// 电流序分量使用同一组序分量 characteristic,但 QuantityMeasured 为 CURRENT;
|
||
// 部分厂家仍可能只在通道名里写 I SPos / I SNeg / I SZero / I S2S1,因此也做名称兜底。
|
||
if (p.quantity_measured_id == ID_QM_CURRENT)
|
||
{
|
||
if (stat_contains_any_alias(compact_text, {
|
||
"IS2S1", "CURRENTS2S1", "CURRENTNEGATIVESEQUENCEUNBALANCE",
|
||
"CURRENTNEGSEQUNBALANCE", "INEGSEQUNBALANCE", "NEGSEQUNBALANCEI",
|
||
"CURRENTUNBALANCE", "IUNBALANCE", "CURRENTIMBALANCE", "IIMBALANCE"
|
||
}))
|
||
{
|
||
matched_by_name_fallback = true;
|
||
return StatMetricId::INegSeqUnbalance;
|
||
}
|
||
|
||
if (stat_contains_any_alias(compact_text, {
|
||
"CURRENTZEROSEQUENCE", "CURRENTZEROSEQ", "IZEROSEQUENCE", "IZEROSEQ",
|
||
"ISZERO", "IZEROSQ", "I0"
|
||
}))
|
||
{
|
||
matched_by_name_fallback = true;
|
||
return StatMetricId::IZeroSeq;
|
||
}
|
||
|
||
if (stat_contains_any_alias(compact_text, {
|
||
"CURRENTNEGATIVESEQUENCE", "CURRENTNEGSEQ", "INEGATIVESEQUENCE", "INEGSEQ",
|
||
"ISNEG", "INEGSQ", "I2"
|
||
}))
|
||
{
|
||
matched_by_name_fallback = true;
|
||
return StatMetricId::INegSeq;
|
||
}
|
||
|
||
if (stat_contains_any_alias(compact_text, {
|
||
"CURRENTPOSITIVESEQUENCE", "CURRENTPOSSEQ", "IPOSITIVESEQUENCE", "IPOSSEQ",
|
||
"ISPOS", "IPOSSQ", "I1"
|
||
}))
|
||
{
|
||
matched_by_name_fallback = true;
|
||
return StatMetricId::IPosSeq;
|
||
}
|
||
}
|
||
|
||
return StatMetricId::Unknown;
|
||
}
|
||
|
||
struct StatMetricSourceKey
|
||
{
|
||
int observation_index = -1;
|
||
int channel_instance_index = -1;
|
||
|
||
bool operator<(const StatMetricSourceKey& other) const
|
||
{
|
||
if (observation_index != other.observation_index)
|
||
return observation_index < other.observation_index;
|
||
return channel_instance_index < other.channel_instance_index;
|
||
}
|
||
|
||
bool operator==(const StatMetricSourceKey& other) const
|
||
{
|
||
return observation_index == other.observation_index &&
|
||
channel_instance_index == other.channel_instance_index;
|
||
}
|
||
};
|
||
|
||
struct StatMetricSourceStats
|
||
{
|
||
StatMetricId metric_id = StatMetricId::Unknown;
|
||
StatMetricSourceKey key;
|
||
std::string observation_name;
|
||
std::string channel_name;
|
||
std::string quantity_name;
|
||
unsigned int phase_id = 0;
|
||
unsigned int quantity_measured_id = 0;
|
||
unsigned int quantity_units_id = 0;
|
||
PqdifGuidValue quantity_characteristic_id;
|
||
int channel_def_index = -1;
|
||
|
||
size_t point_count = 0;
|
||
size_t non_zero_count = 0;
|
||
double first_value = 0.0;
|
||
double min_value = 0.0;
|
||
double max_value = 0.0;
|
||
double sum_abs_value = 0.0;
|
||
bool has_first = false;
|
||
bool has_min_kind = false;
|
||
bool has_max_kind = false;
|
||
bool has_avg_kind = false;
|
||
bool has_p95_kind = false;
|
||
|
||
void add(const ExpandedStatPoint& p)
|
||
{
|
||
if (!has_first)
|
||
{
|
||
has_first = true;
|
||
first_value = p.value;
|
||
min_value = p.value;
|
||
max_value = p.value;
|
||
metric_id = p.metric_id;
|
||
key.observation_index = p.observation_index;
|
||
key.channel_instance_index = p.channel_instance_index;
|
||
observation_name = p.observation_name;
|
||
channel_name = p.channel_name;
|
||
quantity_name = p.quantity_name;
|
||
phase_id = p.phase_id;
|
||
quantity_measured_id = p.quantity_measured_id;
|
||
quantity_units_id = p.quantity_units_id;
|
||
quantity_characteristic_id = p.quantity_characteristic_id;
|
||
channel_def_index = p.channel_def_index;
|
||
}
|
||
|
||
++point_count;
|
||
if (std::fabs(p.value) > 1e-12)
|
||
++non_zero_count;
|
||
if (p.value < min_value)
|
||
min_value = p.value;
|
||
if (p.value > max_value)
|
||
max_value = p.value;
|
||
sum_abs_value += std::fabs(p.value);
|
||
|
||
switch (p.stat_kind)
|
||
{
|
||
case StatValueKind::Min: has_min_kind = true; break;
|
||
case StatValueKind::Max: has_max_kind = true; break;
|
||
case StatValueKind::Avg: has_avg_kind = true; break;
|
||
case StatValueKind::P95: has_p95_kind = true; break;
|
||
default: break;
|
||
}
|
||
}
|
||
|
||
double avg_abs_value() const
|
||
{
|
||
if (point_count == 0)
|
||
return 0.0;
|
||
return sum_abs_value / static_cast<double>(point_count);
|
||
}
|
||
|
||
bool all_zero() const
|
||
{
|
||
return point_count > 0 && non_zero_count == 0;
|
||
}
|
||
};
|
||
|
||
struct StatMetricQualityInfo
|
||
{
|
||
StatMetricQuality quality = StatMetricQuality::Normal;
|
||
std::string reason;
|
||
StatMetricSourceKey source_key;
|
||
std::string source_channel_name;
|
||
};
|
||
|
||
StatMetricSourceKey stat_make_source_key(const ExpandedStatPoint& p)
|
||
{
|
||
StatMetricSourceKey key;
|
||
key.observation_index = p.observation_index;
|
||
key.channel_instance_index = p.channel_instance_index;
|
||
return key;
|
||
}
|
||
|
||
bool stat_metric_has_alias(StatMetricId metric_id, const std::string& compact_channel)
|
||
{
|
||
if (stat_is_voltage_harmonic_metric(metric_id))
|
||
{
|
||
const int order = stat_voltage_harmonic_order(metric_id);
|
||
const int phase = stat_voltage_harmonic_phase_index(metric_id);
|
||
const std::string n = std::to_string(order);
|
||
const std::string nn = stat_two_digit(order);
|
||
const char p = phase == 0 ? 'A' : (phase == 1 ? 'B' : 'C');
|
||
std::string phase_text(1, p);
|
||
|
||
std::vector<std::string> aliases = {
|
||
"VHARM" + n + phase_text,
|
||
"VHARM" + nn + phase_text,
|
||
"VHARMONIC" + n + phase_text,
|
||
"VHARMONIC" + nn + phase_text,
|
||
"VHRMS" + n + phase_text,
|
||
"VHRMS" + nn + phase_text,
|
||
"UHARM" + n + phase_text,
|
||
"UHARM" + nn + phase_text,
|
||
"UHARMONIC" + n + phase_text,
|
||
"UHARMONIC" + nn + phase_text,
|
||
"UHRMS" + n + phase_text,
|
||
"UHRMS" + nn + phase_text,
|
||
"H" + n + phase_text,
|
||
"H" + nn + phase_text,
|
||
"HARM" + n + phase_text,
|
||
"HARM" + nn + phase_text,
|
||
"HARMONIC" + n + phase_text,
|
||
"HARMONIC" + nn + phase_text
|
||
};
|
||
return stat_contains_any_alias(compact_channel, aliases);
|
||
}
|
||
|
||
std::vector<std::string> aliases;
|
||
switch (metric_id)
|
||
{
|
||
case StatMetricId::UaRms:
|
||
aliases = { "VRMSA", "VARMS", "VAN", "UA", "URMSA" };
|
||
break;
|
||
case StatMetricId::UbRms:
|
||
aliases = { "VRMSB", "VBRMS", "VBN", "UB", "URMSB" };
|
||
break;
|
||
case StatMetricId::UcRms:
|
||
aliases = { "VRMSC", "VCRMS", "VCN", "UC", "URMSC" };
|
||
break;
|
||
case StatMetricId::IaRms:
|
||
aliases = { "IRMSA", "IARMS", "IA" };
|
||
break;
|
||
case StatMetricId::IbRms:
|
||
aliases = { "IRMSB", "IBRMS", "IB" };
|
||
break;
|
||
case StatMetricId::IcRms:
|
||
aliases = { "IRMSC", "ICRMS", "IC" };
|
||
break;
|
||
case StatMetricId::UabRms:
|
||
aliases = { "VRMSAB", "VABRMS", "VAB", "UAB", "URMSAB" };
|
||
break;
|
||
case StatMetricId::UbcRms:
|
||
aliases = { "VRMSBC", "VBCRMS", "VBC", "UBC", "URMSBC" };
|
||
break;
|
||
case StatMetricId::UcaRms:
|
||
aliases = { "VRMSCA", "VCARMS", "VCA", "UCA", "URMSCA" };
|
||
break;
|
||
case StatMetricId::UaDeviation:
|
||
aliases = { "UADEVIATION", "VADEVIATION", "VDEVA", "DEVA" };
|
||
break;
|
||
case StatMetricId::UbDeviation:
|
||
aliases = { "UBDEVIATION", "VBDEVIATION", "VDEVB", "DEVB" };
|
||
break;
|
||
case StatMetricId::UcDeviation:
|
||
aliases = { "UCDEVIATION", "VCDEVIATION", "VDEVC", "DEVC" };
|
||
break;
|
||
case StatMetricId::UabDeviation:
|
||
aliases = { "UABDEVIATION", "VABDEVIATION", "VDEVAB", "DEVAB" };
|
||
break;
|
||
case StatMetricId::UbcDeviation:
|
||
aliases = { "UBCDEVIATION", "VBCDEVIATION", "VDEVBC", "DEVBC" };
|
||
break;
|
||
case StatMetricId::UcaDeviation:
|
||
aliases = { "UCADEVIATION", "VCADEVIATION", "VDEVCA", "DEVCA" };
|
||
break;
|
||
case StatMetricId::Frequency:
|
||
aliases = { "FREQUENCY", "FREQ", "HZ" };
|
||
break;
|
||
case StatMetricId::FrequencyDeviation:
|
||
aliases = { "FREQDEV", "FREQUENCYDEV", "DELTAF", "FDEV" };
|
||
break;
|
||
case StatMetricId::UZeroSeq:
|
||
aliases = { "ZEROSEQUENCE", "ZEROSEQ", "ZEROSQ", "SZERO", "VZEROSEQ", "UZEROSEQ", "V0", "U0" };
|
||
break;
|
||
case StatMetricId::UNegSeq:
|
||
aliases = { "NEGATIVESEQUENCE", "NEGSEQ", "NEGSQ", "SNEG", "VNEGSEQ", "UNEGSEQ", "V2", "U2" };
|
||
break;
|
||
case StatMetricId::UPosSeq:
|
||
aliases = { "POSITIVESEQUENCE", "POSSEQ", "POSSQ", "SPOS", "VPOSSEQ", "UPOSSEQ", "V1", "U1" };
|
||
break;
|
||
case StatMetricId::UNegSeqUnbalance:
|
||
aliases = { "S2S1", "NEGATIVESEQUENCEUNBALANCE", "NEGSEQUNBALANCE", "NEGATIVEUNBALANCE", "NEGUNBALANCE", "VOLTAGEUNBALANCE", "VUNBALANCE", "VOLTAGEIMBALANCE" };
|
||
break;
|
||
case StatMetricId::IZeroSeq:
|
||
aliases = { "CURRENTZEROSEQUENCE", "CURRENTZEROSEQ", "IZEROSEQUENCE", "IZEROSEQ", "ISZERO", "IZEROSQ", "I0" };
|
||
break;
|
||
case StatMetricId::INegSeq:
|
||
aliases = { "CURRENTNEGATIVESEQUENCE", "CURRENTNEGSEQ", "INEGATIVESEQUENCE", "INEGSEQ", "ISNEG", "INEGSQ", "I2" };
|
||
break;
|
||
case StatMetricId::IPosSeq:
|
||
aliases = { "CURRENTPOSITIVESEQUENCE", "CURRENTPOSSEQ", "IPOSITIVESEQUENCE", "IPOSSEQ", "ISPOS", "IPOSSQ", "I1" };
|
||
break;
|
||
case StatMetricId::INegSeqUnbalance:
|
||
aliases = { "IS2S1", "CURRENTS2S1", "CURRENTNEGATIVESEQUENCEUNBALANCE", "CURRENTNEGSEQUNBALANCE", "INEGSEQUNBALANCE", "NEGSEQUNBALANCEI", "CURRENTUNBALANCE", "IUNBALANCE", "CURRENTIMBALANCE", "IIMBALANCE" };
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
|
||
return stat_contains_any_alias(compact_channel, aliases);
|
||
}
|
||
|
||
int stat_metric_source_score(const StatMetricSourceStats& s)
|
||
{
|
||
int score = 0;
|
||
const std::string compact_channel = stat_compact_upper(s.channel_name);
|
||
const std::string compact_quantity = stat_compact_upper(s.quantity_name);
|
||
|
||
if (stat_metric_has_alias(s.metric_id, compact_channel))
|
||
score += 300;
|
||
if (stat_metric_has_alias(s.metric_id, compact_quantity))
|
||
score += 80;
|
||
|
||
if (s.has_min_kind) score += 10;
|
||
if (s.has_max_kind) score += 10;
|
||
if (s.has_avg_kind) score += 10;
|
||
if (s.has_p95_kind) score += 10;
|
||
|
||
// 统计点数越完整越优先,但不要让点数压过名称/语义。
|
||
if (s.point_count > 0)
|
||
score += static_cast<int>(std::min<size_t>(s.point_count / 100, 50));
|
||
|
||
// 电压/频率一般不应全为 0;但电流、零序/负序分量、负序不平衡为 0 在业务上可能是有效状态,
|
||
// 所以这些指标不在来源择优阶段扣分,只在质量状态中显式标记。
|
||
if (s.all_zero() && !stat_allow_all_zero_without_score_penalty(s.metric_id))
|
||
score -= 200;
|
||
|
||
// 线电压 RMS 若是毫伏级,极大概率不是线电压有效值,但仍保留为候选并标记质量,
|
||
// 这里仅降低其优先级,避免有更可信来源时被选中。
|
||
if (stat_is_line_voltage_metric(s.metric_id) && s.avg_abs_value() < 1.0)
|
||
score -= 500;
|
||
|
||
// 频率值通常应在合理范围;异常值降低优先级。
|
||
if (stat_is_frequency_metric(s.metric_id))
|
||
{
|
||
const double avg_abs = s.avg_abs_value();
|
||
if (avg_abs > 0.0 && (avg_abs < 1.0 || avg_abs > 1000.0))
|
||
score -= 200;
|
||
}
|
||
|
||
return score;
|
||
}
|
||
|
||
void stat_print_stream_stats_line(
|
||
const char* prefix,
|
||
const StatMetricSourceStats& s,
|
||
int score,
|
||
const char* extra_text)
|
||
{
|
||
std::cout << prefix
|
||
<< " metric=" << stat_metric_name(s.metric_id)
|
||
<< ", ch=" << s.key.channel_instance_index
|
||
<< ", ch_def=" << s.channel_def_index
|
||
<< ", channel=" << s.channel_name
|
||
<< ", phase=" << pqdif_sem::FindPhaseName(s.phase_id)
|
||
<< ", measured=" << pqdif_sem::FindQuantityMeasuredName(s.quantity_measured_id)
|
||
<< ", unit=" << pqdif_sem::FindQuantityUnitsName(s.quantity_units_id)
|
||
<< ", characteristic=" << short_guid_name(s.quantity_characteristic_id)
|
||
<< ", points=" << s.point_count
|
||
<< ", first=" << s.first_value
|
||
<< ", min=" << s.min_value
|
||
<< ", max=" << s.max_value
|
||
<< ", avg_abs=" << s.avg_abs_value()
|
||
<< ", kinds="
|
||
<< (s.has_min_kind ? "Min" : "-") << "/"
|
||
<< (s.has_max_kind ? "Max" : "-") << "/"
|
||
<< (s.has_avg_kind ? "Avg" : "-") << "/"
|
||
<< (s.has_p95_kind ? "P95" : "-")
|
||
<< ", score=" << score;
|
||
if (extra_text != nullptr && extra_text[0] != '\0')
|
||
std::cout << ", " << extra_text;
|
||
std::cout << std::endl;
|
||
}
|
||
|
||
/// @brief 对同一 metric 的多来源候选流进行择优,避免进入聚合层后静默覆盖。
|
||
/// @details
|
||
/// 保留同一 metric 最可信的一个通道来源;详细模式下打印所有候选来源。
|
||
/// 核心模式只打印汇总和必要的重复来源提示,避免谐波指标造成海量日志。
|
||
std::vector<ExpandedStatPoint> stat_select_best_metric_sources(
|
||
const std::vector<ExpandedStatPoint>& points)
|
||
{
|
||
typedef std::map<StatMetricSourceKey, StatMetricSourceStats> SourceMap;
|
||
std::map<StatMetricId, SourceMap> by_metric;
|
||
|
||
for (const auto& p : points)
|
||
{
|
||
StatMetricSourceKey key = stat_make_source_key(p);
|
||
StatMetricSourceStats& stats = by_metric[p.metric_id][key];
|
||
stats.add(p);
|
||
}
|
||
|
||
std::map<StatMetricId, StatMetricSourceKey> selected_source;
|
||
const bool detail_log = pqdif_is_detail_log_enabled();
|
||
size_t selected_metric_count = 0;
|
||
size_t selected_voltage_harmonic_count = 0;
|
||
size_t selected_dynamic_metric_count = 0;
|
||
size_t duplicate_metric_count = 0;
|
||
|
||
std::cout << "========== STAT STREAM SOURCE SUMMARY ==========" << std::endl;
|
||
std::cout << "candidate_metric_count=" << by_metric.size()
|
||
<< ", candidate_point_count=" << points.size()
|
||
<< ", detail_log=" << (detail_log ? "on" : "off")
|
||
<< std::endl;
|
||
|
||
for (auto& metric_pair : by_metric)
|
||
{
|
||
const StatMetricId metric_id = metric_pair.first;
|
||
SourceMap& sources = metric_pair.second;
|
||
|
||
bool has_selected = false;
|
||
StatMetricSourceKey best_key;
|
||
int best_score = std::numeric_limits<int>::min();
|
||
|
||
for (auto& src_pair : sources)
|
||
{
|
||
const int score = stat_metric_source_score(src_pair.second);
|
||
if (!has_selected || score > best_score)
|
||
{
|
||
has_selected = true;
|
||
best_score = score;
|
||
best_key = src_pair.first;
|
||
}
|
||
}
|
||
|
||
if (has_selected)
|
||
{
|
||
selected_source[metric_id] = best_key;
|
||
++selected_metric_count;
|
||
if (stat_is_voltage_harmonic_metric(metric_id))
|
||
++selected_voltage_harmonic_count;
|
||
if (stat_is_dynamic_metric(metric_id))
|
||
++selected_dynamic_metric_count;
|
||
}
|
||
|
||
if (sources.size() > 1)
|
||
{
|
||
++duplicate_metric_count;
|
||
if (pqdif_log_enabled(PqdifLogLevel::Info))
|
||
{
|
||
std::cout << " [DUPLICATE METRIC SOURCES] metric="
|
||
<< stat_metric_name(metric_id)
|
||
<< ", source_count=" << sources.size()
|
||
<< ", dynamic=" << (stat_is_dynamic_metric(metric_id) ? "true" : "false")
|
||
<< ", action=select_best_source_and_drop_others"
|
||
<< std::endl;
|
||
}
|
||
}
|
||
|
||
// 只有 Debug/Trace 才展开每个 selected/dropped source。Core/Info 保持短日志,
|
||
// 避免一个谱类指标重复 50 个来源时刷屏。
|
||
if (detail_log)
|
||
{
|
||
for (auto& src_pair : sources)
|
||
{
|
||
const int score = stat_metric_source_score(src_pair.second);
|
||
const bool selected = has_selected && (src_pair.first == best_key);
|
||
stat_print_stream_stats_line(
|
||
selected ? " [STREAM SELECTED]" : " [STREAM DROPPED]",
|
||
src_pair.second,
|
||
score,
|
||
selected ? "selected=true" : "reason=duplicate_or_lower_score");
|
||
}
|
||
}
|
||
}
|
||
|
||
std::cout << " [STAT STREAM CORE] selected_metric_count=" << selected_metric_count
|
||
<< ", selected_dynamic_metrics=" << selected_dynamic_metric_count << "/" << stat_all_dynamic_metric_order().size()
|
||
<< ", selected_voltage_harmonics=" << selected_voltage_harmonic_count << "/147"
|
||
<< ", duplicate_metric_count=" << duplicate_metric_count
|
||
<< std::endl;
|
||
std::cout << "================================================" << std::endl;
|
||
|
||
std::vector<ExpandedStatPoint> out;
|
||
out.reserve(points.size());
|
||
for (const auto& p : points)
|
||
{
|
||
const auto it = selected_source.find(p.metric_id);
|
||
if (it == selected_source.end())
|
||
continue;
|
||
if (stat_make_source_key(p) == it->second)
|
||
out.push_back(p);
|
||
}
|
||
return out;
|
||
}
|
||
|
||
std::map<StatMetricId, StatMetricSourceStats> stat_collect_metric_stats(
|
||
const std::vector<ExpandedStatPoint>& points)
|
||
{
|
||
std::map<StatMetricId, StatMetricSourceStats> out;
|
||
for (const auto& p : points)
|
||
{
|
||
StatMetricSourceStats& stats = out[p.metric_id];
|
||
stats.add(p);
|
||
}
|
||
return out;
|
||
}
|
||
|
||
std::map<StatMetricId, StatMetricQualityInfo> stat_analyze_metric_quality(
|
||
const std::vector<ExpandedStatPoint>& points)
|
||
{
|
||
std::map<StatMetricId, StatMetricQualityInfo> out;
|
||
std::map<StatMetricId, StatMetricSourceStats> stats_by_metric = stat_collect_metric_stats(points);
|
||
|
||
double phase_voltage_avg_sum = 0.0;
|
||
int phase_voltage_avg_count = 0;
|
||
const StatMetricId phase_metrics[3] = {
|
||
StatMetricId::UaRms,
|
||
StatMetricId::UbRms,
|
||
StatMetricId::UcRms
|
||
};
|
||
for (int i = 0; i < 3; ++i)
|
||
{
|
||
const auto it = stats_by_metric.find(phase_metrics[i]);
|
||
if (it != stats_by_metric.end() && it->second.avg_abs_value() > 1.0)
|
||
{
|
||
phase_voltage_avg_sum += it->second.avg_abs_value();
|
||
++phase_voltage_avg_count;
|
||
}
|
||
}
|
||
const double phase_voltage_avg =
|
||
phase_voltage_avg_count > 0 ? phase_voltage_avg_sum / static_cast<double>(phase_voltage_avg_count) : 0.0;
|
||
|
||
for (const auto& metric_pair : stats_by_metric)
|
||
{
|
||
const StatMetricId metric_id = metric_pair.first;
|
||
const StatMetricSourceStats& s = metric_pair.second;
|
||
|
||
StatMetricQualityInfo qi;
|
||
qi.source_key = s.key;
|
||
qi.source_channel_name = s.channel_name;
|
||
qi.quality = StatMetricQuality::Normal;
|
||
qi.reason = "ok";
|
||
|
||
if (s.all_zero())
|
||
{
|
||
qi.quality = StatMetricQuality::AllZero;
|
||
qi.reason = "all selected values are zero";
|
||
}
|
||
else if (stat_is_phase_voltage_metric(metric_id) && s.avg_abs_value() < 1.0)
|
||
{
|
||
qi.quality = StatMetricQuality::SuspiciousRange;
|
||
qi.reason = "phase voltage RMS avg_abs < 1V";
|
||
}
|
||
else if (stat_is_line_voltage_metric(metric_id))
|
||
{
|
||
const double line_avg = s.avg_abs_value();
|
||
bool suspicious = false;
|
||
std::ostringstream reason;
|
||
|
||
if (line_avg < 1.0)
|
||
{
|
||
suspicious = true;
|
||
reason << "line voltage RMS avg_abs < 1V";
|
||
}
|
||
|
||
if (phase_voltage_avg > 1.0)
|
||
{
|
||
const double ratio = line_avg / phase_voltage_avg;
|
||
if (ratio < 1.25 || ratio > 2.20)
|
||
{
|
||
if (suspicious)
|
||
reason << "; ";
|
||
suspicious = true;
|
||
reason << "line/phase ratio=" << ratio
|
||
<< " outside expected Wye range [1.25,2.20]";
|
||
}
|
||
}
|
||
|
||
if (suspicious)
|
||
{
|
||
qi.quality = StatMetricQuality::SuspiciousRange;
|
||
qi.reason = reason.str();
|
||
}
|
||
}
|
||
else if (stat_is_dynamic_metric(metric_id))
|
||
{
|
||
// 谐波/间谐波/相角类指标可以为 0 或很小;这里只标记明显量级异常。
|
||
const double avg_abs = s.avg_abs_value();
|
||
if (avg_abs > 1000000.0)
|
||
{
|
||
qi.quality = StatMetricQuality::SuspiciousRange;
|
||
qi.reason = "dynamic spectrum metric avg_abs > 1e6";
|
||
}
|
||
}
|
||
else if (stat_is_voltage_sequence_component_metric(metric_id))
|
||
{
|
||
// 序分量可以很小,尤其是零序/负序;这里只对明显不合理的大幅值做提示,
|
||
// 不使用 <1V 判定,避免把健康系统的低负序/低零序误标为异常。
|
||
const double avg_abs = s.avg_abs_value();
|
||
if (avg_abs > 1000000.0)
|
||
{
|
||
qi.quality = StatMetricQuality::SuspiciousRange;
|
||
qi.reason = "voltage sequence component avg_abs > 1e6 V";
|
||
}
|
||
}
|
||
else if (stat_is_voltage_sequence_unbalance_metric(metric_id))
|
||
{
|
||
const double avg_abs = s.avg_abs_value();
|
||
if (avg_abs > 1000.0)
|
||
{
|
||
qi.quality = StatMetricQuality::SuspiciousRange;
|
||
qi.reason = "voltage negative sequence unbalance avg_abs > 1000";
|
||
}
|
||
}
|
||
else if (stat_is_current_sequence_component_metric(metric_id))
|
||
{
|
||
// 电流序分量可以为 0 或很小;这里只对明显异常的大幅值做提示。
|
||
const double avg_abs = s.avg_abs_value();
|
||
if (avg_abs > 1000000.0)
|
||
{
|
||
qi.quality = StatMetricQuality::SuspiciousRange;
|
||
qi.reason = "current sequence component avg_abs > 1e6 A";
|
||
}
|
||
}
|
||
else if (stat_is_current_sequence_unbalance_metric(metric_id))
|
||
{
|
||
const double avg_abs = s.avg_abs_value();
|
||
if (avg_abs > 1000.0)
|
||
{
|
||
qi.quality = StatMetricQuality::SuspiciousRange;
|
||
qi.reason = "current negative sequence unbalance avg_abs > 1000";
|
||
}
|
||
}
|
||
else if (metric_id == StatMetricId::Frequency)
|
||
{
|
||
const double avg_abs = s.avg_abs_value();
|
||
if (avg_abs < 1.0 || avg_abs > 1000.0)
|
||
{
|
||
qi.quality = StatMetricQuality::SuspiciousRange;
|
||
qi.reason = "frequency avg_abs outside [1,1000] Hz";
|
||
}
|
||
}
|
||
|
||
out[metric_id] = qi;
|
||
}
|
||
|
||
return out;
|
||
}
|
||
|
||
const StatMetricQualityInfo* stat_find_quality_info(
|
||
const std::map<StatMetricId, StatMetricQualityInfo>& quality_by_metric,
|
||
StatMetricId metric_id)
|
||
{
|
||
const auto it = quality_by_metric.find(metric_id);
|
||
if (it == quality_by_metric.end())
|
||
return nullptr;
|
||
return &it->second;
|
||
}
|
||
|
||
/// @brief 将一个 observation 中的单个通道实例展开为若干统计样本点。
|
||
|
||
|
||
bool stat_channel_has_explicit_harmonic_group_id(const PqdifChannelInstance& ch)
|
||
{
|
||
return ch.channel_group_id >= 2 && ch.channel_group_id <= 50;
|
||
}
|
||
|
||
bool stat_same_spectrum_channel_family(
|
||
const PqdifChannelInstance& a,
|
||
const PqdifChannelInstance& b)
|
||
{
|
||
if (a.channel_def_index != b.channel_def_index)
|
||
return false;
|
||
if (a.phase_id != b.phase_id)
|
||
return false;
|
||
if (a.quantity_measured_id != b.quantity_measured_id)
|
||
return false;
|
||
return stat_compact_upper(a.channel_name) == stat_compact_upper(b.channel_name);
|
||
}
|
||
|
||
int stat_make_channel_spectrum_order_hint(
|
||
const PqdifObservationRecord& obs,
|
||
const PqdifChannelInstance& ch,
|
||
int& block_offset,
|
||
int& block_size)
|
||
{
|
||
block_offset = -1;
|
||
block_size = 0;
|
||
|
||
// 已经有明确 group=2..50 时,必须优先走标准字段,不使用顺序推导。
|
||
if (stat_channel_has_explicit_harmonic_group_id(ch))
|
||
return -1;
|
||
|
||
int offset = 0;
|
||
for (const auto& other : obs.channel_instances)
|
||
{
|
||
if (!stat_same_spectrum_channel_family(other, ch))
|
||
continue;
|
||
|
||
// 只在“同一通道族内 group/base/nominal 缺失”的谱线中按顺序补次数。
|
||
// 这样不会把已经有标准 group 的通道也混入顺序计算。
|
||
if (stat_channel_has_explicit_harmonic_group_id(other))
|
||
continue;
|
||
|
||
if (other.channel_instance_index == ch.channel_instance_index)
|
||
block_offset = offset;
|
||
|
||
++offset;
|
||
++block_size;
|
||
}
|
||
|
||
if (block_offset < 0)
|
||
return -1;
|
||
|
||
// 2-50 次谐波通常有 49 条谱线;个别厂家会额外带一个总量或占位,
|
||
// 所以允许 >=49,但只映射前 49 条为 2..50。
|
||
if (block_size < 49)
|
||
return -1;
|
||
|
||
const int order = block_offset + 2;
|
||
return (order >= 2 && order <= 50) ? order : -1;
|
||
}
|
||
|
||
/// @details
|
||
/// 处理内容:
|
||
/// 1) 自动解析共享时间轴;
|
||
/// 2) 自动完成工程值还原;
|
||
/// 3) 依据 C.2 表语义 + 相别 + 值类型 识别业务指标。
|
||
/// @param lf 完整 PQDIF 逻辑对象。
|
||
/// @param connection_kind 当前文件接线方式。
|
||
/// @param obs 当前 observation。
|
||
/// @param ch 当前通道实例。
|
||
/// @return 当前通道展开出的统计样本点集合。
|
||
std::vector<ExpandedStatPoint> stat_expand_channel_points(
|
||
const PqdifLogicalFile& lf,
|
||
ParsedConnectionKind connection_kind,
|
||
const PqdifObservationRecord& obs,
|
||
const PqdifChannelInstance& ch)
|
||
{
|
||
std::vector<ExpandedStatPoint> out;
|
||
|
||
if (!pqdif_sem::IsQuantityTypeValueLog(ch.quantity_type_id.value))
|
||
return out;
|
||
|
||
const PqdifDataSourceRecord* ds = stat_find_related_data_source(lf, obs);
|
||
const PqdifChannelDefinition* ch_def = stat_find_channel_definition(ds, ch.channel_def_index);
|
||
|
||
const PqdifSeriesInstance* time_series = nullptr;
|
||
for (const auto& si : ch.series_instances)
|
||
{
|
||
if (pqdif_sem::IsValueTypeTime(si.value_type_id.value))
|
||
{
|
||
time_series = stat_resolve_shared_series(obs, si);
|
||
break;
|
||
}
|
||
}
|
||
|
||
// Pst/Plt/DVC 等窗口统计量在部分设备中只有“一个值序列”,甚至不写独立时间轴。
|
||
// 这里不再因为缺失时间轴直接丢弃整个通道;后面仅允许可识别的标量统计指标使用
|
||
// observation 的 start/create time 作为兜底时间,避免 Plt 通道存在但被静默忽略。
|
||
const bool has_usable_time_series = (time_series != nullptr && time_series->values.count > 0);
|
||
|
||
std::set<StatValueKind> explicit_stat_value_kinds;
|
||
for (const auto& stat_probe_si : ch.series_instances)
|
||
{
|
||
const PqdifSeriesDefinition* stat_probe_sd = stat_find_series_definition(ch_def, stat_probe_si.series_def_index);
|
||
const StatValueKind probe_kind = stat_identify_value_kind(stat_probe_si, stat_probe_sd);
|
||
if (probe_kind != StatValueKind::Unknown)
|
||
explicit_stat_value_kinds.insert(probe_kind);
|
||
}
|
||
const bool channel_has_single_or_no_stat_value_kind = explicit_stat_value_kinds.size() <= 1;
|
||
|
||
for (size_t i = 0; i < ch.series_instances.size(); ++i)
|
||
{
|
||
const auto& si = ch.series_instances[i];
|
||
const PqdifSeriesDefinition* sd = stat_find_series_definition(ch_def, si.series_def_index);
|
||
|
||
const StatValueKind stat_kind = stat_identify_value_kind(si, sd);
|
||
|
||
const PqdifSeriesInstance* resolved_value_series = stat_resolve_shared_series(obs, si);
|
||
if (resolved_value_series == nullptr || resolved_value_series->values.count <= 0)
|
||
continue;
|
||
|
||
const unsigned int storage_method_id = (sd != nullptr) ? sd->storage_method_id : 0;
|
||
const size_t value_count = static_cast<size_t>(std::max<long>(resolved_value_series->values.count, 0));
|
||
const size_t time_count = has_usable_time_series ?
|
||
static_cast<size_t>(std::max<long>(time_series->values.count, 0)) : value_count;
|
||
const size_t point_count = std::min(time_count, value_count);
|
||
|
||
for (size_t k = 0; k < point_count; ++k)
|
||
{
|
||
time_t ts = 0;
|
||
if (has_usable_time_series)
|
||
{
|
||
if (!stat_resolve_timestamp_at(obs, *time_series, k, ts))
|
||
continue;
|
||
}
|
||
else
|
||
{
|
||
// 单值统计通道无时间轴时,使用 observation 起始时间兜底;没有起始时间则用创建时间。
|
||
ts = (obs.time_start.unix_time != 0) ? obs.time_start.unix_time : obs.time_create.unix_time;
|
||
if (ts == 0)
|
||
ts = std::time(nullptr);
|
||
if (k > 0)
|
||
ts += static_cast<time_t>(k);
|
||
}
|
||
|
||
double raw_value = 0.0;
|
||
if (!stat_try_get_raw_numeric_at(resolved_value_series->values, k, raw_value))
|
||
continue;
|
||
|
||
ExpandedStatPoint p;
|
||
p.timestamp = ts;
|
||
p.timestamp_text = format_time_text(ts);
|
||
p.observation_index = obs.observation_index;
|
||
p.channel_instance_index = ch.channel_instance_index;
|
||
p.channel_def_index = ch.channel_def_index;
|
||
p.channel_group_id = ch.channel_group_id;
|
||
p.channel_spectrum_order_hint = stat_make_channel_spectrum_order_hint(
|
||
obs,
|
||
ch,
|
||
p.channel_spectrum_block_offset,
|
||
p.channel_spectrum_block_size);
|
||
p.series_instance_index = static_cast<int>(i);
|
||
p.series_def_index = si.series_def_index;
|
||
p.sample_index = static_cast<int>(k);
|
||
p.observation_name = obs.observation_name;
|
||
p.channel_name = ch.channel_name;
|
||
p.quantity_name = (ch_def != nullptr) ? ch_def->quantity_name : std::string();
|
||
p.phase_id = ch.phase_id;
|
||
p.quantity_type_id = ch.quantity_type_id;
|
||
p.quantity_measured_id = ch.quantity_measured_id;
|
||
p.quantity_units_id = si.quantity_units_id;
|
||
p.quantity_characteristic_id = si.quantity_characteristic_id;
|
||
p.value_type_id = si.value_type_id;
|
||
p.prob_percentile = (sd != nullptr) ? sd->prob_percentile : 0.0;
|
||
p.series_base_quantity = si.series_base_quantity;
|
||
p.nominal_quantity = si.nominal_quantity;
|
||
p.connection_kind = connection_kind;
|
||
p.stat_kind = stat_kind;
|
||
p.value = stat_decode_engineering_value(
|
||
raw_value,
|
||
*resolved_value_series,
|
||
storage_method_id,
|
||
resolved_value_series->values.physical_type);
|
||
p.metric_id = stat_identify_metric_id(connection_kind, p, p.matched_by_name_fallback);
|
||
|
||
if (p.metric_id == StatMetricId::Unknown)
|
||
continue;
|
||
|
||
const bool scalar_stat_metric =
|
||
stat_metric_can_use_scalar_series_as_all_kinds(p.metric_id) &&
|
||
stat_series_can_be_scalar_stat_value(si, sd);
|
||
|
||
// 没有时间轴的通道只允许 Pst/Plt/DVC 这类窗口标量指标走兜底,
|
||
// 防止其它普通统计通道在无时间轴时被错误展开。
|
||
if (!has_usable_time_series && !scalar_stat_metric)
|
||
continue;
|
||
|
||
// 修复 Pst/Plt:很多设备把 Pst/Plt 写成单个 Avg/Value 序列,而不是四条
|
||
// Min/Max/Avg/P95 序列。只要该通道只有单个统计序列或无明确统计值类型,就用同一物理值补齐四种
|
||
// 统计值,确保最终 bucket 与 METRIC STATUS 都能看到完整 max/min/avg/p95。
|
||
if (scalar_stat_metric && channel_has_single_or_no_stat_value_kind)
|
||
{
|
||
const StatValueKind fallback_kinds[] = {
|
||
StatValueKind::Min,
|
||
StatValueKind::Max,
|
||
StatValueKind::Avg,
|
||
StatValueKind::P95
|
||
};
|
||
for (const StatValueKind fallback_kind : fallback_kinds)
|
||
{
|
||
ExpandedStatPoint q = p;
|
||
q.stat_kind = fallback_kind;
|
||
out.push_back(std::move(q));
|
||
}
|
||
continue;
|
||
}
|
||
|
||
if (stat_kind != StatValueKind::Unknown)
|
||
{
|
||
out.push_back(std::move(p));
|
||
continue;
|
||
}
|
||
}
|
||
}
|
||
|
||
return out;
|
||
}
|
||
|
||
bool stat_metric_in_primary_targets(StatMetricId id)
|
||
{
|
||
const std::vector<StatMetricId>& targets = stat_primary_metric_print_order();
|
||
return std::find(targets.begin(), targets.end(), id) != targets.end();
|
||
}
|
||
|
||
/// @brief 从全部 observation 中补齐主 observation 缺失的目标指标。
|
||
/// @details
|
||
/// 统一 fallback 策略:所有指标都先从主统计 observation 解析;如果某个目标指标在主
|
||
/// observation 中完全找不到,则遍历其他 observations,直到找到该指标的数据来源。
|
||
/// 这样后续新增谐波、间谐波、电流谐波等指标时,不需要为每类指标写一套独立的
|
||
/// observation 查找流程。
|
||
std::vector<ExpandedStatPoint> stat_expand_missing_metrics_from_all_observations(
|
||
const PqdifLogicalFile& lf,
|
||
ParsedConnectionKind connection_kind,
|
||
const std::vector<ExpandedStatPoint>& primary_points,
|
||
int selected_observation_index)
|
||
{
|
||
std::vector<ExpandedStatPoint> out;
|
||
|
||
std::set<StatMetricId> present_metrics;
|
||
for (const auto& p : primary_points)
|
||
{
|
||
if (p.metric_id != StatMetricId::Unknown)
|
||
present_metrics.insert(p.metric_id);
|
||
}
|
||
|
||
std::set<StatMetricId> missing_targets;
|
||
for (const auto metric_id : stat_primary_metric_print_order())
|
||
{
|
||
if (present_metrics.find(metric_id) == present_metrics.end())
|
||
missing_targets.insert(metric_id);
|
||
}
|
||
|
||
if (missing_targets.empty())
|
||
return out;
|
||
|
||
std::map<StatMetricId, std::vector<ExpandedStatPoint>> fallback_by_metric;
|
||
|
||
for (const auto& obs : lf.observations)
|
||
{
|
||
if (obs.observation_index == selected_observation_index)
|
||
continue;
|
||
|
||
for (const auto& ch : obs.channel_instances)
|
||
{
|
||
auto points = stat_expand_channel_points(lf, connection_kind, obs, ch);
|
||
for (auto& p : points)
|
||
{
|
||
if (missing_targets.find(p.metric_id) == missing_targets.end())
|
||
continue;
|
||
fallback_by_metric[p.metric_id].push_back(std::move(p));
|
||
}
|
||
}
|
||
|
||
// 已经为所有缺失指标找到了候选点,可以停止继续扫后续 observation。
|
||
bool all_found = true;
|
||
for (const auto metric_id : missing_targets)
|
||
{
|
||
if (fallback_by_metric.find(metric_id) == fallback_by_metric.end())
|
||
{
|
||
all_found = false;
|
||
break;
|
||
}
|
||
}
|
||
if (all_found)
|
||
break;
|
||
}
|
||
|
||
for (const auto metric_id : stat_primary_metric_print_order())
|
||
{
|
||
const auto it = fallback_by_metric.find(metric_id);
|
||
if (it == fallback_by_metric.end())
|
||
continue;
|
||
out.insert(out.end(),
|
||
std::make_move_iterator(it->second.begin()),
|
||
std::make_move_iterator(it->second.end()));
|
||
}
|
||
|
||
if (!out.empty() && pqdif_log_enabled(PqdifLogLevel::Info))
|
||
{
|
||
size_t dynamic_metrics = 0;
|
||
size_t core_metrics = 0;
|
||
std::set<StatMetricId> loaded_metrics;
|
||
for (const auto& p : out)
|
||
loaded_metrics.insert(p.metric_id);
|
||
for (const auto metric_id : loaded_metrics)
|
||
{
|
||
if (stat_is_dynamic_metric(metric_id))
|
||
++dynamic_metrics;
|
||
else
|
||
++core_metrics;
|
||
}
|
||
|
||
std::cout << "[PQDIF] observation fallback loaded "
|
||
<< out.size() << " points for " << loaded_metrics.size()
|
||
<< " missing metrics"
|
||
<< " (core=" << core_metrics
|
||
<< ", dynamic_spectrum=" << dynamic_metrics << ")"
|
||
<< std::endl;
|
||
}
|
||
else if (out.empty() && pqdif_log_enabled(PqdifLogLevel::Debug))
|
||
{
|
||
std::cout << "[PQDIF] observation fallback: no missing target metric found in other observations"
|
||
<< std::endl;
|
||
}
|
||
|
||
return out;
|
||
}
|
||
|
||
bool stat_has_any_voltage_harmonic_points(const std::vector<ExpandedStatPoint>& points)
|
||
{
|
||
for (const auto& p : points)
|
||
{
|
||
if (stat_is_voltage_harmonic_metric(p.metric_id))
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/// @brief 展开统计样本点,并对缺失指标执行 observation fallback。
|
||
/// @details
|
||
/// 第一阶段仍以主统计 observation 为基础,避免普通 RMS/频率/序分量在多 observation 之间互相覆盖。
|
||
/// 第二阶段针对当前已知会出现在其他 observation 的三相电压谐波 RMS,遍历全部 observations 补充。
|
||
/// 后续新增间谐波、电流谐波等指标时,应优先复用这种“主 observation + 指标族 fallback”的模式。
|
||
std::vector<ExpandedStatPoint> stat_expand_selected_statistical_observation(
|
||
const PqdifLogicalFile& lf,
|
||
ParsedConnectionKind connection_kind,
|
||
int& selected_observation_index,
|
||
std::string& selected_observation_name)
|
||
{
|
||
selected_observation_index = -1;
|
||
selected_observation_name.clear();
|
||
|
||
std::vector<ExpandedStatPoint> out;
|
||
|
||
const PqdifObservationRecord* selected = stat_select_primary_statistical_observation(lf);
|
||
if (selected != nullptr)
|
||
{
|
||
selected_observation_index = selected->observation_index;
|
||
selected_observation_name = selected->observation_name;
|
||
|
||
for (const auto& ch : selected->channel_instances)
|
||
{
|
||
auto points = stat_expand_channel_points(lf, connection_kind, *selected, ch);
|
||
out.insert(out.end(),
|
||
std::make_move_iterator(points.begin()),
|
||
std::make_move_iterator(points.end()));
|
||
}
|
||
}
|
||
|
||
// 对主 observation 缺失的目标指标执行统一 observation fallback。
|
||
// 例如:普通趋势指标可能在 Trend observation,而 V HRMS A/B/C 2-50 次谐波在另一条 observation。
|
||
auto fallback_points = stat_expand_missing_metrics_from_all_observations(
|
||
lf,
|
||
connection_kind,
|
||
out,
|
||
selected_observation_index);
|
||
if (!fallback_points.empty())
|
||
{
|
||
out.insert(out.end(),
|
||
std::make_move_iterator(fallback_points.begin()),
|
||
std::make_move_iterator(fallback_points.end()));
|
||
}
|
||
|
||
// 对同一 metric 的多来源候选流先做择优,避免后续按 timestamp 聚合时静默覆盖。
|
||
return stat_select_best_metric_sources(out);
|
||
}
|
||
|
||
bool pqdif_probe_text_looks_like_flicker(const std::string& text)
|
||
{
|
||
const std::string key = normalize_key(text);
|
||
return key.find("FLICKER") != std::string::npos ||
|
||
key.find("FLKR") != std::string::npos ||
|
||
key.find("PST") != std::string::npos ||
|
||
key.find("PLT") != std::string::npos ||
|
||
key.find("DVC") != std::string::npos ||
|
||
key.find("DELTAV") != std::string::npos;
|
||
}
|
||
|
||
/// @brief DEBUG 级别闪变候选通道诊断。
|
||
/// @details 用于核查 Pst/Plt/DVC 通道是否真实有数据;尤其是 Plt 通道存在但 values.count=0 时,
|
||
/// 这里能直接看出来,避免误以为是名称匹配失败。
|
||
void dump_flicker_candidate_probe(const ParsedPqdifFile& parsed_file)
|
||
{
|
||
const auto& lf = parsed_file.logical_file;
|
||
std::cout << "========== PQDIF FLICKER CANDIDATE PROBE V18 ==========" << std::endl;
|
||
std::cout << "file=" << parsed_file.source_file << std::endl;
|
||
std::cout << "rule=DEBUG level: print all Pst/Plt/DVC/Flicker candidate channels and series counts" << std::endl;
|
||
|
||
size_t hit_count = 0;
|
||
size_t plt_channel_count = 0;
|
||
size_t plt_value_point_count = 0;
|
||
|
||
for (const auto& obs : lf.observations)
|
||
{
|
||
const PqdifDataSourceRecord* ds = stat_find_related_data_source(lf, obs);
|
||
for (const auto& ch : obs.channel_instances)
|
||
{
|
||
const PqdifChannelDefinition* ch_def = stat_find_channel_definition(ds, ch.channel_def_index);
|
||
bool candidate = pqdif_probe_text_looks_like_flicker(ch.channel_name) ||
|
||
pqdif_probe_text_looks_like_flicker(ch.quantity_type_id.symbolic_name);
|
||
if (ch_def != nullptr)
|
||
{
|
||
candidate = candidate ||
|
||
pqdif_probe_text_looks_like_flicker(ch_def->channel_name) ||
|
||
pqdif_probe_text_looks_like_flicker(ch_def->quantity_name);
|
||
}
|
||
for (const auto& si : ch.series_instances)
|
||
{
|
||
candidate = candidate ||
|
||
pqdif_probe_text_looks_like_flicker(si.value_type_id.symbolic_name) ||
|
||
pqdif_probe_text_looks_like_flicker(si.quantity_characteristic_id.symbolic_name);
|
||
}
|
||
if (!candidate)
|
||
continue;
|
||
|
||
++hit_count;
|
||
const std::string compact_name = normalize_key(ch.channel_name + " " + ((ch_def != nullptr) ? ch_def->channel_name : std::string()));
|
||
const bool is_plt_candidate = compact_name.find("PLT") != std::string::npos;
|
||
if (is_plt_candidate)
|
||
++plt_channel_count;
|
||
|
||
std::cout << " [FLICKER-CH] obs=" << obs.observation_index
|
||
<< ", obs_name=" << obs.observation_name
|
||
<< ", obs_start=" << obs.time_start.text
|
||
<< ", ch=" << ch.channel_instance_index
|
||
<< ", ch_def=" << ch.channel_def_index
|
||
<< ", channel=" << ch.channel_name
|
||
<< ", def_channel=" << ((ch_def != nullptr) ? ch_def->channel_name : std::string())
|
||
<< ", phase=" << pqdif_sem::FindPhaseName(ch.phase_id)
|
||
<< ", measured=" << pqdif_sem::FindQuantityMeasuredName(ch.quantity_measured_id)
|
||
<< ", series_instances=" << ch.series_instances.size()
|
||
<< std::endl;
|
||
|
||
for (size_t si_index = 0; si_index < ch.series_instances.size(); ++si_index)
|
||
{
|
||
const auto& si = ch.series_instances[si_index];
|
||
const PqdifSeriesDefinition* sd = stat_find_series_definition(ch_def, si.series_def_index);
|
||
const PqdifSeriesInstance* resolved = stat_resolve_shared_series(obs, si);
|
||
const long value_count = (resolved != nullptr) ? resolved->values.count : -1;
|
||
if (is_plt_candidate && value_count > 0)
|
||
plt_value_point_count += static_cast<size_t>(value_count);
|
||
|
||
std::cout << " [FLICKER-SER] ser=" << si_index
|
||
<< ", ser_def=" << si.series_def_index
|
||
<< ", value_type=" << short_guid_name(si.value_type_id)
|
||
<< ", characteristic=" << short_guid_name(si.quantity_characteristic_id)
|
||
<< ", unit=" << pqdif_sem::FindQuantityUnitsName(si.quantity_units_id)
|
||
<< ", resolved_count=" << value_count;
|
||
|
||
if (resolved != nullptr && value_count > 0)
|
||
{
|
||
double raw_value = 0.0;
|
||
if (stat_try_get_raw_numeric_at(resolved->values, 0, raw_value))
|
||
{
|
||
const unsigned int storage_method_id = (sd != nullptr) ? sd->storage_method_id : 0;
|
||
const double first_value = stat_decode_engineering_value(
|
||
raw_value,
|
||
*resolved,
|
||
storage_method_id,
|
||
resolved->values.physical_type);
|
||
std::cout << ", first=" << first_value;
|
||
}
|
||
}
|
||
std::cout << std::endl;
|
||
}
|
||
}
|
||
}
|
||
|
||
std::cout << " [FLICKER SUMMARY] candidate_channels=" << hit_count
|
||
<< ", plt_candidate_channels=" << plt_channel_count
|
||
<< ", plt_value_points=" << plt_value_point_count
|
||
<< std::endl;
|
||
std::cout << "===================================================" << std::endl;
|
||
}
|
||
|
||
/// @brief 将单个样本点应用到同一时间桶里的某个指标聚合值上。
|
||
/// @details
|
||
/// 这里不再允许静默覆盖:同一 timestamp + metric + kind 再次写入时,保留首次写入值,
|
||
/// 并把该 metric 标记为 DuplicateSource,同时打印冲突来源。
|
||
/// @param bucket 目标时间桶。
|
||
/// @param p 已识别统计样本点。
|
||
/// @param quality_by_metric 指标级质量状态。
|
||
void stat_apply_point_to_bucket(
|
||
TimeAggregatedStatBucket& bucket,
|
||
const ExpandedStatPoint& p,
|
||
const std::map<StatMetricId, StatMetricQualityInfo>& quality_by_metric)
|
||
{
|
||
auto& agg = bucket.metrics[p.metric_id];
|
||
|
||
const StatMetricQualityInfo* qi = stat_find_quality_info(quality_by_metric, p.metric_id);
|
||
if (qi != nullptr && agg.quality == StatMetricQuality::Normal)
|
||
{
|
||
agg.quality = qi->quality;
|
||
agg.quality_reason = qi->reason;
|
||
}
|
||
|
||
if (agg.source_observation_index < 0)
|
||
{
|
||
agg.source_observation_index = p.observation_index;
|
||
agg.source_channel_instance_index = p.channel_instance_index;
|
||
agg.source_channel_name = p.channel_name;
|
||
}
|
||
|
||
if (stat_has_value_kind(agg, p.stat_kind))
|
||
{
|
||
const double old_value = stat_get_value_by_kind(agg, p.stat_kind);
|
||
|
||
std::ostringstream reason;
|
||
reason << "duplicate write: kind=" << stat_value_kind_name(p.stat_kind)
|
||
<< ", old_value=" << old_value
|
||
<< ", new_value=" << p.value
|
||
<< ", old_ch=" << agg.source_channel_instance_index
|
||
<< ", new_ch=" << p.channel_instance_index;
|
||
|
||
agg.quality = StatMetricQuality::DuplicateSource;
|
||
agg.quality_reason = reason.str();
|
||
|
||
static size_t duplicate_print_count = 0;
|
||
if (duplicate_print_count < 80)
|
||
{
|
||
std::cout << " [DUPLICATE STAT VALUE]"
|
||
<< " time=" << p.timestamp_text
|
||
<< ", metric=" << stat_metric_name(p.metric_id)
|
||
<< ", kind=" << stat_value_kind_name(p.stat_kind)
|
||
<< ", old_value=" << old_value
|
||
<< ", new_value=" << p.value
|
||
<< ", old_ch=" << agg.source_channel_instance_index
|
||
<< ", new_ch=" << p.channel_instance_index
|
||
<< ", new_ser=" << p.series_instance_index
|
||
<< ", new_channel=" << p.channel_name
|
||
<< std::endl;
|
||
++duplicate_print_count;
|
||
if (duplicate_print_count == 80)
|
||
{
|
||
std::cout << " [DUPLICATE STAT VALUE] print limit reached, further duplicate details suppressed"
|
||
<< std::endl;
|
||
}
|
||
}
|
||
|
||
// 调试和修复阶段保留首次写入,拒绝覆盖,避免结果继续被污染。
|
||
return;
|
||
}
|
||
|
||
agg.source_series_instance_index = p.series_instance_index;
|
||
|
||
switch (p.stat_kind)
|
||
{
|
||
case StatValueKind::Min:
|
||
agg.has_min = true;
|
||
agg.min_value = p.value;
|
||
break;
|
||
case StatValueKind::Max:
|
||
agg.has_max = true;
|
||
agg.max_value = p.value;
|
||
break;
|
||
case StatValueKind::Avg:
|
||
agg.has_avg = true;
|
||
agg.avg_value = p.value;
|
||
break;
|
||
case StatValueKind::P95:
|
||
agg.has_p95 = true;
|
||
agg.p95_value = p.value;
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
}
|
||
|
||
/// @brief 将统计样本点按 timestamp 聚合成时间桶。
|
||
/// @details
|
||
/// 当前阶段已经先筛掉非统计 observation,并做过 metric 来源择优;这里仍保留覆盖检测,
|
||
/// 用于防止后续新增指标时重新引入静默覆盖。
|
||
/// @param points 已识别统计样本点。
|
||
/// @return 聚合后的时间桶数组,按时间升序输出。
|
||
std::vector<TimeAggregatedStatBucket> stat_group_points_by_timestamp(
|
||
const std::vector<ExpandedStatPoint>& points)
|
||
{
|
||
const std::map<StatMetricId, StatMetricQualityInfo> quality_by_metric =
|
||
stat_analyze_metric_quality(points);
|
||
|
||
std::map<time_t, TimeAggregatedStatBucket> buckets;
|
||
for (const auto& p : points)
|
||
{
|
||
auto& bucket = buckets[p.timestamp];
|
||
if (bucket.timestamp == 0)
|
||
{
|
||
bucket.timestamp = p.timestamp;
|
||
bucket.timestamp_text = p.timestamp_text;
|
||
}
|
||
stat_apply_point_to_bucket(bucket, p, quality_by_metric);
|
||
}
|
||
|
||
std::vector<TimeAggregatedStatBucket> out;
|
||
out.reserve(buckets.size());
|
||
for (auto& kv : buckets)
|
||
out.push_back(std::move(kv.second));
|
||
return out;
|
||
}
|
||
|
||
void stat_print_aggregated_metric_line(StatMetricId metric_id, const AggregatedStatValues* agg)
|
||
{
|
||
std::cout << " metric=" << stat_metric_name(metric_id);
|
||
|
||
if (agg == nullptr)
|
||
{
|
||
std::cout << ", quality=" << stat_metric_quality_name(StatMetricQuality::Missing)
|
||
<< std::endl;
|
||
return;
|
||
}
|
||
|
||
if (agg->has_min) std::cout << ", min=" << agg->min_value;
|
||
else std::cout << ", min=N/A";
|
||
if (agg->has_max) std::cout << ", max=" << agg->max_value;
|
||
else std::cout << ", max=N/A";
|
||
if (agg->has_avg) std::cout << ", avg=" << agg->avg_value;
|
||
else std::cout << ", avg=N/A";
|
||
if (agg->has_p95) std::cout << ", p95=" << agg->p95_value;
|
||
else std::cout << ", p95=N/A";
|
||
|
||
std::cout << ", quality=" << stat_metric_quality_name(agg->quality);
|
||
if (!agg->quality_reason.empty())
|
||
std::cout << ", reason=" << agg->quality_reason;
|
||
if (agg->source_channel_instance_index >= 0)
|
||
{
|
||
std::cout << ", source_ch=" << agg->source_channel_instance_index;
|
||
if (!agg->source_channel_name.empty())
|
||
std::cout << ", source_channel=" << agg->source_channel_name;
|
||
}
|
||
std::cout << std::endl;
|
||
}
|
||
|
||
|
||
size_t stat_count_present_metrics(const std::vector<ExpandedStatPoint>& points)
|
||
{
|
||
std::set<StatMetricId> ids;
|
||
for (const auto& p : points)
|
||
ids.insert(p.metric_id);
|
||
return ids.size();
|
||
}
|
||
|
||
size_t stat_count_present_voltage_harmonic_metrics(const std::vector<ExpandedStatPoint>& points)
|
||
{
|
||
std::set<StatMetricId> ids;
|
||
for (const auto& p : points)
|
||
{
|
||
if (stat_is_voltage_harmonic_metric(p.metric_id))
|
||
ids.insert(p.metric_id);
|
||
}
|
||
return ids.size();
|
||
}
|
||
|
||
std::vector<StatMetricId> stat_present_voltage_harmonic_metrics(
|
||
const std::map<StatMetricId, StatMetricSourceStats>& stats_by_metric)
|
||
{
|
||
std::vector<StatMetricId> out;
|
||
for (const auto& kv : stats_by_metric)
|
||
{
|
||
if (stat_is_voltage_harmonic_metric(kv.first))
|
||
out.push_back(kv.first);
|
||
}
|
||
std::sort(out.begin(), out.end(), [](StatMetricId a, StatMetricId b) {
|
||
return static_cast<int>(a) < static_cast<int>(b);
|
||
});
|
||
return out;
|
||
}
|
||
|
||
|
||
const char* stat_dynamic_group_display_name(StatDynamicMetricGroup group)
|
||
{
|
||
switch (group)
|
||
{
|
||
case StatDynamicMetricGroup::VoltageHarmonic: return "VoltageHarmonic";
|
||
case StatDynamicMetricGroup::LineVoltageHarmonic: return "LineVoltageHarmonic";
|
||
case StatDynamicMetricGroup::CurrentHarmonic: return "CurrentHarmonic";
|
||
case StatDynamicMetricGroup::VoltageHarmonicAngle: return "VoltageHarmonicAngle";
|
||
case StatDynamicMetricGroup::LineVoltageHarmonicAngle: return "LineVoltageHarmonicAngle";
|
||
case StatDynamicMetricGroup::CurrentHarmonicAngle: return "CurrentHarmonicAngle";
|
||
case StatDynamicMetricGroup::HarmonicActivePower: return "HarmonicActivePower";
|
||
case StatDynamicMetricGroup::HarmonicReactivePower: return "HarmonicReactivePower";
|
||
case StatDynamicMetricGroup::HarmonicApparentPower: return "HarmonicApparentPower";
|
||
case StatDynamicMetricGroup::VoltageHarmonicRatio: return "VoltageHarmonicRatio";
|
||
case StatDynamicMetricGroup::LineVoltageHarmonicRatio: return "LineVoltageHarmonicRatio";
|
||
case StatDynamicMetricGroup::CurrentHarmonicRatio: return "CurrentHarmonicRatio";
|
||
case StatDynamicMetricGroup::VoltageInterharmonic: return "VoltageInterharmonic";
|
||
case StatDynamicMetricGroup::LineVoltageInterharmonic: return "LineVoltageInterharmonic";
|
||
case StatDynamicMetricGroup::CurrentInterharmonic: return "CurrentInterharmonic";
|
||
default: return "UnknownDynamicGroup";
|
||
}
|
||
}
|
||
|
||
size_t stat_dynamic_group_expected_count(StatDynamicMetricGroup group)
|
||
{
|
||
return stat_dynamic_metric_order_for_group(group).size();
|
||
}
|
||
|
||
std::vector<StatMetricId> stat_present_dynamic_group_metrics(
|
||
const std::map<StatMetricId, StatMetricSourceStats>& stats_by_metric,
|
||
StatDynamicMetricGroup group)
|
||
{
|
||
std::vector<StatMetricId> out;
|
||
for (const auto& kv : stats_by_metric)
|
||
{
|
||
if (stat_is_dynamic_metric_group(kv.first, group))
|
||
out.push_back(kv.first);
|
||
}
|
||
std::sort(out.begin(), out.end(), [](StatMetricId a, StatMetricId b) {
|
||
return static_cast<int>(a) < static_cast<int>(b);
|
||
});
|
||
return out;
|
||
}
|
||
|
||
size_t stat_count_present_dynamic_group_metrics(
|
||
const std::vector<ExpandedStatPoint>& points,
|
||
StatDynamicMetricGroup group)
|
||
{
|
||
std::set<StatMetricId> ids;
|
||
for (const auto& p : points)
|
||
{
|
||
if (stat_is_dynamic_metric_group(p.metric_id, group))
|
||
ids.insert(p.metric_id);
|
||
}
|
||
return ids.size();
|
||
}
|
||
|
||
size_t stat_count_present_all_dynamic_metrics(const std::vector<ExpandedStatPoint>& points)
|
||
{
|
||
std::set<StatMetricId> ids;
|
||
for (const auto& p : points)
|
||
{
|
||
if (stat_is_dynamic_metric(p.metric_id))
|
||
ids.insert(p.metric_id);
|
||
}
|
||
return ids.size();
|
||
}
|
||
|
||
const std::vector<StatDynamicMetricGroup>& stat_dynamic_summary_groups()
|
||
{
|
||
static const std::vector<StatDynamicMetricGroup> groups = {
|
||
StatDynamicMetricGroup::VoltageHarmonic,
|
||
StatDynamicMetricGroup::LineVoltageHarmonic,
|
||
StatDynamicMetricGroup::CurrentHarmonic,
|
||
StatDynamicMetricGroup::VoltageHarmonicAngle,
|
||
StatDynamicMetricGroup::LineVoltageHarmonicAngle,
|
||
StatDynamicMetricGroup::CurrentHarmonicAngle,
|
||
StatDynamicMetricGroup::HarmonicActivePower,
|
||
StatDynamicMetricGroup::HarmonicReactivePower,
|
||
StatDynamicMetricGroup::HarmonicApparentPower,
|
||
StatDynamicMetricGroup::VoltageHarmonicRatio,
|
||
StatDynamicMetricGroup::LineVoltageHarmonicRatio,
|
||
StatDynamicMetricGroup::CurrentHarmonicRatio,
|
||
StatDynamicMetricGroup::VoltageInterharmonic,
|
||
StatDynamicMetricGroup::LineVoltageInterharmonic,
|
||
StatDynamicMetricGroup::CurrentInterharmonic
|
||
};
|
||
return groups;
|
||
}
|
||
|
||
void stat_print_dynamic_group_compact_summaries(
|
||
const std::map<StatMetricId, StatMetricSourceStats>& stats_by_metric)
|
||
{
|
||
for (auto group : stat_dynamic_summary_groups())
|
||
{
|
||
const std::vector<StatMetricId> present = stat_present_dynamic_group_metrics(stats_by_metric, group);
|
||
std::cout << " [DYNAMIC SPECTRUM SUMMARY] group=" << stat_dynamic_group_display_name(group)
|
||
<< ", present=" << present.size() << "/" << stat_dynamic_group_expected_count(group);
|
||
if (!present.empty())
|
||
{
|
||
const double first_order = stat_dynamic_metric_order_value(present.front());
|
||
const double last_order = stat_dynamic_metric_order_value(present.back());
|
||
std::cout << ", range=" << first_order << "-" << last_order;
|
||
}
|
||
std::cout << std::endl;
|
||
}
|
||
}
|
||
|
||
void stat_print_voltage_harmonic_compact_summary(
|
||
const std::map<StatMetricId, StatMetricSourceStats>& stats_by_metric)
|
||
{
|
||
bool present[3][51] = {};
|
||
for (const auto& kv : stats_by_metric)
|
||
{
|
||
if (!stat_is_voltage_harmonic_metric(kv.first))
|
||
continue;
|
||
const int phase = stat_voltage_harmonic_phase_index(kv.first);
|
||
const int order = stat_voltage_harmonic_order(kv.first);
|
||
if (phase >= 0 && phase < 3 && order >= 2 && order <= 50)
|
||
present[phase][order] = true;
|
||
}
|
||
|
||
size_t total_present = 0;
|
||
for (int phase = 0; phase < 3; ++phase)
|
||
{
|
||
for (int order = 2; order <= 50; ++order)
|
||
{
|
||
if (present[phase][order])
|
||
++total_present;
|
||
}
|
||
}
|
||
|
||
std::cout << " [VOLTAGE HARMONIC SUMMARY] present=" << total_present
|
||
<< "/147";
|
||
|
||
const char* phase_names[3] = { "A", "B", "C" };
|
||
for (int phase = 0; phase < 3; ++phase)
|
||
{
|
||
int first = -1;
|
||
int last = -1;
|
||
int count = 0;
|
||
for (int order = 2; order <= 50; ++order)
|
||
{
|
||
if (!present[phase][order])
|
||
continue;
|
||
if (first < 0)
|
||
first = order;
|
||
last = order;
|
||
++count;
|
||
}
|
||
std::cout << ", " << phase_names[phase] << "=";
|
||
if (count == 0)
|
||
std::cout << "missing";
|
||
else if (count == 49)
|
||
std::cout << "2-50";
|
||
else
|
||
std::cout << "count:" << count << " range:" << first << "-" << last;
|
||
}
|
||
std::cout << std::endl;
|
||
}
|
||
|
||
/// @brief 打印已展开统计样本点预览。
|
||
/// @details
|
||
/// 不再只打印前 12 个样本点,而是按“主要指标 × Min/Max/Avg/P95”打印每个流的首样本,
|
||
/// 这样后续新增指标时可以直接看出哪个 metric/kind 缺失或来自哪个通道。
|
||
/// @param parsed_file 当前已解析文件对象。
|
||
void dump_expanded_stat_preview(const ParsedPqdifFile& parsed_file)
|
||
{
|
||
const auto& points = parsed_file.expanded_stat_points;
|
||
|
||
if (!pqdif_is_trace_log_enabled())
|
||
{
|
||
std::cout << "========== EXPANDED STAT SUMMARY ==========" << std::endl;
|
||
std::cout << "connection_kind=" << stat_connection_kind_name(parsed_file.connection_kind)
|
||
<< ", selected_observation_index=" << parsed_file.selected_observation_index
|
||
<< ", selected_observation_name=" << parsed_file.selected_observation_name
|
||
<< ", selected_points=" << points.size()
|
||
<< ", present_metrics=" << stat_count_present_metrics(points)
|
||
<< ", present_dynamic_spectrum=" << stat_count_present_all_dynamic_metrics(points)
|
||
<< "/" << stat_all_dynamic_metric_order().size()
|
||
<< ", present_voltage_harmonics=" << stat_count_present_voltage_harmonic_metrics(points)
|
||
<< "/147"
|
||
<< std::endl;
|
||
std::cout << "===========================================" << std::endl;
|
||
return;
|
||
}
|
||
|
||
std::cout << "========== EXPANDED STAT PREVIEW ==========" << std::endl;
|
||
std::cout << "connection_kind=" << stat_connection_kind_name(parsed_file.connection_kind)
|
||
<< ", selected_observation_index=" << parsed_file.selected_observation_index
|
||
<< ", selected_observation_name=" << parsed_file.selected_observation_name
|
||
<< ", selected_points=" << points.size()
|
||
<< std::endl;
|
||
|
||
typedef std::pair<StatMetricId, StatValueKind> StreamKey;
|
||
std::map<StreamKey, const ExpandedStatPoint*> first_point_by_stream;
|
||
for (const auto& p : points)
|
||
{
|
||
StreamKey key(p.metric_id, p.stat_kind);
|
||
if (first_point_by_stream.find(key) == first_point_by_stream.end())
|
||
first_point_by_stream[key] = &p;
|
||
}
|
||
|
||
const StatValueKind kinds[4] = {
|
||
StatValueKind::Min,
|
||
StatValueKind::Max,
|
||
StatValueKind::Avg,
|
||
StatValueKind::P95
|
||
};
|
||
|
||
std::cout << " [PRIMARY METRIC STREAMS] metric_slots="
|
||
<< stat_primary_metric_print_order().size()
|
||
<< ", stream_slots=" << stat_primary_metric_print_order().size() * 4
|
||
<< std::endl;
|
||
|
||
for (const auto metric_id : stat_primary_metric_print_order())
|
||
{
|
||
for (int i = 0; i < 4; ++i)
|
||
{
|
||
const StatValueKind kind = kinds[i];
|
||
StreamKey key(metric_id, kind);
|
||
const auto it = first_point_by_stream.find(key);
|
||
|
||
std::cout << " [STREAM]"
|
||
<< " metric=" << stat_metric_name(metric_id)
|
||
<< ", kind=" << stat_value_kind_name(kind);
|
||
|
||
if (it == first_point_by_stream.end())
|
||
{
|
||
std::cout << ", status=MISSING" << std::endl;
|
||
continue;
|
||
}
|
||
|
||
const ExpandedStatPoint& p = *it->second;
|
||
std::cout
|
||
<< ", status=OK"
|
||
<< ", first_time=" << p.timestamp_text
|
||
<< ", first_value=" << p.value
|
||
<< ", obs=" << p.observation_index
|
||
<< ", ch=" << p.channel_instance_index
|
||
<< ", ch_def=" << p.channel_def_index
|
||
<< ", group=" << p.channel_group_id
|
||
<< ", ser=" << p.series_instance_index
|
||
<< ", ser_def=" << p.series_def_index
|
||
<< ", channel=" << p.channel_name
|
||
<< ", phase=" << pqdif_sem::FindPhaseName(p.phase_id)
|
||
<< ", measured=" << pqdif_sem::FindQuantityMeasuredName(p.quantity_measured_id)
|
||
<< ", unit=" << pqdif_sem::FindQuantityUnitsName(p.quantity_units_id)
|
||
<< (p.matched_by_name_fallback ? ", by_name=true" : "")
|
||
<< std::endl;
|
||
}
|
||
}
|
||
|
||
// 扩展指标只有命中时才打印,避免干扰当前 14 项核查。
|
||
for (const auto metric_id : stat_extra_metric_print_order())
|
||
{
|
||
bool has_any = false;
|
||
for (int i = 0; i < 4; ++i)
|
||
{
|
||
StreamKey key(metric_id, kinds[i]);
|
||
if (first_point_by_stream.find(key) != first_point_by_stream.end())
|
||
{
|
||
has_any = true;
|
||
break;
|
||
}
|
||
}
|
||
if (!has_any)
|
||
continue;
|
||
|
||
for (int i = 0; i < 4; ++i)
|
||
{
|
||
StreamKey key(metric_id, kinds[i]);
|
||
const auto it = first_point_by_stream.find(key);
|
||
std::cout << " [EXTRA STREAM]"
|
||
<< " metric=" << stat_metric_name(metric_id)
|
||
<< ", kind=" << stat_value_kind_name(kinds[i]);
|
||
if (it == first_point_by_stream.end())
|
||
{
|
||
std::cout << ", status=MISSING" << std::endl;
|
||
continue;
|
||
}
|
||
const ExpandedStatPoint& p = *it->second;
|
||
std::cout
|
||
<< ", status=OK"
|
||
<< ", first_time=" << p.timestamp_text
|
||
<< ", first_value=" << p.value
|
||
<< ", ch=" << p.channel_instance_index
|
||
<< ", channel=" << p.channel_name
|
||
<< std::endl;
|
||
}
|
||
}
|
||
|
||
std::cout << "===========================================" << std::endl;
|
||
}
|
||
|
||
/// @brief 打印时间聚合桶预览。
|
||
/// @details
|
||
/// 每个预览桶固定打印当前核查的主要指标,不再用 shown>=12 截断。
|
||
/// 若某项缺失,会显式打印 quality=MISSING。
|
||
/// @param parsed_file 当前已解析文件对象。
|
||
void dump_grouped_bucket_preview(const ParsedPqdifFile& parsed_file)
|
||
{
|
||
if (!pqdif_is_trace_log_enabled())
|
||
{
|
||
std::cout << "========== GROUPED STAT CORE SUMMARY ==========" << std::endl;
|
||
std::cout << "connection_kind=" << stat_connection_kind_name(parsed_file.connection_kind)
|
||
<< ", selected_observation_index=" << parsed_file.selected_observation_index
|
||
<< ", selected_observation_name=" << parsed_file.selected_observation_name
|
||
<< ", expanded_points=" << parsed_file.expanded_stat_points.size()
|
||
<< ", buckets=" << parsed_file.aggregated_stat_buckets.size()
|
||
<< ", core_metric_slots=" << stat_core_metric_print_order().size()
|
||
<< ", dynamic_spectrum_slots=" << stat_all_dynamic_metric_order().size()
|
||
<< std::endl;
|
||
|
||
std::map<StatMetricId, StatMetricSourceStats> stats_by_metric =
|
||
stat_collect_metric_stats(parsed_file.expanded_stat_points);
|
||
std::map<StatMetricId, StatMetricQualityInfo> quality_by_metric =
|
||
stat_analyze_metric_quality(parsed_file.expanded_stat_points);
|
||
|
||
stat_print_dynamic_group_compact_summaries(stats_by_metric);
|
||
|
||
const size_t bucket_limit = std::min<size_t>(parsed_file.aggregated_stat_buckets.size(), 3);
|
||
for (size_t i = 0; i < bucket_limit; ++i)
|
||
{
|
||
const auto& b = parsed_file.aggregated_stat_buckets[i];
|
||
std::cout << " [BUCKET " << i << "]"
|
||
<< " time=" << b.timestamp_text
|
||
<< ", metric_count_present=" << b.metrics.size()
|
||
<< std::endl;
|
||
|
||
for (const auto metric_id : stat_core_metric_print_order())
|
||
{
|
||
const auto it = b.metrics.find(metric_id);
|
||
stat_print_aggregated_metric_line(
|
||
metric_id,
|
||
it == b.metrics.end() ? nullptr : &it->second);
|
||
}
|
||
|
||
// 核心日志仅抽样打印 2/3/5 次谐波,避免每桶输出 147 行。
|
||
const int sample_orders[] = { 2, 3, 5 };
|
||
bool printed_header = false;
|
||
for (int phase = 0; phase < 3; ++phase)
|
||
{
|
||
for (int oi = 0; oi < 3; ++oi)
|
||
{
|
||
const StatMetricId hid = stat_voltage_harmonic_metric_id(phase, sample_orders[oi]);
|
||
const auto hit = b.metrics.find(hid);
|
||
if (hit == b.metrics.end())
|
||
continue;
|
||
if (!printed_header)
|
||
{
|
||
std::cout << " [VOLTAGE HARMONIC SAMPLE] orders=2/3/5" << std::endl;
|
||
printed_header = true;
|
||
}
|
||
stat_print_aggregated_metric_line(hid, &hit->second);
|
||
}
|
||
}
|
||
}
|
||
|
||
std::cout << "========== METRIC STATUS CORE SUMMARY ==========" << std::endl;
|
||
for (const auto metric_id : stat_core_metric_print_order())
|
||
{
|
||
const auto stats_it = stats_by_metric.find(metric_id);
|
||
const auto quality_it = quality_by_metric.find(metric_id);
|
||
|
||
std::cout << " [METRIC STATUS] metric=" << stat_metric_name(metric_id);
|
||
if (stats_it == stats_by_metric.end())
|
||
{
|
||
std::cout << ", quality=" << stat_metric_quality_name(StatMetricQuality::Missing)
|
||
<< ", points=0" << std::endl;
|
||
continue;
|
||
}
|
||
|
||
const StatMetricSourceStats& st = stats_it->second;
|
||
const StatMetricQuality quality =
|
||
quality_it == quality_by_metric.end() ? StatMetricQuality::Missing : quality_it->second.quality;
|
||
const std::string reason =
|
||
quality_it == quality_by_metric.end() ? std::string("missing") : quality_it->second.reason;
|
||
|
||
std::cout
|
||
<< ", quality=" << stat_metric_quality_name(quality)
|
||
<< ", reason=" << reason
|
||
<< ", points=" << st.point_count
|
||
<< ", source_obs=" << st.key.observation_index
|
||
<< ", source_ch=" << st.key.channel_instance_index
|
||
<< ", source_channel=" << st.channel_name
|
||
<< ", min=" << st.min_value
|
||
<< ", max=" << st.max_value
|
||
<< ", avg_abs=" << st.avg_abs_value()
|
||
<< std::endl;
|
||
}
|
||
|
||
for (auto group : stat_dynamic_summary_groups())
|
||
{
|
||
const std::vector<StatMetricId> present = stat_present_dynamic_group_metrics(stats_by_metric, group);
|
||
std::cout << " [DYNAMIC SPECTRUM STATUS] group=" << stat_dynamic_group_display_name(group)
|
||
<< ", present_metrics=" << present.size()
|
||
<< "/" << stat_dynamic_group_expected_count(group) << std::endl;
|
||
|
||
size_t sample_count = 0;
|
||
for (const auto metric_id : present)
|
||
{
|
||
if (sample_count >= 6)
|
||
break;
|
||
const auto stats_it = stats_by_metric.find(metric_id);
|
||
if (stats_it == stats_by_metric.end())
|
||
continue;
|
||
const StatMetricSourceStats& st = stats_it->second;
|
||
std::cout << " [DYNAMIC STATUS SAMPLE] metric=" << stat_metric_name(metric_id)
|
||
<< ", points=" << st.point_count
|
||
<< ", source_obs=" << st.key.observation_index
|
||
<< ", source_ch=" << st.key.channel_instance_index
|
||
<< ", order=" << stat_dynamic_metric_order_value(metric_id)
|
||
<< ", source_channel=" << st.channel_name
|
||
<< ", min=" << st.min_value
|
||
<< ", max=" << st.max_value
|
||
<< ", avg_abs=" << st.avg_abs_value()
|
||
<< std::endl;
|
||
++sample_count;
|
||
}
|
||
}
|
||
|
||
std::cout << "=================================================" << std::endl;
|
||
return;
|
||
}
|
||
|
||
std::cout << "========== GROUPED STAT BUCKET PREVIEW ==========" << std::endl;
|
||
std::cout << "connection_kind=" << stat_connection_kind_name(parsed_file.connection_kind)
|
||
<< ", selected_observation_index=" << parsed_file.selected_observation_index
|
||
<< ", selected_observation_name=" << parsed_file.selected_observation_name
|
||
<< ", expanded_points=" << parsed_file.expanded_stat_points.size()
|
||
<< ", buckets=" << parsed_file.aggregated_stat_buckets.size()
|
||
<< ", primary_metric_slots=" << stat_primary_metric_print_order().size()
|
||
<< std::endl;
|
||
|
||
const size_t bucket_limit = std::min<size_t>(parsed_file.aggregated_stat_buckets.size(), 3);
|
||
for (size_t i = 0; i < bucket_limit; ++i)
|
||
{
|
||
const auto& b = parsed_file.aggregated_stat_buckets[i];
|
||
std::cout << " [BUCKET " << i << "]"
|
||
<< " time=" << b.timestamp_text
|
||
<< ", metric_count_present=" << b.metrics.size()
|
||
<< std::endl;
|
||
|
||
for (const auto metric_id : stat_primary_metric_print_order())
|
||
{
|
||
const auto it = b.metrics.find(metric_id);
|
||
stat_print_aggregated_metric_line(
|
||
metric_id,
|
||
it == b.metrics.end() ? nullptr : &it->second);
|
||
}
|
||
|
||
for (const auto metric_id : stat_extra_metric_print_order())
|
||
{
|
||
const auto it = b.metrics.find(metric_id);
|
||
if (it != b.metrics.end())
|
||
stat_print_aggregated_metric_line(metric_id, &it->second);
|
||
}
|
||
}
|
||
|
||
std::cout << "========== METRIC STATUS SUMMARY ==========" << std::endl;
|
||
std::map<StatMetricId, StatMetricSourceStats> stats_by_metric =
|
||
stat_collect_metric_stats(parsed_file.expanded_stat_points);
|
||
std::map<StatMetricId, StatMetricQualityInfo> quality_by_metric =
|
||
stat_analyze_metric_quality(parsed_file.expanded_stat_points);
|
||
|
||
for (const auto metric_id : stat_primary_metric_print_order())
|
||
{
|
||
const auto stats_it = stats_by_metric.find(metric_id);
|
||
const auto quality_it = quality_by_metric.find(metric_id);
|
||
|
||
std::cout << " [METRIC STATUS] metric=" << stat_metric_name(metric_id);
|
||
if (stats_it == stats_by_metric.end())
|
||
{
|
||
std::cout << ", quality=" << stat_metric_quality_name(StatMetricQuality::Missing)
|
||
<< ", points=0" << std::endl;
|
||
continue;
|
||
}
|
||
|
||
const StatMetricSourceStats& s = stats_it->second;
|
||
const StatMetricQuality quality =
|
||
quality_it == quality_by_metric.end() ? StatMetricQuality::Missing : quality_it->second.quality;
|
||
const std::string reason =
|
||
quality_it == quality_by_metric.end() ? std::string("missing") : quality_it->second.reason;
|
||
|
||
std::cout
|
||
<< ", quality=" << stat_metric_quality_name(quality)
|
||
<< ", reason=" << reason
|
||
<< ", points=" << s.point_count
|
||
<< ", source_ch=" << s.key.channel_instance_index
|
||
<< ", source_channel=" << s.channel_name
|
||
<< ", min=" << s.min_value
|
||
<< ", max=" << s.max_value
|
||
<< ", avg_abs=" << s.avg_abs_value()
|
||
<< std::endl;
|
||
}
|
||
|
||
std::cout << "=================================================" << std::endl;
|
||
}
|
||
|
||
bool parse_container_record(CPQDIF& file_convert,
|
||
CPQDIF_R_General* record,
|
||
long record_index,
|
||
PqdifContainerRecord& out)
|
||
{
|
||
if (record == nullptr)
|
||
return false;
|
||
|
||
out.header = build_record_header_info(record, record_index);
|
||
CPQDIF_E_Collection* main = record->GetMainCollection();
|
||
if (main == nullptr)
|
||
return true;
|
||
|
||
out.version_info = read_vector_uint_values(main, tagVersionInfo);
|
||
read_string_tag(main, tagFileName, out.file_name);
|
||
read_timestamp_tag(main, tagCreation, file_convert, out.creation_time);
|
||
read_timestamp_tag(main, tagLastSaved, file_convert, out.last_saved_time);
|
||
read_uint_tag(main, tagTimesSaved, out.times_saved);
|
||
read_string_tag(main, tagLanguage, out.language);
|
||
read_string_tag(main, tagTitle, out.title);
|
||
read_string_tag(main, tagSubject, out.subject);
|
||
read_string_tag(main, tagAuthor, out.author);
|
||
read_string_tag(main, tagKeywords, out.keywords);
|
||
read_string_tag(main, tagComments, out.comments);
|
||
read_string_tag(main, tagLastSavedBy, out.last_saved_by);
|
||
read_string_tag(main, tagApplication, out.application);
|
||
read_guid_tag(main, tagCompressionStyleID, out.compression_style_id);
|
||
read_uint_tag(main, tagCompressionAlgorithmID, out.compression_algorithm_id);
|
||
read_uint_tag(main, tagCompressionChecksum, out.compression_checksum);
|
||
read_string_tag(main, tagOwner, out.owner);
|
||
read_string_tag(main, tagCopyright, out.copyright);
|
||
read_string_tag(main, tagTrademarks, out.trademarks);
|
||
read_string_tag(main, tagNotes, out.notes);
|
||
read_string_tag(main, tagAddress1, out.address1);
|
||
read_string_tag(main, tagAddress2, out.address2);
|
||
read_string_tag(main, tagCity, out.city);
|
||
read_string_tag(main, tagState, out.state);
|
||
read_string_tag(main, tagPostalCode, out.postal_code);
|
||
read_string_tag(main, tagCountry, out.country);
|
||
read_string_tag(main, tagPhoneVoice, out.phone_voice);
|
||
read_string_tag(main, tagPhoneFAX, out.phone_fax);
|
||
read_string_tag(main, tagEMail, out.email);
|
||
|
||
const std::set<std::string> known = {
|
||
"tagVersionInfo", "tagFileName", "tagCreation", "tagLastSaved", "tagTimesSaved",
|
||
"tagLanguage", "tagTitle", "tagSubject", "tagAuthor", "tagKeywords", "tagComments",
|
||
"tagLastSavedBy", "tagApplication", "tagCompressionStyleID", "tagCompressionAlgorithmID",
|
||
"tagCompressionChecksum", "tagOwner", "tagCopyright", "tagTrademarks", "tagNotes",
|
||
"tagAddress1", "tagAddress2", "tagCity", "tagState", "tagPostalCode", "tagCountry",
|
||
"tagPhoneVoice", "tagPhoneFAX", "tagEMail"
|
||
};
|
||
collect_extra_tags(main, known, file_convert, out.extra_tags);
|
||
return true;
|
||
}
|
||
|
||
bool parse_data_source_record(CPQDIF& file_convert,
|
||
CPQDIF_R_DataSource* record,
|
||
long record_index,
|
||
int data_source_index,
|
||
PqdifDataSourceRecord& out)
|
||
{
|
||
if (record == nullptr)
|
||
return false;
|
||
|
||
out.header = build_record_header_info(record, record_index);
|
||
out.data_source_index = data_source_index;
|
||
out.record_index = record_index;
|
||
|
||
GUID ds_type{};
|
||
GUID vendor{};
|
||
GUID equip{};
|
||
std::string serial;
|
||
std::string version;
|
||
std::string name;
|
||
std::string owner;
|
||
std::string location;
|
||
std::string time_zone;
|
||
if (record->GetInfo(ds_type, vendor, equip, serial, version, name, owner, location, time_zone))
|
||
{
|
||
out.data_source_type_id = make_guid_value(ds_type);
|
||
out.vendor_id = make_guid_value(vendor);
|
||
out.equipment_id = make_guid_value(equip);
|
||
out.serial_number = serial;
|
||
out.version = version;
|
||
out.name = name;
|
||
out.owner = owner;
|
||
out.location = location;
|
||
out.time_zone = time_zone;
|
||
}
|
||
|
||
CPQDIF_E_Collection* main = record->GetMainCollection();
|
||
if (main != nullptr)
|
||
{
|
||
read_string_tag(main, tagCustomSourceInfo, out.custom_source_info);
|
||
read_guid_tag(main, tagInstrumentTypeID, out.instrument_type_id);
|
||
read_string_tag(main, tagInstrumentModelName, out.instrument_model_name);
|
||
read_string_tag(main, tagInstrumentModelNumber, out.instrument_model_number);
|
||
read_string_tag(main, tagSerialNumberDS, out.serial_number);
|
||
read_string_tag(main, tagVersionDS, out.version);
|
||
read_string_tag(main, tagNameDS, out.name);
|
||
read_string_tag(main, tagOwnerDS, out.owner);
|
||
read_string_tag(main, tagLocationDS, out.location);
|
||
read_string_tag(main, tagTimeZoneDS, out.time_zone);
|
||
read_string_tag(main, tagCoordinatesDS, out.coordinates);
|
||
|
||
const std::set<std::string> known = {
|
||
"tagDataSourceTypeID", "tagVendorID", "tagEquipmentID", "tagCustomSourceInfo",
|
||
"tagInstrumentTypeID", "tagInstrumentModelName", "tagInstrumentModelNumber",
|
||
"tagSerialNumberDS", "tagVersionDS", "tagNameDS", "tagOwnerDS", "tagLocationDS",
|
||
"tagTimeZoneDS", "tagCoordinatesDS", "tagChannelDefns"
|
||
};
|
||
collect_extra_tags(main, known, file_convert, out.extra_tags);
|
||
}
|
||
|
||
const long channel_count = record->GetCountChannelDefns();
|
||
out.channel_definitions.reserve(static_cast<size_t>(std::max<long>(channel_count, 0)));
|
||
for (long i = 0; i < channel_count; ++i)
|
||
{
|
||
PqdifChannelDefinition channel_def;
|
||
channel_def.channel_def_index = static_cast<int>(i);
|
||
|
||
std::string channel_name;
|
||
UINT4 phase_id = 0;
|
||
GUID quantity_type{};
|
||
UINT4 quantity_measured = 0;
|
||
if (record->GetChannelDefnInfo(i, channel_name, phase_id, quantity_type, quantity_measured))
|
||
{
|
||
channel_def.channel_name = channel_name;
|
||
channel_def.phase_id = phase_id;
|
||
channel_def.quantity_type_id = make_guid_value(quantity_type);
|
||
channel_def.quantity_measured_id = quantity_measured;
|
||
}
|
||
|
||
long primary_series = -1;
|
||
if (record->GetChannelPrimarySeries(i, primary_series))
|
||
channel_def.primary_series_index = static_cast<int>(primary_series);
|
||
|
||
CPQDIF_E_Collection* channel_coll = record->GetOneChannelDefn(i);
|
||
if (channel_coll != nullptr)
|
||
{
|
||
read_string_tag(channel_coll, tagOtherChannelIdentifier, channel_def.other_channel_identifier);
|
||
read_string_tag(channel_coll, tagGroupName, channel_def.group_name);
|
||
read_uint_tag(channel_coll, tagPhysicalChannel, channel_def.physical_channel);
|
||
read_string_tag(channel_coll, tagQuantityName, channel_def.quantity_name);
|
||
|
||
const std::set<std::string> known_channel = {
|
||
"tagChannelName", "tagPhaseID", "tagOtherChannelIdentifier", "tagGroupName",
|
||
"tagQuantityTypeID", "tagQuantityMeasuredID", "tagPhysicalChannel",
|
||
"tagQuantityName", "tagPrimarySeriesIdx", "tagSeriesDefns"
|
||
};
|
||
collect_extra_tags(channel_coll, known_channel, file_convert, channel_def.extra_tags);
|
||
}
|
||
|
||
const long series_count = record->GetCountSeriesDefns(static_cast<int>(i));
|
||
channel_def.series_definitions.reserve(static_cast<size_t>(std::max<long>(series_count, 0)));
|
||
for (long j = 0; j < series_count; ++j)
|
||
{
|
||
PqdifSeriesDefinition series_def;
|
||
series_def.series_def_index = static_cast<int>(j);
|
||
|
||
UINT4 quantity_units = 0;
|
||
GUID value_type{};
|
||
GUID quantity_characteristic{};
|
||
UINT4 storage_method = 0;
|
||
if (record->GetSeriesDefnInfo(i, j, quantity_units, value_type, quantity_characteristic, storage_method))
|
||
{
|
||
series_def.quantity_units_id = quantity_units;
|
||
series_def.value_type_id = make_guid_value(value_type);
|
||
series_def.quantity_characteristic_id = make_guid_value(quantity_characteristic);
|
||
series_def.storage_method_id = storage_method;
|
||
}
|
||
|
||
UINT4 precision = 0;
|
||
double resolution = 0.0;
|
||
if (record->GetSeriesDefnPrecisionAndResolution(i, j, precision, resolution))
|
||
{
|
||
series_def.significant_digits_id = precision;
|
||
series_def.quantity_resolution = resolution;
|
||
}
|
||
|
||
double nominal = 0.0;
|
||
if (record->GetSeriesDefnNominal(i, j, nominal))
|
||
series_def.nominal_quantity = nominal;
|
||
|
||
CPQDIF_E_Collection* series_coll = record->GetOneSeriesDefn(i, j);
|
||
if (series_coll != nullptr)
|
||
{
|
||
read_string_tag(series_coll, tagValueTypeName, series_def.value_type_name);
|
||
read_uint_tag(series_coll, tagHintGreekPrefixID, series_def.hint_greek_prefix_id);
|
||
read_uint_tag(series_coll, tagHintPreferredUnitsID, series_def.hint_preferred_units_id);
|
||
read_uint_tag(series_coll, tagHintDefaultDisplayID, series_def.hint_default_display_id);
|
||
read_double_tag(series_coll, tagProbInterval, series_def.prob_interval);
|
||
read_double_tag(series_coll, tagProbPercentile, series_def.prob_percentile);
|
||
read_double_tag(series_coll, tagSeriesNominalQuantity, series_def.nominal_quantity);
|
||
read_timestamp_tag(series_coll, tagEffective, file_convert, series_def.effective_time);
|
||
|
||
const std::set<std::string> known_series = {
|
||
"tagValueTypeID", "tagQuantityUnitsID", "tagQuantityCharacteristicID",
|
||
"tagQuantitySignificantDigitsID", "tagQuantityResolutionID", "tagStorageMethodID",
|
||
"tagValueTypeName", "tagHintGreekPrefixID", "tagHintPreferredUnitsID",
|
||
"tagHintDefaultDisplayID", "tagProbInterval", "tagProbPercentile",
|
||
"tagSeriesNominalQuantity", "tagEffective"
|
||
};
|
||
collect_extra_tags(series_coll, known_series, file_convert, series_def.extra_tags);
|
||
}
|
||
|
||
channel_def.series_definitions.push_back(std::move(series_def));
|
||
}
|
||
|
||
out.channel_definitions.push_back(std::move(channel_def));
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
bool parse_monitor_settings_record(CPQDIF& file_convert,
|
||
CPQDIF_R_Settings* record,
|
||
long record_index,
|
||
int settings_index,
|
||
PqdifMonitorSettingsRecord& out)
|
||
{
|
||
if (record == nullptr)
|
||
return false;
|
||
|
||
out.header = build_record_header_info(record, record_index);
|
||
out.settings_index = settings_index;
|
||
out.record_index = record_index;
|
||
|
||
TIMESTAMPPQDIF time_effective{};
|
||
TIMESTAMPPQDIF time_installed{};
|
||
TIMESTAMPPQDIF time_removed{};
|
||
bool use_cal = false;
|
||
bool use_trans = false;
|
||
if (record->GetInfo(time_effective, time_installed, time_removed, use_cal, use_trans))
|
||
{
|
||
out.effective_time = make_timestamp_value(file_convert, time_effective);
|
||
out.time_installed = make_timestamp_value(file_convert, time_installed);
|
||
out.time_removed = make_timestamp_value(file_convert, time_removed);
|
||
out.use_calibration = use_cal;
|
||
out.use_transducer = use_trans;
|
||
}
|
||
|
||
UINT4 connection_type = 0;
|
||
if (record->GetConnectionInfo(connection_type))
|
||
out.physical_connection = connection_type;
|
||
|
||
REAL8 nominal_frequency = 0.0;
|
||
if (record->GetNominalFrequency(nominal_frequency))
|
||
out.nominal_frequency = nominal_frequency;
|
||
|
||
CPQDIF_E_Collection* main = record->GetMainCollection();
|
||
if (main != nullptr)
|
||
{
|
||
read_double_tag(main, tagNominalVoltage, out.nominal_voltage);
|
||
read_bool_tag(main, tagIsPCC, out.is_pcc);
|
||
|
||
const std::set<std::string> known = {
|
||
"tagEffective", "tagTimeInstalled", "tagTimeRemoved", "tagUseCalibration",
|
||
"tagUseTransducer", "tagNominalFrequency", "tagSettingPhysicalConnection",
|
||
"tagNominalVoltage", "tagIsPCC", "tagChannelSettings"
|
||
};
|
||
collect_extra_tags(main, known, file_convert, out.extra_tags);
|
||
}
|
||
|
||
const long channel_count = record->GetCountChannels();
|
||
out.channel_settings.reserve(static_cast<size_t>(std::max<long>(channel_count, 0)));
|
||
for (long i = 0; i < channel_count; ++i)
|
||
{
|
||
PqdifChannelSetting channel_setting;
|
||
channel_setting.channel_setting_index = static_cast<int>(i);
|
||
|
||
UINT4 channel_def_index = 0;
|
||
if (record->GetChannelInfo(i, channel_def_index))
|
||
channel_setting.channel_def_index = static_cast<int>(channel_def_index);
|
||
|
||
CPQDIF_E_Collection* channel_coll = record->GetOneChannelSetting(i);
|
||
if (channel_coll != nullptr)
|
||
{
|
||
read_uint_tag(channel_coll, tagTriggerTypeID, channel_setting.trigger_type_id);
|
||
read_double_tag(channel_coll, tagFullScale, channel_setting.full_scale);
|
||
read_double_tag(channel_coll, tagNoiseFloor, channel_setting.noise_floor);
|
||
read_uint_tag(channel_coll, tagXDTransformerTypeID, channel_setting.xd_transformer_type_id);
|
||
read_double_tag(channel_coll, tagXDSystemSideRatio, channel_setting.xd_system_side_ratio);
|
||
read_double_tag(channel_coll, tagXDMonitorSideRatio, channel_setting.xd_monitor_side_ratio);
|
||
read_double_tag(channel_coll, tagCalTimeSkew, channel_setting.cal_time_skew);
|
||
read_double_tag(channel_coll, tagCalOffset, channel_setting.cal_offset);
|
||
read_double_tag(channel_coll, tagCalRatio, channel_setting.cal_ratio);
|
||
read_bool_tag(channel_coll, tagCalMustUseARCal, channel_setting.cal_must_use_arcal);
|
||
read_double_tag(channel_coll, tagTriggerHighHigh, channel_setting.trigger_high_high);
|
||
read_double_tag(channel_coll, tagTriggerHigh, channel_setting.trigger_high);
|
||
read_double_tag(channel_coll, tagTriggerLow, channel_setting.trigger_low);
|
||
read_double_tag(channel_coll, tagTriggerLowLow, channel_setting.trigger_low_low);
|
||
read_double_tag(channel_coll, tagTriggerDeadBand, channel_setting.trigger_deadband);
|
||
read_double_tag(channel_coll, tagTriggerRate, channel_setting.trigger_rate);
|
||
|
||
CPQDIF_Element* trigger_shape = channel_coll->GetElement(tagTriggerShapeParam, ID_ELEMENT_TYPE_VECTOR);
|
||
if (trigger_shape != nullptr)
|
||
channel_setting.trigger_shape_param = extract_vector_values(static_cast<CPQDIF_E_Vector*>(trigger_shape), file_convert);
|
||
|
||
CPQDIF_Element* xd_response = channel_coll->GetElement(tagXDFrequencyResponse, ID_ELEMENT_TYPE_VECTOR);
|
||
if (xd_response != nullptr)
|
||
channel_setting.xd_frequency_response = extract_vector_values(static_cast<CPQDIF_E_Vector*>(xd_response), file_convert);
|
||
|
||
CPQDIF_Element* cal_applied = channel_coll->GetElement(tagCalApplied, ID_ELEMENT_TYPE_VECTOR);
|
||
if (cal_applied != nullptr)
|
||
channel_setting.cal_applied = extract_vector_values(static_cast<CPQDIF_E_Vector*>(cal_applied), file_convert);
|
||
|
||
CPQDIF_Element* cal_recorded = channel_coll->GetElement(tagCalRecorded, ID_ELEMENT_TYPE_VECTOR);
|
||
if (cal_recorded != nullptr)
|
||
channel_setting.cal_recorded = extract_vector_values(static_cast<CPQDIF_E_Vector*>(cal_recorded), file_convert);
|
||
|
||
const std::set<std::string> known_channel = {
|
||
"tagChannelDefnIdx", "tagTriggerTypeID", "tagFullScale", "tagNoiseFloor",
|
||
"tagTriggerShapeParam", "tagXDTransformerTypeID", "tagXDSystemSideRatio",
|
||
"tagXDMonitorSideRatio", "tagXDFrequencyResponse", "tagCalTimeSkew",
|
||
"tagCalOffset", "tagCalRatio", "tagCalMustUseARCal", "tagCalApplied",
|
||
"tagCalRecorded", "tagTriggerHighHigh", "tagTriggerHigh", "tagTriggerLow",
|
||
"tagTriggerLowLow", "tagTriggerDeadBand", "tagTriggerRate"
|
||
};
|
||
collect_extra_tags(channel_coll, known_channel, file_convert, channel_setting.extra_tags);
|
||
}
|
||
|
||
out.channel_settings.push_back(std::move(channel_setting));
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
bool parse_observation_record(CPQDIF& file_convert,
|
||
CPQDIF_R_Observation* record,
|
||
long record_index,
|
||
int observation_index,
|
||
int related_data_source_index,
|
||
long related_data_source_record_index,
|
||
int related_settings_index,
|
||
long related_settings_record_index,
|
||
PqdifObservationRecord& out)
|
||
{
|
||
if (record == nullptr)
|
||
return false;
|
||
|
||
out.header = build_record_header_info(record, record_index);
|
||
out.observation_index = observation_index;
|
||
out.record_index = record_index;
|
||
out.related_data_source_index = related_data_source_index;
|
||
out.related_data_source_record_index = related_data_source_record_index;
|
||
out.related_settings_index = related_settings_index;
|
||
out.related_settings_record_index = related_settings_record_index;
|
||
|
||
TIMESTAMPPQDIF time_start{};
|
||
TIMESTAMPPQDIF time_create{};
|
||
std::string observation_name;
|
||
if (record->GetInfo(time_start, time_create, observation_name))
|
||
{
|
||
out.observation_name = observation_name;
|
||
out.time_start = make_timestamp_value(file_convert, time_start);
|
||
out.time_create = make_timestamp_value(file_convert, time_create);
|
||
}
|
||
|
||
UINT4 trigger_method = 0;
|
||
CPQDIF_E_Vector* trigger_channels = nullptr;
|
||
TIMESTAMPPQDIF time_triggered{};
|
||
if (record->GetTriggerInfo(trigger_method, &trigger_channels, time_triggered))
|
||
{
|
||
out.trigger_method_id = trigger_method;
|
||
out.time_triggered = make_timestamp_value(file_convert, time_triggered);
|
||
|
||
if (trigger_channels != nullptr)
|
||
{
|
||
PqdifValueArray trigger_indexes = extract_vector_values(trigger_channels, file_convert);
|
||
for (size_t i = 0; i < trigger_indexes.int_values.size(); ++i)
|
||
out.channel_trigger_indexes.push_back(static_cast<int>(trigger_indexes.int_values[i]));
|
||
for (size_t i = 0; i < trigger_indexes.uint_values.size(); ++i)
|
||
out.channel_trigger_indexes.push_back(static_cast<int>(trigger_indexes.uint_values[i]));
|
||
}
|
||
}
|
||
|
||
CPQDIF_E_Collection* main = record->GetMainCollection();
|
||
if (main != nullptr)
|
||
{
|
||
read_uint_tag(main, tagObservationSerial, out.observation_serial);
|
||
read_uint_tag(main, tagObservationAggregationSerial, out.observation_aggregation_serial);
|
||
read_guid_tag(main, tagDisturbanceCategoryID, out.disturbance_category_id);
|
||
|
||
const std::set<std::string> known = {
|
||
"tagObservationName", "tagTimeCreate", "tagTimeStart", "tagTriggerMethodID",
|
||
"tagTimeTriggered", "tagChannelTriggerIdx", "tagObservationSerial",
|
||
"tagObservationAggregationSerial", "tagDisturbanceCategoryID", "tagChannelInstances"
|
||
};
|
||
collect_extra_tags(main, known, file_convert, out.extra_tags);
|
||
}
|
||
|
||
const long channel_count = record->GetCountChannels();
|
||
out.channel_instances.reserve(static_cast<size_t>(std::max<long>(channel_count, 0)));
|
||
for (long i = 0; i < channel_count; ++i)
|
||
{
|
||
PqdifChannelInstance channel_instance;
|
||
channel_instance.channel_instance_index = static_cast<int>(i);
|
||
|
||
long channel_def_index = -1;
|
||
if (record->GetChannelDefnIdx(i, channel_def_index))
|
||
channel_instance.channel_def_index = static_cast<int>(channel_def_index);
|
||
|
||
std::string channel_name;
|
||
UINT4 phase_id = 0;
|
||
GUID quantity_type{};
|
||
UINT4 quantity_measured = 0;
|
||
if (record->GetChannelInfo(i, channel_name, phase_id, quantity_type, quantity_measured))
|
||
{
|
||
channel_instance.channel_name = channel_name;
|
||
channel_instance.phase_id = phase_id;
|
||
channel_instance.quantity_type_id = make_guid_value(quantity_type);
|
||
channel_instance.quantity_measured_id = quantity_measured;
|
||
}
|
||
|
||
long primary_series = -1;
|
||
if (record->GetChannelPrimarySeries(i, primary_series))
|
||
channel_instance.primary_series_index = static_cast<int>(primary_series);
|
||
|
||
CPQDIF_E_Collection* channel_coll = record->GetOneChannel(i);
|
||
if (channel_coll != nullptr)
|
||
{
|
||
read_double_tag(channel_coll, tagCharactDuration, channel_instance.charact_duration);
|
||
read_double_tag(channel_coll, tagCharactMagnitude, channel_instance.charact_magnitude);
|
||
read_double_tag(channel_coll, tagCharactFrequency, channel_instance.charact_frequency);
|
||
read_double_tag(channel_coll, tagChannelFrequency, channel_instance.channel_frequency);
|
||
read_int_tag(channel_coll, tagChannelGroupID, channel_instance.channel_group_id);
|
||
|
||
const std::set<std::string> known_channel = {
|
||
"tagChannelDefnIdx", "tagCharactDuration", "tagCharactMagnitude",
|
||
"tagCharactFrequency", "tagChannelFrequency", "tagChannelGroupID",
|
||
"tagSeriesInstances"
|
||
};
|
||
collect_extra_tags(channel_coll, known_channel, file_convert, channel_instance.extra_tags);
|
||
}
|
||
|
||
const long series_count = record->GetCountSeries(static_cast<int>(i));
|
||
channel_instance.series_instances.reserve(static_cast<size_t>(std::max<long>(series_count, 0)));
|
||
for (long j = 0; j < series_count; ++j)
|
||
{
|
||
PqdifSeriesInstance series_instance;
|
||
series_instance.series_instance_index = static_cast<int>(j);
|
||
series_instance.series_def_index = static_cast<int>(j);
|
||
|
||
UINT4 quantity_units = 0;
|
||
GUID quantity_characteristic{};
|
||
GUID value_type{};
|
||
if (record->GetSeriesInfo(i, j, quantity_units, quantity_characteristic, value_type))
|
||
{
|
||
series_instance.quantity_units_id = quantity_units;
|
||
series_instance.quantity_characteristic_id = make_guid_value(quantity_characteristic);
|
||
series_instance.value_type_id = make_guid_value(value_type);
|
||
}
|
||
|
||
long base_type = -1;
|
||
if (record->GetSeriesBaseType(i, j, base_type))
|
||
series_instance.series_base_type = base_type;
|
||
|
||
record->GetSeriesBaseQuantity(i, j, series_instance.series_base_quantity);
|
||
record->GetSeriesScale(i, j, series_instance.scale, series_instance.offset);
|
||
record->GetSeriesDefnNominal(i, j, series_instance.nominal_quantity);
|
||
|
||
UINT4 precision = 0;
|
||
double resolution = 0.0;
|
||
if (record->GetSeriesDefnPrecisionAndResolution(i, j, precision, resolution))
|
||
{
|
||
series_instance.significant_digits_id = precision;
|
||
series_instance.quantity_resolution = resolution;
|
||
}
|
||
|
||
CPQDIF_E_Collection* series_coll = record->GetOneSeries(i, j);
|
||
if (series_coll != nullptr)
|
||
{
|
||
read_int_tag(series_coll, tagSeriesShareChannelIdx, series_instance.share_channel_index);
|
||
read_int_tag(series_coll, tagSeriesShareSeriesIdx, series_instance.share_series_index);
|
||
|
||
const std::set<std::string> known_series = {
|
||
"tagSeriesBaseQuantity", "tagSeriesScale", "tagSeriesOffset",
|
||
"tagSeriesShareChannelIdx", "tagSeriesShareSeriesIdx", "tagSeriesValues"
|
||
};
|
||
collect_extra_tags(series_coll, known_series, file_convert, series_instance.extra_tags);
|
||
}
|
||
|
||
CPQDIF_E_Vector* series_values = record->GetSeriesValueVector(i, j);
|
||
if (series_values != nullptr)
|
||
series_instance.values = extract_vector_values(series_values, file_convert);
|
||
|
||
channel_instance.series_instances.push_back(std::move(series_instance));
|
||
}
|
||
|
||
out.channel_instances.push_back(std::move(channel_instance));
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
bool parse_pqdif_file_full(const std::string& file_path, PqdifLogicalFile& out_file, std::string& err)
|
||
{
|
||
out_file = PqdifLogicalFile{};
|
||
|
||
CPQDIF file_convert;
|
||
file_convert.put_FlatFileName(file_path);
|
||
|
||
std::string basic_err;
|
||
if (!dump_file_basic_info(file_path, basic_err))
|
||
{
|
||
err = "precheck failed: " + basic_err;
|
||
return false;
|
||
}
|
||
|
||
if (!file_convert.Read())
|
||
{
|
||
err = "CPQDIF::Read() failed";
|
||
return false;
|
||
}
|
||
|
||
const int record_count = static_cast<int>(file_convert.RecordGetCount());
|
||
|
||
int current_data_source_index = -1;
|
||
long current_data_source_record_index = -1;
|
||
int current_settings_index = -1;
|
||
long current_settings_record_index = -1;
|
||
|
||
for (int i_record = 0; i_record < record_count; ++i_record)
|
||
{
|
||
GUID record_guid{};
|
||
std::string record_name;
|
||
if (!file_convert.RecordGetInfo(i_record, &record_guid, record_name))
|
||
continue;
|
||
|
||
long raw_record_handle = 0;
|
||
if (file_convert.RecordRequestRecord(i_record, &raw_record_handle))
|
||
{
|
||
CPQDIFRecord* raw_record = reinterpret_cast<CPQDIFRecord*>(raw_record_handle);
|
||
out_file.record_headers.push_back(build_record_header_info(raw_record, i_record));
|
||
}
|
||
|
||
if (PQDIF_IsEqualGUID(record_guid, tagContainer))
|
||
{
|
||
CPQDIF_R_General* record = reinterpret_cast<CPQDIF_R_General*>(raw_record_handle);
|
||
PqdifContainerRecord container;
|
||
if (parse_container_record(file_convert, record, i_record, container))
|
||
out_file.containers.push_back(std::move(container));
|
||
continue;
|
||
}
|
||
|
||
if (PQDIF_IsEqualGUID(record_guid, tagRecDataSource))
|
||
{
|
||
CPQDIF_R_DataSource* record = reinterpret_cast<CPQDIF_R_DataSource*>(raw_record_handle);
|
||
PqdifDataSourceRecord data_source;
|
||
const int data_source_index = static_cast<int>(out_file.data_sources.size());
|
||
if (parse_data_source_record(file_convert, record, i_record, data_source_index, data_source))
|
||
{
|
||
out_file.data_sources.push_back(std::move(data_source));
|
||
current_data_source_index = data_source_index;
|
||
current_data_source_record_index = i_record;
|
||
current_settings_index = -1;
|
||
current_settings_record_index = -1;
|
||
}
|
||
continue;
|
||
}
|
||
|
||
if (PQDIF_IsEqualGUID(record_guid, tagRecMonitorSettings))
|
||
{
|
||
CPQDIF_R_Settings* record = reinterpret_cast<CPQDIF_R_Settings*>(raw_record_handle);
|
||
PqdifMonitorSettingsRecord settings;
|
||
const int settings_index = static_cast<int>(out_file.monitor_settings.size());
|
||
if (parse_monitor_settings_record(file_convert, record, i_record, settings_index, settings))
|
||
{
|
||
out_file.monitor_settings.push_back(std::move(settings));
|
||
current_settings_index = settings_index;
|
||
current_settings_record_index = i_record;
|
||
}
|
||
continue;
|
||
}
|
||
|
||
if (PQDIF_IsEqualGUID(record_guid, tagRecObservation))
|
||
{
|
||
long observation_handle = 0;
|
||
if (!file_convert.RecordRequestObservation(i_record, &observation_handle))
|
||
continue;
|
||
|
||
CPQDIF_R_Observation* observation = reinterpret_cast<CPQDIF_R_Observation*>(observation_handle);
|
||
PqdifObservationRecord record;
|
||
const int observation_index = static_cast<int>(out_file.observations.size());
|
||
parse_observation_record(
|
||
file_convert,
|
||
observation,
|
||
i_record,
|
||
observation_index,
|
||
current_data_source_index,
|
||
current_data_source_record_index,
|
||
current_settings_index,
|
||
current_settings_record_index,
|
||
record);
|
||
out_file.observations.push_back(std::move(record));
|
||
file_convert.RecordReleaseObservation(observation_handle);
|
||
}
|
||
}
|
||
|
||
file_convert.Close();
|
||
return true;
|
||
}
|
||
|
||
|
||
|
||
constexpr float kPqdifBase64MissingFloat = 3.14159f;
|
||
|
||
int stat_value_kind_code_for_base64(StatValueKind kind)
|
||
{
|
||
switch (kind)
|
||
{
|
||
case StatValueKind::Max: return 1;
|
||
case StatValueKind::Min: return 2;
|
||
case StatValueKind::Avg: return 3;
|
||
case StatValueKind::P95: return 4;
|
||
default: return 0;
|
||
}
|
||
}
|
||
|
||
std::string pqdif_base64_encode_bytes(const unsigned char* bytes_to_encode, size_t in_len)
|
||
{
|
||
static const char base64_chars[] =
|
||
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||
"abcdefghijklmnopqrstuvwxyz"
|
||
"0123456789+/";
|
||
|
||
std::string ret;
|
||
ret.reserve(((in_len + 2) / 3) * 4);
|
||
|
||
int i = 0;
|
||
unsigned char char_array_3[3] = { 0, 0, 0 };
|
||
unsigned char char_array_4[4] = { 0, 0, 0, 0 };
|
||
|
||
while (in_len--)
|
||
{
|
||
char_array_3[i++] = *(bytes_to_encode++);
|
||
if (i == 3)
|
||
{
|
||
char_array_4[0] = static_cast<unsigned char>((char_array_3[0] & 0xfc) >> 2);
|
||
char_array_4[1] = static_cast<unsigned char>(((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4));
|
||
char_array_4[2] = static_cast<unsigned char>(((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6));
|
||
char_array_4[3] = static_cast<unsigned char>(char_array_3[2] & 0x3f);
|
||
|
||
for (i = 0; i < 4; ++i)
|
||
ret += base64_chars[char_array_4[i]];
|
||
i = 0;
|
||
}
|
||
}
|
||
|
||
if (i != 0)
|
||
{
|
||
int j = 0;
|
||
for (j = i; j < 3; ++j)
|
||
char_array_3[j] = '\0';
|
||
|
||
char_array_4[0] = static_cast<unsigned char>((char_array_3[0] & 0xfc) >> 2);
|
||
char_array_4[1] = static_cast<unsigned char>(((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4));
|
||
char_array_4[2] = static_cast<unsigned char>(((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6));
|
||
char_array_4[3] = static_cast<unsigned char>(char_array_3[2] & 0x3f);
|
||
|
||
for (j = 0; j < i + 1; ++j)
|
||
ret += base64_chars[char_array_4[j]];
|
||
|
||
while (i++ < 3)
|
||
ret += '=';
|
||
}
|
||
|
||
return ret;
|
||
}
|
||
|
||
std::string pqdif_base64_encode_float_vector(const std::vector<float>& values)
|
||
{
|
||
if (values.empty())
|
||
return std::string();
|
||
const unsigned char* byte_data = reinterpret_cast<const unsigned char*>(values.data());
|
||
const size_t byte_size = values.size() * sizeof(float);
|
||
return pqdif_base64_encode_bytes(byte_data, byte_size);
|
||
}
|
||
|
||
size_t pqdif_stat_base64_count_records_in_batch(const PqdifStatBase64FileBatch& batch)
|
||
{
|
||
size_t n = 0;
|
||
for (const auto& tp : batch.time_points)
|
||
n += tp.records.size();
|
||
return n;
|
||
}
|
||
|
||
// 统计任意 Base64 文件级批次队列内部包含的子记录总数。
|
||
// 对象用途:既可以统计“生成队列”,也可以统计“待后续处理队列”。
|
||
// 调用约定:调用方必须已经持有对应队列的互斥锁。
|
||
size_t pqdif_stat_base64_count_records_in_queue_unlocked(const std::deque<PqdifStatBase64FileBatch>& queue)
|
||
{
|
||
size_t n = 0;
|
||
for (const auto& batch : queue)
|
||
n += pqdif_stat_base64_count_records_in_batch(batch);
|
||
return n;
|
||
}
|
||
|
||
// 统计“生成队列”内部的子记录总数。
|
||
// 对象用途:保留旧函数名,供已有 GetPqdifStatBase64RecordCountInQueue() 复用。
|
||
// 调用约定:调用方必须已经持有 g_pqdif_stat_base64_mutex。
|
||
size_t pqdif_stat_base64_count_records_in_queue_unlocked()
|
||
{
|
||
return pqdif_stat_base64_count_records_in_queue_unlocked(g_pqdif_stat_base64_queue);
|
||
}
|
||
|
||
void pqdif_dump_stat_base64_file_batch_full(const PqdifStatBase64FileBatch& batch)
|
||
{
|
||
std::cout << "========== PQDIF BASE64 SAVED OBJECT FULL DUMP ==========" << std::endl;
|
||
std::cout << "[FILE BATCH] file=" << batch.pqdif_file_path
|
||
<< ", source_file=" << batch.source_file
|
||
<< ", mac=" << batch.mac
|
||
<< ", parsed_at=" << batch.parsed_at_text
|
||
<< ", connection_kind=" << stat_connection_kind_name(batch.connection_kind)
|
||
<< ", time_point_count=" << batch.time_point_count
|
||
<< ", total_record_count=" << batch.total_record_count
|
||
<< ", total_float_count=" << batch.total_float_count
|
||
<< ", total_placeholder_count=" << batch.total_placeholder_count
|
||
<< ", total_base64_chars=" << batch.total_base64_chars
|
||
<< std::endl;
|
||
|
||
for (size_t i = 0; i < batch.time_points.size(); ++i)
|
||
{
|
||
const PqdifStatBase64TimePointPacket& tp = batch.time_points[i];
|
||
std::cout << " [TIME POINT " << i << "] time=" << tp.timestamp_text
|
||
<< ", timestamp=" << static_cast<long long>(tp.timestamp)
|
||
<< ", record_count=" << tp.record_count
|
||
<< ", total_float_count=" << tp.total_float_count
|
||
<< ", total_placeholder_count=" << tp.total_placeholder_count
|
||
<< ", total_base64_chars=" << tp.total_base64_chars
|
||
<< std::endl;
|
||
|
||
for (size_t j = 0; j < tp.records.size(); ++j)
|
||
{
|
||
const PqdifStatBase64Record& rec = tp.records[j];
|
||
std::cout << " [BASE64 SUB RECORD " << j << "] file=" << rec.pqdif_file_path
|
||
<< ", time=" << rec.timestamp_text
|
||
<< ", timestamp=" << static_cast<long long>(rec.timestamp)
|
||
<< ", kind=" << rec.value_kind_name
|
||
<< ", kind_code=" << rec.value_kind_code
|
||
<< ", connection_kind=" << stat_connection_kind_name(rec.connection_kind)
|
||
<< ", float_count=" << rec.float_count
|
||
<< ", placeholder_count=" << rec.placeholder_count
|
||
<< ", base64_len=" << rec.base64_payload.size()
|
||
<< std::endl;
|
||
std::cout << " base64_payload=" << rec.base64_payload << std::endl;
|
||
}
|
||
}
|
||
std::cout << "==========================================================" << std::endl;
|
||
}
|
||
|
||
bool push_pqdif_stat_base64_file_batch(PqdifStatBase64FileBatch&& batch)
|
||
{
|
||
// 对象用途:把“一个 PQDIF 文件解析完成后的 Base64 文件级批次”放入生成队列。
|
||
// 这里不直接做入库/上传,避免解析线程被后续业务阻塞。
|
||
std::lock_guard<std::mutex> guard(g_pqdif_stat_base64_mutex);
|
||
if (g_pqdif_stat_base64_queue.size() >= kPqdifStatBase64QueueLimit)
|
||
{
|
||
std::cout << "[PQDIF BASE64] file-batch queue full, drop oldest file batch: file="
|
||
<< g_pqdif_stat_base64_queue.front().pqdif_file_path
|
||
<< ", time_points=" << g_pqdif_stat_base64_queue.front().time_point_count
|
||
<< ", records=" << g_pqdif_stat_base64_queue.front().total_record_count
|
||
<< std::endl;
|
||
g_pqdif_stat_base64_queue.pop_front();
|
||
}
|
||
g_pqdif_stat_base64_queue.emplace_back(std::move(batch));
|
||
return true;
|
||
}
|
||
|
||
bool pqdif_move_one_generated_base64_batch_to_ready_queue()
|
||
{
|
||
// 对象用途:RunPqdifScanLoop() 每轮循环末尾调用。
|
||
// 从“生成队列”取出最多一个 PQDIF 文件级批次,移动到“待后续处理队列”。
|
||
// 注意:这里只做快速移动和核心摘要打印,不在锁内执行耗时业务逻辑。
|
||
PqdifStatBase64FileBatch batch;
|
||
|
||
{
|
||
std::lock_guard<std::mutex> guard(g_pqdif_stat_base64_mutex);
|
||
if (g_pqdif_stat_base64_queue.empty())
|
||
return false;
|
||
|
||
batch = std::move(g_pqdif_stat_base64_queue.front());
|
||
g_pqdif_stat_base64_queue.pop_front();
|
||
}
|
||
|
||
const std::string moved_file = batch.pqdif_file_path;
|
||
const size_t moved_time_points = batch.time_point_count;
|
||
const size_t moved_records = batch.total_record_count;
|
||
const size_t moved_float_count = batch.total_float_count;
|
||
const size_t moved_placeholder_count = batch.total_placeholder_count;
|
||
|
||
{
|
||
std::lock_guard<std::mutex> guard(g_pqdif_stat_base64_ready_mutex);
|
||
if (g_pqdif_stat_base64_ready_queue.size() >= kPqdifStatBase64QueueLimit)
|
||
{
|
||
std::cout << "[PQDIF BASE64 READY] ready queue full, drop oldest file batch: file="
|
||
<< g_pqdif_stat_base64_ready_queue.front().pqdif_file_path
|
||
<< ", time_points=" << g_pqdif_stat_base64_ready_queue.front().time_point_count
|
||
<< ", records=" << g_pqdif_stat_base64_ready_queue.front().total_record_count
|
||
<< std::endl;
|
||
g_pqdif_stat_base64_ready_queue.pop_front();
|
||
}
|
||
|
||
g_pqdif_stat_base64_ready_queue.emplace_back(std::move(batch));
|
||
}
|
||
|
||
std::cout << "[PQDIF BASE64 READY] moved one file batch for next step"
|
||
<< ", file=" << moved_file
|
||
<< ", time_points=" << moved_time_points
|
||
<< ", records=" << moved_records
|
||
<< ", float_count=" << moved_float_count
|
||
<< ", placeholders=" << moved_placeholder_count
|
||
<< std::endl;
|
||
|
||
return true;
|
||
}
|
||
|
||
std::string pqdif_absolute_path_text(const std::string& path)
|
||
{
|
||
try
|
||
{
|
||
return fs::absolute(fs::path(path)).string();
|
||
}
|
||
catch (...)
|
||
{
|
||
return path;
|
||
}
|
||
}
|
||
|
||
bool bucket_has_metric_value_for_kind(const AggregatedStatValues& values, StatValueKind kind, float& out)
|
||
{
|
||
switch (kind)
|
||
{
|
||
case StatValueKind::Min:
|
||
if (!values.has_min) return false;
|
||
out = static_cast<float>(values.min_value);
|
||
return true;
|
||
case StatValueKind::Max:
|
||
if (!values.has_max) return false;
|
||
out = static_cast<float>(values.max_value);
|
||
return true;
|
||
case StatValueKind::Avg:
|
||
if (!values.has_avg) return false;
|
||
out = static_cast<float>(values.avg_value);
|
||
return true;
|
||
case StatValueKind::P95:
|
||
if (!values.has_p95) return false;
|
||
out = static_cast<float>(values.p95_value);
|
||
return true;
|
||
default:
|
||
return false;
|
||
}
|
||
}
|
||
|
||
struct PqdifBase64BuildContext
|
||
{
|
||
PqdifBase64BuildContext(
|
||
const ParsedPqdifFile& file_ref,
|
||
const TimeAggregatedStatBucket& bucket_ref,
|
||
StatValueKind value_kind,
|
||
std::vector<float>& value_buffer)
|
||
: parsed_file(file_ref),
|
||
bucket(bucket_ref),
|
||
kind(value_kind),
|
||
values(value_buffer),
|
||
placeholder_count(0),
|
||
missing_metric_count(0)
|
||
{
|
||
}
|
||
|
||
const ParsedPqdifFile& parsed_file;
|
||
const TimeAggregatedStatBucket& bucket;
|
||
StatValueKind kind;
|
||
std::vector<float>& values;
|
||
size_t placeholder_count;
|
||
size_t missing_metric_count;
|
||
size_t delta_line_phase_fallback_count = 0;
|
||
std::vector<std::string> missing_metric_names;
|
||
};
|
||
|
||
bool pqdif_base64_try_get_metric_value(
|
||
const TimeAggregatedStatBucket& bucket,
|
||
StatMetricId metric_id,
|
||
StatValueKind kind,
|
||
float& out_value)
|
||
{
|
||
auto it = bucket.metrics.find(metric_id);
|
||
if (it == bucket.metrics.end())
|
||
return false;
|
||
return bucket_has_metric_value_for_kind(it->second, kind, out_value);
|
||
}
|
||
|
||
StatMetricId pqdif_base64_delta_line_to_phase_metric_id(StatMetricId metric_id)
|
||
{
|
||
switch (metric_id)
|
||
{
|
||
case StatMetricId::UabRms: return StatMetricId::UaRms;
|
||
case StatMetricId::UbcRms: return StatMetricId::UbRms;
|
||
case StatMetricId::UcaRms: return StatMetricId::UcRms;
|
||
case StatMetricId::UabDeviation: return StatMetricId::UaDeviation;
|
||
case StatMetricId::UbcDeviation: return StatMetricId::UbDeviation;
|
||
case StatMetricId::UcaDeviation: return StatMetricId::UcDeviation;
|
||
case StatMetricId::UabThd: return StatMetricId::UaThd;
|
||
case StatMetricId::UbcThd: return StatMetricId::UbThd;
|
||
case StatMetricId::UcaThd: return StatMetricId::UcThd;
|
||
case StatMetricId::UabDvc: return StatMetricId::UaDvc;
|
||
case StatMetricId::UbcDvc: return StatMetricId::UbDvc;
|
||
case StatMetricId::UcaDvc: return StatMetricId::UcDvc;
|
||
case StatMetricId::UabPst: return StatMetricId::UaPst;
|
||
case StatMetricId::UbcPst: return StatMetricId::UbPst;
|
||
case StatMetricId::UcaPst: return StatMetricId::UcPst;
|
||
case StatMetricId::UabPlt: return StatMetricId::UaPlt;
|
||
case StatMetricId::UbcPlt: return StatMetricId::UbPlt;
|
||
case StatMetricId::UcaPlt: return StatMetricId::UcPlt;
|
||
case StatMetricId::UabFundRms: return StatMetricId::UaFundRms;
|
||
case StatMetricId::UbcFundRms: return StatMetricId::UbFundRms;
|
||
case StatMetricId::UcaFundRms: return StatMetricId::UcFundRms;
|
||
case StatMetricId::UabFundAngle: return StatMetricId::UaFundAngle;
|
||
case StatMetricId::UbcFundAngle: return StatMetricId::UbFundAngle;
|
||
case StatMetricId::UcaFundAngle: return StatMetricId::UcFundAngle;
|
||
default: break;
|
||
}
|
||
|
||
const StatDynamicMetricRange* r = stat_find_dynamic_metric_range(metric_id);
|
||
if (r == nullptr)
|
||
return StatMetricId::Unknown;
|
||
|
||
const int offset = stat_dynamic_metric_order_or_slot(metric_id);
|
||
|
||
if (r->group == StatDynamicMetricGroup::LineVoltageHarmonic)
|
||
return stat_dynamic_metric_id(StatDynamicMetricGroup::VoltageHarmonic, r->phase_index, offset);
|
||
if (r->group == StatDynamicMetricGroup::LineVoltageHarmonicAngle)
|
||
return stat_dynamic_metric_id(StatDynamicMetricGroup::VoltageHarmonicAngle, r->phase_index, offset);
|
||
if (r->group == StatDynamicMetricGroup::LineVoltageInterharmonic)
|
||
return stat_dynamic_metric_id(StatDynamicMetricGroup::VoltageInterharmonic, r->phase_index, offset);
|
||
if (r->group == StatDynamicMetricGroup::LineVoltageHarmonicRatio)
|
||
return stat_dynamic_metric_id(StatDynamicMetricGroup::VoltageHarmonicRatio, r->phase_index, offset);
|
||
|
||
return StatMetricId::Unknown;
|
||
}
|
||
|
||
void pqdif_base64_push_metric(PqdifBase64BuildContext& ctx, StatMetricId metric_id)
|
||
{
|
||
float value = kPqdifBase64MissingFloat;
|
||
bool ok = false;
|
||
|
||
ok = pqdif_base64_try_get_metric_value(ctx.bucket, metric_id, ctx.kind, value);
|
||
|
||
if (!ok)
|
||
{
|
||
value = kPqdifBase64MissingFloat;
|
||
++ctx.placeholder_count;
|
||
++ctx.missing_metric_count;
|
||
const std::string name = stat_metric_name(metric_id);
|
||
ctx.missing_metric_names.push_back(name);
|
||
|
||
if (pqdif_log_enabled(PqdifLogLevel::Debug))
|
||
{
|
||
std::cout << " [BASE64 MISSING METRIC] time=" << ctx.bucket.timestamp_text
|
||
<< ", kind=" << stat_value_kind_name(ctx.kind)
|
||
<< ", metric=" << name
|
||
<< ", placeholder=" << kPqdifBase64MissingFloat
|
||
<< std::endl;
|
||
}
|
||
}
|
||
|
||
ctx.values.push_back(value);
|
||
}
|
||
|
||
void pqdif_base64_push_delta_line_metric_with_phase_fallback(PqdifBase64BuildContext& ctx, StatMetricId line_metric_id)
|
||
{
|
||
float value = kPqdifBase64MissingFloat;
|
||
|
||
if (pqdif_base64_try_get_metric_value(ctx.bucket, line_metric_id, ctx.kind, value))
|
||
{
|
||
ctx.values.push_back(value);
|
||
return;
|
||
}
|
||
|
||
const StatMetricId phase_metric_id = pqdif_base64_delta_line_to_phase_metric_id(line_metric_id);
|
||
if (phase_metric_id != StatMetricId::Unknown &&
|
||
pqdif_base64_try_get_metric_value(ctx.bucket, phase_metric_id, ctx.kind, value))
|
||
{
|
||
ctx.values.push_back(value);
|
||
++ctx.delta_line_phase_fallback_count;
|
||
|
||
if (pqdif_log_enabled(PqdifLogLevel::Info))
|
||
{
|
||
std::cout << " [BASE64 DELTA LINE FALLBACK] time=" << ctx.bucket.timestamp_text
|
||
<< ", kind=" << stat_value_kind_name(ctx.kind)
|
||
<< ", line_metric=" << stat_metric_name(line_metric_id)
|
||
<< ", fallback_phase_metric=" << stat_metric_name(phase_metric_id)
|
||
<< ", value=" << value
|
||
<< std::endl;
|
||
}
|
||
return;
|
||
}
|
||
|
||
ctx.values.push_back(kPqdifBase64MissingFloat);
|
||
++ctx.placeholder_count;
|
||
++ctx.missing_metric_count;
|
||
|
||
const std::string line_name = stat_metric_name(line_metric_id);
|
||
ctx.missing_metric_names.push_back(line_name);
|
||
|
||
if (pqdif_log_enabled(PqdifLogLevel::Debug))
|
||
{
|
||
std::cout << " [BASE64 MISSING METRIC] time=" << ctx.bucket.timestamp_text
|
||
<< ", kind=" << stat_value_kind_name(ctx.kind)
|
||
<< ", metric=" << line_name
|
||
<< ", placeholder=" << kPqdifBase64MissingFloat
|
||
<< ", reason=delta_line_metric_and_phase_fallback_missing";
|
||
if (phase_metric_id != StatMetricId::Unknown)
|
||
std::cout << ", fallback_phase_metric=" << stat_metric_name(phase_metric_id);
|
||
std::cout << std::endl;
|
||
}
|
||
}
|
||
|
||
void pqdif_base64_push_placeholder(PqdifBase64BuildContext& ctx, const std::string& label)
|
||
{
|
||
ctx.values.push_back(kPqdifBase64MissingFloat);
|
||
++ctx.placeholder_count;
|
||
|
||
if (pqdif_log_enabled(PqdifLogLevel::Trace))
|
||
{
|
||
std::cout << " [BASE64 PLACEHOLDER] time=" << ctx.bucket.timestamp_text
|
||
<< ", kind=" << stat_value_kind_name(ctx.kind)
|
||
<< ", label=" << label
|
||
<< ", value=" << kPqdifBase64MissingFloat
|
||
<< std::endl;
|
||
}
|
||
}
|
||
|
||
void pqdif_base64_push_placeholder_block(PqdifBase64BuildContext& ctx, const std::string& label, size_t count)
|
||
{
|
||
for (size_t i = 0; i < count; ++i)
|
||
{
|
||
ctx.values.push_back(kPqdifBase64MissingFloat);
|
||
++ctx.placeholder_count;
|
||
}
|
||
|
||
if (pqdif_log_enabled(PqdifLogLevel::Info))
|
||
{
|
||
std::cout << " [BASE64 PLACEHOLDER BLOCK] time=" << ctx.bucket.timestamp_text
|
||
<< ", kind=" << stat_value_kind_name(ctx.kind)
|
||
<< ", label=" << label
|
||
<< ", count=" << count
|
||
<< ", value=" << kPqdifBase64MissingFloat
|
||
<< std::endl;
|
||
}
|
||
}
|
||
|
||
void pqdif_base64_push_dynamic_order_range(
|
||
PqdifBase64BuildContext& ctx,
|
||
StatDynamicMetricGroup group,
|
||
int phase_index,
|
||
int first_offset,
|
||
int last_offset)
|
||
{
|
||
for (int offset = first_offset; offset <= last_offset; ++offset)
|
||
{
|
||
pqdif_base64_push_metric(ctx, stat_dynamic_metric_id(group, phase_index, offset));
|
||
}
|
||
}
|
||
|
||
void pqdif_base64_push_dynamic_three_phases(
|
||
PqdifBase64BuildContext& ctx,
|
||
StatDynamicMetricGroup group,
|
||
int first_offset,
|
||
int last_offset)
|
||
{
|
||
for (int phase = 0; phase < 3; ++phase)
|
||
pqdif_base64_push_dynamic_order_range(ctx, group, phase, first_offset, last_offset);
|
||
}
|
||
|
||
void pqdif_base64_push_dynamic_order_range_delta_line_fallback(
|
||
PqdifBase64BuildContext& ctx,
|
||
StatDynamicMetricGroup line_group,
|
||
int phase_index,
|
||
int first_offset,
|
||
int last_offset)
|
||
{
|
||
for (int offset = first_offset; offset <= last_offset; ++offset)
|
||
{
|
||
pqdif_base64_push_delta_line_metric_with_phase_fallback(
|
||
ctx,
|
||
stat_dynamic_metric_id(line_group, phase_index, offset));
|
||
}
|
||
}
|
||
|
||
void pqdif_base64_push_dynamic_three_line_phases_with_delta_fallback(
|
||
PqdifBase64BuildContext& ctx,
|
||
StatDynamicMetricGroup line_group,
|
||
int first_offset,
|
||
int last_offset)
|
||
{
|
||
for (int phase = 0; phase < 3; ++phase)
|
||
{
|
||
pqdif_base64_push_dynamic_order_range_delta_line_fallback(
|
||
ctx,
|
||
line_group,
|
||
phase,
|
||
first_offset,
|
||
last_offset);
|
||
}
|
||
}
|
||
|
||
void pqdif_base64_push_harmonic_power_phase(
|
||
PqdifBase64BuildContext& ctx,
|
||
int phase_index)
|
||
{
|
||
pqdif_base64_push_dynamic_order_range(ctx, StatDynamicMetricGroup::HarmonicActivePower, phase_index, 2, 50);
|
||
pqdif_base64_push_dynamic_order_range(ctx, StatDynamicMetricGroup::HarmonicReactivePower, phase_index, 2, 50);
|
||
pqdif_base64_push_dynamic_order_range(ctx, StatDynamicMetricGroup::HarmonicApparentPower, phase_index, 2, 50);
|
||
}
|
||
|
||
std::vector<float> pqdif_build_star_float_buffer_for_bucket(
|
||
const ParsedPqdifFile& parsed_file,
|
||
const TimeAggregatedStatBucket& bucket,
|
||
StatValueKind kind,
|
||
size_t& placeholder_count,
|
||
size_t& missing_metric_count,
|
||
std::vector<std::string>& missing_metric_names)
|
||
{
|
||
std::vector<float> float_buffer;
|
||
float_buffer.reserve(2463);
|
||
|
||
PqdifBase64BuildContext ctx{ parsed_file, bucket, kind, float_buffer };
|
||
|
||
// 1) 三相电压有效值、三相电流有效值、三线电压有效值
|
||
pqdif_base64_push_metric(ctx, StatMetricId::UaRms);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::UbRms);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::UcRms);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::IaRms);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::IbRms);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::IcRms);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::UabRms);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::UbcRms);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::UcaRms);
|
||
|
||
// 2) 三相电压偏差 + 三线电压偏差占位
|
||
pqdif_base64_push_metric(ctx, StatMetricId::UaDeviation);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::UbDeviation);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::UcDeviation);
|
||
pqdif_base64_push_placeholder_block(ctx, "line voltage deviation placeholder(Uab/Ubc/Uca)", 3);
|
||
|
||
// 3) 频率偏差 + 频率
|
||
pqdif_base64_push_metric(ctx, StatMetricId::FrequencyDeviation);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::Frequency);
|
||
|
||
// 4) 电压零/负/正序和负序不平衡
|
||
pqdif_base64_push_metric(ctx, StatMetricId::UZeroSeq);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::UNegSeq);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::UPosSeq);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::UNegSeqUnbalance);
|
||
|
||
// 5) 电流零/负/正序和负序不平衡
|
||
pqdif_base64_push_metric(ctx, StatMetricId::IZeroSeq);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::INegSeq);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::IPosSeq);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::INegSeqUnbalance);
|
||
|
||
// 6) 三相电压谐波 2-50 次指标
|
||
pqdif_base64_push_dynamic_three_phases(ctx, StatDynamicMetricGroup::VoltageHarmonic, 2, 50);
|
||
|
||
// 7) 三相电流谐波 2-50 次指标
|
||
pqdif_base64_push_dynamic_three_phases(ctx, StatDynamicMetricGroup::CurrentHarmonic, 2, 50);
|
||
|
||
// 8) 三线电压谐波占位
|
||
pqdif_base64_push_placeholder_block(ctx, "line voltage harmonic placeholder(Uab/Ubc/Uca,2-50)", 49 * 3);
|
||
|
||
// 9) 三相电压谐波 2-50 次相角指标
|
||
pqdif_base64_push_dynamic_three_phases(ctx, StatDynamicMetricGroup::VoltageHarmonicAngle, 2, 50);
|
||
|
||
// 10) 三相电流谐波 2-50 次相角指标
|
||
pqdif_base64_push_dynamic_three_phases(ctx, StatDynamicMetricGroup::CurrentHarmonicAngle, 2, 50);
|
||
|
||
// 11) 三线电压谐波相角占位
|
||
pqdif_base64_push_placeholder_block(ctx, "line voltage harmonic angle placeholder(Uab/Ubc/Uca,2-50)", 49 * 3);
|
||
|
||
// 12) 三相电压间谐波 0.5-49.5 次指标
|
||
pqdif_base64_push_dynamic_three_phases(ctx, StatDynamicMetricGroup::VoltageInterharmonic, 0, 49);
|
||
|
||
// 13) 三相电流间谐波 0.5-49.5 次指标
|
||
pqdif_base64_push_dynamic_three_phases(ctx, StatDynamicMetricGroup::CurrentInterharmonic, 0, 49);
|
||
|
||
// 14) 三线电压间谐波占位
|
||
pqdif_base64_push_placeholder_block(ctx, "line voltage interharmonic placeholder(Uab/Ubc/Uca,0.5-49.5)", 50 * 3);
|
||
|
||
// 15) A/B/C/总 有功/无功/视在功率
|
||
pqdif_base64_push_metric(ctx, StatMetricId::PaPower);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::QaPower);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::SaPower);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::PbPower);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::QbPower);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::SbPower);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::PcPower);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::QcPower);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::ScPower);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::PTotalPower);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::QTotalPower);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::STotalPower);
|
||
|
||
// 16) A/B/C/总 谐波 2-50 次 有功/无功/视在功率
|
||
pqdif_base64_push_harmonic_power_phase(ctx, 0);
|
||
pqdif_base64_push_harmonic_power_phase(ctx, 1);
|
||
pqdif_base64_push_harmonic_power_phase(ctx, 2);
|
||
pqdif_base64_push_harmonic_power_phase(ctx, 3);
|
||
|
||
// 17) 谐波含有率:三相电压、三相电流、三线电压占位
|
||
pqdif_base64_push_dynamic_three_phases(ctx, StatDynamicMetricGroup::VoltageHarmonicRatio, 2, 50);
|
||
pqdif_base64_push_dynamic_three_phases(ctx, StatDynamicMetricGroup::CurrentHarmonicRatio, 2, 50);
|
||
pqdif_base64_push_placeholder_block(ctx, "line voltage harmonic ratio placeholder(Uab/Ubc/Uca,2-50)", 49 * 3);
|
||
|
||
// 18) 谐波总畸变率:三相电压、三相电流、三线电压占位
|
||
pqdif_base64_push_metric(ctx, StatMetricId::UaThd);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::UbThd);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::UcThd);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::IaThd);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::IbThd);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::IcThd);
|
||
pqdif_base64_push_placeholder_block(ctx, "line voltage THD placeholder(Uab/Ubc/Uca)", 3);
|
||
|
||
// 19) 三相功率因数和总功率因数
|
||
pqdif_base64_push_metric(ctx, StatMetricId::PFa);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::PFb);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::PFc);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::PFTotal);
|
||
|
||
// 20) 三相基波功率因数和总基波功率因数
|
||
pqdif_base64_push_metric(ctx, StatMetricId::FundPFa);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::FundPFb);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::FundPFc);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::FundPFTotal);
|
||
|
||
// 21) 三相电压变动幅值 + 三线占位
|
||
pqdif_base64_push_metric(ctx, StatMetricId::UaDvc);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::UbDvc);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::UcDvc);
|
||
pqdif_base64_push_placeholder_block(ctx, "line voltage DVC placeholder(Uab/Ubc/Uca)", 3);
|
||
|
||
// 22) 三相短闪 + 三线占位
|
||
pqdif_base64_push_metric(ctx, StatMetricId::UaPst);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::UbPst);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::UcPst);
|
||
pqdif_base64_push_placeholder_block(ctx, "line voltage Pst placeholder(Uab/Ubc/Uca)", 3);
|
||
|
||
// 23) 三相长闪 + 三线占位
|
||
pqdif_base64_push_metric(ctx, StatMetricId::UaPlt);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::UbPlt);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::UcPlt);
|
||
pqdif_base64_push_placeholder_block(ctx, "line voltage Plt placeholder(Uab/Ubc/Uca)", 3);
|
||
|
||
// 24) A/B/C/总 基波有功/无功/视在功率
|
||
pqdif_base64_push_metric(ctx, StatMetricId::PaFundPower);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::QaFundPower);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::SaFundPower);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::PbFundPower);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::QbFundPower);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::SbFundPower);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::PcFundPower);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::QcFundPower);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::ScFundPower);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::PTotalFundPower);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::QTotalFundPower);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::STotalFundPower);
|
||
|
||
// 25) 基波有效值:三相电压、三相电流、三线电压占位
|
||
pqdif_base64_push_metric(ctx, StatMetricId::UaFundRms);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::UbFundRms);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::UcFundRms);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::IaFundRms);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::IbFundRms);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::IcFundRms);
|
||
pqdif_base64_push_placeholder_block(ctx, "line voltage fundamental RMS placeholder(Uab/Ubc/Uca)", 3);
|
||
|
||
// 26) 基波相角:三相电压、三相电流、三线电压相角占位
|
||
pqdif_base64_push_metric(ctx, StatMetricId::UaFundAngle);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::UbFundAngle);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::UcFundAngle);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::IaFundAngle);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::IbFundAngle);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::IcFundAngle);
|
||
pqdif_base64_push_placeholder_block(ctx, "line voltage fundamental angle placeholder(Uab/Ubc/Uca)", 3);
|
||
|
||
placeholder_count = ctx.placeholder_count;
|
||
missing_metric_count = ctx.missing_metric_count;
|
||
missing_metric_names = ctx.missing_metric_names;
|
||
return float_buffer;
|
||
}
|
||
|
||
|
||
std::vector<float> pqdif_build_delta_float_buffer_for_bucket(
|
||
const ParsedPqdifFile& parsed_file,
|
||
const TimeAggregatedStatBucket& bucket,
|
||
StatValueKind kind,
|
||
size_t& placeholder_count,
|
||
size_t& missing_metric_count,
|
||
std::vector<std::string>& missing_metric_names)
|
||
{
|
||
std::vector<float> float_buffer;
|
||
// Delta 角型与 Wye 星型必须保持完全相同的 float 数量,便于后续统一解码。
|
||
float_buffer.reserve(2463);
|
||
|
||
PqdifBase64BuildContext ctx(parsed_file, bucket, kind, float_buffer);
|
||
|
||
// 1) 三相电压有效值、三相电流有效值、三线电压有效值
|
||
pqdif_base64_push_metric(ctx, StatMetricId::UaRms);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::UbRms);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::UcRms);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::IaRms);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::IbRms);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::IcRms);
|
||
pqdif_base64_push_delta_line_metric_with_phase_fallback(ctx, StatMetricId::UabRms);
|
||
pqdif_base64_push_delta_line_metric_with_phase_fallback(ctx, StatMetricId::UbcRms);
|
||
pqdif_base64_push_delta_line_metric_with_phase_fallback(ctx, StatMetricId::UcaRms);
|
||
|
||
// 2) 三相电压偏差占位 + 三线电压偏差
|
||
pqdif_base64_push_placeholder_block(ctx, "phase voltage deviation placeholder(Ua/Ub/Uc)", 3);
|
||
pqdif_base64_push_delta_line_metric_with_phase_fallback(ctx, StatMetricId::UabDeviation);
|
||
pqdif_base64_push_delta_line_metric_with_phase_fallback(ctx, StatMetricId::UbcDeviation);
|
||
pqdif_base64_push_delta_line_metric_with_phase_fallback(ctx, StatMetricId::UcaDeviation);
|
||
|
||
// 3) 频率偏差 + 频率
|
||
pqdif_base64_push_metric(ctx, StatMetricId::FrequencyDeviation);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::Frequency);
|
||
|
||
// 4) 电压零/负/正序和负序不平衡
|
||
pqdif_base64_push_metric(ctx, StatMetricId::UZeroSeq);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::UNegSeq);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::UPosSeq);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::UNegSeqUnbalance);
|
||
|
||
// 5) 电流零/负/正序和负序不平衡
|
||
pqdif_base64_push_metric(ctx, StatMetricId::IZeroSeq);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::INegSeq);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::IPosSeq);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::INegSeqUnbalance);
|
||
|
||
// 6) 三相电压谐波 2-50 次指标占位
|
||
pqdif_base64_push_placeholder_block(ctx, "phase voltage harmonic placeholder(Ua/Ub/Uc,2-50)", 49 * 3);
|
||
|
||
// 7) 三相电流谐波 2-50 次指标
|
||
pqdif_base64_push_dynamic_three_phases(ctx, StatDynamicMetricGroup::CurrentHarmonic, 2, 50);
|
||
|
||
// 8) 三线电压谐波 2-50 次指标
|
||
pqdif_base64_push_dynamic_three_line_phases_with_delta_fallback(ctx, StatDynamicMetricGroup::LineVoltageHarmonic, 2, 50);
|
||
|
||
// 9) 三相电压谐波 2-50 次相角指标占位
|
||
pqdif_base64_push_placeholder_block(ctx, "phase voltage harmonic angle placeholder(Ua/Ub/Uc,2-50)", 49 * 3);
|
||
|
||
// 10) 三相电流谐波 2-50 次相角指标
|
||
pqdif_base64_push_dynamic_three_phases(ctx, StatDynamicMetricGroup::CurrentHarmonicAngle, 2, 50);
|
||
|
||
// 11) 三线电压谐波 2-50 次相角指标
|
||
pqdif_base64_push_dynamic_three_line_phases_with_delta_fallback(ctx, StatDynamicMetricGroup::LineVoltageHarmonicAngle, 2, 50);
|
||
|
||
// 12) 三相电压间谐波 0.5-49.5 次指标占位
|
||
pqdif_base64_push_placeholder_block(ctx, "phase voltage interharmonic placeholder(Ua/Ub/Uc,0.5-49.5)", 50 * 3);
|
||
|
||
// 13) 三相电流间谐波 0.5-49.5 次指标
|
||
pqdif_base64_push_dynamic_three_phases(ctx, StatDynamicMetricGroup::CurrentInterharmonic, 0, 49);
|
||
|
||
// 14) 三线电压间谐波 0.5-49.5 次指标
|
||
pqdif_base64_push_dynamic_three_line_phases_with_delta_fallback(ctx, StatDynamicMetricGroup::LineVoltageInterharmonic, 0, 49);
|
||
|
||
// 15) A/B/C/总 有功/无功/视在功率
|
||
pqdif_base64_push_metric(ctx, StatMetricId::PaPower);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::QaPower);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::SaPower);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::PbPower);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::QbPower);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::SbPower);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::PcPower);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::QcPower);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::ScPower);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::PTotalPower);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::QTotalPower);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::STotalPower);
|
||
|
||
// 16) A/B/C/总 谐波 2-50 次 有功/无功/视在功率
|
||
pqdif_base64_push_harmonic_power_phase(ctx, 0);
|
||
pqdif_base64_push_harmonic_power_phase(ctx, 1);
|
||
pqdif_base64_push_harmonic_power_phase(ctx, 2);
|
||
pqdif_base64_push_harmonic_power_phase(ctx, 3);
|
||
|
||
// 17) 谐波含有率:三相电压占位、三相电流、三线电压
|
||
pqdif_base64_push_placeholder_block(ctx, "phase voltage harmonic ratio placeholder(Ua/Ub/Uc,2-50)", 49 * 3);
|
||
pqdif_base64_push_dynamic_three_phases(ctx, StatDynamicMetricGroup::CurrentHarmonicRatio, 2, 50);
|
||
pqdif_base64_push_dynamic_three_line_phases_with_delta_fallback(ctx, StatDynamicMetricGroup::LineVoltageHarmonicRatio, 2, 50);
|
||
|
||
// 18) 谐波总畸变率:三相电压占位、三相电流、三线电压
|
||
pqdif_base64_push_placeholder_block(ctx, "phase voltage THD placeholder(Ua/Ub/Uc)", 3);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::IaThd);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::IbThd);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::IcThd);
|
||
pqdif_base64_push_delta_line_metric_with_phase_fallback(ctx, StatMetricId::UabThd);
|
||
pqdif_base64_push_delta_line_metric_with_phase_fallback(ctx, StatMetricId::UbcThd);
|
||
pqdif_base64_push_delta_line_metric_with_phase_fallback(ctx, StatMetricId::UcaThd);
|
||
|
||
// 19) 三相功率因数和总功率因数
|
||
pqdif_base64_push_metric(ctx, StatMetricId::PFa);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::PFb);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::PFc);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::PFTotal);
|
||
|
||
// 20) 三相基波功率因数和总基波功率因数
|
||
pqdif_base64_push_metric(ctx, StatMetricId::FundPFa);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::FundPFb);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::FundPFc);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::FundPFTotal);
|
||
|
||
// 21) 三相电压变动幅值占位 + 三线电压变动幅值
|
||
pqdif_base64_push_placeholder_block(ctx, "phase voltage DVC placeholder(Ua/Ub/Uc)", 3);
|
||
pqdif_base64_push_delta_line_metric_with_phase_fallback(ctx, StatMetricId::UabDvc);
|
||
pqdif_base64_push_delta_line_metric_with_phase_fallback(ctx, StatMetricId::UbcDvc);
|
||
pqdif_base64_push_delta_line_metric_with_phase_fallback(ctx, StatMetricId::UcaDvc);
|
||
|
||
// 22) 三相短闪占位 + 三线短闪
|
||
pqdif_base64_push_placeholder_block(ctx, "phase voltage Pst placeholder(Ua/Ub/Uc)", 3);
|
||
pqdif_base64_push_delta_line_metric_with_phase_fallback(ctx, StatMetricId::UabPst);
|
||
pqdif_base64_push_delta_line_metric_with_phase_fallback(ctx, StatMetricId::UbcPst);
|
||
pqdif_base64_push_delta_line_metric_with_phase_fallback(ctx, StatMetricId::UcaPst);
|
||
|
||
// 23) 三相长闪占位 + 三线长闪
|
||
pqdif_base64_push_placeholder_block(ctx, "phase voltage Plt placeholder(Ua/Ub/Uc)", 3);
|
||
pqdif_base64_push_delta_line_metric_with_phase_fallback(ctx, StatMetricId::UabPlt);
|
||
pqdif_base64_push_delta_line_metric_with_phase_fallback(ctx, StatMetricId::UbcPlt);
|
||
pqdif_base64_push_delta_line_metric_with_phase_fallback(ctx, StatMetricId::UcaPlt);
|
||
|
||
// 24) A/B/C/总 基波有功/无功/视在功率
|
||
pqdif_base64_push_metric(ctx, StatMetricId::PaFundPower);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::QaFundPower);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::SaFundPower);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::PbFundPower);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::QbFundPower);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::SbFundPower);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::PcFundPower);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::QcFundPower);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::ScFundPower);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::PTotalFundPower);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::QTotalFundPower);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::STotalFundPower);
|
||
|
||
// 25) 基波有效值:三相基波电压占位、三相基波电流、三线基波电压
|
||
pqdif_base64_push_placeholder_block(ctx, "phase voltage fundamental RMS placeholder(Ua/Ub/Uc)", 3);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::IaFundRms);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::IbFundRms);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::IcFundRms);
|
||
pqdif_base64_push_delta_line_metric_with_phase_fallback(ctx, StatMetricId::UabFundRms);
|
||
pqdif_base64_push_delta_line_metric_with_phase_fallback(ctx, StatMetricId::UbcFundRms);
|
||
pqdif_base64_push_delta_line_metric_with_phase_fallback(ctx, StatMetricId::UcaFundRms);
|
||
|
||
// 26) 基波相角:三相基波电压相角占位、三相基波电流相角、三线基波电压相角
|
||
pqdif_base64_push_placeholder_block(ctx, "phase voltage fundamental angle placeholder(Ua/Ub/Uc)", 3);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::IaFundAngle);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::IbFundAngle);
|
||
pqdif_base64_push_metric(ctx, StatMetricId::IcFundAngle);
|
||
pqdif_base64_push_delta_line_metric_with_phase_fallback(ctx, StatMetricId::UabFundAngle);
|
||
pqdif_base64_push_delta_line_metric_with_phase_fallback(ctx, StatMetricId::UbcFundAngle);
|
||
pqdif_base64_push_delta_line_metric_with_phase_fallback(ctx, StatMetricId::UcaFundAngle);
|
||
|
||
if (ctx.delta_line_phase_fallback_count > 0 && pqdif_log_enabled(PqdifLogLevel::Core))
|
||
{
|
||
std::cout << " [BASE64 DELTA LINE FALLBACK SUMMARY] time=" << ctx.bucket.timestamp_text
|
||
<< ", kind=" << stat_value_kind_name(ctx.kind)
|
||
<< ", fallback_count=" << ctx.delta_line_phase_fallback_count
|
||
<< std::endl;
|
||
}
|
||
|
||
placeholder_count = ctx.placeholder_count;
|
||
missing_metric_count = ctx.missing_metric_count;
|
||
missing_metric_names = ctx.missing_metric_names;
|
||
return float_buffer;
|
||
}
|
||
|
||
void pqdif_build_and_queue_base64_records(ParsedPqdifFile& parsed_file)
|
||
{
|
||
if (parsed_file.aggregated_stat_buckets.empty())
|
||
{
|
||
std::cout << "[PQDIF BASE64] skip: no aggregated buckets, file="
|
||
<< parsed_file.source_file << std::endl;
|
||
return;
|
||
}
|
||
|
||
// Wye 星型按星型顺序组装;Delta 角型按角型顺序组装。
|
||
// Unknown 暂按星型顺序组装,避免因为接线方式缺失直接丢弃数据。
|
||
const bool use_delta_assembly = (parsed_file.connection_kind == ParsedConnectionKind::Delta);
|
||
const char* assembly_name = use_delta_assembly ? "DELTA" : "STAR";
|
||
|
||
const std::string full_path = pqdif_absolute_path_text(parsed_file.source_file);
|
||
const std::vector<StatValueKind> kind_order = {
|
||
StatValueKind::Max,
|
||
StatValueKind::Min,
|
||
StatValueKind::Avg,
|
||
StatValueKind::P95
|
||
};
|
||
|
||
PqdifStatBase64FileBatch file_batch;
|
||
file_batch.pqdif_file_path = full_path;
|
||
file_batch.source_file = parsed_file.source_file;
|
||
file_batch.mac = parsed_file.mac;
|
||
file_batch.parsed_at = parsed_file.parsed_at;
|
||
file_batch.parsed_at_text = format_time_text(parsed_file.parsed_at);
|
||
file_batch.connection_kind = parsed_file.connection_kind;
|
||
file_batch.time_points.reserve(parsed_file.aggregated_stat_buckets.size());
|
||
|
||
size_t generated = 0;
|
||
size_t total_placeholders = 0;
|
||
size_t total_missing_metrics = 0;
|
||
size_t expected_float_count = 0;
|
||
|
||
std::cout << "========== PQDIF BASE64 ASSEMBLY ==========" << std::endl;
|
||
std::cout << "file=" << full_path
|
||
<< ", buckets=" << parsed_file.aggregated_stat_buckets.size()
|
||
<< ", connection_kind=" << stat_connection_kind_name(parsed_file.connection_kind)
|
||
<< ", assembly=" << assembly_name
|
||
<< ", kind_order=Max/Min/Avg/P95"
|
||
<< ", queue_mode=file_batch/time_point/records"
|
||
<< std::endl;
|
||
|
||
for (const auto& bucket : parsed_file.aggregated_stat_buckets)
|
||
{
|
||
PqdifStatBase64TimePointPacket time_packet;
|
||
time_packet.timestamp = bucket.timestamp;
|
||
time_packet.timestamp_text = bucket.timestamp_text;
|
||
time_packet.records.reserve(kind_order.size());
|
||
|
||
for (StatValueKind kind : kind_order)
|
||
{
|
||
size_t placeholder_count = 0;
|
||
size_t missing_metric_count = 0;
|
||
std::vector<std::string> missing_metric_names;
|
||
|
||
std::vector<float> floats = use_delta_assembly ?
|
||
pqdif_build_delta_float_buffer_for_bucket(
|
||
parsed_file,
|
||
bucket,
|
||
kind,
|
||
placeholder_count,
|
||
missing_metric_count,
|
||
missing_metric_names) :
|
||
pqdif_build_star_float_buffer_for_bucket(
|
||
parsed_file,
|
||
bucket,
|
||
kind,
|
||
placeholder_count,
|
||
missing_metric_count,
|
||
missing_metric_names);
|
||
|
||
if (expected_float_count == 0)
|
||
expected_float_count = floats.size();
|
||
|
||
PqdifStatBase64Record rec;
|
||
rec.pqdif_file_path = full_path;
|
||
rec.value_kind = kind;
|
||
rec.value_kind_name = stat_value_kind_name(kind);
|
||
rec.value_kind_code = stat_value_kind_code_for_base64(kind);
|
||
rec.timestamp = bucket.timestamp;
|
||
rec.timestamp_text = bucket.timestamp_text;
|
||
rec.float_count = floats.size();
|
||
rec.placeholder_count = placeholder_count;
|
||
rec.connection_kind = parsed_file.connection_kind;
|
||
rec.base64_payload = pqdif_base64_encode_float_vector(floats);
|
||
|
||
time_packet.total_float_count += rec.float_count;
|
||
time_packet.total_placeholder_count += rec.placeholder_count;
|
||
time_packet.total_base64_chars += rec.base64_payload.size();
|
||
time_packet.records.emplace_back(std::move(rec));
|
||
|
||
++generated;
|
||
total_placeholders += placeholder_count;
|
||
total_missing_metrics += missing_metric_count;
|
||
|
||
if (pqdif_log_enabled(PqdifLogLevel::Info) || generated <= 4)
|
||
{
|
||
const PqdifStatBase64Record& log_rec = time_packet.records.back();
|
||
std::cout << " [BASE64 SUB RECORD] time=" << bucket.timestamp_text
|
||
<< ", kind=" << log_rec.value_kind_name
|
||
<< ", kind_code=" << log_rec.value_kind_code
|
||
<< ", floats=" << log_rec.float_count
|
||
<< ", placeholders=" << log_rec.placeholder_count
|
||
<< ", missing_metrics=" << missing_metric_count
|
||
<< ", base64_len=" << log_rec.base64_payload.size()
|
||
<< std::endl;
|
||
}
|
||
|
||
if (pqdif_log_enabled(PqdifLogLevel::Info) && !missing_metric_names.empty())
|
||
{
|
||
std::cout << " [BASE64 MISSING SUMMARY] time=" << bucket.timestamp_text
|
||
<< ", kind=" << stat_value_kind_name(kind)
|
||
<< ", count=" << missing_metric_names.size()
|
||
<< ", first_metrics=";
|
||
const size_t limit = std::min<size_t>(missing_metric_names.size(), 12);
|
||
for (size_t i = 0; i < limit; ++i)
|
||
{
|
||
if (i != 0) std::cout << "/";
|
||
std::cout << missing_metric_names[i];
|
||
}
|
||
if (missing_metric_names.size() > limit)
|
||
std::cout << "/...";
|
||
std::cout << std::endl;
|
||
}
|
||
}
|
||
|
||
time_packet.record_count = time_packet.records.size();
|
||
file_batch.total_record_count += time_packet.record_count;
|
||
file_batch.total_float_count += time_packet.total_float_count;
|
||
file_batch.total_placeholder_count += time_packet.total_placeholder_count;
|
||
file_batch.total_base64_chars += time_packet.total_base64_chars;
|
||
file_batch.time_points.emplace_back(std::move(time_packet));
|
||
}
|
||
|
||
file_batch.time_point_count = file_batch.time_points.size();
|
||
|
||
std::cout << " [BASE64 FILE BATCH SUMMARY] generated_records=" << generated
|
||
<< ", time_points=" << file_batch.time_point_count
|
||
<< ", expected_float_count_per_record=" << expected_float_count
|
||
<< ", total_placeholders=" << total_placeholders
|
||
<< ", total_missing_metrics=" << total_missing_metrics
|
||
<< ", total_base64_chars=" << file_batch.total_base64_chars
|
||
<< std::endl;
|
||
|
||
// 按你的要求,入队前完整打印保存对象内部结构:文件批次 -> 时间点 -> Max/Min/Avg/P95 子记录 -> Base64 内容。
|
||
// 日志会很长;确认完成后可以临时注释掉这一行,或改成 if (pqdif_log_enabled(PqdifLogLevel::Info)) 包裹。
|
||
//pqdif_dump_stat_base64_file_batch_full(file_batch);
|
||
|
||
push_pqdif_stat_base64_file_batch(std::move(file_batch));
|
||
|
||
std::cout << " [BASE64 QUEUE SUMMARY] file_batch_queue_size=" << GetPqdifStatBase64QueueSize()
|
||
<< ", total_sub_records_in_queue=" << GetPqdifStatBase64RecordCountInQueue()
|
||
<< std::endl;
|
||
std::cout << "================================================" << std::endl;
|
||
}
|
||
|
||
bool process_single_pqdif_file(const fs::path& file_path, const std::string& mac)
|
||
{
|
||
ParsedPqdifFile parsed_file;
|
||
parsed_file.mac = mac;
|
||
parsed_file.source_file = file_path.string();
|
||
parsed_file.parsed_at = std::time(nullptr);
|
||
|
||
std::string err;
|
||
if (!parse_pqdif_file_full(file_path.string(), parsed_file.logical_file, err))
|
||
{
|
||
std::cout << "[PQDIF] parse failed: " << file_path.string()
|
||
<< " reason=" << err << std::endl;
|
||
return false;
|
||
}
|
||
|
||
dump_logical_summary(parsed_file);
|
||
dump_monitor_settings_probe(parsed_file);
|
||
|
||
if (pqdif_log_enabled(PqdifLogLevel::Debug))
|
||
{
|
||
// Debug 级别:打印全部 observation 概览,帮助确认指标是否位于非 Trend observation 中。
|
||
dump_observation_list(parsed_file);
|
||
|
||
// Debug 级别:打印动态谱类候选通道,用于确认线电压谐波、相角、间谐波是否存在。
|
||
dump_dynamic_spectrum_candidate_probe(parsed_file);
|
||
|
||
// Debug 级别:打印闪变候选通道,用于确认 Plt 通道是否存在数据点。
|
||
dump_flicker_candidate_probe(parsed_file);
|
||
}
|
||
|
||
if (pqdif_is_trace_log_enabled())
|
||
{
|
||
// Trace 级别:保留旧的疑似谐波全量探针;日志量更大,仅排查复杂问题时打开。
|
||
dump_harmonic_channel_probe(parsed_file);
|
||
}
|
||
|
||
// 1) 识别接线方式。
|
||
// 后续指标判断需要先区分星型 / 角型两套规则。
|
||
parsed_file.connection_kind = stat_classify_connection_kind(parsed_file.logical_file);
|
||
|
||
// 2) 只筛选“统计类 observation”,并对其展开目标统计指标。
|
||
// 当前阶段不处理全部 observation,避免不同 observation 混入同一时间聚合结果。
|
||
parsed_file.expanded_stat_points = stat_expand_selected_statistical_observation(
|
||
parsed_file.logical_file,
|
||
parsed_file.connection_kind,
|
||
parsed_file.selected_observation_index,
|
||
parsed_file.selected_observation_name);
|
||
|
||
// 3) 对已筛选出的统计 observation,按 timestamp 聚合。
|
||
parsed_file.aggregated_stat_buckets =
|
||
stat_group_points_by_timestamp(parsed_file.expanded_stat_points);
|
||
|
||
if (pqdif_is_trace_log_enabled())
|
||
dump_semantic_probe(parsed_file);
|
||
|
||
dump_expanded_stat_preview(parsed_file);
|
||
|
||
// 分组结果平时也要打印核心摘要;开启详细日志时才会打印完整明细。
|
||
dump_grouped_bucket_preview(parsed_file);
|
||
|
||
// 4) 将每个时间桶按接线方式组装为 Max/Min/Avg/P95 四条 Base64 暂存记录。
|
||
// Wye 使用星型顺序,Delta 使用角型顺序;两者 float 数量保持一致。
|
||
pqdif_build_and_queue_base64_records(parsed_file);
|
||
|
||
if (!push_parsed_result_to_cache(std::move(parsed_file)))
|
||
{
|
||
std::cout << "[PQDIF] push cache failed: " << file_path.string() << std::endl;
|
||
return false;
|
||
}
|
||
|
||
std::cout << "[PQDIF] processed ok: " << file_path.string()
|
||
<< ", cache_size=" << GetParsedPqdifCacheSize()
|
||
<< std::endl;
|
||
|
||
return true;
|
||
}
|
||
|
||
struct PendingPqdifFile
|
||
{
|
||
fs::path path;
|
||
std::string mac;
|
||
};
|
||
|
||
void scan_once()
|
||
{
|
||
ensure_dir(kPqdRootDir);
|
||
ensure_dir(kDoneRootDir);
|
||
ensure_dir(kFailRootDir);
|
||
|
||
std::error_code ec;
|
||
if (!fs::exists(kPqdRootDir, ec) || !fs::is_directory(kPqdRootDir, ec))
|
||
return;
|
||
|
||
std::vector<PendingPqdifFile> pending_files;
|
||
|
||
// 先收集所有 MAC 目录下的 PQDIF 文件,但本轮不会全部处理。
|
||
// 后面会按文件修改时间排序,并最多处理 kMaxPqdifFilesPerScan 个文件。
|
||
for (fs::directory_iterator mac_it(kPqdRootDir, ec), mac_end; !ec && mac_it != mac_end; mac_it.increment(ec))
|
||
{
|
||
if (ec || !fs::is_directory(mac_it->path()))
|
||
continue;
|
||
|
||
const std::string mac = mac_it->path().filename().string();
|
||
|
||
std::error_code file_ec;
|
||
for (fs::directory_iterator file_it(mac_it->path(), file_ec), file_end; !file_ec && file_it != file_end; file_it.increment(file_ec))
|
||
{
|
||
if (file_ec || !is_pqdif_file(file_it->path()))
|
||
continue;
|
||
|
||
PendingPqdifFile item;
|
||
item.path = file_it->path();
|
||
item.mac = mac;
|
||
pending_files.push_back(item);
|
||
}
|
||
}
|
||
|
||
if (pending_files.empty())
|
||
return;
|
||
|
||
// 旧文件优先处理,避免目录中长期积压的旧 PQDIF 一直排在后面。
|
||
std::sort(pending_files.begin(), pending_files.end(), [](const PendingPqdifFile& a, const PendingPqdifFile& b) {
|
||
std::error_code ea, eb;
|
||
const auto ta = fs::last_write_time(a.path, ea);
|
||
const auto tb = fs::last_write_time(b.path, eb);
|
||
|
||
if (ea && !eb)
|
||
return false;
|
||
if (!ea && eb)
|
||
return true;
|
||
if (!ea && !eb && ta != tb)
|
||
return ta < tb;
|
||
|
||
return a.path.string() < b.path.string();
|
||
});
|
||
|
||
int processed_count = 0;
|
||
for (const auto& item : pending_files)
|
||
{
|
||
if (processed_count >= kMaxPqdifFilesPerScan)
|
||
break;
|
||
|
||
std::cout << "[PQDIF] scan selected file: " << item.path.string()
|
||
<< ", pending_count=" << pending_files.size()
|
||
<< ", max_per_scan=" << kMaxPqdifFilesPerScan
|
||
<< std::endl;
|
||
|
||
const bool ok = process_single_pqdif_file(item.path, item.mac);
|
||
|
||
const fs::path target_root = ok ? fs::path(kDoneRootDir) : fs::path(kFailRootDir);
|
||
const fs::path dst = target_root / item.mac / item.path.filename();
|
||
|
||
// 处理完成后移动文件,避免下一轮 scan_once() 重复解析同一个 PQDIF。
|
||
// 解析成功:download/<mac>/<file>.pqd -> download_done/<mac>/<file>.pqd
|
||
// 解析失败:download/<mac>/<file>.pqd -> download_fail/<mac>/<file>.pqd
|
||
//
|
||
// 调试时如果想让文件保留在 download 目录中、方便反复解析同一个文件,
|
||
// 可以临时注释掉下面这个 if 块;调试结束后建议恢复,否则每一轮都会重复解析。
|
||
if (!move_file_with_fallback(item.path, dst))
|
||
{
|
||
std::cout << "[PQDIF] move failed: "
|
||
<< item.path.string() << " -> " << dst.string()
|
||
<< std::endl;
|
||
}
|
||
|
||
cleanup_backup_dir(fs::path(kDoneRootDir) / item.mac, kBackupLimit);
|
||
cleanup_backup_dir(fs::path(kFailRootDir) / item.mac, kBackupLimit);
|
||
|
||
++processed_count;
|
||
}
|
||
}
|
||
|
||
} // namespace
|
||
|
||
bool PopOldestParsedPqdifFile(ParsedPqdifFile& out)
|
||
{
|
||
std::lock_guard<std::mutex> guard(g_parsed_cache_mutex);
|
||
|
||
if (g_parsed_cache.empty())
|
||
return false;
|
||
|
||
out = std::move(g_parsed_cache.front());
|
||
g_parsed_cache.pop_front();
|
||
return true;
|
||
}
|
||
|
||
bool PeekOldestParsedPqdifFile(ParsedPqdifFile& out)
|
||
{
|
||
std::lock_guard<std::mutex> guard(g_parsed_cache_mutex);
|
||
|
||
if (g_parsed_cache.empty())
|
||
return false;
|
||
|
||
out = g_parsed_cache.front();
|
||
return true;
|
||
}
|
||
|
||
size_t GetParsedPqdifCacheSize()
|
||
{
|
||
std::lock_guard<std::mutex> guard(g_parsed_cache_mutex);
|
||
return g_parsed_cache.size();
|
||
}
|
||
|
||
void ClearParsedPqdifCache()
|
||
{
|
||
std::lock_guard<std::mutex> guard(g_parsed_cache_mutex);
|
||
g_parsed_cache.clear();
|
||
}
|
||
|
||
|
||
|
||
bool PopOldestPqdifStatBase64FileBatch(PqdifStatBase64FileBatch& out)
|
||
{
|
||
std::lock_guard<std::mutex> guard(g_pqdif_stat_base64_mutex);
|
||
if (g_pqdif_stat_base64_queue.empty())
|
||
return false;
|
||
|
||
out = std::move(g_pqdif_stat_base64_queue.front());
|
||
g_pqdif_stat_base64_queue.pop_front();
|
||
return true;
|
||
}
|
||
|
||
bool PeekOldestPqdifStatBase64FileBatch(PqdifStatBase64FileBatch& out)
|
||
{
|
||
std::lock_guard<std::mutex> guard(g_pqdif_stat_base64_mutex);
|
||
if (g_pqdif_stat_base64_queue.empty())
|
||
return false;
|
||
|
||
out = g_pqdif_stat_base64_queue.front();
|
||
return true;
|
||
}
|
||
|
||
bool PopOldestPqdifStatBase64Record(PqdifStatBase64Record& out)
|
||
{
|
||
// 兼容旧接口:从“文件级批次队列”的最早文件、最早时间点中拆出一条子记录。
|
||
std::lock_guard<std::mutex> guard(g_pqdif_stat_base64_mutex);
|
||
|
||
while (!g_pqdif_stat_base64_queue.empty())
|
||
{
|
||
PqdifStatBase64FileBatch& batch = g_pqdif_stat_base64_queue.front();
|
||
|
||
while (!batch.time_points.empty() && batch.time_points.front().records.empty())
|
||
batch.time_points.erase(batch.time_points.begin());
|
||
|
||
if (batch.time_points.empty())
|
||
{
|
||
g_pqdif_stat_base64_queue.pop_front();
|
||
continue;
|
||
}
|
||
|
||
PqdifStatBase64TimePointPacket& tp = batch.time_points.front();
|
||
out = std::move(tp.records.front());
|
||
|
||
const size_t out_base64_len = out.base64_payload.size();
|
||
if (tp.record_count > 0) --tp.record_count;
|
||
if (tp.total_float_count >= out.float_count) tp.total_float_count -= out.float_count;
|
||
if (tp.total_placeholder_count >= out.placeholder_count) tp.total_placeholder_count -= out.placeholder_count;
|
||
if (tp.total_base64_chars >= out_base64_len) tp.total_base64_chars -= out_base64_len;
|
||
|
||
if (batch.total_record_count > 0) --batch.total_record_count;
|
||
if (batch.total_float_count >= out.float_count) batch.total_float_count -= out.float_count;
|
||
if (batch.total_placeholder_count >= out.placeholder_count) batch.total_placeholder_count -= out.placeholder_count;
|
||
if (batch.total_base64_chars >= out_base64_len) batch.total_base64_chars -= out_base64_len;
|
||
|
||
tp.records.erase(tp.records.begin());
|
||
if (tp.records.empty())
|
||
{
|
||
batch.time_points.erase(batch.time_points.begin());
|
||
if (batch.time_point_count > 0) --batch.time_point_count;
|
||
}
|
||
|
||
if (batch.time_points.empty())
|
||
g_pqdif_stat_base64_queue.pop_front();
|
||
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
bool PeekOldestPqdifStatBase64Record(PqdifStatBase64Record& out)
|
||
{
|
||
// 兼容旧接口:只查看“文件级批次队列”的最早文件、最早时间点中的第一条子记录。
|
||
std::lock_guard<std::mutex> guard(g_pqdif_stat_base64_mutex);
|
||
|
||
for (const auto& batch : g_pqdif_stat_base64_queue)
|
||
{
|
||
for (const auto& tp : batch.time_points)
|
||
{
|
||
if (!tp.records.empty())
|
||
{
|
||
out = tp.records.front();
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
size_t GetPqdifStatBase64QueueSize()
|
||
{
|
||
// v20 起返回文件级批次数量,不再是子记录数量。
|
||
std::lock_guard<std::mutex> guard(g_pqdif_stat_base64_mutex);
|
||
return g_pqdif_stat_base64_queue.size();
|
||
}
|
||
|
||
size_t GetPqdifStatBase64RecordCountInQueue()
|
||
{
|
||
std::lock_guard<std::mutex> guard(g_pqdif_stat_base64_mutex);
|
||
return pqdif_stat_base64_count_records_in_queue_unlocked();
|
||
}
|
||
|
||
void ClearPqdifStatBase64Queue()
|
||
{
|
||
// 清空 Base64 生成队列。
|
||
// 对象用途:调试或重置解析状态时使用;不会清空待后续处理队列。
|
||
std::lock_guard<std::mutex> guard(g_pqdif_stat_base64_mutex);
|
||
g_pqdif_stat_base64_queue.clear();
|
||
}
|
||
|
||
bool PopReadyPqdifStatBase64FileBatch(PqdifStatBase64FileBatch& out)
|
||
{
|
||
// 从“待后续处理队列”取出一个完整 PQDIF 文件批次。
|
||
// 对象用途:后续入库/上传/推送逻辑应优先调用这个接口,而不是直接访问全局 deque。
|
||
std::lock_guard<std::mutex> guard(g_pqdif_stat_base64_ready_mutex);
|
||
if (g_pqdif_stat_base64_ready_queue.empty())
|
||
return false;
|
||
|
||
out = std::move(g_pqdif_stat_base64_ready_queue.front());
|
||
g_pqdif_stat_base64_ready_queue.pop_front();
|
||
return true;
|
||
}
|
||
|
||
bool PeekReadyPqdifStatBase64FileBatch(PqdifStatBase64FileBatch& out)
|
||
{
|
||
// 查看“待后续处理队列”最早的 PQDIF 文件批次,但不移除。
|
||
// 对象用途:仅检查当前准备处理的数据内容,适合调试或状态展示。
|
||
std::lock_guard<std::mutex> guard(g_pqdif_stat_base64_ready_mutex);
|
||
if (g_pqdif_stat_base64_ready_queue.empty())
|
||
return false;
|
||
|
||
out = g_pqdif_stat_base64_ready_queue.front();
|
||
return true;
|
||
}
|
||
|
||
size_t GetReadyPqdifStatBase64QueueSize()
|
||
{
|
||
// 返回待后续处理队列中的文件级批次数量。
|
||
// 注意:返回的是文件数量,不是 Max/Min/Avg/P95 子记录数量。
|
||
std::lock_guard<std::mutex> guard(g_pqdif_stat_base64_ready_mutex);
|
||
return g_pqdif_stat_base64_ready_queue.size();
|
||
}
|
||
|
||
size_t GetReadyPqdifStatBase64RecordCountInQueue()
|
||
{
|
||
// 返回待后续处理队列中全部文件批次包含的 Base64 子记录数量。
|
||
std::lock_guard<std::mutex> guard(g_pqdif_stat_base64_ready_mutex);
|
||
return pqdif_stat_base64_count_records_in_queue_unlocked(g_pqdif_stat_base64_ready_queue);
|
||
}
|
||
|
||
void ClearReadyPqdifStatBase64Queue()
|
||
{
|
||
// 清空待后续处理队列。
|
||
// 对象用途:调试或重置后续处理状态时使用;不会清空解析生成队列。
|
||
std::lock_guard<std::mutex> guard(g_pqdif_stat_base64_ready_mutex);
|
||
g_pqdif_stat_base64_ready_queue.clear();
|
||
}
|
||
|
||
void RunPqdifScanLoop()
|
||
{
|
||
std::cout << "[PQDIF] scan loop started, root=" << kPqdRootDir
|
||
<< ", interval=" << kScanIntervalSec << "s"
|
||
<< std::endl;
|
||
|
||
while (true)
|
||
{
|
||
try
|
||
{
|
||
scan_once();
|
||
}
|
||
catch (const std::exception& ex)
|
||
{
|
||
std::cout << "[PQDIF] scan exception: " << ex.what() << std::endl;
|
||
}
|
||
catch (...)
|
||
{
|
||
std::cout << "[PQDIF] scan exception: unknown" << std::endl;
|
||
}
|
||
|
||
try
|
||
{
|
||
// 循环最后,检查 Base64 生成队列中是否已经存在一个完整 PQDIF 文件批次。
|
||
// 如果存在,则取出最多一个并移动到“待后续处理队列”。
|
||
// 后续业务处理请调用 PopReadyPqdifStatBase64FileBatch() 从待处理队列取数据。
|
||
pqdif_move_one_generated_base64_batch_to_ready_queue();
|
||
|
||
PqdifStatBase64FileBatch batch;
|
||
|
||
if (PopReadyPqdifStatBase64FileBatch(batch)) {
|
||
// batch 就是一个 PQDIF 文件完整的 Base64 组装结果
|
||
// 在此处处理上送逻辑
|
||
}
|
||
}
|
||
catch (const std::exception& ex)
|
||
{
|
||
std::cout << "[PQDIF BASE64 READY] move exception: " << ex.what() << std::endl;
|
||
}
|
||
catch (...)
|
||
{
|
||
std::cout << "[PQDIF BASE64 READY] move exception: unknown" << std::endl;
|
||
}
|
||
|
||
std::this_thread::sleep_for(std::chrono::seconds(kScanIntervalSec));
|
||
}
|
||
}
|