446 lines
15 KiB
C++
446 lines
15 KiB
C++
// gpr_poc —— POC-B headless 度量 CLI。
|
||
//
|
||
// 串起整条地基:发现 14 通道 .iprb + .ord → assembleGprSurvey → buildGprVolume
|
||
// → ChunkedVolumeStore::write → buildPyramid → WholeVolumeSource(load),
|
||
// 在真实/合成数据上输出可测的真实指标(耗时/维度/体积/压缩比/加载/峰值内存)。
|
||
//
|
||
// 子命令:
|
||
// gpr_poc build <dir> [--line 001] [--cellXY 0.2] [--cellZ 0.05] [--out <storeDir>] [--levels 2]
|
||
// gpr_poc load <storeDir>
|
||
// gpr_poc selftest
|
||
|
||
#include <algorithm>
|
||
#include <cmath>
|
||
#include <cstdint>
|
||
#include <cstdlib>
|
||
#include <filesystem>
|
||
#include <fstream>
|
||
#include <iostream>
|
||
#include <map>
|
||
#include <string>
|
||
#include <vector>
|
||
|
||
#include "Probe.hpp"
|
||
|
||
#include "core/algo/GprVolumeBuilder.hpp"
|
||
#include "core/algo/IInterpolator.hpp"
|
||
#include "core/model/GprSurvey.hpp"
|
||
#include "core/model/ScalarVolumeI16.hpp"
|
||
#include "data/store/ChunkedVolumeStore.hpp"
|
||
#include "io/gpr/GprSurveyAssembler.hpp"
|
||
#include "render/source/WholeVolumeSource.hpp"
|
||
|
||
namespace fs = std::filesystem;
|
||
using geopro::tools::Probe;
|
||
using geopro::tools::Stopwatch;
|
||
|
||
namespace {
|
||
|
||
constexpr int kChannels = 14;
|
||
|
||
// ---- 命令行参数解析(极简 --key value)----
|
||
struct Args {
|
||
std::map<std::string, std::string> kv;
|
||
std::vector<std::string> positional;
|
||
|
||
std::string get(const std::string& key, const std::string& def) const {
|
||
auto it = kv.find(key);
|
||
return it != kv.end() ? it->second : def;
|
||
}
|
||
};
|
||
|
||
Args parseArgs(int argc, char** argv, int start) {
|
||
Args a;
|
||
for (int i = start; i < argc; ++i) {
|
||
std::string tok = argv[i];
|
||
if (tok.rfind("--", 0) == 0 && i + 1 < argc) {
|
||
a.kv[tok.substr(2)] = argv[i + 1];
|
||
++i;
|
||
} else {
|
||
a.positional.push_back(tok);
|
||
}
|
||
}
|
||
return a;
|
||
}
|
||
|
||
// 把一行指标追加写入 last-metrics.txt(与可执行同目录的工具源目录无关,
|
||
// 写到当前工作目录便于汇总;CSV 风格一行)。
|
||
void writeMetricLine(const std::string& line) {
|
||
std::ofstream f("last-metrics.txt", std::ios::app);
|
||
if (f) f << line << "\n";
|
||
}
|
||
|
||
// 发现某线 14 通道 .iprb(按通道号 A01..A14 排序)+ 该线 .ord。
|
||
struct LineFiles {
|
||
std::vector<std::string> iprb; // 已按通道号排序
|
||
std::string ord;
|
||
};
|
||
|
||
LineFiles discoverLine(const std::string& dir, const std::string& line) {
|
||
LineFiles lf;
|
||
std::map<int, std::string> byChannel; // 通道号 → 路径(自动按号排序)
|
||
std::string ordPath;
|
||
|
||
for (const auto& e : fs::directory_iterator(dir)) {
|
||
if (!e.is_regular_file()) continue;
|
||
const std::string name = e.path().filename().string();
|
||
const std::string ext = e.path().extension().string();
|
||
|
||
// .ord:优先匹配本线(含 "_<line>" 且以 .ord 结尾),否则记下工区任一 .ord 作兜底。
|
||
if (ext == ".ord") {
|
||
if (name.find("_" + line + ".") != std::string::npos) {
|
||
ordPath = e.path().string();
|
||
} else if (ordPath.empty()) {
|
||
ordPath = e.path().string();
|
||
}
|
||
continue;
|
||
}
|
||
|
||
// .iprb:匹配 "*_<line>_A<NN>.iprb"。
|
||
if (ext != ".iprb") continue;
|
||
const std::string tag = "_" + line + "_A";
|
||
const std::size_t pos = name.find(tag);
|
||
if (pos == std::string::npos) continue;
|
||
const std::size_t numStart = pos + tag.size();
|
||
std::size_t numEnd = numStart;
|
||
while (numEnd < name.size() &&
|
||
std::isdigit(static_cast<unsigned char>(name[numEnd]))) {
|
||
++numEnd;
|
||
}
|
||
if (numEnd == numStart) continue;
|
||
const int ch = std::stoi(name.substr(numStart, numEnd - numStart));
|
||
byChannel[ch] = e.path().string();
|
||
}
|
||
|
||
for (const auto& [ch, path] : byChannel) lf.iprb.push_back(path);
|
||
lf.ord = ordPath;
|
||
return lf;
|
||
}
|
||
|
||
// 由 survey 推 GridSpec:X 沿测线,Y 跨通道,Z 深度。
|
||
geopro::core::GridSpec specFromSurvey(const geopro::core::GprSurvey& s,
|
||
double cellXY, double cellZ) {
|
||
geopro::core::GridSpec spec{};
|
||
|
||
const double rangeX =
|
||
(s.ntraces > 1) ? (s.ntraces - 1) * s.dx : 0.0;
|
||
const double y0 = s.channelY.empty() ? 0.0 : s.channelY.front();
|
||
const double y1 = s.channelY.empty() ? 0.0 : s.channelY.back();
|
||
const double rangeY = y1 - y0;
|
||
const double rangeZ =
|
||
(s.samples > 1) ? (s.samples - 1) * s.dz : 0.0;
|
||
|
||
auto cells = [](double range, double cell) {
|
||
if (cell <= 0.0) return 1;
|
||
return static_cast<int>(std::ceil(range / cell)) + 1;
|
||
};
|
||
|
||
spec.ox = s.x0;
|
||
spec.oy = y0;
|
||
spec.oz = s.z0;
|
||
spec.dx = cellXY;
|
||
spec.dy = cellXY;
|
||
spec.dz = cellZ;
|
||
spec.nx = cells(rangeX, cellXY);
|
||
spec.ny = cells(rangeY, cellXY);
|
||
spec.nz = cells(rangeZ, cellZ);
|
||
spec.power = 2.0;
|
||
spec.maxDist = cellXY * 2.0;
|
||
return spec;
|
||
}
|
||
|
||
// 落盘 data.bin 体积(所有 data*.bin 之和,含金字塔各级)。
|
||
std::int64_t storeDataBytes(const std::string& dir) {
|
||
std::int64_t total = 0;
|
||
for (const auto& e : fs::directory_iterator(dir)) {
|
||
if (!e.is_regular_file()) continue;
|
||
const std::string name = e.path().filename().string();
|
||
if (name.rfind("data", 0) == 0 &&
|
||
e.path().extension().string() == ".bin") {
|
||
total += static_cast<std::int64_t>(e.file_size());
|
||
}
|
||
}
|
||
return total;
|
||
}
|
||
|
||
int cmdBuild(int argc, char** argv) {
|
||
const Args a = parseArgs(argc, argv, 2);
|
||
if (a.positional.empty()) {
|
||
std::cerr << "用法: gpr_poc build <dir> [--line 001] [--cellXY 0.2] "
|
||
"[--cellZ 0.05] [--out <storeDir>] [--levels 2]\n";
|
||
return 2;
|
||
}
|
||
const std::string dir = a.positional[0];
|
||
const std::string line = a.get("line", "001");
|
||
const double cellXY = std::stod(a.get("cellXY", "0.2"));
|
||
const double cellZ = std::stod(a.get("cellZ", "0.05"));
|
||
const int levels = std::stoi(a.get("levels", "2"));
|
||
const std::string out =
|
||
a.get("out", (fs::temp_directory_path() / ("gpr_store_" + line)).string());
|
||
|
||
std::cout << "[build] dir=" << dir << " line=" << line
|
||
<< " cellXY=" << cellXY << " cellZ=" << cellZ
|
||
<< " levels=" << levels << " out=" << out << "\n";
|
||
|
||
const LineFiles lf = discoverLine(dir, line);
|
||
std::cout << "[build] 发现通道数=" << lf.iprb.size()
|
||
<< " ord=" << (lf.ord.empty() ? "(无)" : lf.ord) << "\n";
|
||
if (lf.iprb.size() != static_cast<std::size_t>(kChannels)) {
|
||
std::cerr << "[build] 警告: 通道数 != " << kChannels
|
||
<< "(仍按发现数继续)\n";
|
||
}
|
||
if (lf.iprb.empty() || lf.ord.empty()) {
|
||
std::cerr << "[build] 错误: 未发现 .iprb 或 .ord\n";
|
||
return 1;
|
||
}
|
||
|
||
// 1) 装配
|
||
Stopwatch swAsm;
|
||
geopro::core::GprSurvey survey =
|
||
geopro::io::gpr::assembleGprSurvey(lf.iprb, lf.ord);
|
||
const double asmMs = swAsm.elapsedMs();
|
||
std::cout << "[build] 装配完成 ntraces=" << survey.ntraces
|
||
<< " samples=" << survey.samples
|
||
<< " channels=" << survey.channelY.size()
|
||
<< " dx=" << survey.dx << " dz=" << survey.dz << "\n";
|
||
|
||
// 2) 建体
|
||
const geopro::core::GridSpec spec = specFromSurvey(survey, cellXY, cellZ);
|
||
std::cout << "[build] GridSpec nx=" << spec.nx << " ny=" << spec.ny
|
||
<< " nz=" << spec.nz << " dx=" << spec.dx << " dy=" << spec.dy
|
||
<< " dz=" << spec.dz << " maxDist=" << spec.maxDist << "\n";
|
||
|
||
Stopwatch swBuild;
|
||
geopro::core::BuiltI16 built = geopro::core::buildGprVolume(survey, spec);
|
||
const double buildMs = swBuild.elapsedMs();
|
||
|
||
const std::int64_t nx = built.vol.nx(), ny = built.vol.ny(), nz = built.vol.nz();
|
||
const std::int64_t voxels = nx * ny * nz;
|
||
const std::int64_t rawBytes = voxels * 2; // int16
|
||
|
||
// 3) 落盘 + 金字塔
|
||
fs::create_directories(out);
|
||
Stopwatch swWrite;
|
||
geopro::data::ChunkedVolumeStore::write(out, built);
|
||
const double writeMs = swWrite.elapsedMs();
|
||
|
||
Stopwatch swPyr;
|
||
{
|
||
geopro::data::ChunkedVolumeStore store(out);
|
||
store.buildPyramid(levels);
|
||
}
|
||
const double pyrMs = swPyr.elapsedMs();
|
||
|
||
const std::int64_t dataBytes = storeDataBytes(out);
|
||
const double ratio =
|
||
dataBytes > 0 ? static_cast<double>(rawBytes) / dataBytes : 0.0;
|
||
const double peak = Probe::peakMemMB();
|
||
|
||
std::cout << "\n=== build 指标 ===\n";
|
||
std::cout << "装配耗时(ms) : " << asmMs << "\n";
|
||
std::cout << "建体耗时(ms) : " << buildMs << "\n";
|
||
std::cout << "落盘耗时(ms) : " << writeMs << "\n";
|
||
std::cout << "金字塔耗时(ms) : " << pyrMs << "\n";
|
||
std::cout << "体维度 : " << nx << " x " << ny << " x " << nz << "\n";
|
||
std::cout << "体素数 : " << voxels << "\n";
|
||
std::cout << "原始体积(B) : " << rawBytes << " ("
|
||
<< rawBytes / (1024.0 * 1024.0) << " MB)\n";
|
||
std::cout << "data.bin(B) : " << dataBytes << " ("
|
||
<< dataBytes / (1024.0 * 1024.0) << " MB)\n";
|
||
std::cout << "压缩比 : " << ratio << " x\n";
|
||
std::cout << "峰值内存(MB) : " << peak << "\n";
|
||
|
||
writeMetricLine(
|
||
"build,line=" + line + ",cellXY=" + std::to_string(cellXY) +
|
||
",cellZ=" + std::to_string(cellZ) + ",nx=" + std::to_string(nx) +
|
||
",ny=" + std::to_string(ny) + ",nz=" + std::to_string(nz) +
|
||
",voxels=" + std::to_string(voxels) +
|
||
",rawB=" + std::to_string(rawBytes) +
|
||
",dataB=" + std::to_string(dataBytes) +
|
||
",ratio=" + std::to_string(ratio) + ",asmMs=" + std::to_string(asmMs) +
|
||
",buildMs=" + std::to_string(buildMs) +
|
||
",writeMs=" + std::to_string(writeMs) +
|
||
",pyrMs=" + std::to_string(pyrMs) + ",peakMB=" + std::to_string(peak));
|
||
return 0;
|
||
}
|
||
|
||
int cmdLoad(int argc, char** argv) {
|
||
const Args a = parseArgs(argc, argv, 2);
|
||
if (a.positional.empty()) {
|
||
std::cerr << "用法: gpr_poc load <storeDir>\n";
|
||
return 2;
|
||
}
|
||
const std::string dir = a.positional[0];
|
||
std::cout << "[load] storeDir=" << dir << "\n";
|
||
|
||
Stopwatch sw;
|
||
geopro::render::WholeVolumeSource src(dir);
|
||
const double loadMs = sw.elapsedMs();
|
||
|
||
const auto& m = src.meta();
|
||
const std::int64_t voxels =
|
||
static_cast<std::int64_t>(m.nx) * m.ny * m.nz;
|
||
const std::int64_t wholeBytes = voxels * 2; // VTK_SHORT
|
||
const double peak = Probe::peakMemMB();
|
||
|
||
std::cout << "\n=== load 指标 ===\n";
|
||
std::cout << "加载耗时(ms) : " << loadMs << "\n";
|
||
std::cout << "整卷维度 : " << m.nx << " x " << m.ny << " x " << m.nz
|
||
<< "\n";
|
||
std::cout << "整卷字节(B) : " << wholeBytes << " ("
|
||
<< wholeBytes / (1024.0 * 1024.0) << " MB)\n";
|
||
std::cout << "峰值内存(MB) : " << peak << "\n";
|
||
|
||
writeMetricLine("load,dir=" + dir + ",nx=" + std::to_string(m.nx) +
|
||
",ny=" + std::to_string(m.ny) +
|
||
",nz=" + std::to_string(m.nz) +
|
||
",wholeB=" + std::to_string(wholeBytes) +
|
||
",loadMs=" + std::to_string(loadMs) +
|
||
",peakMB=" + std::to_string(peak));
|
||
return 0;
|
||
}
|
||
|
||
// ---- selftest:合成极小数据走完整 build→load 管线 ----
|
||
|
||
// 写一个极小通道的 .iprb + .iprh(samples 采样、traces 道,值 = base + t + s)。
|
||
void writeSyntheticChannel(const fs::path& iprbPath, int samples, int traces,
|
||
std::int16_t base) {
|
||
const fs::path iprhPath =
|
||
fs::path(iprbPath).replace_extension(".iprh");
|
||
std::ofstream h(iprhPath);
|
||
h << "SAMPLES: " << samples << "\n";
|
||
h << "LAST TRACE: " << (traces - 1) << "\n";
|
||
h << "CHANNELS: 2\n";
|
||
h << "TIMEWINDOW: 100.0\n";
|
||
h << "SOIL VELOCITY: 100.0\n"; // m/µs → ×1e6 → 1e8 m/s
|
||
h << "DISTANCE INTERVAL: 0.05\n";
|
||
h.close();
|
||
|
||
std::ofstream b(iprbPath, std::ios::binary);
|
||
// 布局 [trace*samples + s],s 最快。
|
||
for (int t = 0; t < traces; ++t) {
|
||
for (int s = 0; s < samples; ++s) {
|
||
const std::int16_t v =
|
||
static_cast<std::int16_t>(base + t + s);
|
||
b.write(reinterpret_cast<const char*>(&v), sizeof(v));
|
||
}
|
||
}
|
||
}
|
||
|
||
int cmdSelftest() {
|
||
std::cout << "[selftest] 构造极小合成 survey(2 通道)...\n";
|
||
const fs::path tmp =
|
||
fs::temp_directory_path() / "gpr_poc_selftest";
|
||
std::error_code ec;
|
||
fs::remove_all(tmp, ec);
|
||
fs::create_directories(tmp);
|
||
|
||
const int samples = 8;
|
||
const int traces = 12;
|
||
|
||
// 2 通道 .iprb/.iprh + .ord(末列==1 标记有效通道,第 2 列为横偏 Y)。
|
||
writeSyntheticChannel(tmp / "syn_001_A01.iprb", samples, traces,
|
||
/*base=*/100);
|
||
writeSyntheticChannel(tmp / "syn_001_A02.iprb", samples, traces,
|
||
/*base=*/200);
|
||
{
|
||
std::ofstream ord(tmp / "syn_001.ord");
|
||
ord << "0 0.000000 -1.5 1\n";
|
||
ord << "1 1.000000 -1.5 1\n";
|
||
}
|
||
|
||
const std::vector<std::string> iprb = {
|
||
(tmp / "syn_001_A01.iprb").string(),
|
||
(tmp / "syn_001_A02.iprb").string()};
|
||
const std::string ord = (tmp / "syn_001.ord").string();
|
||
|
||
bool ok = true;
|
||
auto check = [&](bool cond, const std::string& msg) {
|
||
if (!cond) {
|
||
std::cerr << "[selftest] FAIL: " << msg << "\n";
|
||
ok = false;
|
||
}
|
||
};
|
||
|
||
try {
|
||
// 装配
|
||
geopro::core::GprSurvey survey =
|
||
geopro::io::gpr::assembleGprSurvey(iprb, ord);
|
||
check(survey.ntraces == traces, "ntraces");
|
||
check(survey.samples == samples, "samples");
|
||
check(survey.channelY.size() == 2, "channels");
|
||
// channelY 升序:A01 偏移 0.0 在前,A02 偏移 1.0 在后。
|
||
check(survey.channelY.front() < survey.channelY.back(), "channelY 升序");
|
||
|
||
// 建体:cellXY 取通道间距 1.0 → ny=2;cellZ 较细确保 nz>1。
|
||
const double cellXY = 1.0;
|
||
const double cellZ = std::max(survey.dz, 1e-12);
|
||
const geopro::core::GridSpec spec =
|
||
specFromSurvey(survey, cellXY, cellZ);
|
||
std::cout << "[selftest] GridSpec " << spec.nx << "x" << spec.ny << "x"
|
||
<< spec.nz << " dz=" << spec.dz << "\n";
|
||
check(spec.ny == 2, "ny==2");
|
||
|
||
geopro::core::BuiltI16 built =
|
||
geopro::core::buildGprVolume(survey, spec);
|
||
check(built.vol.nx() == spec.nx, "built nx");
|
||
check(built.vol.ny() == spec.ny, "built ny");
|
||
check(built.vol.nz() == spec.nz, "built nz");
|
||
|
||
// 落盘 + 金字塔
|
||
const std::string store = (tmp / "store").string();
|
||
fs::create_directories(store);
|
||
geopro::data::ChunkedVolumeStore::write(store, built, /*brick=*/4);
|
||
{
|
||
geopro::data::ChunkedVolumeStore s(store);
|
||
s.buildPyramid(1);
|
||
check(s.levels() == 2, "金字塔层数==2");
|
||
}
|
||
|
||
// 加载整卷,校验维度一致
|
||
geopro::render::WholeVolumeSource src(store);
|
||
check(src.meta().nx == spec.nx, "load nx");
|
||
check(src.meta().ny == spec.ny, "load ny");
|
||
check(src.meta().nz == spec.nz, "load nz");
|
||
|
||
// 某体素值合理性:x0/y0 角点应有非 blank 量化值(落格命中首道首通道)。
|
||
const std::int16_t q = built.vol.at(0, 0, 0);
|
||
check(q != geopro::core::ScalarVolumeI16::kBlank, "(0,0,0) 非 blank");
|
||
} catch (const std::exception& e) {
|
||
std::cerr << "[selftest] 异常: " << e.what() << "\n";
|
||
ok = false;
|
||
}
|
||
|
||
fs::remove_all(tmp, ec);
|
||
std::cout << "[selftest] " << (ok ? "PASS" : "FAIL") << "\n";
|
||
return ok ? 0 : 1;
|
||
}
|
||
|
||
void usage() {
|
||
std::cerr << "gpr_poc —— POC-B headless 度量 CLI\n"
|
||
" gpr_poc build <dir> [--line 001] [--cellXY 0.2] "
|
||
"[--cellZ 0.05] [--out <storeDir>] [--levels 2]\n"
|
||
" gpr_poc load <storeDir>\n"
|
||
" gpr_poc selftest\n";
|
||
}
|
||
|
||
} // namespace
|
||
|
||
int main(int argc, char** argv) {
|
||
if (argc < 2) {
|
||
usage();
|
||
return 2;
|
||
}
|
||
const std::string cmd = argv[1];
|
||
try {
|
||
if (cmd == "build") return cmdBuild(argc, argv);
|
||
if (cmd == "load") return cmdLoad(argc, argv);
|
||
if (cmd == "selftest") return cmdSelftest();
|
||
} catch (const std::exception& e) {
|
||
std::cerr << "错误: " << e.what() << "\n";
|
||
return 1;
|
||
}
|
||
usage();
|
||
return 2;
|
||
}
|