Files
front_linux/LFtid1056/pqdif_thread_processor.cpp

7927 lines
341 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#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));
}
}