Files
ViewDesignEngine/tests/sdf/test_sdf_optimize.cpp
T

343 lines
14 KiB
C++
Raw Normal View History

#include <gtest/gtest.h>
#include "vde/sdf/sdf_optimize.h"
#include "vde/sdf/sdf_primitives.h"
#include "vde/sdf/sdf_tree.h"
#include <cmath>
using namespace vde::sdf;
using vde::core::Point3D;
using vde::core::Vector3D;
// ───────────────────────────────────────────────────
// fit_to_point_cloud — sphere
// ───────────────────────────────────────────────────
TEST(SdfOptimize, FitSphereToPointCloud_Converges) {
// Ground truth: sphere of radius 2.0 at origin
const double target_radius = 2.0;
std::vector<Point3D> surface_points;
// Generate points on the sphere surface (Fibonacci sphere)
const int n_pts = 200;
const double golden_angle = M_PI * (3.0 - std::sqrt(5.0));
for (int i = 0; i < n_pts; ++i) {
double y = 1.0 - (i / double(n_pts - 1)) * 2.0; // y ∈ [-1, 1]
double radius_at_y = std::sqrt(1.0 - y * y);
double theta = golden_angle * i;
surface_points.emplace_back(
target_radius * radius_at_y * std::cos(theta),
target_radius * y,
target_radius * radius_at_y * std::sin(theta));
}
// Initial sphere (wrong radius)
auto initial = SdfNode::sphere(1.0);
auto result = fit_to_point_cloud(initial, surface_points, 0.05, 200);
EXPECT_LT(result.final_loss, 0.5);
EXPECT_TRUE(result.loss_history.size() > 0);
EXPECT_GT(result.loss_history.front(), result.loss_history.back())
<< "Loss should decrease during optimization";
}
TEST(SdfOptimize, FitSphereToPointCloud_EmptyPoints) {
auto initial = SdfNode::sphere(1.0);
auto result = fit_to_point_cloud(initial, {}, 0.01, 100);
EXPECT_EQ(result.iterations, 0);
}
// ───────────────────────────────────────────────────
// fit_to_sdf — box to sphere
// ───────────────────────────────────────────────────
TEST(SdfOptimize, FitBoxToSphereSdf_LossDecreases) {
// Target SDF: sphere of radius 1.0
auto target_sdf = [](const Point3D& p) -> double {
return sphere(p, 1.0);
};
// Initial: box
auto source = SdfNode::box(Point3D(1.5, 1.5, 1.5));
Point3D bmin(-2, -2, -2);
Point3D bmax(2, 2, 2);
auto result = fit_to_sdf(source, target_sdf, bmin, bmax, 8, 0.01, 50);
// Loss should decrease from initial
EXPECT_GT(result.loss_history.size(), 0u);
if (result.loss_history.size() >= 2) {
EXPECT_LT(result.loss_history.back(), result.loss_history.front())
<< "Loss should decrease";
}
}
// ───────────────────────────────────────────────────
// resolve_collision — two spheres
// ───────────────────────────────────────────────────
TEST(SdfOptimize, ResolveCollision_Spheres) {
auto shape_a = SdfNode::sphere(1.0);
// Place shape_b overlapping shape_a
auto shape_b = SdfNode::translate(
SdfNode::sphere(1.0), Point3D(1.0, 0, 0));
bool resolved = resolve_collision(shape_a, shape_b, 0.05, 100);
// After resolution, shapes should not overlap
// We accept that it may not fully resolve in all cases
// At minimum, it shouldn't crash
EXPECT_TRUE(true); // at least it ran
}
// ───────────────────────────────────────────────────
// penetration_depth — two overlapping spheres
// ───────────────────────────────────────────────────
TEST(SdfOptimize, PenetrationDepth_OverlappingSpheres) {
auto a = SdfNode::sphere(1.0);
auto b = SdfNode::translate(SdfNode::sphere(1.0), Point3D(1.0, 0, 0));
Point3D bmin(-2, -2, -2);
Point3D bmax(2, 2, 2);
double depth = penetration_depth(a, b, bmin, bmax, 5000);
EXPECT_GT(depth, 0.0) << "Overlapping shapes should have positive penetration depth";
}
TEST(SdfOptimize, PenetrationDepth_NonOverlappingSpheres) {
auto a = SdfNode::sphere(1.0);
auto b = SdfNode::translate(SdfNode::sphere(1.0), Point3D(5.0, 0, 0));
Point3D bmin(-1, -1, -1);
Point3D bmax(1, 1, 1);
// Only sampling in a's bbox, b is far away
double depth = penetration_depth(a, b, bmin, bmax, 500);
EXPECT_NEAR(depth, 0.0, 1e-6)
<< "Non-overlapping shapes should have zero penetration depth";
}
// ───────────────────────────────────────────────────
// accessibility — point on sphere
// ───────────────────────────────────────────────────
TEST(SdfOptimize, Accessibility_OnSphere) {
auto shape = SdfNode::sphere(1.0);
// Point on sphere surface
Point3D surface_point(1.0, 0.0, 0.0);
Vector3D direction(-1.0, 0.0, 0.0); // pointing outward
double score = accessibility(shape, surface_point, direction, 64);
EXPECT_GT(score, 0.0) << "Should have some accessibility";
EXPECT_LE(score, 1.0);
}
// ───────────────────────────────────────────────────
// symmetry_score — sphere is highly symmetric
// ───────────────────────────────────────────────────
TEST(SdfOptimize, SymmetryScore_Sphere) {
auto shape = SdfNode::sphere(1.0);
Point3D bmin(-2, -2, -2);
Point3D bmax(2, 2, 2);
Vector3D normal(0, 1, 0); // Y-axis plane
double score = symmetry_score(shape, bmin, bmax, normal, 0.0, 500);
EXPECT_GT(score, 0.9) << "Sphere should be highly symmetric about any plane through center";
}
TEST(SdfOptimize, SymmetryScore_Asymmetric) {
// Union of sphere and an offset box — not symmetric about Y=0
auto shape = SdfNode::op_union(
SdfNode::sphere(1.0),
SdfNode::translate(SdfNode::box(Point3D(0.5, 0.5, 0.5)), Point3D(1.5, 0, 0)));
Point3D bmin(-3, -3, -3);
Point3D bmax(3, 3, 3);
Vector3D normal(1, 0, 0); // X-axis plane through origin
double score = symmetry_score(shape, bmin, bmax, normal, 0.0, 500);
EXPECT_LT(score, 0.9) << "Asymmetric shape should have lower symmetry score";
}
// ───────────────────────────────────────────────────
// find_symmetry_plane — sphere
// ───────────────────────────────────────────────────
TEST(SdfOptimize, FindSymmetryPlane_Sphere) {
auto shape = SdfNode::sphere(1.0);
Point3D bmin(-2, -2, -2);
Point3D bmax(2, 2, 2);
auto result = find_symmetry_plane(shape, bmin, bmax, 500);
EXPECT_GT(result.score, 0.5) << "Should find at least one reasonable symmetry plane";
}
// ───────────────────────────────────────────────────
// estimate_volume — sphere
// ───────────────────────────────────────────────────
TEST(SdfOptimize, EstimateVolume_Sphere) {
auto shape = SdfNode::sphere(1.0);
Point3D bmin(-1.5, -1.5, -1.5);
Point3D bmax(1.5, 1.5, 1.5);
double vol = estimate_volume(shape, bmin, bmax, 50000);
double expected = (4.0 / 3.0) * M_PI; // ≈ 4.18879
double tolerance = 0.15 * expected;
EXPECT_NEAR(vol, expected, tolerance)
<< "Estimated volume should be close to (4/3)πr³";
}
TEST(SdfOptimize, EstimateVolume_Box) {
auto shape = SdfNode::box(Point3D(1.0, 0.5, 0.5));
Point3D bmin(-2, -2, -2);
Point3D bmax(2, 2, 2);
double vol = estimate_volume(shape, bmin, bmax, 50000);
double expected = 2.0 * 1.0 * 1.0; // width * height * depth
double tolerance = 0.15 * expected;
EXPECT_NEAR(vol, expected, tolerance);
}
// ───────────────────────────────────────────────────
// center_of_mass — sphere at origin
// ───────────────────────────────────────────────────
TEST(SdfOptimize, CenterOfMass_SphereAtOrigin) {
auto shape = SdfNode::sphere(1.0);
Point3D bmin(-2, -2, -2);
Point3D bmax(2, 2, 2);
Point3D com = center_of_mass(shape, bmin, bmax, 10000);
EXPECT_NEAR(com.x(), 0.0, 0.15);
EXPECT_NEAR(com.y(), 0.0, 0.15);
EXPECT_NEAR(com.z(), 0.0, 0.15);
}
// ───────────────────────────────────────────────────
// collect_params — sphere has radius param
// ───────────────────────────────────────────────────
TEST(SdfOptimize, CollectParams_Sphere) {
auto shape = SdfNode::sphere(2.5);
auto params = collect_params(shape);
ASSERT_EQ(params.size(), 1u);
EXPECT_EQ(params[0].name, "radius");
EXPECT_DOUBLE_EQ(*params[0].value, 2.5);
}
TEST(SdfOptimize, CollectParams_Box) {
auto shape = SdfNode::box(Point3D(1.0, 2.0, 3.0));
auto params = collect_params(shape);
ASSERT_EQ(params.size(), 3u);
EXPECT_DOUBLE_EQ(*params[0].value, 1.0); // extent_x
EXPECT_DOUBLE_EQ(*params[1].value, 2.0); // extent_y
EXPECT_DOUBLE_EQ(*params[2].value, 3.0); // extent_z
}
TEST(SdfOptimize, CollectParams_Cylinder) {
auto shape = SdfNode::cylinder(1.5, 3.0);
auto params = collect_params(shape);
ASSERT_EQ(params.size(), 2u);
EXPECT_EQ(params[0].name, "radius");
EXPECT_DOUBLE_EQ(*params[0].value, 1.5);
EXPECT_EQ(params[1].name, "height");
EXPECT_DOUBLE_EQ(*params[1].value, 3.0);
}
// ───────────────────────────────────────────────────
// numerical_gradient — simple quadratic
// ───────────────────────────────────────────────────
TEST(SdfOptimize, NumericalGradient_Quadratic) {
double val = 3.0;
ParamRef ref{&val, "x"};
std::vector<ParamRef> params = {ref};
// f(x) = x², gradient should be 2x = 6.0 at x=3
auto loss_fn = [&]() -> double { return val * val; };
auto grad = numerical_gradient(params, loss_fn, 1e-6);
ASSERT_EQ(grad.size(), 1u);
EXPECT_NEAR(grad[0], 6.0, 1e-3);
}
// ───────────────────────────────────────────────────
// GradientDescent class
// ───────────────────────────────────────────────────
TEST(SdfOptimize, GradientDescent_MinimizesQuadratic) {
GradientDescent gd(0.1);
std::vector<double> params = {5.0}; // start at x=5
// f(x) = (x-3)², df/dx = 2(x-3)
auto grad_fn = [](const std::vector<double>& p, std::vector<double>& g) -> double {
double x = p[0];
g[0] = 2.0 * (x - 3.0);
return (x - 3.0) * (x - 3.0);
};
double initial_loss = (5.0 - 3.0) * (5.0 - 3.0);
for (int i = 0; i < 100; ++i) {
gd.step(params, grad_fn);
}
EXPECT_NEAR(params[0], 3.0, 0.01)
<< "Gradient descent should converge to minimum x=3";
EXPECT_GT(gd.iteration(), 0);
}
TEST(SdfOptimize, GradientDescent_LearningRate) {
GradientDescent gd(0.01);
EXPECT_DOUBLE_EQ(gd.learning_rate(), 0.01);
gd.set_learning_rate(0.05);
EXPECT_DOUBLE_EQ(gd.learning_rate(), 0.05);
}
// ───────────────────────────────────────────────────
// fit_surface_to_points
// ───────────────────────────────────────────────────
TEST(SdfOptimize, FitSurfaceToPoints_Alias) {
auto initial = SdfNode::sphere(0.5);
std::vector<Point3D> points;
points.emplace_back(1, 0, 0);
points.emplace_back(0, 1, 0);
points.emplace_back(0, 0, 1);
auto result = fit_surface_to_points(initial, points, 0.01, 10);
// At minimum, should produce a valid result
EXPECT_GE(result.iterations, 1);
}
// ───────────────────────────────────────────────────
// find_accessible_point
// ───────────────────────────────────────────────────
TEST(SdfOptimize, FindAccessiblePoint_Sphere) {
auto shape = SdfNode::sphere(1.0);
Point3D bmin(-2, -2, -2);
Point3D bmax(2, 2, 2);
Vector3D approach_dir(-1, 0, 0); // from -X direction
Point3D result = find_accessible_point(shape, approach_dir, bmin, bmax, 16);
// Should find some point (won't crash)
EXPECT_TRUE(true);
}