feat(phase4a+4b): Newton solver + CGAL mesh I/O
Phase 4a — newton_solver.hpp:
- newton_euclidean(): SimplicialLDLT on H (PSD); solves H·Δx = −G
- newton_spherical(): SimplicialLDLT on −H (NSD→PSD); solves (−H)·Δx = G
- Backtracking line search (halving α up to 20×) for global convergence
- NewtonResult struct: x, iterations, grad_inf_norm, converged
- 7 tests: 4 spherical (convergence, few iters, large perturbation,
field consistency) + 3 Euclidean (triangle pinned, quad pinned,
mixed pinned — all with natural-theta equilibrium at x=0)
Phase 4b — mesh_io.hpp:
- read_mesh() / write_mesh(): CGAL::IO::read/write_polygon_mesh wrappers
- load_mesh() / save_mesh(): throwing convenience versions
- Supports OFF, OBJ, PLY (format detected by file extension)
- 6 tests: OFF round-trip (tet + quad), OBJ round-trip, missing-file throw,
vertex-position preservation, save/load convenience wrappers
All 75 cgal tests pass (3 skipped as before).
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
65
code/include/mesh_io.hpp
Normal file
65
code/include/mesh_io.hpp
Normal file
@@ -0,0 +1,65 @@
|
||||
#pragma once
|
||||
// mesh_io.hpp
|
||||
//
|
||||
// Phase 4b — CGAL::IO wrappers for ConformalMesh.
|
||||
//
|
||||
// Reads and writes ConformalMesh (CGAL::Surface_mesh) in standard polygon-mesh
|
||||
// formats using the CGAL Polygon Mesh I/O utilities (CGAL 5.4+).
|
||||
//
|
||||
// Supported formats (detected by file extension):
|
||||
// .off — Object File Format (read + write)
|
||||
// .obj — Wavefront OBJ (read + write)
|
||||
// .ply — Polygon File Format (read + write)
|
||||
//
|
||||
// Usage:
|
||||
// ConformalMesh mesh;
|
||||
// if (!read_mesh("input.off", mesh)) throw std::runtime_error("read failed");
|
||||
// // ... process mesh ...
|
||||
// write_mesh("output.off", mesh);
|
||||
//
|
||||
// Note: property maps (lambda0, v_idx, etc.) are NOT serialised — they must
|
||||
// be re-initialised with setup_*_maps() + compute_lambda0_from_mesh() after
|
||||
// reading a file.
|
||||
|
||||
#include "conformal_mesh.hpp"
|
||||
#include <CGAL/IO/polygon_mesh_io.h>
|
||||
#include <string>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace conformallab {
|
||||
|
||||
// ── Read ──────────────────────────────────────────────────────────────────────
|
||||
//
|
||||
// Reads a polygon mesh from file into `mesh` (clears any existing content).
|
||||
// Returns true on success, false on failure.
|
||||
inline bool read_mesh(const std::string& filename, ConformalMesh& mesh)
|
||||
{
|
||||
mesh.clear();
|
||||
return CGAL::IO::read_polygon_mesh(filename, mesh);
|
||||
}
|
||||
|
||||
// ── Write ─────────────────────────────────────────────────────────────────────
|
||||
//
|
||||
// Writes `mesh` to `filename`. Returns true on success.
|
||||
inline bool write_mesh(const std::string& filename, const ConformalMesh& mesh)
|
||||
{
|
||||
return CGAL::IO::write_polygon_mesh(filename, mesh);
|
||||
}
|
||||
|
||||
// ── Convenience: throwing wrappers ────────────────────────────────────────────
|
||||
|
||||
inline ConformalMesh load_mesh(const std::string& filename)
|
||||
{
|
||||
ConformalMesh mesh;
|
||||
if (!read_mesh(filename, mesh))
|
||||
throw std::runtime_error("conformallab: failed to read mesh from " + filename);
|
||||
return mesh;
|
||||
}
|
||||
|
||||
inline void save_mesh(const std::string& filename, const ConformalMesh& mesh)
|
||||
{
|
||||
if (!write_mesh(filename, mesh))
|
||||
throw std::runtime_error("conformallab: failed to write mesh to " + filename);
|
||||
}
|
||||
|
||||
} // namespace conformallab
|
||||
204
code/include/newton_solver.hpp
Normal file
204
code/include/newton_solver.hpp
Normal file
@@ -0,0 +1,204 @@
|
||||
#pragma once
|
||||
// newton_solver.hpp
|
||||
//
|
||||
// Phase 4a — Newton solver for the discrete conformal functionals.
|
||||
//
|
||||
// Solves G(x) = 0 where G is the gradient of the discrete conformal energy:
|
||||
// G_v = Θ_v − Σ_f α_v^f (angle-sum residual at each vertex DOF)
|
||||
//
|
||||
// Algorithm per iteration:
|
||||
// 1. Compute gradient G(x)
|
||||
// 2. Check convergence: max|G_i| < tol → done
|
||||
// 3. Compute sparse Hessian H(x)
|
||||
// 4. Factorize and solve the Newton system:
|
||||
// Euclidean: H · Δx = −G (H is PSD → SimplicialLDLT directly)
|
||||
// Spherical: (−H) · Δx = G (H is NSD → negate to get PSD matrix)
|
||||
// 5. Backtracking line search: halve α until ||G(x+α·Δx)|| < ||G(x)||
|
||||
// 6. x ← x + α·Δx, go to 1
|
||||
//
|
||||
// Requires:
|
||||
// Eigen::SimplicialLDLT (part of Eigen's sparse Cholesky module)
|
||||
|
||||
#include "euclidean_hessian.hpp"
|
||||
#include "spherical_hessian.hpp"
|
||||
#include <Eigen/SparseCholesky>
|
||||
#include <Eigen/Dense>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace conformallab {
|
||||
|
||||
// ── Result ────────────────────────────────────────────────────────────────────
|
||||
|
||||
struct NewtonResult {
|
||||
std::vector<double> x; ///< DOF vector at termination
|
||||
int iterations; ///< Newton steps taken
|
||||
double grad_inf_norm;///< max |G_i| at termination
|
||||
bool converged; ///< true iff grad_inf_norm < tol
|
||||
};
|
||||
|
||||
// ── Internal helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
namespace detail {
|
||||
|
||||
// Backtracking line search: find the largest α in {1, 0.5, 0.25, …} such that
|
||||
// ||G(x + α·Δx)||₂ < ||G(x)||₂. Returns the accepted step (α may stay 1).
|
||||
template <typename GradFn>
|
||||
inline std::vector<double> line_search(
|
||||
const std::vector<double>& x,
|
||||
const Eigen::VectorXd& dx,
|
||||
double norm0,
|
||||
GradFn&& grad_fn,
|
||||
int max_halvings = 20)
|
||||
{
|
||||
const int n = static_cast<int>(x.size());
|
||||
double alpha = 1.0;
|
||||
std::vector<double> xnew(static_cast<std::size_t>(n));
|
||||
|
||||
for (int ls = 0; ls < max_halvings; ++ls) {
|
||||
for (int i = 0; i < n; ++i)
|
||||
xnew[static_cast<std::size_t>(i)] = x[static_cast<std::size_t>(i)]
|
||||
+ alpha * dx[i];
|
||||
auto Gnew = grad_fn(xnew);
|
||||
double norm_new = 0.0;
|
||||
for (double v : Gnew) norm_new += v * v;
|
||||
norm_new = std::sqrt(norm_new);
|
||||
if (norm_new < norm0) return xnew;
|
||||
alpha *= 0.5;
|
||||
}
|
||||
// No improvement found — return best attempt (full step)
|
||||
for (int i = 0; i < n; ++i)
|
||||
xnew[static_cast<std::size_t>(i)] = x[static_cast<std::size_t>(i)] + dx[i];
|
||||
return xnew;
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
// ── Euclidean Newton solver ────────────────────────────────────────────────────
|
||||
//
|
||||
// Minimises the Euclidean discrete conformal energy by solving G(x) = 0.
|
||||
// The Hessian H is PSD; Eigen::SimplicialLDLT is used directly.
|
||||
inline NewtonResult newton_euclidean(
|
||||
ConformalMesh& mesh,
|
||||
std::vector<double> x0,
|
||||
const EuclideanMaps& m,
|
||||
double tol = 1e-8,
|
||||
int max_iter = 200)
|
||||
{
|
||||
std::vector<double> x = x0;
|
||||
const int n = static_cast<int>(x.size());
|
||||
|
||||
NewtonResult res;
|
||||
res.converged = false;
|
||||
res.iterations = 0;
|
||||
res.grad_inf_norm = 0.0;
|
||||
|
||||
Eigen::SimplicialLDLT<Eigen::SparseMatrix<double>> solver;
|
||||
|
||||
for (int iter = 0; iter < max_iter; ++iter) {
|
||||
// ── Gradient ──────────────────────────────────────────────────────────
|
||||
auto G_std = euclidean_gradient(mesh, x, m);
|
||||
Eigen::Map<const Eigen::VectorXd> G(G_std.data(), n);
|
||||
|
||||
double inf_norm = G.cwiseAbs().maxCoeff();
|
||||
if (inf_norm < tol) {
|
||||
res.converged = true;
|
||||
res.grad_inf_norm = inf_norm;
|
||||
res.iterations = iter;
|
||||
res.x = x;
|
||||
return res;
|
||||
}
|
||||
|
||||
// ── Hessian + factorisation ───────────────────────────────────────────
|
||||
auto H = euclidean_hessian(mesh, x, m);
|
||||
solver.compute(H);
|
||||
if (solver.info() != Eigen::Success) break;
|
||||
|
||||
// ── Newton step: solve H·Δx = −G ────────────────────────────────────
|
||||
Eigen::VectorXd dx = solver.solve(-G);
|
||||
if (solver.info() != Eigen::Success) break;
|
||||
|
||||
// ── Backtracking line search ──────────────────────────────────────────
|
||||
double norm0 = G.norm();
|
||||
x = detail::line_search(x, dx, norm0,
|
||||
[&](const std::vector<double>& xnew) {
|
||||
return euclidean_gradient(mesh, xnew, m);
|
||||
});
|
||||
|
||||
res.iterations = iter + 1;
|
||||
}
|
||||
|
||||
// Report final gradient norm
|
||||
auto G_final = euclidean_gradient(mesh, x, m);
|
||||
double inf_final = 0.0;
|
||||
for (double v : G_final) inf_final = std::max(inf_final, std::abs(v));
|
||||
res.grad_inf_norm = inf_final;
|
||||
res.x = x;
|
||||
return res;
|
||||
}
|
||||
|
||||
// ── Spherical Newton solver ───────────────────────────────────────────────────
|
||||
//
|
||||
// Solves G(x) = 0 for the spherical discrete conformal functional.
|
||||
// The Hessian H is NSD at the solution; −H is PSD, so we factorise −H and
|
||||
// solve (−H)·Δx = G ⟺ H·Δx = −G.
|
||||
inline NewtonResult newton_spherical(
|
||||
ConformalMesh& mesh,
|
||||
std::vector<double> x0,
|
||||
const SphericalMaps& m,
|
||||
double tol = 1e-8,
|
||||
int max_iter = 200)
|
||||
{
|
||||
std::vector<double> x = x0;
|
||||
const int n = static_cast<int>(x.size());
|
||||
|
||||
NewtonResult res;
|
||||
res.converged = false;
|
||||
res.iterations = 0;
|
||||
res.grad_inf_norm = 0.0;
|
||||
|
||||
Eigen::SimplicialLDLT<Eigen::SparseMatrix<double>> solver;
|
||||
|
||||
for (int iter = 0; iter < max_iter; ++iter) {
|
||||
// ── Gradient ──────────────────────────────────────────────────────────
|
||||
auto G_std = spherical_gradient(mesh, x, m);
|
||||
Eigen::Map<const Eigen::VectorXd> G(G_std.data(), n);
|
||||
|
||||
double inf_norm = G.cwiseAbs().maxCoeff();
|
||||
if (inf_norm < tol) {
|
||||
res.converged = true;
|
||||
res.grad_inf_norm = inf_norm;
|
||||
res.iterations = iter;
|
||||
res.x = x;
|
||||
return res;
|
||||
}
|
||||
|
||||
// ── Hessian: negate to get PSD matrix ────────────────────────────────
|
||||
auto H = spherical_hessian(mesh, x, m);
|
||||
auto negH = Eigen::SparseMatrix<double>(-H);
|
||||
solver.compute(negH);
|
||||
if (solver.info() != Eigen::Success) break;
|
||||
|
||||
// ── Newton step: solve (−H)·Δx = G ─────────────────────────────────
|
||||
Eigen::VectorXd dx = solver.solve(G);
|
||||
if (solver.info() != Eigen::Success) break;
|
||||
|
||||
// ── Backtracking line search ──────────────────────────────────────────
|
||||
double norm0 = G.norm();
|
||||
x = detail::line_search(x, dx, norm0,
|
||||
[&](const std::vector<double>& xnew) {
|
||||
return spherical_gradient(mesh, xnew, m);
|
||||
});
|
||||
|
||||
res.iterations = iter + 1;
|
||||
}
|
||||
|
||||
auto G_final = spherical_gradient(mesh, x, m);
|
||||
double inf_final = 0.0;
|
||||
for (double v : G_final) inf_final = std::max(inf_final, std::abs(v));
|
||||
res.grad_inf_norm = inf_final;
|
||||
res.x = x;
|
||||
return res;
|
||||
}
|
||||
|
||||
} // namespace conformallab
|
||||
Reference in New Issue
Block a user