#include "core/algo/GeoVolumeBuilder.hpp" #include #include #include #include #include #include #include #include #include "data/store/ChunkedVolumeStore.hpp" namespace fs = std::filesystem; using namespace geopro::core; // ---- principalAxisAngle 纯函数 ---- TEST(GeoVolumeBuilder, PcaFindsRoadDirection) { // 点沿 +Y(北向)排布 → 主轴角约 ±90°(±pi/2)。 std::vector xs, ys; for (int i = 0; i < 20; ++i) { xs.push_back(0.01 * i); // 微小横向噪声 ys.push_back(static_cast(i)); } const double ang = principalAxisAngle(xs, ys); EXPECT_NEAR(std::abs(ang), 3.14159265358979323846 / 2.0, 0.05); } TEST(GeoVolumeBuilder, PcaAlongEastIsZero) { std::vector xs, ys; for (int i = 0; i < 20; ++i) { xs.push_back(static_cast(i)); ys.push_back(0.01 * i); } EXPECT_NEAR(principalAxisAngle(xs, ys), 0.0, 0.05); } TEST(GeoVolumeBuilder, PcaDegenerateReturnsZero) { EXPECT_DOUBLE_EQ(principalAxisAngle({}, {}), 0.0); EXPECT_DOUBLE_EQ(principalAxisAngle({1.0}, {1.0}), 0.0); } // ---- 合成小线建体 ---- namespace { // 写一通道 .iprb + .iprh:值恒为 fixedVal(便于校验重叠均值)。 void writeChannel(const fs::path& iprb, int samples, int traces, std::int16_t fixedVal) { fs::path iprh = fs::path(iprb).replace_extension(".iprh"); std::ofstream h(iprh); 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 → 1e8 m/s h << "DISTANCE INTERVAL: 0.05\n"; h.close(); std::ofstream b(iprb, std::ios::binary); for (int t = 0; t < traces; ++t) for (int s = 0; s < samples; ++s) b.write(reinterpret_cast(&fixedVal), sizeof(fixedVal)); } // 写一条南北直线的 .gps(lat 从 lat0 递增到 lat1,lon 固定)。 void writeGps(const fs::path& path, double lat0, double lat1, double lon, int pts) { std::ofstream f(path); for (int i = 0; i < pts; ++i) { const double frac = pts > 1 ? static_cast(i) / (pts - 1) : 0.0; const double lat = lat0 + (lat1 - lat0) * frac; f << "2023-06-03\t00:00:00:000\t" << std::fixed << std::setprecision(10) << lat << "\tN\t" << lon << "\tE\t9.0\tM\t4\n"; } } // 写两通道 .ord(横偏 -0.5 / +0.5,末列=1 有效)。 void writeOrd(const fs::path& path) { std::ofstream f(path); f << "0 -0.500000 -1.5 1\n"; f << "1 0.500000 -1.5 1\n"; } GeoLineInput makeLine(const fs::path& dir, const std::string& tag, double lat0, double lat1, double lon, int traces, std::int16_t val) { const int samples = 8; writeChannel(dir / (tag + "_A01.iprb"), samples, traces, val); writeChannel(dir / (tag + "_A02.iprb"), samples, traces, val); writeOrd(dir / (tag + ".ord")); writeGps(dir / (tag + ".gps"), lat0, lat1, lon, traces); GeoLineInput in; in.iprb = {(dir / (tag + "_A01.iprb")).string(), (dir / (tag + "_A02.iprb")).string()}; in.ord = (dir / (tag + ".ord")).string(); in.gps = (dir / (tag + ".gps")).string(); return in; } // 写一条 L 形弯轨迹的 .gps:先沿北(纬增,lon 固定)走半程,再沿东(经增,lat 固定)走半程。 void writeLGps(const fs::path& path, double lat0, double lon0, double dLatHalf, double dLonHalf, int pts) { std::ofstream f(path); const int half = pts / 2; for (int i = 0; i < pts; ++i) { double lat, lon; if (i <= half) { const double frac = half > 0 ? static_cast(i) / half : 0.0; lat = lat0 + dLatHalf * frac; // 北段:纬增 lon = lon0; } else { const double frac = (pts - 1 - half) > 0 ? static_cast(i - half) / (pts - 1 - half) : 0.0; lat = lat0 + dLatHalf; // 拐角后纬固定 lon = lon0 + dLonHalf * frac; // 东段:经增 } f << "2023-06-03\t00:00:00:000\t" << std::fixed << std::setprecision(10) << lat << "\tN\t" << lon << "\tE\t9.0\tM\t4\n"; } } GeoLineInput makeLLine(const fs::path& dir, const std::string& tag, double lat0, double lon0, double dLatHalf, double dLonHalf, int traces, std::int16_t val) { const int samples = 8; writeChannel(dir / (tag + "_A01.iprb"), samples, traces, val); writeChannel(dir / (tag + "_A02.iprb"), samples, traces, val); writeOrd(dir / (tag + ".ord")); writeLGps(dir / (tag + ".gps"), lat0, lon0, dLatHalf, dLonHalf, traces); GeoLineInput in; in.iprb = {(dir / (tag + "_A01.iprb")).string(), (dir / (tag + "_A02.iprb")).string()}; in.ord = (dir / (tag + ".ord")).string(); in.gps = (dir / (tag + ".gps")).string(); return in; } } // namespace TEST(GeoVolumeBuilder, BuildsSyntheticLinesOverlapAveraged) { const fs::path tmp = fs::temp_directory_path() / "geopro_geovol_test"; std::error_code ec; fs::remove_all(tmp, ec); fs::create_directories(tmp); // 两条同位置南北线(lat 30.200→30.201,~111m),值不同 → 重叠 cell 取均值。 const double lat0 = 30.200, lat1 = 30.201, lon = 120.244; std::vector lines = { makeLine(tmp, "synA_001", lat0, lat1, lon, /*traces=*/40, /*val=*/100), makeLine(tmp, "synB_002", lat0, lat1, lon, /*traces=*/40, /*val=*/300), }; const std::string store = (tmp / "store").string(); GeoGridSpec spec{/*cellXY=*/0.5, /*cellZ=*/0.1}; GeoBuildResult r = buildGeoVolume(lines, spec, store, /*pyramidLevels=*/1); // 维度合理:约 111m 长 → nx 在数百量级;横路窄(~1m 阵列) → ny 小;nz>1。 EXPECT_GT(r.nx, 50); EXPECT_GE(r.ny, 1); EXPECT_GT(r.nz, 1); EXPECT_GT(r.filled, 0); EXPECT_LE(r.filled, r.total); // store 可读,维度一致。 geopro::data::ChunkedVolumeStore s(store); EXPECT_EQ(s.meta().nx, r.nx); EXPECT_EQ(s.meta().ny, r.ny); EXPECT_EQ(s.meta().nz, r.nz); EXPECT_EQ(s.levels(), 2); // level0 + 1 // 重叠均值:两线值 100/300,命中同 cell → 均值 200。扫所有块找非 blank 体素, // 其物理值应接近 200(量化误差内)。 const auto& m = s.meta(); bool foundNonBlank = false; double sampleVal = 0.0; for (int bz = 0; bz < s.bricksZ() && !foundNonBlank; ++bz) for (int by = 0; by < s.bricksY() && !foundNonBlank; ++by) for (int bx = 0; bx < s.bricksX() && !foundNonBlank; ++bx) { auto vox = s.readBrick(bx, by, bz); for (std::int16_t q : vox) { if (q != geopro::core::ScalarVolumeI16::kBlank) { sampleVal = m.quant.toPhys(q); foundNonBlank = true; break; } } } ASSERT_TRUE(foundNonBlank); EXPECT_NEAR(sampleVal, 200.0, 2.0); fs::remove_all(tmp, ec); } // L 形弯线:PCA 版把拐后段甩进大 Y'(虚高 ny、稀疏),曲线版沿 X 拉直展开 // (ny 接近真实横向、填充更密)。断言曲线版 ny < PCA 版 ny 且填充率更高。 TEST(GeoVolumeBuilder, CurvilinearStraightensBendVsPca) { const fs::path tmp = fs::temp_directory_path() / "geopro_geovol_curv_test"; std::error_code ec; fs::remove_all(tmp, ec); fs::create_directories(tmp); // L 形:北 ~111m(0.001° 纬)+ 东 ~96m(0.001° 经)。 const double lat0 = 30.200, lon0 = 120.244; std::vector lines = {makeLLine( tmp, "synL_001", lat0, lon0, /*dLatHalf=*/0.001, /*dLonHalf=*/0.001, /*traces=*/120, /*val=*/150)}; GeoGridSpec spec{/*cellXY=*/0.5, /*cellZ=*/0.1}; const std::string storePca = (tmp / "pca").string(); GeoBuildResult rPca = buildGeoVolume(lines, spec, storePca, /*pyramidLevels=*/0, /*curvilinear=*/false); const std::string storeCurv = (tmp / "curv").string(); GeoBuildResult rCurv = buildGeoVolume(lines, spec, storeCurv, /*pyramidLevels=*/0, /*curvilinear=*/true); const double fillPca = rPca.total > 0 ? static_cast(rPca.filled) / rPca.total : 0.0; const double fillCurv = rCurv.total > 0 ? static_cast(rCurv.filled) / rCurv.total : 0.0; // 曲线版把弯路拉直:横向维度 ny 明显小于 PCA 版。 EXPECT_LT(rCurv.ny, rPca.ny); // 填充更密。 EXPECT_GT(fillCurv, fillPca); // 曲线版仍建出有效体。 EXPECT_GT(rCurv.filled, 0); EXPECT_GT(rCurv.nx, 50); fs::remove_all(tmp, ec); } // 距离权纯函数:距 cell 中心越近权越大,单调递减,中心处取最大 1。 TEST(GeoVolumeBuilder, DistanceWeightDecreasesWithDistance) { EXPECT_DOUBLE_EQ(distanceWeight(0.0), 1.0); // 中心权最大 EXPECT_LT(distanceWeight(1.0), distanceWeight(0.5)); // 单调递减 EXPECT_LT(distanceWeight(2.0), distanceWeight(1.0)); EXPECT_GT(distanceWeight(0.5), 0.0); } // 加权均值偏向近点:同 cell 两样本,近点(dist=0.1)值 100、远点(dist=0.9)值 300。 // 等权均值 = 200;距离加权均值应明显 < 200(偏向近点的 100)。 TEST(GeoVolumeBuilder, WeightedMeanFavorsNearerPoint) { const double vNear = 100.0, vFar = 300.0; const double wNear = distanceWeight(0.1); const double wFar = distanceWeight(0.9); const double weighted = (wNear * vNear + wFar * vFar) / (wNear + wFar); const double equal = 0.5 * (vNear + vFar); // 200 EXPECT_LT(weighted, equal); // 偏离等权 EXPECT_LT(weighted - vNear, vFar - weighted); // 更靠近近点值 }