Files
front_linux/LFtid1056/pqdif_thread_processor.cpp

7927 lines
341 KiB
C++
Raw Normal View History

#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 命中情况、文件移动等;
/// - Debugobservation 列表、疑似通道、指标来源排查;
/// - 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));
}
}