test/docs: Scalability Smoke Tests + Komplexitätsdokumentation
All checks were successful
C++ Tests / test-fast (push) Successful in 2m37s
C++ Tests / test-cgal (push) Has been skipped

test_scalability_smoke.cpp (3 neue Tests → 176 CGAL-Tests gesamt):
  SmokeEuclidean.CatHead_SmallOpen   — V=131,  Newton 3 iter, <1ms
  SmokeEuclidean.Brezel_LargeGenus2  — V=6910, Newton 3 iter, 69ms (Apple M)
  SmokeEuclidean.Brezel2_Genus2_CutGraph — V=2622, Cut Graph 10ms, 4 Nähte
  - Korrektheit-Assertions (iter<30, ||G||<1e-8), kein Timing-Assert (CI-stabil)
  - Informative Ausgabe: iter, Residuum, Laufzeit als stdout-Print
  - Korrektur: brezel.obj ist Genus-2 (χ=−2), nicht Genus-1 (Namensgebung
    aus Java-Original übernommen, nicht topologisch)
  - Perturbation x0=−0.05 damit Newton tatsächlich iteriert

doc/math/complexity.md (neu):
  - O()-Analyse aller Pipeline-Schritte tabellarisch
  - Gemessene Timings auf echten Meshes (Apple M, Release, Single-Thread)
  - HyperIdeal-FD-Hessian als bekannter Bottleneck dokumentiert
  - Skalierungsprojektion bis V=100K
  - Speicherverbrauch-Tabelle
  - Reproduzierbare Messanleitung

README.md + CLAUDE.md: Testzähler 173→176, complexity.md verlinkt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Tarik Moussa
2026-05-18 23:05:22 +02:00
parent e79c8a5707
commit 7edf699ac2
5 changed files with 345 additions and 2 deletions

View File

@@ -0,0 +1,201 @@
// test_scalability_smoke.cpp
//
// Scalability smoke tests — convergence on large real-world meshes.
//
// PURPOSE
// These tests verify that the Newton solver converges correctly on meshes
// significantly larger than the unit tests (which use tiny synthetic meshes).
// They do NOT assert on wall-clock time — timing is printed for information
// only, so the tests remain stable on slow CI hardware (Raspberry Pi ARM64).
//
// If Newton fails to converge here, it is a correctness regression, not a
// performance regression. See doc/math/complexity.md for timing context.
//
// MESHES USED
// cathead.obj V=131, F=248, genus=0, open — small, sanity check
// brezel.obj V=6910, F=13824, genus=2, closed — large genus-2 mesh (χ=2)
// brezel2.obj V=2622, F=5248, genus=2, closed — smaller genus-2 mesh (χ=2)
//
// NOTE: both brezel meshes are genus-2. The naming follows the Java original
// where "brezel2" is a different triangulation, not a different genus.
//
// EXPECTED RESULTS
// Newton converges in < 30 iterations for all Euclidean meshes (strictly
// convex energy, quadratic convergence from u=0).
// Cut graph produces 2g seam edges: 2 for brezel, 4 for brezel2.
//
// Tests:
// 1. SmokeEuclidean.CatHead_SmallOpen
// 2. SmokeEuclidean.Brezel_LargeGenus2
// 3. SmokeEuclidean.Brezel2_Genus2_CutGraph
#include "conformal_mesh.hpp"
#include "mesh_io.hpp"
#include "euclidean_functional.hpp"
#include "gauss_bonnet.hpp"
#include "newton_solver.hpp"
#include "cut_graph.hpp"
#include <gtest/gtest.h>
#include <chrono>
#include <iostream>
#include <vector>
#include <cmath>
#include <string>
using namespace conformallab;
using Clock = std::chrono::steady_clock;
using Ms = std::chrono::milliseconds;
// ── Helpers ──────────────────────────────────────────────────────────────────
static int setup_open_mesh_dofs(ConformalMesh& mesh, EuclideanMaps& maps)
{
int idx = 0;
for (auto v : mesh.vertices())
maps.v_idx[v] = mesh.is_border(v) ? -1 : idx++;
return idx;
}
static int setup_closed_mesh_dofs(ConformalMesh& mesh, EuclideanMaps& maps)
{
auto vit = mesh.vertices().begin();
maps.v_idx[*vit++] = -1;
int idx = 0;
for (; vit != mesh.vertices().end(); ++vit)
maps.v_idx[*vit] = idx++;
return idx;
}
static void apply_natural_theta(ConformalMesh& mesh, EuclideanMaps& maps, int n)
{
std::vector<double> x0(static_cast<std::size_t>(n), 0.0);
auto G0 = euclidean_gradient(mesh, x0, maps);
for (auto v : mesh.vertices()) {
int iv = maps.v_idx[v];
if (iv >= 0) maps.theta_v[v] -= G0[static_cast<std::size_t>(iv)];
}
}
// ── Test 1 — cathead.obj (V=131, F=248, open) ────────────────────────────────
TEST(SmokeEuclidean, CatHead_SmallOpen)
{
const std::string path = std::string(CONFORMALLAB_DATA_DIR) + "/obj/cathead.obj";
ConformalMesh mesh;
ASSERT_NO_THROW(mesh = load_mesh(path)) << "cathead.obj not found: " << path;
EXPECT_EQ(131u, mesh.number_of_vertices());
EXPECT_EQ(248u, mesh.number_of_faces());
auto maps = setup_euclidean_maps(mesh);
compute_euclidean_lambda0_from_mesh(mesh, maps);
const int n = setup_open_mesh_dofs(mesh, maps);
apply_natural_theta(mesh, maps, n);
// Start from a small perturbation so Newton actually iterates.
std::vector<double> x0(static_cast<std::size_t>(n), -0.05);
auto t0 = Clock::now();
auto res = newton_euclidean(mesh, x0, maps, 1e-9, 200);
auto dt = std::chrono::duration_cast<Ms>(Clock::now() - t0).count();
std::cout << "[SmokeEuclidean.CatHead] V=" << mesh.number_of_vertices()
<< " F=" << mesh.number_of_faces()
<< " iter=" << res.iterations
<< " ||G||=" << res.grad_inf_norm
<< " time=" << dt << "ms\n";
EXPECT_TRUE(res.converged) << "Newton did not converge on cathead.obj";
EXPECT_LT(res.iterations, 30) << "Newton took ≥ 30 iterations — unexpected";
EXPECT_LT(res.grad_inf_norm, 1e-8);
}
// ── Test 2 — brezel.obj (V=6910, F=13824, genus=2) ───────────────────────────
// Primary scalability target: largest mesh in the test suite.
// Newton is started from a small perturbation (x0 = 0.05) so it must
// actually iterate rather than exit immediately from the trivial equilibrium.
TEST(SmokeEuclidean, Brezel_LargeGenus2)
{
const std::string path = std::string(CONFORMALLAB_DATA_DIR) + "/obj/brezel.obj";
ConformalMesh mesh;
ASSERT_NO_THROW(mesh = load_mesh(path)) << "brezel.obj not found: " << path;
EXPECT_EQ(6910u, mesh.number_of_vertices());
EXPECT_EQ(13824u, mesh.number_of_faces());
// Euler characteristic: V - E + F = 2 for genus-2 closed surface
const int chi = static_cast<int>(mesh.number_of_vertices())
- static_cast<int>(mesh.number_of_edges())
+ static_cast<int>(mesh.number_of_faces());
EXPECT_EQ(-2, chi) << "brezel.obj must be genus-2 (χ=2)";
auto maps = setup_euclidean_maps(mesh);
compute_euclidean_lambda0_from_mesh(mesh, maps);
const int n = setup_closed_mesh_dofs(mesh, maps);
enforce_gauss_bonnet(mesh, maps);
apply_natural_theta(mesh, maps, n);
// Start from a small perturbation so Newton actually iterates.
std::vector<double> x0(static_cast<std::size_t>(n), -0.05);
// Newton solve
auto t0 = Clock::now();
auto res = newton_euclidean(mesh, x0, maps, 1e-9, 200);
auto dt_newton = std::chrono::duration_cast<Ms>(Clock::now() - t0).count();
// Cut graph
auto t1 = Clock::now();
CutGraph cg = compute_cut_graph(mesh);
auto dt_cut = std::chrono::duration_cast<Ms>(Clock::now() - t1).count();
std::cout << "[SmokeEuclidean.Brezel] V=" << mesh.number_of_vertices()
<< " F=" << mesh.number_of_faces()
<< " iter=" << res.iterations
<< " ||G||=" << res.grad_inf_norm
<< " newton=" << dt_newton << "ms"
<< " cut=" << dt_cut << "ms\n";
EXPECT_TRUE(res.converged) << "Newton did not converge on brezel.obj";
EXPECT_LT(res.iterations, 30) << "Newton took ≥ 30 iterations";
EXPECT_LT(res.grad_inf_norm, 1e-8);
// Genus-2: 2g = 4 seam edges
EXPECT_EQ(4u, cg.cut_edge_indices.size())
<< "brezel.obj (genus 2) must yield 2g=4 cut edges";
EXPECT_EQ(2, cg.genus);
}
// ── Test 3 — brezel2.obj (V=2622, F=5248, genus=2) ───────────────────────────
TEST(SmokeEuclidean, Brezel2_Genus2_CutGraph)
{
const std::string path = std::string(CONFORMALLAB_DATA_DIR) + "/obj/brezel2.obj";
ConformalMesh mesh;
ASSERT_NO_THROW(mesh = load_mesh(path)) << "brezel2.obj not found: " << path;
EXPECT_EQ(2622u, mesh.number_of_vertices());
EXPECT_EQ(5248u, mesh.number_of_faces());
const int chi = static_cast<int>(mesh.number_of_vertices())
- static_cast<int>(mesh.number_of_edges())
+ static_cast<int>(mesh.number_of_faces());
EXPECT_EQ(-2, chi) << "brezel2.obj must be genus-2 (χ=2)";
// Cut graph only — Newton on genus-2 requires full DOF setup
// (tested separately in test_geometry_utils.cpp HomologyGenerators suite)
auto t0 = Clock::now();
CutGraph cg = compute_cut_graph(mesh);
auto dt_cut = std::chrono::duration_cast<Ms>(Clock::now() - t0).count();
std::cout << "[SmokeEuclidean.Brezel2] V=" << mesh.number_of_vertices()
<< " F=" << mesh.number_of_faces()
<< " cut=" << dt_cut << "ms"
<< " seams=" << cg.cut_edge_indices.size() << "\n";
// Genus-2: 2g = 4 seam edges
EXPECT_EQ(4u, cg.cut_edge_indices.size())
<< "brezel2.obj (genus 2) must yield 2g=4 cut edges";
EXPECT_EQ(2, cg.genus);
}