Three reviewer-meeting deliverables in one commit.
(1) output_uv_map for the two remaining DCE entries
─────────────────────────────────────────────────
* Discrete_inversive_distance.h: full implementation. After Newton,
reconstruct effective Euclidean edge lengths from the converged
log-radii via the Bowers-Stephenson identity
`ℓᵢⱼ² = rᵢ² + rⱼ² + 2·Iᵢⱼ·rᵢ·rⱼ`, populate a temporary
EuclideanMaps with `lambda0 = log(ℓᵢⱼ²)`, and reuse the existing
`euclidean_layout(mesh, 0, eucl)` priority-BFS. Per-vertex
Point_2 coordinates written into the user-supplied pmap.
Optional `normalise_layout(true)` applies the canonical PCA
centroid + major-axis rotation, same as the other 3 entries.
* Discrete_circle_packing.h: throws std::runtime_error with a
clear pointer to Phase 9c rather than silently producing
nonsense. CP-Euclidean is face-based; the faithful output is a
per-face circle packing in ℝ², not a per-vertex Point_2 map.
A true layout requires BPS-2010 §6 (~150 lines, on the porting
roadmap as Phase 9c). Failing loudly is the honest default.
Tests: 2 new cases in test_cgal_phase8b_lite.cpp
(OutputUvMap_InversiveDistance_PopulatesPmap;
OutputUvMap_CPEuclidean_ThrowsClearly). Both green.
Suite total now 259 (was 257, +2). CGAL subtotal: 234 → 236.
(2) Reviewer meeting documents
──────────────────────────
New directory doc/reviewer/ with three files:
* briefing.md — one-page orientation for the reviewer.
What the project is, where to look first
(https://tmoussa.codeberg.page/ConformalLabpp/), the headline
evidence (tests/coverage/sanitizers/license), what we want from
them, what's deferred and why, and the 5 questions in a separate
file.
* questions.md — the 5 concrete decisions we want their second
opinion on:
Q1 Phase 9c (port-literal vs re-derive)
Q2 Phase 9b-analytic (worth ~2 weeks for ~6× speedup?)
Q3 CP-Euclidean output_uv_map (build now or defer?)
Q4 CGAL submission strategy (one package or five?)
Q5 geometry-central cross-validation co-authorship
Plus an explicit "what would you say no to?" question at the
bottom — negative feedback is the highest-value information.
* agenda.md — my own internal playbook (NOT to be sent).
60-min flow: 5-min thank-you, 10-min architecture tour,
30-min for Q1-Q5 in the order Q4-Q1-Q2-Q5-Q3, 5-min "no"
question, 5-min wrap-up. Includes post-meeting memo template
to fill out in the 30 min after.
* README.md — index for the directory; says which file goes
to whom and when to send.
(3) locked-vs-flexible.md known-limitations update
─────────────────────────────────────────────
"output_uv_map covers 3 of 5 entries" → "covers 4 of 5".
CP-Euclidean's throws-clearly behaviour documented as a Phase 9c
deliverable rather than a passive gap.
Bonus: extended .codespellrc ignore list (acknowledgement, the
British-English spelling I used in agenda.md).
Verifications on this commit:
259/259 tests pass (0 skipped)
scripts/check-test-counts.sh: OK (23 + 236 = 259)
scripts/quality/license-headers.sh: OK (66/66 SPDX)
python3 scripts/quality/cgal-conventions.py: OK (0/6 violations)
scripts/quality/codespell.sh: OK (0 typos)
scripts/quality/shellcheck.sh: OK (0 findings)
python3 scripts/check-markdown-links.py: OK (143/143)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
264 lines
11 KiB
C++
264 lines
11 KiB
C++
// Copyright (c) 2024-2026 Tarik Moussa.
|
||
// SPDX-License-Identifier: MIT
|
||
//
|
||
// Package: conformallab++ / Discrete_conformal_map (Phase 8b-Lite, 2026-05-21)
|
||
|
||
/*!
|
||
\file CGAL/Discrete_inversive_distance.h
|
||
\ingroup PkgConformalMapRef
|
||
|
||
User-facing entry for the **vertex-based** inversive-distance circle-
|
||
packing functional of Luo (2004), with the Bowers-Stephenson (2004)
|
||
initialisation. See `inversive_distance_functional.hpp` for the
|
||
underlying algorithm and `doc/roadmap/research-track.md` (item 9a.2)
|
||
for the research-track classification — this functional has **no Java
|
||
original** (verified empirically), it is from-the-literature research.
|
||
|
||
DOF structure
|
||
─────────────
|
||
* Per-vertex `u_i = log r_i` (compatible with the classical Euclidean
|
||
trait).
|
||
* Per-edge constant `I_ij` computed once by Bowers-Stephenson from the
|
||
input mesh geometry (handled internally by
|
||
`compute_inversive_distance_init_from_mesh`).
|
||
|
||
Because the per-edge constant has a different meaning from the
|
||
Euclidean `λ°_e`, this entry has its own default-trait class
|
||
`Default_inversive_distance_traits`.
|
||
*/
|
||
|
||
#ifndef CGAL_DISCRETE_INVERSIVE_DISTANCE_H
|
||
#define CGAL_DISCRETE_INVERSIVE_DISTANCE_H
|
||
|
||
#include <CGAL/Conformal_map/internal/parameters.h>
|
||
#include <CGAL/Kernel_traits.h>
|
||
#include <CGAL/Named_function_parameters.h>
|
||
#include <CGAL/boost/graph/named_params_helper.h>
|
||
#include <CGAL/Surface_mesh.h>
|
||
#include <CGAL/Simple_cartesian.h>
|
||
#include <boost/graph/graph_traits.hpp>
|
||
|
||
#include <CGAL/Discrete_conformal_map.h> // for Conformal_map_result<FT>
|
||
|
||
#include "../inversive_distance_functional.hpp"
|
||
#include "../newton_solver.hpp"
|
||
|
||
namespace CGAL {
|
||
|
||
// ── Default traits for Inversive-Distance ────────────────────────────────────
|
||
|
||
/*!
|
||
\ingroup PkgConformalMapConcepts
|
||
\brief Traits class for `discrete_inversive_distance_map()` — declares
|
||
the kernel, mesh and property-map types used by Luo's 2004 vertex-based
|
||
inversive-distance circle packing.
|
||
|
||
Primary template; specialise it for non-`Surface_mesh` triangle meshes.
|
||
*/
|
||
template <typename TriangleMesh,
|
||
typename Kernel_ = CGAL::Simple_cartesian<double>>
|
||
struct Default_inversive_distance_traits;
|
||
|
||
/*!
|
||
\ingroup PkgConformalMapConcepts
|
||
\brief Specialisation for `CGAL::Surface_mesh<P>`; the only one shipped
|
||
in Phase 8b-Lite.
|
||
*/
|
||
template <typename K>
|
||
struct Default_inversive_distance_traits<CGAL::Surface_mesh<typename K::Point_3>, K>
|
||
{
|
||
/// CGAL kernel parameter (defaults to `Simple_cartesian<double>`).
|
||
using Kernel = K;
|
||
/// Scalar field type used for all inversive-distance DOFs.
|
||
using FT = typename K::FT;
|
||
/// 3-D point type (vertex coordinates).
|
||
using Point_3 = typename K::Point_3;
|
||
/// Triangle-mesh type this specialisation targets.
|
||
using Triangle_mesh = CGAL::Surface_mesh<Point_3>;
|
||
|
||
/// Boost-graph vertex descriptor for `Triangle_mesh`.
|
||
using Vertex_descriptor = typename boost::graph_traits<Triangle_mesh>::vertex_descriptor;
|
||
/// Boost-graph edge descriptor for `Triangle_mesh`.
|
||
using Edge_descriptor = typename boost::graph_traits<Triangle_mesh>::edge_descriptor;
|
||
|
||
// Inversive-distance specific property maps.
|
||
|
||
/// Property map vertex → contiguous integer DOF index (legacy `iv:idx`).
|
||
using Vertex_index_pmap = typename Triangle_mesh::template Property_map<Vertex_descriptor, int>;
|
||
/// Property map vertex → target cone angle Θᵥ in radians (legacy `iv:theta`).
|
||
using Theta_v_pmap = typename Triangle_mesh::template Property_map<Vertex_descriptor, FT>;
|
||
/// Property map vertex → initial radius r⁰ᵥ (legacy `iv:r0`).
|
||
using R0_pmap = typename Triangle_mesh::template Property_map<Vertex_descriptor, FT>;
|
||
/// Property map edge → inversive distance Iᵢⱼ (legacy `ie:I`).
|
||
using I_e_pmap = typename Triangle_mesh::template Property_map<Edge_descriptor, FT>;
|
||
};
|
||
|
||
// ── Entry function ────────────────────────────────────────────────────────────
|
||
|
||
/*!
|
||
\ingroup PkgConformalMapRef
|
||
|
||
Compute the Luo-2004 vertex-based inversive-distance circle packing of `mesh`.
|
||
|
||
The per-edge constant `I_ij` is computed once at the start from the input
|
||
3-D geometry via the Bowers-Stephenson identity
|
||
`I_ij = (ℓ_ij² − r_i² − r_j²) / (2 r_i r_j)`,
|
||
with `r_i^(0) = (1/3) min{ℓ_e : e adj v_i}` as the default initial radii.
|
||
The user can override the initial radii by writing into the `r0`
|
||
property map before calling this function.
|
||
|
||
\tparam TriangleMesh A `CGAL::Surface_mesh<P>`.
|
||
\tparam NamedParameters Optional CGAL named-parameter pack.
|
||
|
||
\param mesh Input triangle mesh.
|
||
\param np Named parameters:
|
||
- `vertex_curvature_map(pmap)` — per-vertex Θ_v target.
|
||
- `fixed_vertex_map(pmap)` — pinning override.
|
||
- `gradient_tolerance(ε)` — Newton stop.
|
||
- `max_iterations(n)` — Newton iteration cap.
|
||
|
||
\returns A `Conformal_map_result<FT>` with `u_per_vertex[v] = log r_v`
|
||
(the converged log-radius at each vertex).
|
||
|
||
\pre `mesh` is a triangle mesh with positive edge lengths.
|
||
\pre The user-supplied or natural-theta Θ satisfies Gauss–Bonnet.
|
||
|
||
\note Convergence is sensitive to the initial point and to extreme
|
||
`I_ij` values. For testing purposes the natural-theta default
|
||
(Θ_v shifted so that u = 0 is the equilibrium) always converges
|
||
in zero iterations.
|
||
*/
|
||
template <typename TriangleMesh,
|
||
typename CGAL_NP_TEMPLATE_PARAMETERS>
|
||
auto discrete_inversive_distance_map(
|
||
TriangleMesh& mesh,
|
||
const CGAL_NP_CLASS& np = parameters::default_values())
|
||
{
|
||
using Point_type = typename TriangleMesh::Point;
|
||
using Default_kernel = typename CGAL::Kernel_traits<Point_type>::Kernel;
|
||
using Default_traits = Default_inversive_distance_traits<TriangleMesh, Default_kernel>;
|
||
using Traits = typename internal_np::Lookup_named_param_def<
|
||
internal_np::geom_traits_t,
|
||
CGAL_NP_CLASS,
|
||
Default_traits>::type;
|
||
using FT = typename Traits::FT;
|
||
|
||
Conformal_map_result<FT> result;
|
||
|
||
auto maps = ::conformallab::setup_inversive_distance_maps(mesh);
|
||
::conformallab::compute_inversive_distance_init_from_mesh(mesh, maps);
|
||
|
||
auto theta_param = parameters::get_parameter(
|
||
np, Conformal_map::internal_np::vertex_curvature_map);
|
||
constexpr bool has_theta = !std::is_same_v<
|
||
decltype(theta_param), internal_np::Param_not_found>;
|
||
if constexpr (has_theta) {
|
||
for (auto v : mesh.vertices())
|
||
maps.theta_v[v] = get(theta_param, v);
|
||
}
|
||
|
||
// Pin first vertex by default; user can override with fixed_vertex_map.
|
||
constexpr int FREE = 0;
|
||
for (auto v : mesh.vertices()) maps.v_idx[v] = FREE;
|
||
|
||
auto pin_param = parameters::get_parameter(
|
||
np, Conformal_map::internal_np::fixed_vertex_map);
|
||
constexpr bool has_pin = !std::is_same_v<
|
||
decltype(pin_param), internal_np::Param_not_found>;
|
||
|
||
bool any_pinned = false;
|
||
if constexpr (has_pin) {
|
||
for (auto v : mesh.vertices())
|
||
if (get(pin_param, v)) { maps.v_idx[v] = -1; any_pinned = true; }
|
||
}
|
||
if (!any_pinned) {
|
||
auto it = mesh.vertices().begin();
|
||
if (it != mesh.vertices().end()) { maps.v_idx[*it] = -1; any_pinned = true; }
|
||
}
|
||
|
||
int idx = 0;
|
||
for (auto v : mesh.vertices())
|
||
if (maps.v_idx[v] != -1) maps.v_idx[v] = idx++;
|
||
|
||
const FT tol = parameters::choose_parameter(
|
||
parameters::get_parameter(np, Conformal_map::internal_np::gradient_tolerance),
|
||
FT(1e-10));
|
||
const int max_iter = parameters::choose_parameter(
|
||
parameters::get_parameter(np, Conformal_map::internal_np::max_iterations),
|
||
200);
|
||
|
||
// Natural-theta default.
|
||
std::vector<double> x0(static_cast<std::size_t>(idx), 0.0);
|
||
if constexpr (!has_theta) {
|
||
auto G0 = ::conformallab::inversive_distance_gradient(mesh, x0, maps);
|
||
for (auto v : mesh.vertices()) {
|
||
const int j = maps.v_idx[v];
|
||
if (j >= 0) maps.theta_v[v] -= G0[static_cast<std::size_t>(j)];
|
||
}
|
||
}
|
||
|
||
auto nr = ::conformallab::newton_inversive_distance(mesh, x0, maps, tol, max_iter);
|
||
|
||
result.u_per_vertex.assign(num_vertices(mesh), FT(0));
|
||
for (auto v : mesh.vertices()) {
|
||
const int j = maps.v_idx[v];
|
||
if (j >= 0) result.u_per_vertex[v.idx()] = nr.x[static_cast<std::size_t>(j)];
|
||
}
|
||
result.iterations = nr.iterations;
|
||
result.gradient_norm = nr.grad_inf_norm;
|
||
result.converged = nr.converged;
|
||
|
||
// ── Optional layout step (Phase 8b-Lite extension) ─────────────────────
|
||
//
|
||
// If the caller supplied `output_uv_map(pmap)`, lay out the converged
|
||
// packing in ℝ² and write per-vertex `Point_2` coordinates into `pmap`.
|
||
//
|
||
// Method: the converged Inversive-Distance radii `r_i = exp(u_i)`
|
||
// together with the fixed per-edge `I_ij` constants determine effective
|
||
// Euclidean edge lengths via the Bowers-Stephenson identity
|
||
// ℓᵢⱼ² = rᵢ² + rⱼ² + 2·Iᵢⱼ·rᵢ·rⱼ
|
||
// so we can populate a temporary `EuclideanMaps` whose `lambda0` carries
|
||
// `log(ℓᵢⱼ²)` per edge and then reuse `euclidean_layout(mesh, 0, eucl)`
|
||
// — the existing priority-BFS trilateration on the resulting triangle
|
||
// metric. All vertex/edge DOF indices stay at −1 (pinned), so the empty
|
||
// DOF vector `0` produces lengths driven purely by `lambda0`.
|
||
auto uv_param = parameters::get_parameter(
|
||
np, Conformal_map::internal_np::output_uv_map);
|
||
constexpr bool has_uv = !std::is_same_v<
|
||
decltype(uv_param), internal_np::Param_not_found>;
|
||
if constexpr (has_uv) {
|
||
if (nr.converged) {
|
||
auto eucl = ::conformallab::setup_euclidean_maps(mesh);
|
||
for (auto e : mesh.edges()) {
|
||
auto h = mesh.halfedge(e);
|
||
const double u_i = result.u_per_vertex[mesh.source(h).idx()];
|
||
const double u_j = result.u_per_vertex[mesh.target(h).idx()];
|
||
const double I = maps.I_e[e];
|
||
const double l2 = ::conformallab::id_detail::edge_length_squared(u_i, u_j, I);
|
||
eucl.lambda0[e] = (l2 > 0.0) ? std::log(l2) : -30.0;
|
||
}
|
||
// Empty DOF vector: every vertex is pinned (idx=-1), so the
|
||
// layout depends purely on the lambda0 we just computed.
|
||
std::vector<double> zero;
|
||
auto layout = ::conformallab::euclidean_layout(mesh, zero, eucl);
|
||
|
||
const bool do_norm = parameters::choose_parameter(
|
||
parameters::get_parameter(np, Conformal_map::internal_np::normalise_layout),
|
||
false);
|
||
if (do_norm) ::conformallab::normalise_euclidean(layout);
|
||
|
||
for (auto v : mesh.vertices()) {
|
||
const auto& uv = layout.uv[v.idx()];
|
||
put(uv_param, v,
|
||
typename Traits::Kernel::Point_2(uv.x(), uv.y()));
|
||
}
|
||
}
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
} // namespace CGAL
|
||
|
||
#endif // CGAL_DISCRETE_INVERSIVE_DISTANCE_H
|