feat/vtk-3d-view #7

Merged
gaozheng merged 301 commits from feat/vtk-3d-view into main 2026-06-27 18:43:52 +08:00
6 changed files with 710 additions and 0 deletions
Showing only changes of commit bfd7d4aafd - Show all commits

View File

@ -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` 防 `<windows.h>` 宏污染 `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-releasebuild/release成功
cmd 被环境劫持但真实命令仍执行;以 build.ninja 出现 gpr_poc target 确认)。
- 编译PowerShell + vcvars64 直驱 cmake `--build build/release --target gpr_poc`
首次失败:`<windows.h>` min/max 宏污染 → 加 NOMINMAX 修复 → 二次链接成功。
- 运行需 PATH 带 Qt6/VTK/vcpkg binheadless 工具仍依赖这些 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,勿带入其他未暂存改动。

View File

@ -78,5 +78,8 @@ endif()
add_subdirectory(src) add_subdirectory(src)
# POC-B headless CLIgpr_poc io_gpr/core/store/render
add_subdirectory(tools/gpr_poc)
enable_testing() enable_testing()
add_subdirectory(tests) add_subdirectory(tests)

View File

@ -0,0 +1,104 @@
# POC-B 实测结果gpr_poc headless 度量)
工具:`tools/gpr_poc`CLI构建产物 `build/release/tools/gpr_poc/gpr_poc.exe`
执行机Windows 11MSVCVS18 Community+ NinjaRelease/O2
日期2026-06-23。
整条地基链路:
`assembleGprSurvey → buildGprVolume → ChunkedVolumeStore::write → buildPyramid → WholeVolumeSource(load)`
---
## 1. selftest合成极小数据—— PASS
命令:`gpr_poc selftest`
- 构造 2 通道合成 surveysamples=8traces=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-onelastTrace+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 <store> --levels 2`
`gpr_poc load <store>`,本文件 §2 即可补齐真实指标。
---
## 3. 预估几何(供解法落地后核对,非实测)
基于真实头samples=821dx≈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

View File

@ -0,0 +1,30 @@
# POC-B headless CLIgpr_poc
# geopro_io_gpr geopro_core geopro_store/
# geopro_renderWholeVolumeSource
# Windows PsapiProbe.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_LIBRARIESPUBLIC autoinit VTK
# vtkImageData/
vtk_module_autoinit(TARGETS gpr_poc MODULES ${VTK_LIBRARIES})

60
tools/gpr_poc/Probe.hpp Normal file
View File

@ -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 <chrono>
#include <string>
#if defined(_WIN32)
// 防止 <windows.h> 的 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 <windows.h>
#include <psapi.h>
#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<double, std::milli>(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<double>(pmc.PeakWorkingSetSize) / (1024.0 * 1024.0);
}
return 0.0;
#else
return 0.0;
#endif
}
};
} // namespace geopro::tools
#endif // GEOPRO_TOOLS_GPR_POC_PROBE_HPP

445
tools/gpr_poc/main.cpp Normal file
View File

@ -0,0 +1,445 @@
// 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 推 GridSpecX 沿测线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 + .iprhsamples 采样、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] 构造极小合成 survey2 通道)...\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=2cellZ 较细确保 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;
}