Measures wall-clock runtime for flattening configurations on one or more meshes and prints results as a Markdown table. LSCM CG and HLSCM variants are timed at each specified thread count.
Options: –threads N [N ...] Thread counts to benchmark (default: 1, 2, 4, …, hardware concurrency). Accepts one or more values, e.g. –threads 1 4 12. –output-dir DIR Write one flattened OBJ per algorithm per mesh into DIR. –builtin [MAX] Benchmark the built-in wavy-surface sequence (50k, 100k, 200k, 400k, 600k, 800k, 1M faces) up to MAX faces (default: 1000000). Mesh files and –builtin may be combined. –builtin-sphere [MAX] Benchmark the built-in sphere-cap (hemisphere) sequence using the same face-count progression as –builtin. Sphere caps have constant positive Gaussian curvature and exercise ABF+LSCM quality in addition to performance.
#include <chrono>
#include <cmath>
#include <filesystem>
#include <iomanip>
#include <iostream>
#include <sstream>
#include <thread>
#include <vector>
#include <Eigen/Core>
#include <Eigen/IterativeLinearSolvers>
#include <Eigen/SparseLU>
#include "OpenABF/OpenABF.hpp"
namespace fs = std::filesystem;
using Clock = std::chrono::steady_clock;
using Seconds = std::chrono::duration<double>;
using FloatT = double;
template <class Fn>
auto timeIt(Fn&& fn) -> double
{
auto t0 = Clock::now();
fn();
return Seconds(Clock::now() - t0).count();
}
auto buildWavySurface(std::size_t targetFaces) -> typename ABFMesh::Pointer
{
using T = FloatT;
auto n = static_cast<std::size_t>(std::floor(std::sqrt(targetFaces / 2.0))) + 1;
std::size_t rows = n, cols = n;
auto mesh = ABFMesh::New();
for (std::size_t r = 0; r < rows; ++r) {
for (std::size_t c = 0; c < cols; ++c) {
T x = T(c);
T y = T(r);
mesh->insert_vertex(x, y, z);
}
}
std::vector<std::vector<std::size_t>> faces;
faces.reserve(2 * (rows - 1) * (cols - 1));
for (std::size_t r = 0; r < rows - 1; ++r) {
for (std::size_t c = 0; c < cols - 1; ++c) {
auto v0 = r * cols + c;
auto v1 = r * cols + c + 1;
auto v2 = (r + 1) * cols + c;
auto v3 = (r + 1) * cols + c + 1;
faces.push_back({v0, v2, v1});
faces.push_back({v1, v2, v3});
}
}
mesh->insert_faces(faces);
return mesh;
}
auto buildSphereCap(std::size_t targetFaces, FloatT thetaMax =
OpenABF::PI<FloatT> / FloatT(2)) ->
typename ABFMesh::Pointer
{
using T = FloatT;
auto rings = static_cast<std::size_t>(
std::floor(std::sqrt(static_cast<double>(targetFaces) / (2.0 * aspect)))) +
1;
auto sectors = static_cast<std::size_t>(std::floor(aspect * static_cast<double>(rings)));
if (sectors < 3) {
sectors = 3;
}
auto mesh = ABFMesh::New();
mesh->insert_vertex(T(0), T(0), T(1));
for (std::size_t r = 1; r <= rings; ++r) {
T phi = thetaMax * T(r) / T(rings);
T sinPhi = std::sin(phi);
T cosPhi = std::cos(phi);
for (std::size_t s = 0; s < sectors; ++s) {
mesh->insert_vertex(sinPhi * std::cos(theta), sinPhi * std::sin(theta), cosPhi);
}
}
std::vector<std::vector<std::size_t>> faces;
faces.reserve(sectors + 2 * (rings - 1) * sectors);
for (std::size_t s = 0; s < sectors; ++s) {
std::size_t next = (s + 1) % sectors;
faces.push_back({0, 1 + s, 1 + next});
}
for (std::size_t r = 0; r < rings - 1; ++r) {
std::size_t base0 = 1 + r * sectors;
std::size_t base1 = 1 + (r + 1) * sectors;
for (std::size_t s = 0; s < sectors; ++s) {
std::size_t next = (s + 1) % sectors;
faces.push_back({base0 + s, base1 + s, base0 + next});
faces.push_back({base0 + next, base1 + s, base1 + next});
}
}
mesh->insert_faces(faces);
return mesh;
}
std::string label;
typename ABFMesh::Pointer mesh;
};
static constexpr double kFailed = -1.0;
auto main(const int argc, char* argv[]) -> int
{
fs::path outputDir;
bool builtinEnabled = false;
std::size_t builtinMax = 1'000'000;
bool builtinSphereEnabled = false;
std::size_t builtinSphereMax = 1'000'000;
std::vector<fs::path> meshFiles;
std::vector<int> threadCounts;
for (int a = 1; a < argc; ++a) {
std::string arg = argv[a];
if (arg == "--threads") {
while (a + 1 < argc) {
std::string next = argv[a + 1];
if (next.rfind("--", 0) == 0) {
break;
}
try {
threadCounts.push_back(std::stoi(next));
++a;
} catch (...) {
break;
}
}
} else if (arg == "--output-dir" && a + 1 < argc) {
outputDir = argv[++a];
} else if (arg == "--builtin") {
builtinEnabled = true;
if (a + 1 < argc) {
try {
builtinMax = std::stoull(argv[a + 1]);
++a;
} catch (...) {
}
}
} else if (arg == "--builtin-sphere") {
builtinSphereEnabled = true;
if (a + 1 < argc) {
try {
builtinSphereMax = std::stoull(argv[a + 1]);
++a;
} catch (...) {
}
}
} else {
meshFiles.emplace_back(argv[a]);
}
}
if (!builtinEnabled && !builtinSphereEnabled && meshFiles.empty()) {
std::cerr << "Usage: " << fs::path(argv[0]).filename().string()
<< " [--threads N [N ...]] [--output-dir DIR]"
" [--builtin [MAX_FACES]] [--builtin-sphere [MAX_FACES]]"
" [mesh1.(obj|ply) ...]\n";
return EXIT_FAILURE;
}
if (!outputDir.empty()) {
fs::create_directories(outputDir);
}
if (threadCounts.empty()) {
int maxThreads = static_cast<int>(std::thread::hardware_concurrency());
if (maxThreads < 1) {
maxThreads = 1;
}
for (int t = 1; t <= maxThreads; t *= 2) {
threadCounts.push_back(t);
}
if (threadCounts.back() != maxThreads) {
threadCounts.push_back(maxThreads);
}
}
std::vector<int> actualThreads;
for (int t : threadCounts) {
Eigen::setNbThreads(t);
actualThreads.push_back(Eigen::nbThreads());
}
Eigen::setNbThreads(1);
using Mtx = Eigen::SparseMatrix<FloatT>;
using LU = Eigen::SparseLU<Mtx>;
using CG_Diag = Eigen::ConjugateGradient<Mtx, Eigen::Lower | Eigen::Upper>;
using CG_IC = Eigen::ConjugateGradient<Mtx, Eigen::Lower | Eigen::Upper,
Eigen::IncompleteCholesky<FloatT>>;
std::vector<BenchInput> inputs;
if (builtinEnabled) {
for (std::size_t n :
{50'000UL, 100'000UL, 200'000UL, 400'000UL, 600'000UL, 800'000UL, 1'000'000UL}) {
if (n > builtinMax) {
break;
}
auto mesh = buildWavySurface(n);
auto label = "wavy~" + std::to_string(mesh->num_faces()) + "f";
inputs.push_back({label, mesh});
}
}
if (builtinSphereEnabled) {
for (std::size_t n :
{50'000UL, 100'000UL, 200'000UL, 400'000UL, 600'000UL, 800'000UL, 1'000'000UL}) {
if (n > builtinSphereMax) {
break;
}
auto mesh = buildSphereCap(n);
auto label = "sphere~" + std::to_string(mesh->num_faces()) + "f";
inputs.push_back({label, mesh});
}
}
for (const auto& p : meshFiles) {
}
std::cout << "| Mesh | Num. faces | ABF++ (s) | LSCM SparseLU (s)";
for (int t : actualThreads) {
std::cout << " | LSCM CG-Diag (" << t << "t) (s)";
}
for (int t : actualThreads) {
std::cout << " | LSCM CG-IC (" << t << "t) (s)";
}
for (int t : actualThreads) {
std::cout << " | HLSCM LSCG (" << t << "t) (s)";
}
for (int t : actualThreads) {
std::cout << " | HLSCM CG (" << t << "t) (s)";
}
std::cout << " |\n";
std::cout << "|------|-----------|-----------|------------------";
for (std::size_t i = 0; i < 4 * actualThreads.size(); ++i) {
std::cout << "-|------------------";
}
std::cout << "-|\n";
auto fmtTime = [](double t) -> std::string {
if (t < 0) {
return "N/A";
}
std::ostringstream oss;
oss << std::fixed << std::setprecision(2) << t;
return oss.str();
};
for (auto& input : inputs) {
const auto& label = input.label;
const auto& baseMesh = input.mesh;
const auto numFaces = baseMesh->num_faces();
std::cerr << "Benchmarking " << label << " (" << numFaces << " faces)...\n";
auto runLSCM = [&](int threads,
auto computeLSCM) -> std::pair<double, typename ABFMesh::Pointer> {
auto mesh = baseMesh->clone();
std::size_t iters{0};
Eigen::setNbThreads(threads);
double t = kFailed;
try {
t = timeIt([&] { computeLSCM(mesh); });
} catch (const std::bad_alloc&) {
std::cerr << " [OOM]\n";
std::cerr << " [solver error: " << e.what() << "]\n";
}
Eigen::setNbThreads(1);
if (t < 0) {
return {kFailed, nullptr};
}
return {t, mesh};
};
runLSCM(1, [](auto& m) { LSCM_CG_Diag::Compute(m); });
runLSCM(1, [](auto& m) { LSCM_CG_IC::Compute(m); });
runLSCM(1, [](auto& m) { HLSCM_LSCG::Compute(m); });
runLSCM(1, [](auto& m) { HLSCM_CG::Compute(m); });
double abfTime{0};
{
auto mesh = baseMesh->clone();
abfTime = timeIt([&] {
std::size_t iters{0};
});
}
auto [luTime, luMesh] = runLSCM(1, [](auto& m) { LSCM_LU::Compute(m); });
std::vector<double> cgDiagTimes;
typename ABFMesh::Pointer cgDiagMesh;
for (int t : actualThreads) {
auto [time, mesh] = runLSCM(t, [](auto& m) { LSCM_CG_Diag::Compute(m); });
cgDiagTimes.push_back(time);
if (t == actualThreads.front()) {
cgDiagMesh = mesh;
}
}
std::vector<double> cgIcTimes;
typename ABFMesh::Pointer cgIcMesh;
for (int t : actualThreads) {
auto [time, mesh] = runLSCM(t, [](auto& m) { LSCM_CG_IC::Compute(m); });
cgIcTimes.push_back(time);
if (t == actualThreads.front()) {
cgIcMesh = mesh;
}
}
std::vector<double> hlscmLscgTimes;
typename ABFMesh::Pointer hlscmLscgMesh;
for (int t : actualThreads) {
auto [time, mesh] = runLSCM(t, [](auto& m) { HLSCM_LSCG::Compute(m); });
hlscmLscgTimes.push_back(time);
if (t == actualThreads.front()) {
hlscmLscgMesh = mesh;
}
}
std::vector<double> hlscmCgTimes;
typename ABFMesh::Pointer hlscmCgMesh;
for (int t : actualThreads) {
auto [time, mesh] = runLSCM(t, [](auto& m) { HLSCM_CG::Compute(m); });
hlscmCgTimes.push_back(time);
if (t == actualThreads.front()) {
hlscmCgMesh = mesh;
}
}
if (!outputDir.empty()) {
auto stem = label;
for (auto& ch : stem) {
if (ch == '~' || ch == ' ') {
ch = '_';
}
}
if (luMesh) {
}
if (cgDiagMesh) {
}
if (cgIcMesh) {
}
if (hlscmLscgMesh) {
}
if (hlscmCgMesh) {
}
}
std::cout << "| " << label << " | " << numFaces << " | " << std::fixed
<< std::setprecision(2) << abfTime << " | " << fmtTime(luTime);
for (double ct : cgDiagTimes) {
std::cout << " | " << fmtTime(ct);
}
for (double ct : cgIcTimes) {
std::cout << " | " << fmtTime(ct);
}
for (double ht : hlscmLscgTimes) {
std::cout << " | " << fmtTime(ht);
}
for (double ht : hlscmCgTimes) {
std::cout << " | " << fmtTime(ht);
}
std::cout << " |\n";
std::cout.flush();
}
return EXIT_SUCCESS;
}
Compute parameterized interior angles using ABF++.
Definition OpenABF.hpp:2816
static void Compute(typename Mesh::Pointer &mesh, std::size_t &iters, T &gradient, const std::size_t maxIters=10, T gradThreshold=T(0.001))
Compute parameterized interior angles.
Definition OpenABF.hpp:2868
MeshType Mesh
Mesh type alias.
Definition OpenABF.hpp:2819
Compute parameterized mesh using Angle-based LSCM.
Definition OpenABF.hpp:3484
Compute parameterized mesh using Hierarchical LSCM.
Definition OpenABF.hpp:4613
Solver exception.
Definition OpenABF.hpp:33
void WriteMesh(const std::filesystem::path &path, const MeshPtr &mesh)
Write a HalfEdgeMesh to a file.
Definition OpenABF.hpp:5509
constexpr T INF
Inf, templated for floating-point type.
Definition OpenABF.hpp:69