From bfd7d4aafd3f7f860a4715cce08d0c4402bdf560 Mon Sep 17 00:00:00 2001 From: gaozheng Date: Tue, 23 Jun 2026 12:27:10 +0800 Subject: [PATCH] =?UTF-8?q?feat(poc):=20gpr=5Fpoc=20headless=20=E5=BA=A6?= =?UTF-8?q?=E9=87=8F=20CLI=EF=BC=88=E5=9C=B0=E5=9F=BA=E7=AB=AF=E5=88=B0?= =?UTF-8?q?=E7=AB=AF=E4=B8=B2=E8=81=94=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 串起 assembleGprSurvey→buildGprVolume→ChunkedVolumeStore::write→ buildPyramid→WholeVolumeSource,提供 build/load/selftest 三子命令, 输出建体耗时/维度/体积/压缩比/加载/峰值内存指标(Psapi 峰值工作集)。 selftest 合成数据端到端 PASS。真实明星路数据 BLOCKED:前置 readIprb 的 traces=lastTrace+1 严格校验与真实文件「道数=lastTrace」系统性不符, 装配阶段即抛异常,未擅自改前置/其单测,如实记录于 poc-results-B.md。 --- .superpowers/sdd/task-9b-report.md | 68 ++++ CMakeLists.txt | 3 + docs/superpowers/plans/poc-results-B.md | 104 ++++++ tools/gpr_poc/CMakeLists.txt | 30 ++ tools/gpr_poc/Probe.hpp | 60 ++++ tools/gpr_poc/main.cpp | 445 ++++++++++++++++++++++++ 6 files changed, 710 insertions(+) create mode 100644 .superpowers/sdd/task-9b-report.md create mode 100644 docs/superpowers/plans/poc-results-B.md create mode 100644 tools/gpr_poc/CMakeLists.txt create mode 100644 tools/gpr_poc/Probe.hpp create mode 100644 tools/gpr_poc/main.cpp diff --git a/.superpowers/sdd/task-9b-report.md b/.superpowers/sdd/task-9b-report.md new file mode 100644 index 0000000..9f0db28 --- /dev/null +++ b/.superpowers/sdd/task-9b-report.md @@ -0,0 +1,68 @@ +# Task 9b 报告:gpr_poc CLI + 真实数据 headless 度量 + +状态:**PARTIAL / BLOCKED** +- CLI 编译链接通过;`selftest` PASS(合成数据端到端跑通整条地基)。 +- 真实明星路数据 **BLOCKED**:前置 IO 层 `readIprb` 的 `traces=lastTrace+1` 严格校验 + 与真实文件「道数=lastTrace」系统性不符,装配阶段即抛异常,无法实测建体指标。 + **未擅自修改前置/其单测**(被现有测试钉死的契约 + 跨任务边界),故真实指标暂缺,如实记录。 + +--- + +## 1. 交付物(均为本会话自有文件) + +- `tools/gpr_poc/main.cpp` —— CLI:`build` / `load` / `selftest` 三子命令。 +- `tools/gpr_poc/Probe.hpp` —— header-only 计时(steady_clock)+ 峰值内存(Psapi `PeakWorkingSetSize`)。 + 含 `NOMINMAX`/`WIN32_LEAN_AND_MEAN` 防 `` 宏污染 `std::numeric_limits::min/max`。 +- `tools/gpr_poc/CMakeLists.txt` —— 可执行 `gpr_poc`,链 `geopro_io_gpr/geopro_core/geopro_store/geopro_render` + + Windows `Psapi`;`vtk_module_autoinit` 注册 VTK 工厂。 +- 顶层 `CMakeLists.txt` —— 加 `add_subdirectory(tools/gpr_poc)`(在 `add_subdirectory(src)` 之后)。 +- `docs/superpowers/plans/poc-results-B.md` —— 实测结果(selftest PASS + 真实数据 BLOCKED 根因表)。 + +注:库目标实际名为 `geopro_store`(brief 写作 geopro_store/已对齐)与 `geopro_data`; +本工具链 `geopro_store`(分块存储),正确。 + +## 2. 构建 + +- 配置:`cmd /c "build.bat configure"`(preset msvc-release,build/release)成功 + (cmd 被环境劫持但真实命令仍执行;以 build.ninja 出现 gpr_poc target 确认)。 +- 编译:PowerShell + vcvars64 直驱 cmake `--build build/release --target gpr_poc`。 + 首次失败:`` min/max 宏污染 → 加 NOMINMAX 修复 → 二次链接成功。 +- 运行需 PATH 带 Qt6/VTK/vcpkg bin(headless 工具仍依赖这些 DLL)。 + +## 3. selftest 结果 + +``` +gpr_poc selftest +[selftest] GridSpec 2x2x8 dz=0.714286 +[selftest] PASS (exit 0) +``` +覆盖:assembleGprSurvey → buildGprVolume → write(brick=4) → buildPyramid(1) → +WholeVolumeSource,断言维度/层数/体素非 blank 全通过。 + +## 4. 真实数据指标 + +**未实测(BLOCKED)**。根因:`readIprb`(`src/io/gpr/IprbReader.cpp:16`) +`traces=lastTrace+1` 严格字节校验;真实明星路 14 通道每个恰含 `lastTrace` 道(少 1 道), +逐通道实测一致(详见 poc-results-B.md §2 表)。非 OOM/超时——装配前读入即失败, +调 `--cellXY` 无法绕过。现有 `tests/io/gpr/test_iprb_reader.cpp:30-31` 锁定该抛异常契约。 + +用的参数:`--line 001 --cellXY 0.2 --cellZ 0.05 --levels 2`(建体未到达)。 +预估几何(非实测,供核对):nx≈11118, ny≈8, nz≈1(深度尺度因土速单位为微米级, +cellZ=0.05 压成单层——需 POC owner 复核土速/时窗单位与 cellZ)。 + +## 5. 提交前自检 + +- 仅 `git add` 自有文件:`tools/gpr_poc/*`、顶层 `CMakeLists.txt`、 + `docs/superpowers/plans/poc-results-B.md`、本报告 `.superpowers/sdd/task-9b-report.md`。 +- `git diff --cached --stat` 确认无 chart/scatter/quill/rangeslider 等并行会话行。 +- 顶层 CMakeLists 的暂存 diff 应仅含新增的 `add_subdirectory(tools/gpr_poc)` 一行块。 + +## 6. Concerns / 需 owner 决策 + +1. **真实数据 BLOCKER(高)**:`readIprb` 道数契约与真实数据不符。建议放宽为 + 「道数 = 文件字节 / (samples·2)」(容忍 ±N 道),或确认 LAST TRACE 语义后去 +1, + 并同步改单测。落地后重跑两条命令即可补齐 §4 真实指标。 +2. **深度尺度(中)**:SOIL VELOCITY=100 m/s(头单位 m/µs ×1e6)→ 深度跨度微米级, + cellZ=0.05 会把 Z 压成 1 层。影响真实体维度与 9c 渲染基准,需确认单位约定。 +3. 顶层 CMakeLists 当前 working tree 已有他会话的修改(视觉设计/chart 等);本会话只新增 + add_subdirectory 一行,暂存时务必只 stage 该文件并核对 diff,勿带入其他未暂存改动。 diff --git a/CMakeLists.txt b/CMakeLists.txt index 7d766ba..57c0e2f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -78,5 +78,8 @@ endif() add_subdirectory(src) +# POC-B headless 度量 CLI(gpr_poc)。链 io_gpr/core/store/render,在真实数据上跑端到端度量。 +add_subdirectory(tools/gpr_poc) + enable_testing() add_subdirectory(tests) diff --git a/docs/superpowers/plans/poc-results-B.md b/docs/superpowers/plans/poc-results-B.md new file mode 100644 index 0000000..89cdbbf --- /dev/null +++ b/docs/superpowers/plans/poc-results-B.md @@ -0,0 +1,104 @@ +# POC-B 实测结果(gpr_poc headless 度量) + +工具:`tools/gpr_poc`(CLI),构建产物 `build/release/tools/gpr_poc/gpr_poc.exe`。 +执行机:Windows 11,MSVC(VS18 Community)+ Ninja,Release(/O2)。 +日期:2026-06-23。 + +整条地基链路: +`assembleGprSurvey → buildGprVolume → ChunkedVolumeStore::write → buildPyramid → WholeVolumeSource(load)`。 + +--- + +## 1. selftest(合成极小数据)—— PASS + +命令:`gpr_poc selftest` + +- 构造 2 通道合成 survey(samples=8,traces=12),写临时 `.iprb/.iprh/.ord`, + 走完整 `assembleGprSurvey → buildGprVolume → write(brick=4) → buildPyramid(1) → WholeVolumeSource`。 +- 断言:ntraces/samples/channels、channelY 升序、GridSpec `2x2x8`、建体维度、 + 金字塔层数==2、整卷维度一致、(0,0,0) 非 blank。 +- 结果:**PASS**(退出码 0)。 + +结论:除真实 `.iprb` 读入外,**整条地基管线在合成数据上端到端跑通** +(装配几何、建体量化、分块压缩落盘、金字塔降采样、整卷重组加载均正确)。 + +--- + +## 2. 真实数据(D:\Downloads\明星路,线 001)—— **BLOCKED(前置 IO 层与真实数据契约不符)** + +命令: +``` +gpr_poc build "D:\Downloads\明星路" --line 001 --cellXY 0.2 --cellZ 0.05 --out %TEMP%\gpr_store_001 --levels 2 +``` + +进度: +- 文件发现 **成功**:定位 14 通道 `明星路_001_A01..A14.iprb` + `明星路_001.ord`。 +- 装配 **失败**:`assembleGprSurvey` → `readIprb` 抛 + `文件大小与 samples*traces*2 不符: ..._A01.iprb`。 + +### 根因(off-by-one:lastTrace+1 vs 真实道数) + +`readIprb`(前置 IO 层,`src/io/gpr/IprbReader.cpp:16`)硬编码 +`traces = h.lastTrace + 1` 并对文件字节数做**严格相等**校验。 +真实明星路每个通道文件恰好含 `lastTrace` 条道(少 1 道),逐通道实测: + +| 通道 | 文件字节 | samples | LAST TRACE | 期望(=samples·(lastTrace+1)·2) | 实际道数(=bytes/(samples·2)) | +|------|----------|---------|------------|--------------------------------|------------------------------| +| A01 | 74390810 | 821 | 45305 | 74392452 | 45305 | +| A02 | 74394094 | 821 | 45307 | 74395736 | 45307 | +| A03 | 74390810 | 821 | 45305 | 74392452 | 45305 | +| A04 | 74394094 | 821 | 45307 | 74395736 | 45307 | +| A05 | 74390810 | 821 | 45305 | 74392452 | 45305 | +| A06 | 74394094 | 821 | 45307 | 74395736 | 45307 | +| A07 | 74390810 | 821 | 45305 | 74392452 | 45305 | +| A08 | 74394094 | 821 | 45307 | 74395736 | 45307 | +| A09 | 74390810 | 821 | 45305 | 74392452 | 45305 | +| A10 | 74394094 | 821 | 45307 | 74395736 | 45307 | +| A11 | 74390810 | 821 | 45305 | 74392452 | 45305 | +| A12 | 74392452 | 821 | 45306 | 74394094 | 45306 | +| A13 | 74390810 | 821 | 45305 | 74392452 | 45305 | +| A14 | 74394094 | 821 | 45307 | 74395736 | 45307 | + +**规律一致**:所有通道「实际道数 == LAST TRACE」,即真实文件道数 = `lastTrace`, +而 `readIprb` 期望 `lastTrace + 1`,每通道恰差 1 道(`samples·2 = 1642` 字节)。 + +这是**前置 IO 层契约与真实数据约定的系统性不符**,不是 gpr_poc CLI 的问题。 +现有单测(`tests/io/gpr/test_iprb_reader.cpp:30-31`)显式锁定 +「字节数 != samples·(lastTrace+1)·2 → 抛异常」,故 `lastTrace+1` 是被测试钉死的契约。 + +### 处置(本任务未擅自改前置/测试) + +按任务纪律(外科手术式改动、不动他人已测契约、严禁编造指标), +本会话**未修改** `readIprb` 或其单测,故真实数据的建体/维度/压缩比/加载/峰值内存 +**暂无法实测**,如实记录为 BLOCKED。 + +不是 OOM、不是超时——是装配前的文件读入契约不符,调粗 `--cellXY` 也无法绕过 +(在到达建体之前就抛了)。 + +### 建议解法(需 POC/IO 层 owner 决策,任一) + +1. **放宽 `readIprb`**:以「文件字节数 / (samples·2)」反推真实道数(容忍 ±N 道), + 而非硬用 `lastTrace+1`;同步更新 `test_iprb_reader.cpp`。这与真实数据约定 + (道数 = `lastTrace`)一致,最贴近现场。 +2. **明确 LAST TRACE 语义**:若约定其为「道数」而非「末道索引」,则 `traces = lastTrace` + (去掉 +1),同样需改实现 + 测试。 +3. 任一方案落地后,重跑: + `gpr_poc build "D:\Downloads\明星路" --line 001 --cellXY 0.2 --cellZ 0.05 --out --levels 2` + 再 `gpr_poc load `,本文件 §2 即可补齐真实指标。 + +--- + +## 3. 预估几何(供解法落地后核对,非实测) + +基于真实头:samples=821,dx≈0.049084 m,通道横偏 Y∈[-0.686, 0.686](跨度≈1.372 m), +土速 100 m/s、时窗 160.352 ns ⇒ 深度跨度 `depthOfSample(820)≈8.0e-6 m`(量级极小)。 + +按 `--cellXY 0.2 --cellZ 0.05`、X 跨度≈`45305·0.049084≈2223 m`: +- nx ≈ ceil(2223/0.2)+1 ≈ **11118** +- ny ≈ ceil(1.372/0.2)+1 ≈ **8** +- nz ≈ ceil(8.0e-6/0.05)+1 = **1**(Z 物理跨度远小于 cellZ,故仅 1 层) + +**注意**:因 SOIL VELOCITY 存为 100 m/s(头文件单位 m/µs ×1e6 后),深度尺度为微米级, +`--cellZ 0.05`(5 cm)会把整个深度压成 1 层。落地解法后,若要在 Z 方向有分辨率, +需把 `--cellZ` 调到与深度跨度匹配的量级(约 1e-8 m),或复核土速/时窗单位约定。 +此项一并提请 POC owner 确认(影响真实体维度与后续渲染基准 9c)。 diff --git a/tools/gpr_poc/CMakeLists.txt b/tools/gpr_poc/CMakeLists.txt new file mode 100644 index 0000000..453a71c --- /dev/null +++ b/tools/gpr_poc/CMakeLists.txt @@ -0,0 +1,30 @@ +# POC-B headless 度量 CLI(gpr_poc)。 +# 串起:geopro_io_gpr(解析装配)→ geopro_core(建体)→ geopro_store(分块落盘/金字塔) +# → geopro_render(WholeVolumeSource 整卷加载)。 +# Windows 峰值内存用 Psapi(Probe.hpp)。 + +# VTK_LIBRARIES 在子作用域内由各 find_package 设定,这里显式再请求一次 +# (与 geopro_render 同组件集),确保本 target 的 vtk_module_autoinit 可用。 +find_package(VTK REQUIRED COMPONENTS + CommonCore CommonDataModel RenderingCore RenderingOpenGL2 GUISupportQt) + +add_executable(gpr_poc main.cpp) + +target_include_directories(gpr_poc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + +target_link_libraries(gpr_poc PRIVATE + geopro_io_gpr + geopro_core + geopro_store + geopro_render) + +if(WIN32) + target_link_libraries(gpr_poc PRIVATE Psapi) +endif() + +target_compile_features(gpr_poc PRIVATE cxx_std_17) +set_target_properties(gpr_poc PROPERTIES AUTOMOC OFF AUTOUIC OFF AUTORCC OFF) + +# geopro_render 透传 VTK_LIBRARIES(PUBLIC),消费方需 autoinit 各 VTK 模块工厂, +# 否则 vtkImageData/渲染对象工厂未注册。 +vtk_module_autoinit(TARGETS gpr_poc MODULES ${VTK_LIBRARIES}) diff --git a/tools/gpr_poc/Probe.hpp b/tools/gpr_poc/Probe.hpp new file mode 100644 index 0000000..3757583 --- /dev/null +++ b/tools/gpr_poc/Probe.hpp @@ -0,0 +1,60 @@ +#ifndef GEOPRO_TOOLS_GPR_POC_PROBE_HPP +#define GEOPRO_TOOLS_GPR_POC_PROBE_HPP + +// 轻量 headless 度量探针:墙钟计时 + 进程峰值工作集内存。 +// 仅供 gpr_poc CLI 使用,header-only,零外部依赖(Windows 用 Psapi)。 + +#include +#include + +#if defined(_WIN32) +// 防止 的 min/max 宏污染 std::numeric_limits<>::min()/max() +// (ScalarVolumeI16.hpp 等会因此编译失败)。 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#include +#endif + +namespace geopro::tools { + +// 墙钟计时器:构造即起表,elapsedMs() 返回毫秒(double)。 +class Stopwatch { + public: + Stopwatch() : start_(std::chrono::steady_clock::now()) {} + + void reset() { start_ = std::chrono::steady_clock::now(); } + + double elapsedMs() const { + const auto now = std::chrono::steady_clock::now(); + return std::chrono::duration(now - start_).count(); + } + + private: + std::chrono::steady_clock::time_point start_; +}; + +// 进程级度量探针。 +struct Probe { + // 进程峰值工作集(MB)。Windows 取 GetProcessMemoryInfo::PeakWorkingSetSize; + // 其它平台返回 0(本任务仅 Windows 实测)。 + static double peakMemMB() { +#if defined(_WIN32) + PROCESS_MEMORY_COUNTERS pmc; + if (GetProcessMemoryInfo(GetCurrentProcess(), &pmc, sizeof(pmc))) { + return static_cast(pmc.PeakWorkingSetSize) / (1024.0 * 1024.0); + } + return 0.0; +#else + return 0.0; +#endif + } +}; + +} // namespace geopro::tools + +#endif // GEOPRO_TOOLS_GPR_POC_PROBE_HPP diff --git a/tools/gpr_poc/main.cpp b/tools/gpr_poc/main.cpp new file mode 100644 index 0000000..2a1259c --- /dev/null +++ b/tools/gpr_poc/main.cpp @@ -0,0 +1,445 @@ +// gpr_poc —— POC-B headless 度量 CLI。 +// +// 串起整条地基:发现 14 通道 .iprb + .ord → assembleGprSurvey → buildGprVolume +// → ChunkedVolumeStore::write → buildPyramid → WholeVolumeSource(load), +// 在真实/合成数据上输出可测的真实指标(耗时/维度/体积/压缩比/加载/峰值内存)。 +// +// 子命令: +// gpr_poc build [--line 001] [--cellXY 0.2] [--cellZ 0.05] [--out ] [--levels 2] +// gpr_poc load +// gpr_poc selftest + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 kv; + std::vector 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 iprb; // 已按通道号排序 + std::string ord; +}; + +LineFiles discoverLine(const std::string& dir, const std::string& line) { + LineFiles lf; + std::map 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:优先匹配本线(含 "_" 且以 .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:匹配 "*__A.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(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(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(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 [--line 001] [--cellXY 0.2] " + "[--cellZ 0.05] [--out ] [--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(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(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 \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(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(base + t + s); + b.write(reinterpret_cast(&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 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 [--line 001] [--cellXY 0.2] " + "[--cellZ 0.05] [--out ] [--levels 2]\n" + " gpr_poc load \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; +}