refactor: strip non-kernel code — keep pure geometry engine only
Removed (application layer, not kernel): - blender_addon/ — Blender plugin - dotnet/ — .NET/C# bindings - viewer/ — Web 3D viewer - include/vde/ai/ — AI/ML (GNN, generative design) - include/vde/cloud/ — Cloud-native (OT, sessions) - include/vde/distributed/ — Cluster computing - include/vde/kbe/ — Knowledge-based engineering - include/vde/wasm/ — WASM compilation - include/vde/digital_twin/ — Digital twin - include/vde/plugin/ — Plugin system - include/vde/gpu/ — GPU acceleration - src/ai/, src/cloud/, src/distributed/, src/kbe/, src/wasm/, src/digital_twin/, src/plugin/, src/gpu/ - tests/ai/, tests/cloud/, tests/distributed/, tests/kbe/, tests/gpu/ Kept (pure geometry kernel): - brep/ curves/ mesh/ core/ foundation/ spatial/ boolean/ collision/ sdf/ sketch/ capi/ - python/ bindings (kept as thin binding layer) Rationale: Parasolid/ACIS/CGM are pure kernels. AI, cloud, WASM, digital twin, Blender plugins, .NET bindings, and Web viewers belong in the application layer, not in the geometry engine. VDE should focus on being the best geometry kernel.
This commit is contained in:
@@ -1,2 +0,0 @@
|
||||
add_vde_test(test_feature_learning)
|
||||
add_vde_test(test_generative_design)
|
||||
@@ -1,234 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include "vde/ai/feature_learning.h"
|
||||
|
||||
using namespace vde::ai;
|
||||
using namespace vde::brep;
|
||||
using namespace vde::core;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// FeatureClassifier 测试 (4 项)
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
TEST(FeatureClassifierTest, DefaultConstruction) {
|
||||
FeatureClassifier classifier;
|
||||
EXPECT_FALSE(classifier.is_model_loaded());
|
||||
EXPECT_TRUE(classifier.model_version().empty());
|
||||
}
|
||||
|
||||
TEST(FeatureClassifierTest, LoadModelSucceeds) {
|
||||
FeatureClassifier classifier;
|
||||
EXPECT_TRUE(classifier.load_model("models/feature_gnn.onnx"));
|
||||
EXPECT_TRUE(classifier.is_model_loaded());
|
||||
EXPECT_FALSE(classifier.model_version().empty());
|
||||
}
|
||||
|
||||
TEST(FeatureClassifierTest, ClassifyEmptyBody) {
|
||||
FeatureClassifier classifier;
|
||||
classifier.load_model("models/dummy.onnx");
|
||||
|
||||
BrepModel empty_body;
|
||||
auto result = classifier.classify_features(empty_body);
|
||||
|
||||
EXPECT_TRUE(result.success);
|
||||
EXPECT_TRUE(result.features.empty());
|
||||
EXPECT_EQ(result.hole_count, 0);
|
||||
}
|
||||
|
||||
TEST(FeatureClassifierTest, BuildFaceGraphOnBox) {
|
||||
FeatureClassifier classifier;
|
||||
auto box = make_box(2, 2, 2);
|
||||
|
||||
auto graph = classifier.build_face_graph(box);
|
||||
|
||||
// 一个盒子有 6 个面(具体取决于 make_box 实现)
|
||||
EXPECT_GT(graph.nodes.size(), 0u);
|
||||
// 面之间应有邻接关系
|
||||
EXPECT_GT(graph.edges.size(), 0u);
|
||||
EXPECT_EQ(graph.edges.size(), graph.edge_features.size());
|
||||
}
|
||||
|
||||
TEST(FeatureClassifierTest, EncodeFaceNodeReturnsCorrectDim) {
|
||||
FeatureClassifier classifier;
|
||||
auto box = make_box(2, 2, 2);
|
||||
|
||||
// 对每个面编码
|
||||
const auto nf = box.num_faces();
|
||||
if (nf > 0) {
|
||||
auto feats = classifier.encode_face_node(box, 0);
|
||||
EXPECT_EQ(feats.size(), 8u); // kNodeFeatureDim = 8
|
||||
}
|
||||
}
|
||||
|
||||
TEST(FeatureClassifierTest, EncodeEdgeFeatureOnBox) {
|
||||
FeatureClassifier classifier;
|
||||
auto box = make_box(2, 2, 2);
|
||||
|
||||
auto graph = classifier.build_face_graph(box);
|
||||
if (!graph.edges.empty()) {
|
||||
double edge_feat = classifier.encode_edge_feature(
|
||||
box, graph.edges[0].first, graph.edges[0].second);
|
||||
// 盒子边应为凸边(正值)
|
||||
EXPECT_GE(edge_feat, -1.0);
|
||||
EXPECT_LE(edge_feat, 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(FeatureClassifierTest, ClassifyFeaturesOnBox) {
|
||||
FeatureClassifier classifier;
|
||||
classifier.load_model("models/dummy.onnx");
|
||||
|
||||
auto box = make_box(2, 2, 2);
|
||||
auto result = classifier.classify_features(box);
|
||||
|
||||
EXPECT_TRUE(result.success);
|
||||
EXPECT_TRUE(result.error_message.empty());
|
||||
// 盒子可能在启发式分类下被归为某些特征
|
||||
}
|
||||
|
||||
TEST(FeatureClassifierTest, AggregateFeaturesMergesAdjacent) {
|
||||
FeatureClassifier classifier;
|
||||
auto box = make_box(2, 2, 2);
|
||||
auto graph = classifier.build_face_graph(box);
|
||||
|
||||
// 模拟面标签(全标记为 Fillet)
|
||||
std::vector<std::pair<FeatureType, double>> labels(
|
||||
graph.nodes.size(), {FeatureType::Fillet, 0.8});
|
||||
|
||||
auto features = classifier.aggregate_features(graph, labels, box);
|
||||
|
||||
// 连通的面会被聚合
|
||||
EXPECT_GE(features.size(), 1u);
|
||||
for (const auto& f : features) {
|
||||
EXPECT_EQ(f.type, FeatureType::Fillet);
|
||||
EXPECT_GT(f.confidence, 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// SimilaritySearch 测试 (4 项)
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
TEST(SimilaritySearchTest, ComputeESFOnBox) {
|
||||
SimilaritySearch search;
|
||||
auto box = make_box(2, 2, 2);
|
||||
|
||||
auto esf = search.compute_esf(box, 500);
|
||||
|
||||
// 640 维直方图
|
||||
EXPECT_EQ(esf.histogram.size(), 640u);
|
||||
}
|
||||
|
||||
TEST(SimilaritySearchTest, ComputeFPFHOnBox) {
|
||||
SimilaritySearch search;
|
||||
auto box = make_box(2, 2, 2);
|
||||
|
||||
auto fpfh = search.compute_fpfh(box, 300);
|
||||
|
||||
// 33 维直方图
|
||||
EXPECT_EQ(fpfh.histogram.size(), 33u);
|
||||
}
|
||||
|
||||
TEST(SimilaritySearchTest, ShapeDescriptorOnBox) {
|
||||
SimilaritySearch search;
|
||||
auto box = make_box(2, 2, 2);
|
||||
|
||||
auto desc = search.compute_shape_descriptor(box, "box_001", "/path/to/box.step", 500, 300);
|
||||
|
||||
EXPECT_EQ(desc.model_id, "box_001");
|
||||
EXPECT_EQ(desc.model_path, "/path/to/box.step");
|
||||
EXPECT_EQ(desc.esf.histogram.size(), 640u);
|
||||
EXPECT_EQ(desc.fpfh.histogram.size(), 33u);
|
||||
}
|
||||
|
||||
TEST(SimilaritySearchTest, IndexAndQuery) {
|
||||
SimilaritySearch search;
|
||||
|
||||
auto box1 = make_box(2, 2, 2);
|
||||
auto box2 = make_box(3, 3, 3);
|
||||
|
||||
auto desc1 = search.compute_shape_descriptor(box1, "box1", "", 500, 300);
|
||||
auto desc2 = search.compute_shape_descriptor(box2, "box2", "", 500, 300);
|
||||
|
||||
search.build_index({desc1, desc2});
|
||||
EXPECT_EQ(search.index_size(), 2u);
|
||||
|
||||
// 查询
|
||||
auto matches = search.query(box1, 5);
|
||||
EXPECT_GE(matches.size(), 1u);
|
||||
|
||||
// 自身应最相似
|
||||
if (matches.size() >= 1) {
|
||||
EXPECT_EQ(matches[0].model_id, "box1");
|
||||
EXPECT_GT(matches[0].similarity, 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(SimilaritySearchTest, ESFDistanceSelfIsZero) {
|
||||
SimilaritySearch search;
|
||||
auto box = make_box(2, 2, 2);
|
||||
auto esf = search.compute_esf(box, 500);
|
||||
|
||||
double dist = SimilaritySearch::esf_distance(esf, esf);
|
||||
EXPECT_NEAR(dist, 0.0, 1e-10);
|
||||
}
|
||||
|
||||
TEST(SimilaritySearchTest, FPFHDistanceSelfIsZero) {
|
||||
SimilaritySearch search;
|
||||
auto box = make_box(2, 2, 2);
|
||||
auto fpfh = search.compute_fpfh(box, 300);
|
||||
|
||||
double dist = SimilaritySearch::fpfh_distance(fpfh, fpfh);
|
||||
EXPECT_NEAR(dist, 0.0, 1e-10);
|
||||
}
|
||||
|
||||
TEST(SimilaritySearchTest, CombinedSimilarityInRange) {
|
||||
auto box1 = make_box(2, 2, 2);
|
||||
auto box2 = make_box(3, 3, 3);
|
||||
|
||||
SimilaritySearch search;
|
||||
auto desc1 = search.compute_shape_descriptor(box1, "", "", 500, 300);
|
||||
auto desc2 = search.compute_shape_descriptor(box2, "", "", 500, 300);
|
||||
|
||||
double sim = SimilaritySearch::combined_similarity(desc1, desc2);
|
||||
EXPECT_GE(sim, 0.0);
|
||||
EXPECT_LE(sim, 1.0);
|
||||
}
|
||||
|
||||
TEST(SimilaritySearchTest, IndexClear) {
|
||||
SimilaritySearch search;
|
||||
auto box = make_box(2, 2, 2);
|
||||
auto desc = search.compute_shape_descriptor(box);
|
||||
search.add_to_index(desc);
|
||||
EXPECT_EQ(search.index_size(), 1u);
|
||||
|
||||
search.clear_index();
|
||||
EXPECT_EQ(search.index_size(), 0u);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 辅助函数测试 (2 项)
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
TEST(FeatureLearningAuxTest, SampleSurfacePointsOnBox) {
|
||||
auto box = make_box(2, 2, 2);
|
||||
auto pts = sample_surface_points(box, 200);
|
||||
|
||||
EXPECT_GE(pts.size(), 0u);
|
||||
// 采样点应在表面上(近似包围盒内)
|
||||
for (const auto& p : pts) {
|
||||
EXPECT_GE(p.x(), -1.5);
|
||||
EXPECT_LE(p.x(), 1.5);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(FeatureLearningAuxTest, FaceCentroidsOnBox) {
|
||||
auto box = make_box(2, 2, 2);
|
||||
auto centroids = face_centroids(box);
|
||||
|
||||
EXPECT_EQ(centroids.size(), box.num_faces());
|
||||
for (const auto& c : centroids) {
|
||||
EXPECT_TRUE(std::isfinite(c.x()));
|
||||
EXPECT_TRUE(std::isfinite(c.y()));
|
||||
EXPECT_TRUE(std::isfinite(c.z()));
|
||||
}
|
||||
}
|
||||
@@ -1,240 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include "vde/ai/generative_design.h"
|
||||
#include "vde/brep/modeling.h"
|
||||
|
||||
using namespace vde::ai;
|
||||
using namespace vde::brep;
|
||||
using namespace vde::core;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 拓扑优化测试 (4 项)
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
TEST(TopologyOptimizationTest, SIMPOptimizationConverges) {
|
||||
AABB3D design_space{Point3D{0,0,0}, Point3D{10,5,2}};
|
||||
|
||||
std::vector<LoadCondition> loads = {
|
||||
{Point3D{5, 2.5, 2}, Vector3D{0, 0, -1}, 100.0}
|
||||
};
|
||||
|
||||
std::vector<FixedConstraint> constraints = {
|
||||
{Point3D{0, 0, 0}, true, true, true},
|
||||
{Point3D{10, 0, 0}, true, true, true}
|
||||
};
|
||||
|
||||
OptimizationConstraints opt;
|
||||
opt.grid_resolution = 32;
|
||||
opt.max_iterations = 30;
|
||||
opt.volume_fraction = 0.4;
|
||||
opt.convergence_tolerance = 0.01;
|
||||
|
||||
auto result = topology_optimization(design_space, loads, constraints, opt);
|
||||
|
||||
EXPECT_EQ(result.grid_resolution, 32);
|
||||
EXPECT_GT(result.iterations, 0);
|
||||
EXPECT_LE(result.iterations, 30);
|
||||
EXPECT_GT(result.final_volume_fraction, 0.0);
|
||||
EXPECT_LE(result.final_volume_fraction, 1.0);
|
||||
EXPECT_EQ(result.density_field.size(), 32u * 32 * 32);
|
||||
}
|
||||
|
||||
TEST(TopologyOptimizationTest, DensityFieldIsBounded) {
|
||||
AABB3D design_space{Point3D{0,0,0}, Point3D{4,4,4}};
|
||||
std::vector<LoadCondition> loads = {{Point3D{2,2,4}, Vector3D{0,0,-1}, 50.0}};
|
||||
std::vector<FixedConstraint> constraints = {{Point3D{1,1,0}, true, true, true}};
|
||||
|
||||
OptimizationConstraints opt;
|
||||
opt.grid_resolution = 16;
|
||||
opt.max_iterations = 20;
|
||||
|
||||
auto result = topology_optimization(design_space, loads, constraints, opt);
|
||||
|
||||
for (auto rho : result.density_field) {
|
||||
EXPECT_GE(rho, 0.0);
|
||||
EXPECT_LE(rho, 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(TopologyOptimizationTest, ConstraintRegionIsSolid) {
|
||||
AABB3D design_space{Point3D{0,0,0}, Point3D{4,4,4}};
|
||||
std::vector<LoadCondition> loads;
|
||||
std::vector<FixedConstraint> constraints = {
|
||||
{Point3D{0, 0, 0}, true, true, true}
|
||||
};
|
||||
|
||||
OptimizationConstraints opt;
|
||||
opt.grid_resolution = 16;
|
||||
opt.max_iterations = 10;
|
||||
|
||||
auto result = topology_optimization(design_space, loads, constraints, opt);
|
||||
|
||||
// 约束区域 (0,0,0) 附近应为实体
|
||||
int idx = 0; // idx3d(0,0,0,16) → 0
|
||||
EXPECT_NEAR(result.density_field[static_cast<size_t>(idx)], 1.0, 0.1);
|
||||
}
|
||||
|
||||
TEST(TopologyOptimizationTest, ExtractIsosurfaceFromDensityField) {
|
||||
AABB3D bounds{Point3D{0,0,0}, Point3D{2,2,2}};
|
||||
int res = 8;
|
||||
int n = res * res * res;
|
||||
|
||||
// 创建球形密度场
|
||||
std::vector<double> field(static_cast<size_t>(n), 0.0);
|
||||
for (int z = 0; z < res; ++z)
|
||||
for (int y = 0; y < res; ++y)
|
||||
for (int x = 0; x < res; ++x) {
|
||||
double cx = (x - res*0.5) / (res*0.5);
|
||||
double cy = (y - res*0.5) / (res*0.5);
|
||||
double cz = (z - res*0.5) / (res*0.5);
|
||||
double r = std::sqrt(cx*cx + cy*cy + cz*cz);
|
||||
field[static_cast<size_t>((z*res + y)*res + x)] = (r < 0.8) ? 1.0 : 0.0;
|
||||
}
|
||||
|
||||
auto mesh = extract_isosurface(field, res, bounds, 0.5);
|
||||
|
||||
// 应有输出网格(球体应有非零顶点)
|
||||
EXPECT_GE(mesh.num_vertices(), 0u);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 晶格结构生成测试 (4 项)
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
TEST(LatticeGenerationTest, GyroidSDFReturnsFiniteValue) {
|
||||
double sdf = lattice_sdf(1.0, 1.0, 1.0, LatticeCellType::Gyroid, 2.0, 0.1);
|
||||
EXPECT_TRUE(std::isfinite(sdf));
|
||||
}
|
||||
|
||||
TEST(LatticeGenerationTest, DiamondSDFReturnsFiniteValue) {
|
||||
double sdf = lattice_sdf(0.5, 0.5, 0.5, LatticeCellType::Diamond, 1.0, 0.15);
|
||||
EXPECT_TRUE(std::isfinite(sdf));
|
||||
}
|
||||
|
||||
TEST(LatticeGenerationTest, BCCSDFReturnsFiniteValue) {
|
||||
double sdf = lattice_sdf(0.0, 0.0, 0.0, LatticeCellType::BCC, 3.0, 0.1);
|
||||
EXPECT_TRUE(std::isfinite(sdf));
|
||||
}
|
||||
|
||||
TEST(LatticeGenerationTest, FCCSDFReturnsFiniteValue) {
|
||||
double sdf = lattice_sdf(2.0, 2.0, 2.0, LatticeCellType::FCC, 2.0, 0.12);
|
||||
EXPECT_TRUE(std::isfinite(sdf));
|
||||
}
|
||||
|
||||
TEST(LatticeGenerationTest, CubicSDFReturnsFiniteValue) {
|
||||
double sdf = lattice_sdf(1.5, 1.5, 1.5, LatticeCellType::Cubic, 1.5, 0.08);
|
||||
EXPECT_TRUE(std::isfinite(sdf));
|
||||
}
|
||||
|
||||
TEST(LatticeGenerationTest, OctetSDFReturnsFiniteValue) {
|
||||
double sdf = lattice_sdf(0.8, 0.8, 0.8, LatticeCellType::Octet, 1.8, 0.1);
|
||||
EXPECT_TRUE(std::isfinite(sdf));
|
||||
}
|
||||
|
||||
TEST(LatticeGenerationTest, GenerateGyroidLatticeOnBox) {
|
||||
auto box = make_box(5, 5, 5);
|
||||
|
||||
LatticeParams params;
|
||||
params.cell_size = 1.5;
|
||||
params.strut_thickness = 0.12;
|
||||
|
||||
auto result = lattice_generation(box, LatticeCellType::Gyroid, params);
|
||||
|
||||
EXPECT_TRUE(result.success);
|
||||
EXPECT_TRUE(result.error_message.empty());
|
||||
EXPECT_GT(result.cell_count, 0);
|
||||
EXPECT_GE(result.porosity, 0.0);
|
||||
EXPECT_LE(result.porosity, 1.0);
|
||||
// 晶格网格应有输出
|
||||
EXPECT_GE(result.lattice_mesh.num_vertices(), 0u);
|
||||
}
|
||||
|
||||
TEST(LatticeGenerationTest, VariableDensityLattice) {
|
||||
auto box = make_box(5, 5, 5);
|
||||
std::vector<LoadCondition> loads = {{Point3D{2.5, 2.5, 5}, Vector3D{0,0,-1}, 100.0}};
|
||||
|
||||
auto [density_map, res] = compute_variable_density_map(box, loads, 0.5, 1.0);
|
||||
|
||||
EXPECT_EQ(density_map.size(), static_cast<size_t>(res * res * res));
|
||||
EXPECT_GT(res, 0);
|
||||
|
||||
LatticeParams params;
|
||||
params.cell_size = 1.0;
|
||||
params.strut_thickness = 0.1;
|
||||
params.variable_density = true;
|
||||
params.density_map = density_map;
|
||||
params.density_map_resolution = res;
|
||||
|
||||
auto result = lattice_generation(box, LatticeCellType::Gyroid, params);
|
||||
EXPECT_TRUE(result.success);
|
||||
}
|
||||
|
||||
TEST(LatticeGenerationTest, AllCellTypesProduceValidSDF) {
|
||||
// 验证所有晶格类型都在 (0,0,0) 处产生有限的 SDF 值
|
||||
std::vector<LatticeCellType> types = {
|
||||
LatticeCellType::Gyroid, LatticeCellType::Diamond,
|
||||
LatticeCellType::BCC, LatticeCellType::FCC,
|
||||
LatticeCellType::Octet, LatticeCellType::Cubic
|
||||
};
|
||||
|
||||
for (auto ct : types) {
|
||||
double sdf = lattice_sdf(0.0, 0.0, 0.0, ct, 2.0, 0.1);
|
||||
EXPECT_TRUE(std::isfinite(sdf)) << "Cell type " << static_cast<int>(ct) << " produced nan/inf";
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// GAN 生成测试 (2 项)
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
TEST(GANGenerationTest, EncodeConditionsReturns32DVector) {
|
||||
GANCondition conditions;
|
||||
conditions.loads = {{Point3D{1,1,1}, Vector3D{0,0,-1}, 50.0}};
|
||||
conditions.constraints = {{Point3D{0,0,0}, true, true, true}};
|
||||
conditions.target_volume = 0.3;
|
||||
conditions.style_tag = "aerospace";
|
||||
|
||||
auto cond_vec = encode_conditions(conditions);
|
||||
EXPECT_EQ(cond_vec.size(), 32u);
|
||||
}
|
||||
|
||||
TEST(GANGenerationTest, Generative3DProducesValidMesh) {
|
||||
GANCondition conditions;
|
||||
conditions.loads = {{Point3D{0, 0, 1}, Vector3D{0, 0, -1}, 100.0}};
|
||||
conditions.constraints = {{Point3D{0, 0, -1}, true, true, true}};
|
||||
conditions.target_volume = 0.25;
|
||||
conditions.style_tag = "structural";
|
||||
|
||||
auto result = generative_adversarial_3d(conditions, "", 32);
|
||||
|
||||
EXPECT_TRUE(result.success);
|
||||
EXPECT_GT(result.generation_time_ms, 0.0);
|
||||
EXPECT_GE(result.generated_mesh.num_vertices(), 0u);
|
||||
EXPECT_FALSE(result.candidate_tags.empty());
|
||||
}
|
||||
|
||||
TEST(GANGenerationTest, LatentInterpolationProducesSequence) {
|
||||
GANCondition conditions;
|
||||
conditions.style_tag = "smooth_transition";
|
||||
|
||||
std::vector<double> z0(32, -0.5);
|
||||
std::vector<double> z1(32, 0.5);
|
||||
|
||||
auto results = latent_interpolation(z0, z1, 5, conditions);
|
||||
|
||||
EXPECT_EQ(results.size(), 5u);
|
||||
for (const auto& r : results) {
|
||||
EXPECT_TRUE(r.success);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(GANGenerationTest, MultipleStyleConditions) {
|
||||
std::vector<std::string> styles = {"aerospace", "automotive", "biomedical", "consumer"};
|
||||
|
||||
for (const auto& style : styles) {
|
||||
GANCondition conditions;
|
||||
conditions.style_tag = style;
|
||||
|
||||
auto result = generative_adversarial_3d(conditions, "", 16);
|
||||
EXPECT_TRUE(result.success) << "Failed for style: " << style;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
add_executable(test_cloud test_cloud.cpp)
|
||||
target_include_directories(test_cloud PRIVATE ${CMAKE_SOURCE_DIR}/include)
|
||||
target_link_libraries(test_cloud PRIVATE vde_cloud vde_core vde_foundation GTest::gtest GTest::gtest_main)
|
||||
gtest_discover_tests(test_cloud)
|
||||
@@ -1,177 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include "vde/cloud/cloud_native.h"
|
||||
#include <thread>
|
||||
#include <chrono>
|
||||
|
||||
using namespace vde::cloud;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 1. DeltaSync 基本操作
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
TEST(CloudTest, DeltaSync_InitialVersionIsZero) {
|
||||
DeltaSync sync;
|
||||
EXPECT_EQ(sync.version(), 0u);
|
||||
}
|
||||
|
||||
TEST(CloudTest, DeltaSync_CommitIncrementsVersion) {
|
||||
DeltaSync sync;
|
||||
DeltaOp op{OpType::MODIFY, 1001, 1, {}};
|
||||
sync.commit_local({op}, "user1");
|
||||
EXPECT_EQ(sync.version(), 1u);
|
||||
}
|
||||
|
||||
TEST(CloudTest, DeltaSync_MultipleCommits) {
|
||||
DeltaSync sync;
|
||||
sync.commit_local({{OpType::INSERT, 1, 1, {}}}, "user1");
|
||||
sync.commit_local({{OpType::MODIFY, 1, 2, {}}}, "user2");
|
||||
sync.commit_local({{OpType::INSERT, 2, 3, {}}}, "user1");
|
||||
EXPECT_EQ(sync.version(), 3u);
|
||||
}
|
||||
|
||||
TEST(CloudTest, DeltaSync_DirtyEntitiesTracking) {
|
||||
DeltaSync sync;
|
||||
sync.commit_local({{OpType::INSERT, 100, 1, {}}}, "user1");
|
||||
EXPECT_TRUE(sync.is_dirty(100));
|
||||
EXPECT_FALSE(sync.is_dirty(200));
|
||||
}
|
||||
|
||||
TEST(CloudTest, DeltaSync_DeleteRemovesDirty) {
|
||||
DeltaSync sync;
|
||||
sync.commit_local({{OpType::INSERT, 100, 1, {}}}, "user1");
|
||||
EXPECT_TRUE(sync.is_dirty(100));
|
||||
sync.commit_local({{OpType::DELETE, 100, 2, {}}}, "user1");
|
||||
EXPECT_FALSE(sync.is_dirty(100));
|
||||
}
|
||||
|
||||
TEST(CloudTest, DeltaSync_Rollback) {
|
||||
DeltaSync sync;
|
||||
sync.commit_local({{OpType::INSERT, 1, 1, {}}}, "user1");
|
||||
sync.commit_local({{OpType::INSERT, 2, 2, {}}}, "user1");
|
||||
EXPECT_EQ(sync.version(), 2u);
|
||||
EXPECT_TRUE(sync.rollback(1));
|
||||
EXPECT_EQ(sync.version(), 1u);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 2. OperationalTransform 冲突检测
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
TEST(CloudTest, OT_NoConflictOnDifferentEntities) {
|
||||
OperationalTransform ot;
|
||||
DeltaOp op1{OpType::MODIFY, 1, 100, {}};
|
||||
DeltaOp op2{OpType::MODIFY, 2, 200, {}};
|
||||
EXPECT_FALSE(ot.conflicts(op1, op2));
|
||||
}
|
||||
|
||||
TEST(CloudTest, OT_ConflictOnSameEntityModify) {
|
||||
OperationalTransform ot;
|
||||
DeltaOp op1{OpType::MODIFY, 1, 100, {}};
|
||||
DeltaOp op2{OpType::MODIFY, 1, 200, {}};
|
||||
EXPECT_TRUE(ot.conflicts(op1, op2));
|
||||
}
|
||||
|
||||
TEST(CloudTest, OT_ConflictOnDelete) {
|
||||
OperationalTransform ot;
|
||||
DeltaOp op1{OpType::DELETE, 1, 100, {}};
|
||||
DeltaOp op2{OpType::MODIFY, 1, 200, {}};
|
||||
EXPECT_TRUE(ot.conflicts(op1, op2));
|
||||
}
|
||||
|
||||
TEST(CloudTest, OT_TransformDifferentEntitiesNoOp) {
|
||||
OperationalTransform ot;
|
||||
DeltaOp op1{OpType::MODIFY, 1, 100, {1,2,3}};
|
||||
DeltaOp op2{OpType::MODIFY, 2, 200, {4,5,6}};
|
||||
DeltaOp result = ot.transform(op1, op2);
|
||||
EXPECT_EQ(result.entity_id, 1u);
|
||||
EXPECT_EQ(result.type, OpType::MODIFY);
|
||||
EXPECT_EQ(result.data, (std::vector<uint8_t>{1,2,3}));
|
||||
}
|
||||
|
||||
TEST(CloudTest, OT_MergeTwoChangeSets) {
|
||||
OperationalTransform ot;
|
||||
ChangeSet cs1;
|
||||
cs1.base_version = 0;
|
||||
cs1.new_version = 1;
|
||||
cs1.ops = {{OpType::INSERT, 10, 1, {}}};
|
||||
|
||||
ChangeSet cs2;
|
||||
cs2.base_version = 0;
|
||||
cs2.new_version = 1;
|
||||
cs2.ops = {{OpType::INSERT, 20, 2, {}}};
|
||||
|
||||
ChangeSet merged = ot.merge(cs1, cs2);
|
||||
EXPECT_GE(merged.ops.size(), 2u);
|
||||
EXPECT_EQ(merged.base_version, 0u);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 3. CloudSession 协作会话
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
TEST(CloudTest, Session_JoinAndLeave) {
|
||||
CloudSession session("test-session-1");
|
||||
UserInfo user{"user1", "Alice", 0xFF0000, true};
|
||||
|
||||
EXPECT_TRUE(session.join(user));
|
||||
EXPECT_EQ(session.online_users().size(), 1u);
|
||||
EXPECT_TRUE(session.leave("user1"));
|
||||
EXPECT_EQ(session.online_users().size(), 0u);
|
||||
}
|
||||
|
||||
TEST(CloudTest, Session_DuplicateJoinFails) {
|
||||
CloudSession session("test-session-2");
|
||||
UserInfo user{"user1", "Alice", 0xFF0000, true};
|
||||
EXPECT_TRUE(session.join(user));
|
||||
EXPECT_FALSE(session.join(user)); // 重复加入
|
||||
}
|
||||
|
||||
TEST(CloudTest, Session_SubmitOperation) {
|
||||
CloudSession session("test-session-3");
|
||||
UserInfo user{"user1", "Bob", 0x00FF00, true};
|
||||
session.join(user);
|
||||
|
||||
std::vector<DeltaOp> ops = {{OpType::INSERT, 42, 1, {1,2,3}}};
|
||||
EXPECT_TRUE(session.submit_operation("user1", ops));
|
||||
EXPECT_GT(session.version(), 0u);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 4. ObjectStorage 对象存储
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
TEST(CloudTest, ObjectStorage_Configure) {
|
||||
ObjectStorage storage;
|
||||
storage.configure("http://localhost:9000", "vde-models", "minioadmin", "minioadmin");
|
||||
SUCCEED(); // 配置不抛出异常
|
||||
}
|
||||
|
||||
TEST(CloudTest, ObjectStorage_StoreAndLoadModel) {
|
||||
ObjectStorage storage;
|
||||
storage.configure("http://localhost:9000", "vde-models", "key", "secret");
|
||||
|
||||
std::vector<uint8_t> model_data = {0x01, 0x02, 0x03, 0x04};
|
||||
EXPECT_TRUE(storage.store_model("test_part", model_data, "stp"));
|
||||
|
||||
// Stub 返回 nullopt,但调用接口正常
|
||||
auto loaded = storage.load_model("test_part", "stp");
|
||||
// In stub mode, this returns nullopt — that's expected
|
||||
SUCCEED();
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 5. ServerlessFunction
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
TEST(CloudTest, Serverless_Deploy) {
|
||||
ServerlessFunction fn;
|
||||
EXPECT_TRUE(fn.deploy("validate-model", "/code/validate.py", "python3.9"));
|
||||
}
|
||||
|
||||
TEST(CloudTest, Serverless_Invoke) {
|
||||
ServerlessFunction fn;
|
||||
FunctionRequest req{"validate-model", {0x01, 0x02}, {}, std::chrono::milliseconds(5000)};
|
||||
FunctionResponse resp = fn.invoke(req);
|
||||
EXPECT_EQ(resp.status_code, 200);
|
||||
EXPECT_TRUE(resp.ok);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
add_vde_test(test_cluster)
|
||||
@@ -1,418 +0,0 @@
|
||||
/**
|
||||
* @file test_cluster.cpp
|
||||
* @brief 分布式计算引擎单元测试 — 集群管理、消息序列化、任务调度、分布式运算
|
||||
* @ingroup tests
|
||||
*/
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include "vde/distributed/cluster_engine.h"
|
||||
#include "vde/distributed/grpc_service.h"
|
||||
#include "vde/core/aabb.h"
|
||||
#include "vde/mesh/marching_cubes.h"
|
||||
#include <cmath>
|
||||
#include <thread>
|
||||
|
||||
using namespace vde::distributed;
|
||||
using vde::core::AABB3D;
|
||||
using vde::core::Point3D;
|
||||
using vde::mesh::marching_cubes;
|
||||
using vde::mesh::MCMesh;
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
// Test 1: SerializedMessage — basic types
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
TEST(SerializedMessageTest, BasicTypes) {
|
||||
SerializedMessage msg;
|
||||
msg.write_int32(42);
|
||||
msg.write_int64(-1234567890123LL);
|
||||
msg.write_float64(3.141592653589793);
|
||||
msg.write_string("hello world");
|
||||
msg.write_message_type(MessageType::HEARTBEAT);
|
||||
|
||||
msg.reset_read();
|
||||
EXPECT_EQ(msg.read_int32(), 42);
|
||||
EXPECT_EQ(msg.read_int64(), -1234567890123LL);
|
||||
EXPECT_DOUBLE_EQ(msg.read_float64(), 3.141592653589793);
|
||||
EXPECT_EQ(msg.read_string(), "hello world");
|
||||
EXPECT_EQ(msg.read_message_type(), MessageType::HEARTBEAT);
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
// Test 2: SerializedMessage — varint encoding
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
TEST(SerializedMessageTest, VarintEncoding) {
|
||||
SerializedMessage msg;
|
||||
std::vector<uint64_t> test_values = {0, 1, 127, 128, 255, 300, 16384, 1000000, UINT64_MAX};
|
||||
for (auto v : test_values) {
|
||||
msg.write_varint(v);
|
||||
}
|
||||
msg.reset_read();
|
||||
for (auto v : test_values) {
|
||||
EXPECT_EQ(msg.read_varint(), v);
|
||||
}
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
// Test 3: SerializedMessage — binary data
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
TEST(SerializedMessageTest, BinaryData) {
|
||||
SerializedMessage msg;
|
||||
uint8_t test_data[] = {0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE};
|
||||
msg.write_bytes(test_data, sizeof(test_data));
|
||||
msg.reset_read();
|
||||
size_t len = static_cast<size_t>(msg.read_varint());
|
||||
EXPECT_EQ(len, sizeof(test_data));
|
||||
auto result = msg.read_bytes(len);
|
||||
EXPECT_EQ(result.size(), sizeof(test_data));
|
||||
for (size_t i = 0; i < len; ++i) {
|
||||
EXPECT_EQ(result[i], test_data[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
// Test 4: ClusterManager — register node
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
TEST(ClusterManagerTest, RegisterNode) {
|
||||
ClusterConfig cfg;
|
||||
cfg.heartbeat_interval_ms = 500;
|
||||
ClusterManager mgr(cfg);
|
||||
|
||||
std::string id = mgr.register_node("192.168.1.10", 50051, 16, 32768);
|
||||
EXPECT_FALSE(id.empty());
|
||||
EXPECT_EQ(mgr.node_count(), 1);
|
||||
|
||||
auto nodes = mgr.alive_nodes();
|
||||
ASSERT_EQ(nodes.size(), 1);
|
||||
EXPECT_EQ(nodes[0].host, "192.168.1.10");
|
||||
EXPECT_EQ(nodes[0].port, 50051);
|
||||
EXPECT_EQ(nodes[0].cores, 16);
|
||||
EXPECT_EQ(nodes[0].memory_mb, 32768);
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
// Test 5: ClusterManager — heartbeat and timeout
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
TEST(ClusterManagerTest, HeartbeatAndTimeout) {
|
||||
ClusterConfig cfg;
|
||||
cfg.heartbeat_interval_ms = 100;
|
||||
cfg.heartbeat_timeout_ms = 300;
|
||||
ClusterManager mgr(cfg);
|
||||
|
||||
std::string id = mgr.register_node("10.0.0.1", 50051, 8, 16384);
|
||||
EXPECT_EQ(mgr.alive_nodes().size(), 1);
|
||||
|
||||
mgr.heartbeat(id, 0.5, 8192, 3);
|
||||
auto node = mgr.find_node(id);
|
||||
ASSERT_TRUE(node.has_value());
|
||||
EXPECT_DOUBLE_EQ(node->cpu_load, 0.5);
|
||||
EXPECT_EQ(node->memory_used_mb, 8192);
|
||||
EXPECT_EQ(node->active_tasks, 3);
|
||||
|
||||
// Start heartbeat monitor and wait for timeout
|
||||
mgr.start_heartbeat_monitor();
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
||||
auto alive_after_timeout = mgr.alive_nodes();
|
||||
// Node should be marked offline after no heartbeat within timeout
|
||||
EXPECT_EQ(alive_after_timeout.size(), 0);
|
||||
mgr.stop_heartbeat_monitor();
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
// Test 6: ClusterManager — load balancing selection
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
TEST(ClusterManagerTest, LoadBalancing) {
|
||||
ClusterManager mgr;
|
||||
|
||||
// Register nodes with different capacities
|
||||
mgr.register_node("node1", 50051, 4, 8192); // low capacity
|
||||
mgr.register_node("node2", 50052, 16, 65536); // high capacity
|
||||
mgr.register_node("node3", 50053, 8, 16384); // medium capacity
|
||||
|
||||
// Simulate loads
|
||||
auto nodes = mgr.alive_nodes();
|
||||
ASSERT_EQ(nodes.size(), 3);
|
||||
|
||||
// Update loads: node1 heavily loaded, node2 lightly loaded
|
||||
for (auto& n : nodes) {
|
||||
if (n.host == "node1") mgr.heartbeat(n.node_id, 0.9, 7000, 10);
|
||||
if (n.host == "node2") mgr.heartbeat(n.node_id, 0.1, 5000, 1);
|
||||
if (n.host == "node3") mgr.heartbeat(n.node_id, 0.5, 8000, 4);
|
||||
}
|
||||
|
||||
// LEAST_LOADED strategy should pick node2
|
||||
mgr.set_strategy(ClusterManager::Strategy::LEAST_LOADED);
|
||||
auto selected = mgr.select_node();
|
||||
ASSERT_TRUE(selected.has_value());
|
||||
EXPECT_EQ(selected->host, "node2");
|
||||
|
||||
// Test ROUND_ROBIN
|
||||
mgr.set_strategy(ClusterManager::Strategy::ROUND_ROBIN);
|
||||
auto rr1 = mgr.select_node();
|
||||
auto rr2 = mgr.select_node();
|
||||
EXPECT_TRUE(rr1.has_value());
|
||||
EXPECT_TRUE(rr2.has_value());
|
||||
|
||||
// Test with GPU preference
|
||||
std::vector<GpuInfo> gpus = {{"A100", 40960, 80}};
|
||||
mgr.register_node("gpu_node", 50054, 64, 262144, gpus);
|
||||
mgr.heartbeat("gpu_node", 0.2, 10000, 0);
|
||||
|
||||
// After registration, re-fetch node by find
|
||||
auto gpu_node = mgr.find_node("gpu_node");
|
||||
// GPU node exists
|
||||
EXPECT_TRUE(gpu_node.has_value() || mgr.alive_nodes().size() >= 4);
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
// Test 7: ClusterNode — capacity score
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
TEST(ClusterNodeTest, CapacityScore) {
|
||||
ClusterNode n1;
|
||||
n1.cores = 8;
|
||||
n1.memory_mb = 16384;
|
||||
n1.memory_used_mb = 4096;
|
||||
n1.cpu_load = 0.2;
|
||||
n1.alive = true;
|
||||
double s1 = n1.capacity_score();
|
||||
|
||||
ClusterNode n2;
|
||||
n2.cores = 16;
|
||||
n2.memory_mb = 32768;
|
||||
n2.memory_used_mb = 4096;
|
||||
n2.cpu_load = 0.2;
|
||||
n2.alive = true;
|
||||
double s2 = n2.capacity_score();
|
||||
|
||||
// Higher capacity node should have higher score
|
||||
EXPECT_GT(s2, s1);
|
||||
|
||||
ClusterNode dead;
|
||||
dead.alive = false;
|
||||
EXPECT_DOUBLE_EQ(dead.capacity_score(), -1.0);
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
// Test 8: TaskScheduler — DAG execution
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
TEST(TaskSchedulerTest, DagExecution) {
|
||||
auto cluster = std::make_shared<ClusterManager>();
|
||||
cluster->register_node("local", 50051, 4, 8192);
|
||||
|
||||
TaskScheduler scheduler(cluster);
|
||||
|
||||
std::vector<int> execution_order;
|
||||
std::mutex order_mutex;
|
||||
int shared_value = 0;
|
||||
|
||||
// Task 1: no deps
|
||||
scheduler.add_task({.task_id=1, .name="init", .work=[&]() {
|
||||
std::lock_guard<std::mutex> lk(order_mutex);
|
||||
execution_order.push_back(1);
|
||||
shared_value = 100;
|
||||
}});
|
||||
|
||||
// Task 2: depends on 1
|
||||
scheduler.add_task({.task_id=2, .name="step2", .depends_on={1}, .work=[&]() {
|
||||
std::lock_guard<std::mutex> lk(order_mutex);
|
||||
execution_order.push_back(2);
|
||||
shared_value += 50;
|
||||
}});
|
||||
|
||||
// Task 3: depends on 1
|
||||
scheduler.add_task({.task_id=3, .name="step3", .depends_on={1}, .work=[&]() {
|
||||
std::lock_guard<std::mutex> lk(order_mutex);
|
||||
execution_order.push_back(3);
|
||||
shared_value += 30;
|
||||
}});
|
||||
|
||||
// Task 4: depends on 2 and 3
|
||||
scheduler.add_task({.task_id=4, .name="final", .depends_on={2, 3}, .work=[&]() {
|
||||
std::lock_guard<std::mutex> lk(order_mutex);
|
||||
execution_order.push_back(4);
|
||||
shared_value *= 2;
|
||||
}});
|
||||
|
||||
bool success = scheduler.execute_all();
|
||||
EXPECT_TRUE(success);
|
||||
|
||||
// Check execution order constraints
|
||||
EXPECT_EQ(execution_order[0], 1); // init must be first
|
||||
EXPECT_EQ(execution_order[3], 4); // final must be last
|
||||
|
||||
// Check results
|
||||
EXPECT_EQ(shared_value, 360); // (100+50+30)*2 = 360
|
||||
|
||||
auto stats = scheduler.stats();
|
||||
EXPECT_EQ(stats.completed, 4);
|
||||
EXPECT_EQ(stats.failed, 0);
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
// Test 9: TaskScheduler — task failure propagation
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
TEST(TaskSchedulerTest, TaskFailure) {
|
||||
auto cluster = std::make_shared<ClusterManager>();
|
||||
TaskScheduler scheduler(cluster);
|
||||
|
||||
scheduler.add_task({.task_id=1, .name="failing", .work=[&]() {
|
||||
throw std::runtime_error("Simulated failure");
|
||||
}});
|
||||
|
||||
scheduler.add_task({.task_id=2, .name="should_not_run", .depends_on={1}, .work=[&]() {
|
||||
// Should not execute because dependency 1 failed
|
||||
FAIL() << "Task 2 should not execute after dependency failure";
|
||||
}});
|
||||
|
||||
bool success = scheduler.execute_all();
|
||||
EXPECT_FALSE(success);
|
||||
|
||||
EXPECT_EQ(scheduler.get_status(1), TaskStatus::FAILED);
|
||||
EXPECT_EQ(scheduler.get_status(2), TaskStatus::PENDING);
|
||||
|
||||
auto stats = scheduler.stats();
|
||||
EXPECT_EQ(stats.failed, 1);
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
// Test 10: Distributed Marching Cubes — single node fallback
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
TEST(DistributedMCTest, SingleNodeFallback) {
|
||||
auto cluster = std::make_shared<ClusterManager>();
|
||||
cluster->register_node("local", 50051, 4, 8192);
|
||||
|
||||
auto sdf = [](double x, double y, double z) -> double {
|
||||
return std::sqrt(x*x + y*y + z*z) - 1.0; // unit sphere
|
||||
};
|
||||
|
||||
AABB3D bounds(Point3D(-2, -2, -2), Point3D(2, 2, 2));
|
||||
DistributedMCConfig config;
|
||||
config.resolution = 32;
|
||||
config.subdivisions = 1;
|
||||
config.iso_level = 0.0;
|
||||
|
||||
auto result = distributed_marching_cubes(sdf, bounds, config, cluster);
|
||||
|
||||
EXPECT_GT(result.mesh.vertices.size(), 0);
|
||||
EXPECT_GT(result.mesh.triangles.size(), 0);
|
||||
EXPECT_EQ(result.nodes_used, 1);
|
||||
EXPECT_GT(result.elapsed_ms, 0);
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
// Test 11: Distributed Ray Tracing — basic trace
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
TEST(DistributedRayTraceTest, BasicTrace) {
|
||||
auto cluster = std::make_shared<ClusterManager>();
|
||||
cluster->register_node("local", 50051, 4, 8192);
|
||||
|
||||
// Build a simple triangle mesh
|
||||
HalfedgeMesh scene_mesh;
|
||||
std::vector<Point3D> verts = {
|
||||
Point3D(-1, -1, 0), Point3D(1, -1, 0), Point3D(0, 1, 0)
|
||||
};
|
||||
std::vector<std::array<int, 3>> faces = {{0, 1, 2}};
|
||||
scene_mesh.build_from_triangles(verts, faces);
|
||||
|
||||
// Create rays
|
||||
DistributedRayTraceScene scene;
|
||||
scene.mesh = scene_mesh;
|
||||
for (int i = 0; i < 100; ++i) {
|
||||
vde::core::Ray3Dd ray(Point3D(0, 0, 10), vde::core::Vector3D(0, 0, -1));
|
||||
scene.rays.push_back(ray);
|
||||
}
|
||||
|
||||
auto result = distributed_ray_tracing(scene, cluster);
|
||||
|
||||
EXPECT_GT(result.hits.size(), 0);
|
||||
EXPECT_EQ(result.total_rays, 100);
|
||||
EXPECT_EQ(result.nodes_used, 1);
|
||||
EXPECT_GT(result.elapsed_ms, 0);
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
// Test 12: gRPC Service — lifecycle
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
TEST(GrpcServiceTest, Lifecycle) {
|
||||
GrpcServiceConfig cfg;
|
||||
cfg.port = 50052;
|
||||
|
||||
VdeService server(cfg);
|
||||
EXPECT_FALSE(server.is_running());
|
||||
|
||||
bool started = server.start();
|
||||
EXPECT_TRUE(started);
|
||||
EXPECT_TRUE(server.is_running());
|
||||
|
||||
auto hc = server.health_check();
|
||||
EXPECT_TRUE(hc.healthy);
|
||||
|
||||
server.shutdown();
|
||||
EXPECT_FALSE(server.is_running());
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
// Test 13: gRPC Client — connect and health check
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
TEST(GrpcClientTest, ConnectHealthCheck) {
|
||||
GrpcClientConfig cfg;
|
||||
cfg.target = "localhost:50051";
|
||||
|
||||
VdeServiceClient client(cfg);
|
||||
EXPECT_FALSE(client.is_connected());
|
||||
|
||||
bool connected = client.connect();
|
||||
EXPECT_TRUE(connected);
|
||||
EXPECT_TRUE(client.is_connected());
|
||||
|
||||
auto hc = client.health_check();
|
||||
EXPECT_TRUE(hc.healthy);
|
||||
|
||||
client.disconnect();
|
||||
EXPECT_FALSE(client.is_connected());
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
// Test 14: GrpcConnectionPool
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
TEST(ConnectionPoolTest, BasicPool) {
|
||||
GrpcConnectionPool::PoolConfig cfg;
|
||||
cfg.max_connections = 4;
|
||||
GrpcConnectionPool pool(cfg);
|
||||
|
||||
auto c1 = pool.get_client("192.168.1.1:50051");
|
||||
EXPECT_NE(c1, nullptr);
|
||||
EXPECT_EQ(pool.active_count(), 1);
|
||||
|
||||
auto c2 = pool.get_client("192.168.1.2:50051");
|
||||
EXPECT_NE(c2, nullptr);
|
||||
EXPECT_EQ(pool.active_count(), 2);
|
||||
|
||||
// Getting same host twice returns same client
|
||||
auto c1_again = pool.get_client("192.168.1.1:50051");
|
||||
EXPECT_EQ(c1_again, c1);
|
||||
EXPECT_EQ(pool.active_count(), 2);
|
||||
|
||||
pool.release_client("192.168.1.1:50051");
|
||||
EXPECT_EQ(pool.active_count(), 1);
|
||||
|
||||
pool.close_all();
|
||||
EXPECT_EQ(pool.active_count(), 0);
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
// Test 15: Generate UUID
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
TEST(UtilityTest, GenerateUuid) {
|
||||
std::string id1 = generate_uuid();
|
||||
std::string id2 = generate_uuid();
|
||||
|
||||
EXPECT_FALSE(id1.empty());
|
||||
EXPECT_FALSE(id2.empty());
|
||||
EXPECT_NE(id1, id2);
|
||||
// UUID format: 8-4-4-4-12 hex digits
|
||||
EXPECT_EQ(id1.length(), 36);
|
||||
EXPECT_EQ(id1[8], '-');
|
||||
EXPECT_EQ(id1[13], '-');
|
||||
EXPECT_EQ(id1[18], '-');
|
||||
EXPECT_EQ(id1[23], '-');
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
add_vde_test(test_gpu_acceleration)
|
||||
@@ -1,254 +0,0 @@
|
||||
/**
|
||||
* @file test_gpu_acceleration.cpp
|
||||
* @brief GPU 加速模块测试(8 项)
|
||||
*
|
||||
* 测试覆盖:
|
||||
* 1. gpu_available — 检测 CUDA 可用性
|
||||
* 2. gpu_marching_cubes — SDF → 网格(球体)
|
||||
* 3. gpu_marching_cubes — 低分辨率
|
||||
* 4. gpu_marching_cubes — 立方体 SDF
|
||||
* 5. gpu_mesh_simplify — 基本简化
|
||||
* 6. gpu_mesh_simplify — 微小 target_ratio
|
||||
* 7. gpu_boolean_intersect — 两个相交球体
|
||||
* 8. gpu_boolean_intersect — 不相交网格
|
||||
*/
|
||||
|
||||
#include "vde/gpu/gpu_acceleration.h"
|
||||
#include "vde/mesh/marching_cubes.h"
|
||||
#include "vde/mesh/halfedge_mesh.h"
|
||||
#include <gtest/gtest.h>
|
||||
#include <cmath>
|
||||
#include <memory>
|
||||
|
||||
using namespace vde::gpu;
|
||||
using vde::core::AABB3D;
|
||||
using vde::core::Point3D;
|
||||
using vde::core::Vector3D;
|
||||
using vde::mesh::HalfedgeMesh;
|
||||
|
||||
// ── 帮助函数 ─────────────────────────────────────────────────────
|
||||
|
||||
/// 创建一个简单球体 SDF 函数
|
||||
static auto make_sphere_sdf(double cx, double cy, double cz, double r) {
|
||||
return [=](double x, double y, double z) -> double {
|
||||
return std::sqrt((x - cx) * (x - cx) + (y - cy) * (y - cy) + (z - cz) * (z - cz)) - r;
|
||||
};
|
||||
}
|
||||
|
||||
/// 创建立方体 SDF 函数
|
||||
static auto make_box_sdf(double hx, double hy, double hz) {
|
||||
return [=](double x, double y, double z) -> double {
|
||||
double dx = std::abs(x) - hx, dy = std::abs(y) - hy, dz = std::abs(z) - hz;
|
||||
return std::sqrt(std::max(dx, 0.0) * std::max(dx, 0.0) +
|
||||
std::max(dy, 0.0) * std::max(dy, 0.0) +
|
||||
std::max(dz, 0.0) * std::max(dz, 0.0)) +
|
||||
std::min(std::max({dx, dy, dz}), 0.0);
|
||||
};
|
||||
}
|
||||
|
||||
/// 快速创建简单三角网格(正四面体)
|
||||
static HalfedgeMesh make_tetrahedron(double ox, double oy, double oz, double s) {
|
||||
HalfedgeMesh mesh;
|
||||
std::vector<Point3D> verts = {
|
||||
{ox, oy + s, oz},
|
||||
{ox - s * 0.866, oy, oz - s * 0.5},
|
||||
{ox + s * 0.866, oy, oz - s * 0.5},
|
||||
{ox, oy, oz + s}
|
||||
};
|
||||
std::vector<std::array<int, 3>> tris = {
|
||||
{0, 1, 2}, {0, 2, 3}, {0, 3, 1}, {1, 3, 2}
|
||||
};
|
||||
for (auto& v : verts) mesh.add_vertex(v);
|
||||
for (auto& t : tris) mesh.add_face({t[0], t[1], t[2]});
|
||||
return mesh;
|
||||
}
|
||||
|
||||
/// 验证网格基本有效性
|
||||
static void expect_valid_mesh(const HalfedgeMesh& mesh) {
|
||||
EXPECT_GT(mesh.num_vertices(), 0u);
|
||||
EXPECT_GT(mesh.num_faces(), 0u);
|
||||
EXPECT_GT(mesh.num_edges(), 0u);
|
||||
// 欧拉公式:V - E + F ≈ 2(对于闭曲面)
|
||||
// 不作严格检查,但确保拓扑合理
|
||||
int euler = static_cast<int>(mesh.num_vertices()) -
|
||||
static_cast<int>(mesh.num_edges()) +
|
||||
static_cast<int>(mesh.num_faces());
|
||||
EXPECT_GE(euler, 0) << "Euler characteristic should be non-negative";
|
||||
}
|
||||
|
||||
// ======================================================================
|
||||
// 测试 1:gpu_available
|
||||
// ======================================================================
|
||||
TEST(GpuAcceleration, GpuAvailable) {
|
||||
// 无论 CUDA 是否可用,函数必须不崩溃并返回布尔值
|
||||
bool avail = gpu_available();
|
||||
EXPECT_TRUE(avail == true || avail == false);
|
||||
#if VDE_USE_CUDA
|
||||
// CUDA 编译时开启 → 运行时至少应无错误
|
||||
// (CUDA 设备可能为 0,但调用不崩溃)
|
||||
SUCCEED() << "CUDA compiled in, runtime detection: " << (avail ? "YES" : "NO");
|
||||
#else
|
||||
EXPECT_FALSE(avail) << "Without CUDA build, gpu_available must return false";
|
||||
#endif
|
||||
}
|
||||
|
||||
// ======================================================================
|
||||
// 测试 2:gpu_marching_cubes — 球体 SDF
|
||||
// ======================================================================
|
||||
TEST(GpuAcceleration, MarchingCubesSphere) {
|
||||
auto sdf = make_sphere_sdf(0, 0, 0, 1.0);
|
||||
AABB3D bounds({-1.5, -1.5, -1.5}, {1.5, 1.5, 1.5});
|
||||
|
||||
HalfedgeMesh mesh = gpu_marching_cubes(sdf, bounds, 32);
|
||||
|
||||
expect_valid_mesh(mesh);
|
||||
|
||||
// 球体应当构成闭曲面
|
||||
EXPECT_GE(mesh.num_faces(), 50u) << "Low-res sphere should have at least 50 faces";
|
||||
|
||||
// 所有顶点应大致在球体表面上
|
||||
for (size_t i = 0; i < mesh.num_vertices(); ++i) {
|
||||
const auto& p = mesh.vertex(i);
|
||||
double dist = std::sqrt(p.x() * p.x() + p.y() * p.y() + p.z() * p.z());
|
||||
EXPECT_NEAR(dist, 1.0, 0.15) << "Vertex " << i << " distance from origin";
|
||||
}
|
||||
}
|
||||
|
||||
// ======================================================================
|
||||
// 测试 3:gpu_marching_cubes — 低分辨率
|
||||
// ======================================================================
|
||||
TEST(GpuAcceleration, MarchingCubesLowRes) {
|
||||
auto sdf = make_sphere_sdf(0, 0, 0, 1.0);
|
||||
AABB3D bounds({-1.2, -1.2, -1.2}, {1.2, 1.2, 1.2});
|
||||
|
||||
HalfedgeMesh mesh = gpu_marching_cubes(sdf, bounds, 8);
|
||||
|
||||
// 极低分辨率仍有输出
|
||||
EXPECT_GT(mesh.num_vertices(), 0u);
|
||||
EXPECT_GT(mesh.num_faces(), 0u);
|
||||
}
|
||||
|
||||
// ======================================================================
|
||||
// 测试 4:gpu_marching_cubes — 立方体 SDF
|
||||
// ======================================================================
|
||||
TEST(GpuAcceleration, MarchingCubesBox) {
|
||||
auto sdf = make_box_sdf(1.0, 0.5, 0.3);
|
||||
AABB3D bounds({-1.5, -1.0, -0.8}, {1.5, 1.0, 0.8});
|
||||
|
||||
HalfedgeMesh mesh = gpu_marching_cubes(sdf, bounds, 24);
|
||||
|
||||
expect_valid_mesh(mesh);
|
||||
|
||||
// 检查包围盒尺寸与预期一致
|
||||
auto bb = mesh.bounds();
|
||||
double max_dim = std::max({bb.extent().x(), bb.extent().y(), bb.extent().z()});
|
||||
EXPECT_GT(max_dim, 0.5);
|
||||
EXPECT_LT(max_dim, 4.0);
|
||||
}
|
||||
|
||||
// ======================================================================
|
||||
// 测试 5:gpu_mesh_simplify — 基本简化
|
||||
// ======================================================================
|
||||
TEST(GpuAcceleration, MeshSimplifyBasic) {
|
||||
auto sdf = make_sphere_sdf(0, 0, 0, 1.0);
|
||||
AABB3D bounds({-1.5, -1.5, -1.5}, {1.5, 1.5, 1.5});
|
||||
|
||||
HalfedgeMesh original = gpu_marching_cubes(sdf, bounds, 32);
|
||||
size_t orig_faces = original.num_faces();
|
||||
EXPECT_GT(orig_faces, 0u);
|
||||
|
||||
// 简化为 50% 面数
|
||||
HalfedgeMesh simplified = gpu_mesh_simplify(original, 0.5f);
|
||||
|
||||
expect_valid_mesh(simplified);
|
||||
|
||||
// 简化后面数应减少
|
||||
EXPECT_LE(simplified.num_faces(), orig_faces)
|
||||
<< "Simplified mesh should have ≤ original face count";
|
||||
}
|
||||
|
||||
// ======================================================================
|
||||
// 测试 6:gpu_mesh_simplify — 极低 target_ratio
|
||||
// ======================================================================
|
||||
TEST(GpuAcceleration, MeshSimplifyTinyRatio) {
|
||||
auto sdf = make_sphere_sdf(0, 0, 0, 1.0);
|
||||
AABB3D bounds({-1.5, -1.5, -1.5}, {1.5, 1.5, 1.5});
|
||||
|
||||
HalfedgeMesh original = gpu_marching_cubes(sdf, bounds, 20);
|
||||
ASSERT_GT(original.num_faces(), 10u);
|
||||
|
||||
// target_ratio = 0.1 (极低比例)
|
||||
HalfedgeMesh simplified = gpu_mesh_simplify(original, 0.1f);
|
||||
|
||||
// 不应崩溃且至少保留一些面
|
||||
EXPECT_GT(simplified.num_vertices(), 3u);
|
||||
EXPECT_GT(simplified.num_faces(), 1u);
|
||||
EXPECT_LT(simplified.num_faces(), original.num_faces());
|
||||
}
|
||||
|
||||
// ======================================================================
|
||||
// 测试 7:gpu_boolean_intersect — 两个相交球体
|
||||
// ======================================================================
|
||||
TEST(GpuAcceleration, BooleanIntersectIntersecting) {
|
||||
// 两个相交球体(中心相距 1.0,半径各 1.2)
|
||||
auto sdf_a = make_sphere_sdf(-0.5, 0, 0, 1.2);
|
||||
auto sdf_b = make_sphere_sdf(0.5, 0, 0, 1.2);
|
||||
AABB3D bounds({-2.0, -1.5, -1.5}, {2.0, 1.5, 1.5});
|
||||
|
||||
HalfedgeMesh mesh_a = gpu_marching_cubes(sdf_a, bounds, 24);
|
||||
HalfedgeMesh mesh_b = gpu_marching_cubes(sdf_b, bounds, 24);
|
||||
|
||||
ASSERT_GT(mesh_a.num_faces(), 0u);
|
||||
ASSERT_GT(mesh_b.num_faces(), 0u);
|
||||
|
||||
HalfedgeMesh result = gpu_boolean_intersect(mesh_a, mesh_b);
|
||||
|
||||
// 交集存在 → 非空输出
|
||||
EXPECT_GT(result.num_vertices(), 0u) << "Intersection of overlapping spheres should not be empty";
|
||||
}
|
||||
|
||||
// ======================================================================
|
||||
// 测试 8:gpu_boolean_intersect — 不相交网格
|
||||
// ======================================================================
|
||||
TEST(GpuAcceleration, BooleanIntersectDisjoint) {
|
||||
// 两个远距离球体(中心相距 10,半径各 1)
|
||||
auto sdf_a = make_sphere_sdf(-5, 0, 0, 1.0);
|
||||
auto sdf_b = make_sphere_sdf(5, 0, 0, 1.0);
|
||||
AABB3D bounds({-6.5, -1.5, -1.5}, {6.5, 1.5, 1.5});
|
||||
|
||||
HalfedgeMesh mesh_a = gpu_marching_cubes(sdf_a, bounds, 20);
|
||||
HalfedgeMesh mesh_b = gpu_marching_cubes(sdf_b, bounds, 20);
|
||||
|
||||
ASSERT_GT(mesh_a.num_faces(), 0u);
|
||||
ASSERT_GT(mesh_b.num_faces(), 0u);
|
||||
|
||||
HalfedgeMesh result = gpu_boolean_intersect(mesh_a, mesh_b);
|
||||
|
||||
// 不相交 → 空输出(0 顶点或 0 面)
|
||||
// 不要求严格为 0,但不应有大量三角形
|
||||
EXPECT_LE(result.num_faces(), mesh_a.num_faces() / 2u)
|
||||
<< "Disjoint meshes should have few or no intersection faces";
|
||||
}
|
||||
|
||||
// ======================================================================
|
||||
// 额外测试:空网格安全
|
||||
// ======================================================================
|
||||
TEST(GpuAcceleration, EmptyMeshSafety) {
|
||||
HalfedgeMesh empty;
|
||||
|
||||
// simplify 空网格不崩溃
|
||||
HalfedgeMesh r1 = gpu_mesh_simplify(empty, 0.5f);
|
||||
EXPECT_EQ(r1.num_vertices(), 0u);
|
||||
EXPECT_EQ(r1.num_faces(), 0u);
|
||||
|
||||
// boolean intersect 空网格不崩溃
|
||||
auto sdf = make_sphere_sdf(0, 0, 0, 1.0);
|
||||
AABB3D bounds({-1.5, -1.5, -1.5}, {1.5, 1.5, 1.5});
|
||||
HalfedgeMesh sphere = gpu_marching_cubes(sdf, bounds, 16);
|
||||
|
||||
HalfedgeMesh r2 = gpu_boolean_intersect(empty, sphere);
|
||||
EXPECT_EQ(r2.num_faces(), 0u);
|
||||
|
||||
HalfedgeMesh r3 = gpu_boolean_intersect(sphere, empty);
|
||||
EXPECT_EQ(r3.num_faces(), 0u);
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
add_executable(test_knowledge test_knowledge.cpp)
|
||||
target_include_directories(test_knowledge PRIVATE ${CMAKE_SOURCE_DIR}/include)
|
||||
target_link_libraries(test_knowledge PRIVATE vde_kbe vde_core vde_foundation GTest::gtest GTest::gtest_main)
|
||||
gtest_discover_tests(test_knowledge)
|
||||
@@ -1,312 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include "vde/kbe/knowledge_engine.h"
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
#include <chrono>
|
||||
|
||||
using namespace vde::kbe;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 1. CheckMate 设计验证
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
TEST(KBETest, CheckMate_DefaultRulesRegistered) {
|
||||
CheckMate cm;
|
||||
auto rules = cm.rules();
|
||||
// 默认注册至少 10 条规则
|
||||
EXPECT_GE(rules.size(), 10u);
|
||||
}
|
||||
|
||||
TEST(KBETest, CheckMate_RunAllDefaultRules) {
|
||||
CheckMate cm;
|
||||
std::vector<Parameter> params = {
|
||||
{"diameter", 10.0},
|
||||
{"wall_thickness", 0.8},
|
||||
{"aspect_ratio", 5.0},
|
||||
{"density", 2700.0},
|
||||
{"safety_factor", 2.0},
|
||||
};
|
||||
auto results = cm.run_all(params);
|
||||
EXPECT_EQ(results.size(), cm.rules().size());
|
||||
|
||||
auto summary = cm.summary(results);
|
||||
// 默认规则应全部通过(参数值在合法范围内)
|
||||
int total_failures = 0;
|
||||
for (const auto& [sev, count] : summary) total_failures += count;
|
||||
EXPECT_EQ(total_failures, 0);
|
||||
}
|
||||
|
||||
TEST(KBETest, CheckMate_PositiveDimensionFails) {
|
||||
CheckMate cm;
|
||||
std::vector<Parameter> params = {
|
||||
{"diameter", -5.0}, // 非法:负值
|
||||
};
|
||||
auto result = cm.run_rule("R_GEO_POSITIVE_DIM", params);
|
||||
EXPECT_FALSE(result.passed);
|
||||
EXPECT_EQ(result.severity, CheckSeverity::ERROR);
|
||||
}
|
||||
|
||||
TEST(KBETest, CheckMate_CustomCheck) {
|
||||
CheckMate cm;
|
||||
cm.add_check("R_GEO_POSITIVE_DIM", [](const std::vector<Parameter>&) {
|
||||
CheckResult r;
|
||||
r.rule_name = "R_GEO_POSITIVE_DIM";
|
||||
r.severity = CheckSeverity::ERROR;
|
||||
r.passed = false;
|
||||
r.message = "Custom: negative dimension detected";
|
||||
return r;
|
||||
});
|
||||
auto result = cm.run_rule("R_GEO_POSITIVE_DIM", {});
|
||||
EXPECT_FALSE(result.passed);
|
||||
EXPECT_EQ(result.message, "Custom: negative dimension detected");
|
||||
}
|
||||
|
||||
TEST(KBETest, CheckMate_CategoryRun) {
|
||||
CheckMate cm;
|
||||
std::vector<Parameter> params = {
|
||||
{"diameter", 10.0}, {"wall_thickness", 2.0}, {"safety_factor", 2.0},
|
||||
};
|
||||
auto geo_results = cm.run_category("geometry", params);
|
||||
// 应有几何规则产生结果
|
||||
EXPECT_GT(geo_results.size(), 0u);
|
||||
|
||||
auto mat_results = cm.run_category("material", params);
|
||||
EXPECT_GT(mat_results.size(), 0u);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 2. RuleEngine IF-THEN 规则引擎
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
TEST(KBETest, RuleEngine_AddAndGetParam) {
|
||||
RuleEngine engine;
|
||||
engine.set_param("length", 10.0);
|
||||
auto val = engine.get_param("length");
|
||||
ASSERT_TRUE(val.has_value());
|
||||
EXPECT_DOUBLE_EQ(std::get<double>(*val), 10.0);
|
||||
}
|
||||
|
||||
TEST(KBETest, RuleEngine_SimpleInfer) {
|
||||
RuleEngine engine;
|
||||
engine.set_param("diameter", 5.0);
|
||||
engine.set_param("thickness", 1.0);
|
||||
|
||||
// IF diameter > 3 THEN SET thickness = 2.0
|
||||
Rule r{"R1", "diameter > 3", "SET thickness = 2.0", 1, true};
|
||||
engine.add_rule(r);
|
||||
|
||||
auto traces = engine.infer();
|
||||
EXPECT_GE(traces.size(), 1u);
|
||||
|
||||
auto new_thickness = engine.get_param("thickness");
|
||||
ASSERT_TRUE(new_thickness.has_value());
|
||||
EXPECT_DOUBLE_EQ(std::get<double>(*new_thickness), 2.0);
|
||||
}
|
||||
|
||||
TEST(KBETest, RuleEngine_PriorityOrder) {
|
||||
RuleEngine engine;
|
||||
engine.set_param("x", 1.0);
|
||||
|
||||
// 高优先级规则先执行: SET x = 20
|
||||
Rule r1{"low", "x >= 0", "SET x = 10.0", 0, true}; // priority 0
|
||||
Rule r2{"high", "x >= 0", "SET x = 20.0", 10, true}; // priority 10
|
||||
engine.add_rule(r1);
|
||||
engine.add_rule(r2);
|
||||
|
||||
// 高优先级规则先触发,但同一轮迭代中低优先级规则也会触发
|
||||
// 最终结果由最后触发的规则决定(按优先级排序,低优先级后执行)
|
||||
auto traces = engine.infer();
|
||||
EXPECT_GE(traces.size(), 2u); // 两条规则都触发
|
||||
|
||||
auto val = engine.get_param("x");
|
||||
ASSERT_TRUE(val.has_value());
|
||||
// 低优先级(priority 0)后执行,最终为 10
|
||||
EXPECT_DOUBLE_EQ(std::get<double>(*val), 10.0);
|
||||
}
|
||||
|
||||
TEST(KBETest, RuleEngine_ANDCondition) {
|
||||
RuleEngine engine;
|
||||
engine.set_param("a", 10.0);
|
||||
engine.set_param("b", 5.0);
|
||||
|
||||
Rule r{"R_and", "a > 5 AND b < 10", "SET a = 100.0", 1, true};
|
||||
engine.add_rule(r);
|
||||
engine.infer();
|
||||
|
||||
auto val = engine.get_param("a");
|
||||
ASSERT_TRUE(val.has_value());
|
||||
EXPECT_DOUBLE_EQ(std::get<double>(*val), 100.0);
|
||||
}
|
||||
|
||||
TEST(KBETest, RuleEngine_NoInfiniteLoop) {
|
||||
RuleEngine engine;
|
||||
engine.set_param("counter", 0.0);
|
||||
|
||||
// 自引用规则 — 应被循环检测阻止
|
||||
Rule r{"loop", "counter < 1000", "SET counter = counter + 1", 1, true};
|
||||
engine.add_rule(r);
|
||||
|
||||
auto traces = engine.infer();
|
||||
// 最多执行 100 次迭代(由引擎内部限制)
|
||||
EXPECT_LE(traces.size(), 100u);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 3. DesignTable 设计表
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
TEST(KBETest, DesignTable_DefineColumnsAndAddRow) {
|
||||
DesignTable table;
|
||||
table.define_columns({"length", "width", "height"}, {"mm", "mm", "mm"});
|
||||
EXPECT_EQ(table.col_count(), 3u);
|
||||
|
||||
DesignRow row;
|
||||
row["length"] = 100.0;
|
||||
row["width"] = 50.0;
|
||||
row["height"] = 25.0;
|
||||
table.add_row(row);
|
||||
EXPECT_EQ(table.row_count(), 1u);
|
||||
|
||||
auto r = table.get_row(0);
|
||||
ASSERT_TRUE(r.has_value());
|
||||
EXPECT_DOUBLE_EQ(std::get<double>(r->at("length")), 100.0);
|
||||
}
|
||||
|
||||
TEST(KBETest, DesignTable_CSVImportExport) {
|
||||
DesignTable table;
|
||||
std::string csv =
|
||||
"length,width,height\n"
|
||||
"100,50,25\n"
|
||||
"200,80,40\n"
|
||||
"150,60,30\n";
|
||||
|
||||
EXPECT_TRUE(table.import_csv(csv));
|
||||
EXPECT_EQ(table.row_count(), 3u);
|
||||
EXPECT_EQ(table.col_count(), 3u);
|
||||
|
||||
std::string exported = table.export_csv();
|
||||
EXPECT_FALSE(exported.empty());
|
||||
EXPECT_NE(exported.find("length"), std::string::npos);
|
||||
EXPECT_NE(exported.find("100"), std::string::npos);
|
||||
}
|
||||
|
||||
TEST(KBETest, DesignTable_MixedTypes) {
|
||||
DesignTable table;
|
||||
std::string csv =
|
||||
"name,diameter,material,is_standard\n"
|
||||
"Bolt_M10,10,Steel,true\n"
|
||||
"Nut_M8,8,Brass,false\n";
|
||||
|
||||
EXPECT_TRUE(table.import_csv(csv));
|
||||
EXPECT_EQ(table.row_count(), 2u);
|
||||
|
||||
auto r0 = table.get_row(0);
|
||||
ASSERT_TRUE(r0.has_value());
|
||||
EXPECT_EQ(std::get<std::string>(r0->at("name")), "Bolt_M10");
|
||||
EXPECT_DOUBLE_EQ(std::get<double>(r0->at("diameter")), 10.0);
|
||||
EXPECT_EQ(std::get<bool>(r0->at("is_standard")), true);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 4. ParametricOptimization 参数优化
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
TEST(KBETest, Optimization_GA_Minimization) {
|
||||
ParametricOptimization opt;
|
||||
|
||||
// 目标:最小化 (x-5)^2
|
||||
opt.set_fitness([](const std::vector<Parameter>& params) {
|
||||
for (const auto& p : params) {
|
||||
if (p.name == "x" && std::holds_alternative<double>(p.value)) {
|
||||
double x = std::get<double>(p.value);
|
||||
return -((x - 5.0) * (x - 5.0)); // 负值因为 GA 最大化适应度
|
||||
}
|
||||
}
|
||||
return 0.0;
|
||||
});
|
||||
|
||||
std::vector<Parameter> initial = {{"x", 10.0, 0.0, 100.0, "", "", false}};
|
||||
opt.set_bounds("x", 0.0, 100.0);
|
||||
|
||||
GAConfig ga_config;
|
||||
ga_config.population_size = 50;
|
||||
ga_config.generations = 30;
|
||||
|
||||
auto result = opt.optimize_ga(initial, ga_config);
|
||||
EXPECT_TRUE(result.converged);
|
||||
|
||||
// 最优解应接近 x=5.0
|
||||
for (const auto& p : result.optimal_params) {
|
||||
if (p.name == "x") {
|
||||
double val = std::get<double>(p.value);
|
||||
EXPECT_NEAR(val, 5.0, 5.0); // GA 有随机性,容差大些
|
||||
}
|
||||
}
|
||||
|
||||
// 适应度历史应有记录
|
||||
EXPECT_GT(opt.fitness_history().size(), 0u);
|
||||
}
|
||||
|
||||
TEST(KBETest, Optimization_Gradient_Descent) {
|
||||
ParametricOptimization opt;
|
||||
|
||||
// 目标:最小化 (x-3)^2,梯度下降直接最小化 fitness
|
||||
opt.set_fitness([](const std::vector<Parameter>& params) {
|
||||
for (const auto& p : params) {
|
||||
if (p.name == "x" && std::holds_alternative<double>(p.value)) {
|
||||
double x = std::get<double>(p.value);
|
||||
return (x - 3.0) * (x - 3.0); // 正值,梯度下降最小化
|
||||
}
|
||||
}
|
||||
return 0.0;
|
||||
});
|
||||
|
||||
std::vector<Parameter> initial = {{"x", 8.0, 0.0, 100.0, "", "", false}};
|
||||
opt.set_bounds("x", -10.0, 100.0);
|
||||
|
||||
GradientConfig grad_config;
|
||||
grad_config.learning_rate = 0.05;
|
||||
grad_config.max_iterations = 500;
|
||||
grad_config.tolerance = 1e-6;
|
||||
|
||||
auto result = opt.optimize_gradient(initial, grad_config);
|
||||
|
||||
for (const auto& p : result.optimal_params) {
|
||||
if (p.name == "x") {
|
||||
double val = std::get<double>(p.value);
|
||||
EXPECT_NEAR(val, 3.0, 3.0); // 梯度下降有收敛性,容差较大
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(KBETest, Optimization_Constraints) {
|
||||
ParametricOptimization opt;
|
||||
|
||||
opt.set_fitness([](const std::vector<Parameter>& params) {
|
||||
for (const auto& p : params) {
|
||||
if (p.name == "x" && std::holds_alternative<double>(p.value)) {
|
||||
double x = std::get<double>(p.value);
|
||||
return -x; // 最小化 x(GA 最大化适应度 = 最小化 x)
|
||||
}
|
||||
}
|
||||
return 0.0;
|
||||
});
|
||||
|
||||
std::vector<Parameter> initial = {{"x", 5.0, 0.0, 100.0, "", "", false}};
|
||||
// 用 bounds 约束 x 上限为 10
|
||||
opt.set_bounds("x", 0.0, 10.0);
|
||||
|
||||
GAConfig ga_config;
|
||||
ga_config.population_size = 30;
|
||||
ga_config.generations = 30;
|
||||
|
||||
auto result = opt.optimize_ga(initial, ga_config);
|
||||
|
||||
for (const auto& p : result.optimal_params) {
|
||||
if (p.name == "x") {
|
||||
double val = std::get<double>(p.value);
|
||||
EXPECT_LE(val, 10.0); // 必须满足 bound 约束
|
||||
EXPECT_GT(val, -1.0); // 不能为负
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user