Compare commits
1 Commits
fix/citati
...
docs/roadm
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc77afc55f |
@@ -1,17 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/claude-code-settings.json",
|
||||
"env": {
|
||||
"CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1",
|
||||
"CLAUDE_VERBOSE": "1"
|
||||
},
|
||||
"outputStyle": "Explanatory",
|
||||
"teammateMode": "tmux",
|
||||
"preferences": {
|
||||
"tmuxSplitPanes": "true",
|
||||
"responseFormat": "markdown"
|
||||
},
|
||||
"permissions": {
|
||||
"defaultMode": "acceptEdits",
|
||||
"allow": [
|
||||
"Bash(cmake:*)",
|
||||
"Bash(ctest:*)",
|
||||
@@ -30,56 +19,18 @@
|
||||
"Bash(git branch:*)",
|
||||
"Bash(git ls-remote:*)",
|
||||
"Bash(git ls-files:*)",
|
||||
"Bash(git remote:*)",
|
||||
"Bash(git rev-parse:*)",
|
||||
"Bash(git rev-list:*)",
|
||||
"Bash(git merge-base:*)",
|
||||
"Bash(git diff-tree:*)",
|
||||
"Bash(git describe:*)",
|
||||
"Bash(git tag:*)",
|
||||
"Bash(git add:*)",
|
||||
"Bash(git commit:*)",
|
||||
"Bash(git switch:*)",
|
||||
"Bash(git checkout:*)",
|
||||
"Bash(git restore:*)",
|
||||
"Bash(git stash:*)",
|
||||
"Bash(git fetch:*)",
|
||||
"Bash(git pull:*)",
|
||||
"Bash(git push:*)",
|
||||
"Bash(git merge:*)",
|
||||
"Bash(git rebase:*)",
|
||||
"Bash(git worktree:*)",
|
||||
"Bash(git init:*)",
|
||||
"Bash(git mv:*)",
|
||||
"Bash(ls:*)",
|
||||
"Bash(find:*)",
|
||||
"Bash(grep:*)",
|
||||
"Bash(rg:*)",
|
||||
"Bash(wc:*)",
|
||||
"Bash(curl -s*)",
|
||||
"Bash(curl:*)",
|
||||
"Bash(bash -n:*)",
|
||||
"Bash(cat:*)",
|
||||
"Bash(head:*)",
|
||||
"Bash(tail:*)",
|
||||
"Bash(awk:*)",
|
||||
"Bash(jq:*)",
|
||||
"Bash(mkdir:*)",
|
||||
"Bash(cp:*)",
|
||||
"Bash(test:*)",
|
||||
"Bash(echo:*)",
|
||||
"Bash(true)"
|
||||
"Bash(curl -s*)"
|
||||
],
|
||||
"deny": [
|
||||
"Bash(git push --force:*)",
|
||||
"Bash(git push -f:*)",
|
||||
"Bash(git clean -fdx:*)",
|
||||
"Bash(git clean -fd:*)",
|
||||
"Bash(rm -rf /)",
|
||||
"Bash(rm -rf /*)",
|
||||
"Bash(rm -rf ~)",
|
||||
"Bash(rm -rf ~/*)",
|
||||
"Bash(sudo rm:*)"
|
||||
"Bash(git push --force* origin main*)",
|
||||
"Bash(git push -f* origin main*)",
|
||||
"Bash(git reset --hard*)",
|
||||
"Bash(rm -rf /*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
# Forgejo Actions — Codeberg (https://codeberg.org)
|
||||
#
|
||||
# Codeberg counterpart of the eulernest Gitea pipeline
|
||||
# (.gitea/workflows/cpp-tests.yml). Two differences are required because
|
||||
# Codeberg's shared runners are not the Pi runner:
|
||||
#
|
||||
# * runs-on: docker — Codeberg shared-runner label (not "eulernest")
|
||||
# * image: node:20-bookworm — public image (the private git.eulernest.eu
|
||||
# ci-cpp image is unreachable from Codeberg).
|
||||
# node:20 ships the Node runtime that
|
||||
# actions/checkout@v4 needs; build tools are
|
||||
# apt-installed in the first step.
|
||||
#
|
||||
# Scope: "test-fast" only — the pure-math suite (Eigen vendored, GoogleTest via
|
||||
# FetchContent, no CGAL/Boost). A red job here = a red Codeberg CI badge.
|
||||
|
||||
name: C++ Tests (Codeberg)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- "claude/**"
|
||||
- "feature/**"
|
||||
- "review/**"
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test-fast:
|
||||
runs-on: docker
|
||||
container:
|
||||
image: node:20-bookworm
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install build deps (cmake, g++, make)
|
||||
run: |
|
||||
apt-get update -qq
|
||||
apt-get install -y --no-install-recommends \
|
||||
cmake g++ make git ca-certificates
|
||||
|
||||
- name: Configure (tests-only — Eigen + GoogleTest)
|
||||
run: cmake -S code -B build -DCMAKE_BUILD_TYPE=Release
|
||||
|
||||
- name: Build
|
||||
run: cmake --build build --target conformallab_tests -j$(nproc)
|
||||
|
||||
- name: Run tests
|
||||
run: >
|
||||
ctest --test-dir build
|
||||
--output-on-failure
|
||||
--output-junit test-results.xml
|
||||
|
||||
- name: Summary
|
||||
if: always()
|
||||
run: |
|
||||
if [ -f build/test-results.xml ]; then f=build/test-results.xml; else f=test-results.xml; fi
|
||||
if [ -f "$f" ]; then
|
||||
total=$(grep -o 'tests="[0-9]*"' "$f" | grep -o '[0-9]*' | head -1)
|
||||
failed=$(grep -o 'failures="[0-9]*"' "$f" | grep -o '[0-9]*' | head -1)
|
||||
skipped=$(grep -o 'skipped="[0-9]*"' "$f" | grep -o '[0-9]*' | head -1)
|
||||
passed=$(( ${total:-0} - ${failed:-0} - ${skipped:-0} ))
|
||||
echo "FAST ▸ TOTAL ${total:-0} | PASSED $passed | FAILED ${failed:-0} | SKIPPED ${skipped:-0}"
|
||||
fi
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -33,10 +33,3 @@ Testing/
|
||||
# Doxygen output
|
||||
doc/doxygen/
|
||||
*.dox.tmp
|
||||
|
||||
# Downloaded research papers + derived artifacts (regenerable, not tracked)
|
||||
papers/*.pdf
|
||||
papers/txt/
|
||||
papers/mmd/
|
||||
papers/facebook/
|
||||
papers/figures/
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
# Woodpecker CI — Codeberg (https://ci.codeberg.org)
|
||||
#
|
||||
# Runs on every push / PR. Mirrors the "test-fast" job of the
|
||||
# eulernest Gitea pipeline (.gitea/workflows/cpp-tests.yml), but uses a
|
||||
# plain public Debian image instead of the private ci-cpp registry image,
|
||||
# because Codeberg's shared runners cannot pull git.eulernest.eu.
|
||||
#
|
||||
# Pure-math test suite only (Clausen, ImLi2, hyper-ideal geometry):
|
||||
# * Eigen is vendored → code/deps/eigen-3.4.0
|
||||
# * GoogleTest via FetchContent (network at configure time)
|
||||
# * No CGAL / Boost / Wayland needed → fast, headless, < 2 min
|
||||
#
|
||||
# A failing build or any failing ctest makes the Codeberg CI badge red.
|
||||
|
||||
when:
|
||||
- event: [push, pull_request]
|
||||
|
||||
steps:
|
||||
test-fast:
|
||||
image: debian:bookworm
|
||||
commands:
|
||||
- apt-get update -qq
|
||||
- >-
|
||||
apt-get install -y --no-install-recommends
|
||||
cmake g++ make git ca-certificates
|
||||
- cmake -S code -B build -DCMAKE_BUILD_TYPE=Release
|
||||
- cmake --build build --target conformallab_tests -j$(nproc)
|
||||
- >-
|
||||
ctest --test-dir build
|
||||
--output-on-failure
|
||||
--output-junit test-results.xml
|
||||
@@ -142,7 +142,6 @@ Layout2D layout = euclidean_layout(mesh, res.x, maps);
|
||||
| **geometry-central comparison** — shared core, demarcation, adoption candidates, scientific added value | [doc/architecture/geometry-central-comparison.md](doc/architecture/geometry-central-comparison.md) |
|
||||
| **Design decisions** — key architectural choices + rationale | [doc/architecture/design-decisions.md](doc/architecture/design-decisions.md) |
|
||||
| **Project structure** — directory tree + build targets | [doc/architecture/project-structure.md](doc/architecture/project-structure.md) |
|
||||
| **CI / CD** — two-forge topology, runners, trigger keywords, mirror | [doc/architecture/ci-cd.md](doc/architecture/ci-cd.md) |
|
||||
| **Discrete conformal theory** — mathematical background for collaborators | [doc/math/discrete-conformal-theory.md](doc/math/discrete-conformal-theory.md) |
|
||||
| **Validation** — known analytic results + how to verify them | [doc/math/validation.md](doc/math/validation.md) |
|
||||
| **Validation protocol** — concrete commands with expected outputs | [doc/math/validation-protocol.md](doc/math/validation-protocol.md) |
|
||||
|
||||
@@ -1,384 +0,0 @@
|
||||
// Copyright (c) 2024-2026 Tarik Moussa.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// conformal_quality.hpp
|
||||
//
|
||||
// Phase 9g.1 — Quantitative correctness metrics for computed conformal maps.
|
||||
//
|
||||
// Measures the quality and validity of a discrete conformal map layout:
|
||||
// - IsothermicityMeasure: pointwise deviation from conformality (metric anisotropy).
|
||||
// - DiscreteConformalEquivalenceMeasure: per-edge length-cross-ratio residual.
|
||||
// - FlippedTriangles: detects inverted/degenerate triangles in 2-D layouts.
|
||||
// - LengthCrossRatio: the discrete conformal invariant (per-edge).
|
||||
// - ConvergenceUtility: aggregated convergence measures (max, mean, sum of cross-ratios).
|
||||
//
|
||||
// Mathematical references:
|
||||
// Springborn-Schröder-Pinkall 2008: discrete conformal invariant theory.
|
||||
// Bobenko-Springborn 2004: variational foundation.
|
||||
//
|
||||
// Java sources (ported from):
|
||||
// plugin/visualizer/IsothermicityMeasure.java
|
||||
// plugin/visualizer/DiscreteConformalEquivalencemMeasure.java
|
||||
// plugin/visualizer/FlippedTriangles.java
|
||||
// heds/adapter/types/LengthCrossRatio.java
|
||||
// convergence/ConvergenceUtility.java
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "conformal_mesh.hpp"
|
||||
#include "layout.hpp"
|
||||
#include <Eigen/Dense>
|
||||
#include <vector>
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
|
||||
namespace conformallab {
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// LengthCrossRatio — the discrete conformal invariant
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Compute the cross-ratio q = (a·c)/(b·d) of the four edges of a
|
||||
/// quadrilateral formed by two adjacent triangles sharing an edge.
|
||||
/// Input: edge lengths a, b, c, d in order around the quad.
|
||||
/// Returns the cross-ratio q.
|
||||
inline double length_cross_ratio(double a, double b, double c, double d)
|
||||
{
|
||||
const double denom = b * d;
|
||||
if (denom < 1e-16) return 0.0; // degenerate edge
|
||||
return (a * c) / denom;
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// IsothermicityMeasure — pointwise metric anisotropy
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Evaluate the isothermicity measure at a single vertex in a 2-D layout.
|
||||
/// Isothermicity is the local conformality condition: the metric tensor
|
||||
/// is a positive scalar multiple of the identity (no anisotropy).
|
||||
/// Measure: pointwise deviation from a conformal map.
|
||||
/// Returns the anisotropy ratio (1.0 = isotropic / conformal).
|
||||
inline double isothermicity_measure_at_vertex(
|
||||
const ConformalMesh& mesh,
|
||||
Vertex_index v,
|
||||
const Layout2D& layout)
|
||||
{
|
||||
// Collect all halfedges emanating from v.
|
||||
std::vector<Halfedge_index> hs;
|
||||
for (auto h : CGAL::halfedges_around_source(v, mesh))
|
||||
hs.push_back(h);
|
||||
|
||||
if (hs.empty()) return 1.0;
|
||||
|
||||
// Compute metric tensor components at v via edge pairs.
|
||||
// For a conformal map, the metric g = λ²I (λ > 0 scale factor, I identity).
|
||||
// Compute an empirical metric from the layout: edges adjacent to v
|
||||
// span the tangent space.
|
||||
double g11 = 0.0, g12 = 0.0, g22 = 0.0;
|
||||
int n_edges = 0;
|
||||
|
||||
for (std::size_t i = 0; i < hs.size(); ++i) {
|
||||
auto h1 = hs[i];
|
||||
auto h2 = hs[(i + 1) % hs.size()];
|
||||
|
||||
Vertex_index v2 = mesh.target(h1); // = mesh.source(h2)
|
||||
Vertex_index v3 = mesh.target(h2);
|
||||
|
||||
const auto& p1 = layout.uv[v.idx()];
|
||||
const auto& p2 = layout.uv[v2.idx()];
|
||||
const auto& p3 = layout.uv[v3.idx()];
|
||||
|
||||
// Two edge vectors from v.
|
||||
double e1x = p2.x() - p1.x(), e1y = p2.y() - p1.y();
|
||||
double e2x = p3.x() - p1.x(), e2y = p3.y() - p1.y();
|
||||
|
||||
// Metric tensor as outer product (unnormalised).
|
||||
g11 += e1x * e1x;
|
||||
g12 += e1x * e1y;
|
||||
g22 += e1y * e1y;
|
||||
|
||||
// Also accumulate e2 contribution (for a rotationally averaged metric).
|
||||
g11 += e2x * e2x;
|
||||
g12 += e2x * e2y;
|
||||
g22 += e2y * e2y;
|
||||
|
||||
n_edges += 2;
|
||||
}
|
||||
|
||||
if (n_edges <= 0) return 1.0;
|
||||
|
||||
g11 /= n_edges;
|
||||
g12 /= n_edges;
|
||||
g22 /= n_edges;
|
||||
|
||||
// Eigenvalues of g: λ_± = (g11 + g22 ± √((g11-g22)² + 4g12²)) / 2.
|
||||
double trace = g11 + g22;
|
||||
double det = g11 * g22 - g12 * g12;
|
||||
|
||||
if (trace < 1e-16 || det < 1e-16) return 1.0; // degenerate
|
||||
|
||||
double disc = (g11 - g22) * (g11 - g22) + 4.0 * g12 * g12;
|
||||
disc = std::sqrt(disc);
|
||||
|
||||
double lambda_max = (trace + disc) / 2.0;
|
||||
double lambda_min = (trace - disc) / 2.0;
|
||||
|
||||
if (lambda_min < 1e-16) return 1.0; // degenerate
|
||||
|
||||
// Anisotropy: λ_max / λ_min (conformal ⟺ ratio ≈ 1).
|
||||
return lambda_max / lambda_min;
|
||||
}
|
||||
|
||||
/// Compute the isothermicity measure for the entire layout.
|
||||
/// Returns a vector of anisotropy ratios, one per vertex.
|
||||
inline std::vector<double> isothermicity_measure(
|
||||
const ConformalMesh& mesh,
|
||||
const Layout2D& layout)
|
||||
{
|
||||
std::vector<double> result;
|
||||
result.reserve(mesh.number_of_vertices());
|
||||
for (auto v : mesh.vertices())
|
||||
result.push_back(isothermicity_measure_at_vertex(mesh, v, layout));
|
||||
return result;
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// DiscreteConformalEquivalenceMeasure — length-cross-ratio residual
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Evaluate the discrete conformal equivalence condition at a single edge.
|
||||
/// For an edge e = (i,j), form the quad with the two adjacent triangles:
|
||||
/// compute the cross-ratio q from the layout edge lengths.
|
||||
/// The conformal condition is: q + 1/q = 2 (i.e. q = 1, isotropic scaling).
|
||||
/// Measure: |q + 1/q - 2| (residual; 0 = conformal).
|
||||
inline double discrete_conformal_equivalence_at_edge(
|
||||
const ConformalMesh& mesh,
|
||||
Edge_index e,
|
||||
const Layout2D& layout)
|
||||
{
|
||||
// Find the two halfedges for this edge.
|
||||
auto h = mesh.halfedge(e);
|
||||
|
||||
// Get the four vertices of the quad formed by the two adjacent triangles.
|
||||
Vertex_index v1 = mesh.source(h);
|
||||
Vertex_index v2 = mesh.target(h);
|
||||
Vertex_index v3 = mesh.source(mesh.next(h));
|
||||
Vertex_index v4 = mesh.source(mesh.next(mesh.opposite(h)));
|
||||
|
||||
// Compute edge lengths from the layout.
|
||||
auto dist = [&layout](Vertex_index u1, Vertex_index u2) {
|
||||
const auto& p1 = layout.uv[u1.idx()];
|
||||
const auto& p2 = layout.uv[u2.idx()];
|
||||
double dx = p1.x() - p2.x();
|
||||
double dy = p1.y() - p2.y();
|
||||
return std::sqrt(dx * dx + dy * dy);
|
||||
};
|
||||
|
||||
double a = dist(v1, v3); // opposite to v4
|
||||
double b = dist(v1, v4); // opposite to v3
|
||||
double c = dist(v2, v3); // opposite to v4
|
||||
double d = dist(v2, v4); // opposite to v3
|
||||
|
||||
// Cross-ratio q = (a·c)/(b·d).
|
||||
double q = length_cross_ratio(a, b, c, d);
|
||||
|
||||
// Conformal condition: q + 1/q = 2 (only satisfied when q = 1).
|
||||
if (q < 1e-16) return 1.0; // degenerate
|
||||
double residual = q + 1.0 / q - 2.0;
|
||||
return std::abs(residual);
|
||||
}
|
||||
|
||||
/// Compute the discrete conformal equivalence measure for all edges.
|
||||
/// Returns a vector of residuals, one per edge.
|
||||
inline std::vector<double> discrete_conformal_equivalence_measure(
|
||||
const ConformalMesh& mesh,
|
||||
const Layout2D& layout)
|
||||
{
|
||||
std::vector<double> result;
|
||||
result.reserve(mesh.number_of_edges());
|
||||
for (auto e : mesh.edges())
|
||||
result.push_back(discrete_conformal_equivalence_at_edge(mesh, e, layout));
|
||||
return result;
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// FlippedTriangles — embedded validity check
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Check if a single triangle is flipped or degenerate in the 2-D layout.
|
||||
/// A triangle is valid iff its signed area > 0 (positive orientation).
|
||||
/// Degenerate: signed area ≈ 0 (collinear or nearly collinear vertices).
|
||||
/// Returns true if the triangle is flipped or degenerate.
|
||||
inline bool is_flipped_triangle(
|
||||
const ConformalMesh& mesh,
|
||||
Face_index f,
|
||||
const Layout2D& layout)
|
||||
{
|
||||
// Extract the three vertices of the triangle.
|
||||
auto h = mesh.halfedge(f);
|
||||
Vertex_index v1 = mesh.source(h);
|
||||
Vertex_index v2 = mesh.source(mesh.next(h));
|
||||
Vertex_index v3 = mesh.source(mesh.next(mesh.next(h)));
|
||||
|
||||
const auto& p1 = layout.uv[v1.idx()];
|
||||
const auto& p2 = layout.uv[v2.idx()];
|
||||
const auto& p3 = layout.uv[v3.idx()];
|
||||
|
||||
// Signed area (× 2): (p2 - p1) × (p3 - p1) in ℝ².
|
||||
double signed_area_2x = (p2.x() - p1.x()) * (p3.y() - p1.y())
|
||||
- (p2.y() - p1.y()) * (p3.x() - p1.x());
|
||||
|
||||
// Positive area: valid orientation. Zero or negative: flipped/degenerate.
|
||||
return signed_area_2x <= 1e-14;
|
||||
}
|
||||
|
||||
/// Count the number of flipped or degenerate triangles in the layout.
|
||||
/// Returns the count (0 = valid layout).
|
||||
inline int flipped_triangles(
|
||||
const ConformalMesh& mesh,
|
||||
const Layout2D& layout)
|
||||
{
|
||||
int count = 0;
|
||||
for (auto f : mesh.faces())
|
||||
if (is_flipped_triangle(mesh, f, layout))
|
||||
count++;
|
||||
return count;
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// ConvergenceUtility — aggregated convergence measures
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Aggregated cross-ratio statistics for a layout.
|
||||
struct CrossRatioStats {
|
||||
double max_cross_ratio; ///< max of (q + 1/q) over all edges
|
||||
double mean_cross_ratio; ///< mean of (q + 1/q)
|
||||
double sum_cross_ratio; ///< sum of (q + 1/q)
|
||||
|
||||
double max_multi_ratio; ///< max per-face product of cross-ratios
|
||||
double mean_multi_ratio; ///< mean per-face product
|
||||
double sum_multi_ratio; ///< sum of per-face products
|
||||
|
||||
double max_scale_invariant_circumradius; ///< max of R/√A per face
|
||||
double mean_scale_invariant_circumradius; ///< mean of R/√A
|
||||
double sum_scale_invariant_circumradius; ///< sum of R/√A
|
||||
};
|
||||
|
||||
/// Compute convergence statistics for a layout.
|
||||
/// - Cross-ratio (q + 1/q) per edge; aggregated max/mean/sum.
|
||||
/// - Multi-ratio: per-face product ∏(q + 1/q) for the 3 edges of each face.
|
||||
/// (Multi-ratio = 1 iff all edges are conformal.)
|
||||
/// - Scale-invariant circumradius: R/√A per face (mesh quality metric).
|
||||
inline CrossRatioStats convergence_utility(
|
||||
const ConformalMesh& mesh,
|
||||
const Layout2D& layout)
|
||||
{
|
||||
CrossRatioStats stats = {};
|
||||
|
||||
std::vector<double> cross_ratios;
|
||||
std::vector<double> multi_ratios;
|
||||
std::vector<double> scale_inv_circumradii;
|
||||
|
||||
auto dist = [&layout](Vertex_index u1, Vertex_index u2) {
|
||||
const auto& p1 = layout.uv[u1.idx()];
|
||||
const auto& p2 = layout.uv[u2.idx()];
|
||||
double dx = p1.x() - p2.x();
|
||||
double dy = p1.y() - p2.y();
|
||||
return std::sqrt(dx * dx + dy * dy);
|
||||
};
|
||||
|
||||
// Per-face metrics.
|
||||
for (auto f : mesh.faces()) {
|
||||
auto h = mesh.halfedge(f);
|
||||
Vertex_index v1 = mesh.source(h);
|
||||
Vertex_index v2 = mesh.source(mesh.next(h));
|
||||
Vertex_index v3 = mesh.source(mesh.next(mesh.next(h)));
|
||||
|
||||
const auto& p1 = layout.uv[v1.idx()];
|
||||
const auto& p2 = layout.uv[v2.idx()];
|
||||
const auto& p3 = layout.uv[v3.idx()];
|
||||
|
||||
// Signed area.
|
||||
double signed_area_2x = (p2.x() - p1.x()) * (p3.y() - p1.y())
|
||||
- (p2.y() - p1.y()) * (p3.x() - p1.x());
|
||||
double area = std::abs(signed_area_2x) / 2.0;
|
||||
|
||||
if (area < 1e-16) continue; // degenerate
|
||||
|
||||
// Three edge lengths of the triangle.
|
||||
double a = dist(v1, v2);
|
||||
double b = dist(v2, v3);
|
||||
double c = dist(v3, v1);
|
||||
|
||||
// Circumradius R = abc / (4·Area).
|
||||
double circum_radius = (a * b * c) / (4.0 * area);
|
||||
|
||||
// Scale-invariant: R / √A.
|
||||
double scale_inv_cr = circum_radius / std::sqrt(area);
|
||||
scale_inv_circumradii.push_back(scale_inv_cr);
|
||||
|
||||
// Three cross-ratios (per edge/angle of the triangle).
|
||||
// For each edge, form the quad with the opposite vertex and its neighbors.
|
||||
double multi_product = 1.0;
|
||||
for (int ei = 0; ei < 3; ++ei) {
|
||||
auto he = mesh.halfedge(f);
|
||||
for (int k = 0; k < ei; ++k) he = mesh.next(he);
|
||||
|
||||
Vertex_index eu1 = mesh.source(he);
|
||||
Vertex_index eu2 = mesh.target(he);
|
||||
Vertex_index eu3 = mesh.source(mesh.next(he));
|
||||
Vertex_index eu4 = mesh.source(mesh.next(mesh.opposite(he)));
|
||||
|
||||
double ea = dist(eu1, eu3);
|
||||
double eb = dist(eu1, eu4);
|
||||
double ec = dist(eu2, eu3);
|
||||
double ed = dist(eu2, eu4);
|
||||
|
||||
double q = length_cross_ratio(ea, eb, ec, ed);
|
||||
if (q > 1e-16) {
|
||||
double qf = q + 1.0 / q;
|
||||
cross_ratios.push_back(qf);
|
||||
multi_product *= qf;
|
||||
}
|
||||
}
|
||||
multi_ratios.push_back(multi_product);
|
||||
}
|
||||
|
||||
// Aggregate statistics.
|
||||
if (!cross_ratios.empty()) {
|
||||
auto [min_it, max_it] = std::minmax_element(cross_ratios.begin(), cross_ratios.end());
|
||||
stats.max_cross_ratio = *max_it;
|
||||
stats.mean_cross_ratio = 0.0;
|
||||
for (double v : cross_ratios) stats.mean_cross_ratio += v;
|
||||
stats.mean_cross_ratio /= static_cast<double>(cross_ratios.size());
|
||||
stats.sum_cross_ratio = 0.0;
|
||||
for (double v : cross_ratios) stats.sum_cross_ratio += v;
|
||||
}
|
||||
|
||||
if (!multi_ratios.empty()) {
|
||||
auto [min_it, max_it] = std::minmax_element(multi_ratios.begin(), multi_ratios.end());
|
||||
stats.max_multi_ratio = *max_it;
|
||||
stats.mean_multi_ratio = 0.0;
|
||||
for (double v : multi_ratios) stats.mean_multi_ratio += v;
|
||||
stats.mean_multi_ratio /= static_cast<double>(multi_ratios.size());
|
||||
stats.sum_multi_ratio = 0.0;
|
||||
for (double v : multi_ratios) stats.sum_multi_ratio += v;
|
||||
}
|
||||
|
||||
if (!scale_inv_circumradii.empty()) {
|
||||
auto [min_it, max_it] = std::minmax_element(scale_inv_circumradii.begin(),
|
||||
scale_inv_circumradii.end());
|
||||
stats.max_scale_invariant_circumradius = *max_it;
|
||||
stats.mean_scale_invariant_circumradius = 0.0;
|
||||
for (double v : scale_inv_circumradii)
|
||||
stats.mean_scale_invariant_circumradius += v;
|
||||
stats.mean_scale_invariant_circumradius /= static_cast<double>(scale_inv_circumradii.size());
|
||||
stats.sum_scale_invariant_circumradius = 0.0;
|
||||
for (double v : scale_inv_circumradii)
|
||||
stats.sum_scale_invariant_circumradius += v;
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
} // namespace conformallab
|
||||
@@ -44,7 +44,7 @@
|
||||
// double gauss_bonnet_rhs(mesh) — 2π · χ(M)
|
||||
// double gauss_bonnet_deficit(mesh, maps) — lhs − rhs (0 = satisfied)
|
||||
// void check_gauss_bonnet(mesh, maps [, tol]) — throws if violated
|
||||
// double enforce_gauss_bonnet(mesh, maps) — shifts θ_v by uniform Δ; returns |deficit|
|
||||
// void enforce_gauss_bonnet(mesh, maps) — shifts θ_v by uniform Δ
|
||||
// (HyperIdealMaps overloads are deleted — see box above)
|
||||
|
||||
#include "conformal_mesh.hpp"
|
||||
@@ -162,16 +162,11 @@ inline void check_gauss_bonnet(const ConformalMesh& mesh,
|
||||
// After this call, check_gauss_bonnet() will not throw (up to floating-point).
|
||||
// Modifies ALL vertices' θ_v (no v_idx filtering) — the shift is a property
|
||||
// of the target angles, independent of which vertices are free DOFs.
|
||||
//
|
||||
// H3 (test-coverage audit, 2026-06-01): both overloads now return the total
|
||||
// absolute correction applied: |Σ(2π−Θ_v) − 2π·χ|. A large value signals
|
||||
// that the input angles were far from satisfying Gauss–Bonnet.
|
||||
|
||||
/// Distribute the Gauss-Bonnet deficit uniformly across all `Θ_v`:
|
||||
/// add `δ = (lhs − rhs) / V` to every entry so that the identity holds
|
||||
/// exactly afterwards. Overload for a raw property map.
|
||||
/// Returns `|lhs − rhs|` (total absolute correction applied).
|
||||
inline double enforce_gauss_bonnet(
|
||||
inline void enforce_gauss_bonnet(
|
||||
ConformalMesh& mesh,
|
||||
ConformalMesh::Property_map<Vertex_index, double>& theta)
|
||||
{
|
||||
@@ -182,17 +177,15 @@ inline double enforce_gauss_bonnet(
|
||||
double delta = (lhs - rhs) / static_cast<double>(mesh.number_of_vertices());
|
||||
for (auto v : mesh.vertices())
|
||||
theta[v] += delta;
|
||||
return std::abs(lhs - rhs);
|
||||
}
|
||||
|
||||
/// Distribute the Gauss-Bonnet deficit uniformly across `maps.theta_v`.
|
||||
/// Supported for EuclideanMaps and SphericalMaps only.
|
||||
/// HyperIdealMaps overload is deleted — see header comment for why.
|
||||
/// Returns `|lhs − rhs|` (total absolute correction applied; see raw-map overload).
|
||||
template <typename Maps>
|
||||
inline double enforce_gauss_bonnet(ConformalMesh& mesh, Maps& maps)
|
||||
inline void enforce_gauss_bonnet(ConformalMesh& mesh, Maps& maps)
|
||||
{
|
||||
return enforce_gauss_bonnet(mesh, maps.theta_v);
|
||||
enforce_gauss_bonnet(mesh, maps.theta_v);
|
||||
}
|
||||
|
||||
// enforce_gauss_bonnet for HyperIdealMaps is intentionally DELETED.
|
||||
|
||||
@@ -374,8 +374,8 @@ static FaceAngles compute_face_angles(
|
||||
/// Per-face energy contribution U(f) before subtracting the θ·a and Θ·b terms.
|
||||
///
|
||||
/// Supported configurations (faithful port of HyperIdealFunctional.java):
|
||||
/// * All three vertices hyper-ideal (v?b = true) → Ushijima 2006 volume
|
||||
/// * Exactly one vertex ideal (v?b = false, other two true) → Springborn 2008 volume
|
||||
/// * All three vertices hyper-ideal (v?b = true) → Meyerhoff/Ushijima volume
|
||||
/// * Exactly one vertex ideal (v?b = false, other two true) → Kolpakov-Mednykh volume
|
||||
///
|
||||
/// NOT supported — faces with two or three ideal vertices. The Java reference
|
||||
/// (HyperIdealFunctional.java lines 222-231) uses an if/else-if chain that
|
||||
|
||||
@@ -16,8 +16,7 @@
|
||||
namespace conformallab {
|
||||
|
||||
/// Volume of a generalized hyperbolic tetrahedron with dihedral
|
||||
/// angles `A,…,F` via the Ushijima 2006 formula (DOI 10.1007/0-387-29555-0_13,
|
||||
/// arxiv math/0309216). Note: sole author is Ushijima; "Meyerhoff" is not an author.
|
||||
/// angles `A,…,F` via the Meyerhoff / Ushijima 2006 formula.
|
||||
/// Same as Java `HyperIdealUtility.calculateTetrahedronVolume()`.
|
||||
inline double calculateTetrahedronVolume(double A, double B, double C,
|
||||
double D, double E, double F) {
|
||||
@@ -78,7 +77,7 @@ inline double calculateTetrahedronVolume(double A, double B, double C,
|
||||
}
|
||||
|
||||
/// Volume of a hyperideal tetrahedron with one ideal vertex at γ via
|
||||
/// the Springborn 2008 formula (arxiv math/0603097). Same as Java
|
||||
/// the Kolpakov-Mednykh formula (arxiv math/0603097). Same as Java
|
||||
/// `HyperIdealUtility.calculateTetrahedronVolumeWithIdealVertexAtGamma()`.
|
||||
inline double calculateTetrahedronVolumeWithIdealVertexAtGamma(
|
||||
double gamma1, double gamma2, double gamma3,
|
||||
|
||||
@@ -264,27 +264,6 @@ inline void save_result_xml(
|
||||
/// Load a DOF vector from an XML result file written by
|
||||
/// `save_result_xml`. If `res`, `geom`, `layout2d` are non-null they
|
||||
/// are filled as well.
|
||||
///
|
||||
/// V5 (input-validation audit, 2026-06-01): this reader implements a
|
||||
/// **strict internal-only XML subset** — not a general XML parser. It
|
||||
/// expects the exact one-element-per-line layout written by
|
||||
/// `save_result_xml`. Files that are semantically equivalent XML but
|
||||
/// formatted differently (attributes split across lines, extra
|
||||
/// whitespace, XML declaration on its own line, etc.) are explicitly
|
||||
/// *rejected* with `std::runtime_error` rather than silently mis-read
|
||||
/// into zeros. Interoperability with other XML producers is out of
|
||||
/// scope; use the JSON format for that.
|
||||
///
|
||||
/// Strict-subset requirements that are validated:
|
||||
/// 1. A line containing `<ConformalResult` must also carry a `geometry=`
|
||||
/// attribute on the same line.
|
||||
/// 2. A line containing `<Solver` must carry `iterations=` and
|
||||
/// `grad_inf_norm=` on the same line (when `res` is non-null).
|
||||
/// 3. A line containing `<DOFVector` must carry the `>` character (tag
|
||||
/// open) on the same line.
|
||||
/// 4. The `<DOFVector` element must be present and must produce a
|
||||
/// non-empty doubles list (a missing DOFVector element causes a
|
||||
/// `std::runtime_error` — enforced after the parse loop).
|
||||
inline std::vector<double> load_result_xml(
|
||||
const std::string& path,
|
||||
NewtonResult* res = nullptr,
|
||||
@@ -296,26 +275,13 @@ inline std::vector<double> load_result_xml(
|
||||
|
||||
std::vector<double> x;
|
||||
std::string line;
|
||||
bool found_root = false;
|
||||
bool found_dofvector = false;
|
||||
|
||||
while (std::getline(ifs, line)) {
|
||||
// Root element — V5: geometry attribute must be on the same line.
|
||||
// Root element
|
||||
if (line.find("<ConformalResult") != std::string::npos) {
|
||||
found_root = true;
|
||||
// V5: reject if the required geometry= attribute is absent on this line.
|
||||
// (Would be present if written by save_result_xml; absent if reformatted.)
|
||||
std::string g = detail_xml::xml_get_attr(line, "geometry");
|
||||
if (g.empty())
|
||||
throw std::runtime_error(
|
||||
"conformallab: XML strict-subset violation in " + path
|
||||
+ ": <ConformalResult geometry=...> attribute not found on its"
|
||||
" opening line. Only the format written by save_result_xml is"
|
||||
" supported — reformatted XML is rejected to prevent silent"
|
||||
" misreads. Use the JSON format for interoperability.");
|
||||
if (geom) *geom = g;
|
||||
if (geom) *geom = detail_xml::xml_get_attr(line, "geometry");
|
||||
}
|
||||
// Solver metadata — V5: required attributes must be on the same line.
|
||||
// Solver metadata
|
||||
else if (line.find("<Solver") != std::string::npos) {
|
||||
if (res) {
|
||||
res->converged = (detail_xml::xml_get_attr(line, "converged") == "true");
|
||||
@@ -337,17 +303,10 @@ inline std::vector<double> load_result_xml(
|
||||
}
|
||||
}
|
||||
}
|
||||
// DOF vector — V5: the '>' tag-open must be on the same line.
|
||||
// DOF vector
|
||||
else if (line.find("<DOFVector") != std::string::npos) {
|
||||
found_dofvector = true;
|
||||
// V5: require the tag to be closed ('>') on the same line so the
|
||||
// content-extraction below works correctly.
|
||||
// Text may be on same line: <DOFVector n="...">0 1 2...</DOFVector>
|
||||
auto open_end = line.find('>');
|
||||
if (open_end == std::string::npos)
|
||||
throw std::runtime_error(
|
||||
"conformallab: XML strict-subset violation in " + path
|
||||
+ ": <DOFVector> opening '>' not on same line as tag."
|
||||
" Only the format written by save_result_xml is supported.");
|
||||
auto close = line.find("</DOFVector>");
|
||||
std::string text;
|
||||
if (close != std::string::npos) {
|
||||
@@ -371,53 +330,7 @@ inline std::vector<double> load_result_xml(
|
||||
layout2d->success = true;
|
||||
}
|
||||
}
|
||||
|
||||
// V5: if the file was non-empty but never produced a <ConformalResult> root
|
||||
// element, the file is likely reformatted or not a ConformalResult XML at all.
|
||||
if (!found_root) {
|
||||
// Distinguish "empty file" (ifs.peek() == EOF at open) from wrong format.
|
||||
// We re-open to check file size — if it had content but no root element
|
||||
// was found on a single line, it was reformatted.
|
||||
std::ifstream probe(path, std::ios::ate);
|
||||
if (probe && probe.tellg() > 0)
|
||||
throw std::runtime_error(
|
||||
"conformallab: XML strict-subset violation in " + path
|
||||
+ ": <ConformalResult> root element not found on its own line."
|
||||
" Only the format written by save_result_xml is supported.");
|
||||
}
|
||||
|
||||
// V5 rule 4: <DOFVector> must be present in every well-formed ConformalResult.
|
||||
if (found_root && !found_dofvector)
|
||||
throw std::runtime_error(
|
||||
"conformallab: XML strict-subset violation in " + path
|
||||
+ ": <DOFVector> element not found. Only the format written by"
|
||||
" save_result_xml is supported.");
|
||||
|
||||
return x;
|
||||
}
|
||||
|
||||
/// Validate that a loaded DOF vector has the expected number of DOFs.
|
||||
///
|
||||
/// V6 (input-validation audit, 2026-06-01): a result file from a *different*
|
||||
/// mesh loads happily; the size mismatch only surfaces later (out-of-bounds
|
||||
/// or wrong-answer) when `x` is indexed against the new mesh. This helper
|
||||
/// provides a clear early check at the call-site where the loaded vector is
|
||||
/// paired with the mesh.
|
||||
///
|
||||
/// Throws `std::runtime_error` if `x.size() != expected_dofs`.
|
||||
inline void check_dof_vector_size(
|
||||
const std::vector<double>& x,
|
||||
int expected_dofs,
|
||||
const std::string& context = "")
|
||||
{
|
||||
if (static_cast<int>(x.size()) != expected_dofs) {
|
||||
std::ostringstream msg;
|
||||
msg << "conformallab: DOF-vector size mismatch";
|
||||
if (!context.empty()) msg << " in " << context;
|
||||
msg << ": loaded " << x.size()
|
||||
<< " values but mesh has " << expected_dofs << " DOFs.";
|
||||
throw std::runtime_error(msg.str());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace conformallab
|
||||
|
||||
@@ -1,203 +0,0 @@
|
||||
// Copyright (c) 2024-2026 Tarik Moussa.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// stereographic_layout.hpp
|
||||
//
|
||||
// Phase 9d.3 — Stereographic projection for spherical DCE output.
|
||||
//
|
||||
// Converts a spherical layout (points on S²) to a 2-D conformal map via:
|
||||
// 1. Stereographic projection: S² → ℂ ∪ {∞}, mapping the sphere to the complex plane.
|
||||
// 2. Möbius centring: centres the resulting point cloud for canonical position.
|
||||
//
|
||||
// Mathematical reference:
|
||||
// Stereographic projection from the north pole (0,0,1):
|
||||
// (x,y,z) ↦ (x/(1-z), y/(1-z)) in ℂ (complex coordinate u+iv).
|
||||
// North pole (0,0,1) maps to ∞ (removed from the layout).
|
||||
// South pole (0,0,-1) maps to (0,0) in ℂ.
|
||||
// The projection is conformal (angle-preserving).
|
||||
//
|
||||
// Möbius centring: apply a Möbius transformation to centre the layout
|
||||
// (e.g. shift the centroid to the origin, possibly scale/rotate).
|
||||
//
|
||||
// Java source (ported from):
|
||||
// unwrapper/StereographicUnwrapper.java (266 lines)
|
||||
// The supporting math/CP1 + ComplexUtility.stereographic operations
|
||||
// (deliberately NOT ported — redundant with std::complex).
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "conformal_mesh.hpp"
|
||||
#include "layout.hpp"
|
||||
#include <complex>
|
||||
#include <vector>
|
||||
#include <cmath>
|
||||
#include <array>
|
||||
|
||||
namespace conformallab {
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// Stereographic Projection: S² → ℂ
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Stereographic projection from the north pole (0, 0, 1).
|
||||
/// Maps a point on the unit sphere S² to the complex plane ℂ.
|
||||
/// North pole (0,0,1) projects to ∞ (not representable; returns NaN).
|
||||
/// South pole (0,0,-1) projects to 0+0i.
|
||||
///
|
||||
/// Formula: (x,y,z) ↦ x/(1-z) + i·y/(1-z)
|
||||
inline std::complex<double> stereographic_project(double x, double y, double z)
|
||||
{
|
||||
const double denom = 1.0 - z;
|
||||
if (std::abs(denom) < 1e-15) {
|
||||
// North pole (z ≈ 1) — maps to ∞.
|
||||
// Return NaN to signal infinity.
|
||||
return std::complex<double>(std::nan(""), std::nan(""));
|
||||
}
|
||||
return std::complex<double>(x / denom, y / denom);
|
||||
}
|
||||
|
||||
/// Stereographic projection of a 3-D point (as Point3).
|
||||
inline std::complex<double> stereographic_project(const Point3& p)
|
||||
{
|
||||
return stereographic_project(p.x(), p.y(), p.z());
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// Möbius Centring
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Simple centring: translate the point cloud so that its centroid
|
||||
/// is at the origin (u+iv = 0).
|
||||
inline void centre_at_origin(std::vector<std::complex<double>>& points)
|
||||
{
|
||||
if (points.empty()) return;
|
||||
|
||||
// Compute centroid.
|
||||
std::complex<double> centroid(0.0, 0.0);
|
||||
int n_valid = 0;
|
||||
for (const auto& z : points) {
|
||||
if (std::isfinite(z.real()) && std::isfinite(z.imag())) {
|
||||
centroid += z;
|
||||
n_valid++;
|
||||
}
|
||||
}
|
||||
if (n_valid <= 0) return;
|
||||
|
||||
centroid /= static_cast<double>(n_valid);
|
||||
|
||||
// Translate: z' = z - centroid.
|
||||
for (auto& z : points) {
|
||||
if (std::isfinite(z.real()) && std::isfinite(z.imag())) {
|
||||
z -= centroid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// Stereographic Layout: S² → ℂ (2-D)
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Convert a spherical layout (3-D points on S²) to a 2-D conformal map
|
||||
/// via stereographic projection.
|
||||
///
|
||||
/// Output: a Layout2D where:
|
||||
/// - uv[v.idx()] = (Re, Im) of the stereographic projection of the 3-D point.
|
||||
/// - The north pole is excluded (uv[v] = NaN for projections at ∞).
|
||||
///
|
||||
/// Möbius centring: the resulting layout is centred at the origin.
|
||||
///
|
||||
/// \param mesh Input surface mesh.
|
||||
/// \param layout Input spherical layout (3-D points on S²).
|
||||
/// \return Output Layout2D in the complex plane (ℂ).
|
||||
inline Layout2D stereographic_layout(
|
||||
const ConformalMesh& mesh,
|
||||
const Layout3D& layout)
|
||||
{
|
||||
Layout2D result;
|
||||
result.uv.resize(mesh.number_of_vertices());
|
||||
result.halfedge_uv.resize(mesh.number_of_halfedges());
|
||||
|
||||
// Step 1: Stereographic projection for each vertex.
|
||||
std::vector<std::complex<double>> complex_points;
|
||||
complex_points.reserve(mesh.number_of_vertices());
|
||||
|
||||
for (auto v : mesh.vertices()) {
|
||||
const auto& p3d = layout.pos[v.idx()];
|
||||
// Convert Eigen::Vector3d to Point3-like coordinates.
|
||||
double x = p3d[0], y = p3d[1], z = p3d[2];
|
||||
auto z_complex = stereographic_project(x, y, z);
|
||||
complex_points.push_back(z_complex);
|
||||
|
||||
// Store as Eigen::Vector2d (Re, Im).
|
||||
result.uv[v.idx()] = Eigen::Vector2d(z_complex.real(), z_complex.imag());
|
||||
}
|
||||
|
||||
// Step 2: Möbius centring.
|
||||
centre_at_origin(complex_points);
|
||||
|
||||
// Update uv after centring.
|
||||
for (auto v : mesh.vertices()) {
|
||||
const auto& z = complex_points[v.idx()];
|
||||
result.uv[v.idx()] = Eigen::Vector2d(z.real(), z.imag());
|
||||
}
|
||||
|
||||
// Step 3: Halfedge UV (for texture atlasing).
|
||||
// Copy the primary vertex UV to each halfedge's source.
|
||||
for (auto h : mesh.halfedges()) {
|
||||
Vertex_index src = mesh.source(h);
|
||||
result.halfedge_uv[h.idx()] = result.uv[src.idx()];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// Inverse Stereographic Projection: ℂ → S²
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Inverse stereographic projection: ℂ → S².
|
||||
/// Given a complex number z = u + iv, recover the 3-D point on the unit sphere.
|
||||
///
|
||||
/// Formula: (u,v) ↦ (2u/(1+u²+v²), 2v/(1+u²+v²), (u²+v²-1)/(u²+v²+1))
|
||||
/// Inverse of: (x,y,z) ↦ (x/(1-z), y/(1-z)).
|
||||
///
|
||||
/// The origin (u,v) = (0,0) maps back to (0,0,-1) (south pole).
|
||||
inline Point3 inverse_stereographic_project(std::complex<double> z)
|
||||
{
|
||||
double u = z.real();
|
||||
double v = z.imag();
|
||||
|
||||
double u2_plus_v2 = u * u + v * v;
|
||||
double denom = 1.0 + u2_plus_v2;
|
||||
|
||||
double x = 2.0 * u / denom;
|
||||
double y = 2.0 * v / denom;
|
||||
double zz = (u2_plus_v2 - 1.0) / denom;
|
||||
|
||||
return Point3(x, y, zz);
|
||||
}
|
||||
|
||||
/// Inverse stereographic projection from a 2-D layout point.
|
||||
inline Point3 inverse_stereographic_project(const Eigen::Vector2d& uv)
|
||||
{
|
||||
return inverse_stereographic_project(std::complex<double>(uv.x(), uv.y()));
|
||||
}
|
||||
|
||||
/// Round-trip validation: project a 3-D point to 2-D and back.
|
||||
/// Returns the error (distance on S²) between the original and recovered point.
|
||||
inline double stereographic_roundtrip_error(const Point3& original)
|
||||
{
|
||||
auto z = stereographic_project(original);
|
||||
if (!std::isfinite(z.real()) || !std::isfinite(z.imag())) {
|
||||
return std::numeric_limits<double>::infinity(); // north pole
|
||||
}
|
||||
auto recovered = inverse_stereographic_project(z);
|
||||
|
||||
// Distance on the unit sphere: ‖p - q‖.
|
||||
double dx = original.x() - recovered.x();
|
||||
double dy = original.y() - recovered.y();
|
||||
double dz = original.z() - recovered.z();
|
||||
return std::sqrt(dx * dx + dy * dy + dz * dz);
|
||||
}
|
||||
|
||||
} // namespace conformallab
|
||||
@@ -28,8 +28,6 @@
|
||||
#include "euclidean_functional.hpp"
|
||||
#include "spherical_functional.hpp"
|
||||
#include "hyper_ideal_functional.hpp"
|
||||
#include "cp_euclidean_functional.hpp"
|
||||
#include "inversive_distance_functional.hpp"
|
||||
#include "newton_solver.hpp"
|
||||
#include "layout.hpp"
|
||||
#include "serialization.hpp"
|
||||
@@ -130,9 +128,7 @@ static int run_euclidean(ConformalMesh& mesh,
|
||||
const std::string& out_layout,
|
||||
const std::string& out_json,
|
||||
const std::string& out_xml,
|
||||
bool verbose,
|
||||
double tol = 1e-8,
|
||||
int max_iter = 200)
|
||||
bool verbose)
|
||||
{
|
||||
// Setup — Θ_v = 2π (flat target) by default; lengths from the input mesh.
|
||||
auto maps = cl::setup_euclidean_maps(mesh);
|
||||
@@ -154,7 +150,7 @@ static int run_euclidean(ConformalMesh& mesh,
|
||||
|
||||
// Newton — starts at x0 = 0, which is NOT the solution in general.
|
||||
std::vector<double> x0(static_cast<std::size_t>(n), 0.0);
|
||||
auto res = cl::newton_euclidean(mesh, x0, maps, tol, max_iter);
|
||||
auto res = cl::newton_euclidean(mesh, x0, maps);
|
||||
|
||||
if (!res.converged)
|
||||
std::cerr << "[warn] Newton did not converge (|grad|="
|
||||
@@ -224,9 +220,7 @@ static int run_spherical(ConformalMesh& mesh,
|
||||
const std::string& out_layout,
|
||||
const std::string& out_json,
|
||||
const std::string& out_xml,
|
||||
bool verbose,
|
||||
double tol = 1e-8,
|
||||
int max_iter = 200)
|
||||
bool verbose)
|
||||
{
|
||||
// Spherical uniformisation targets a closed genus-0 surface (sphere).
|
||||
for (auto v : mesh.vertices())
|
||||
@@ -244,7 +238,7 @@ static int run_spherical(ConformalMesh& mesh,
|
||||
int n = cl::assign_spherical_vertex_dof_indices(mesh, maps);
|
||||
|
||||
std::vector<double> x0(static_cast<std::size_t>(n), 0.0);
|
||||
auto res = cl::newton_spherical(mesh, x0, maps, tol, max_iter);
|
||||
auto res = cl::newton_spherical(mesh, x0, maps);
|
||||
|
||||
if (!res.converged && verbose)
|
||||
std::cerr << "[warn] Newton did not converge (|grad|=" << res.grad_inf_norm << ")\n";
|
||||
@@ -280,9 +274,7 @@ static int run_hyper_ideal(ConformalMesh& mesh,
|
||||
const std::string& out_layout,
|
||||
const std::string& out_json,
|
||||
const std::string& out_xml,
|
||||
bool verbose,
|
||||
double tol = 1e-8,
|
||||
int max_iter = 200)
|
||||
bool verbose)
|
||||
{
|
||||
auto maps = cl::setup_hyper_ideal_maps(mesh);
|
||||
int n = cl::assign_hyper_ideal_all_dof_indices(mesh, maps);
|
||||
@@ -294,7 +286,7 @@ static int run_hyper_ideal(ConformalMesh& mesh,
|
||||
std::vector<double> x0 = xbase;
|
||||
for (auto& v : x0) v += 0.3;
|
||||
|
||||
auto res = cl::newton_hyper_ideal(mesh, x0, maps, tol, max_iter);
|
||||
auto res = cl::newton_hyper_ideal(mesh, x0, maps);
|
||||
|
||||
if (!res.converged && verbose)
|
||||
std::cerr << "[warn] Newton did not converge (|grad|=" << res.grad_inf_norm << ")\n";
|
||||
@@ -323,134 +315,6 @@ static int run_hyper_ideal(ConformalMesh& mesh,
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// CP-Euclidean pipeline
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
static int run_cp_euclidean(ConformalMesh& mesh,
|
||||
const std::string& out_layout,
|
||||
const std::string& out_json,
|
||||
const std::string& out_xml,
|
||||
bool verbose,
|
||||
double tol = 1e-8,
|
||||
int max_iter = 200)
|
||||
{
|
||||
// Setup CP-Euclidean maps with face-based DOFs.
|
||||
auto maps = cl::setup_cp_euclidean_maps(mesh);
|
||||
cl::compute_cp_euclidean_lambda0_from_mesh(mesh, maps);
|
||||
|
||||
// Assign face DOFs — pin one face and index the rest.
|
||||
int n = cl::assign_cp_euclidean_face_dof_indices(mesh, maps);
|
||||
if (n <= 0) { std::cerr << "Error: no free faces to solve for.\n"; return 1; }
|
||||
|
||||
if (verbose) {
|
||||
std::cout << " CP-Euclidean: face-based DOFs=" << n << "\n";
|
||||
}
|
||||
|
||||
// Natural theta: set target angles from initial configuration.
|
||||
std::vector<double> x0(static_cast<std::size_t>(n), 0.0);
|
||||
auto G0 = cl::evaluate_cp_euclidean(mesh, x0, maps, false).gradient;
|
||||
for (auto f : mesh.faces()) {
|
||||
int ifidx = maps.f_idx[f];
|
||||
if (ifidx >= 0)
|
||||
maps.theta_f[f] -= G0[static_cast<std::size_t>(ifidx)];
|
||||
}
|
||||
|
||||
// Newton solve.
|
||||
auto res = cl::newton_cp_euclidean(mesh, x0, maps, tol, max_iter);
|
||||
|
||||
if (!res.converged && verbose)
|
||||
std::cerr << "[warn] Newton did not converge (|grad|=" << res.grad_inf_norm << ")\n";
|
||||
|
||||
// Layout — circle-pattern embedding.
|
||||
cl::Layout2D layout = cl::cp_euclidean_layout(mesh, res.x, maps);
|
||||
|
||||
// Output
|
||||
if (!out_layout.empty()) cl::save_layout_off(out_layout, mesh, layout);
|
||||
if (!out_json.empty())
|
||||
cl::save_result_json(out_json, res, "cp_euclidean",
|
||||
static_cast<int>(mesh.number_of_vertices()),
|
||||
static_cast<int>(mesh.number_of_faces()),
|
||||
&layout);
|
||||
if (!out_xml.empty())
|
||||
cl::save_result_xml(out_xml, res, "cp_euclidean",
|
||||
static_cast<int>(mesh.number_of_vertices()),
|
||||
static_cast<int>(mesh.number_of_faces()),
|
||||
&layout);
|
||||
|
||||
std::cout << "CP-Euclidean: converged=" << (res.converged ? "yes" : "no")
|
||||
<< " iter=" << res.iterations
|
||||
<< " |grad|_inf=" << std::scientific << std::setprecision(3)
|
||||
<< res.grad_inf_norm << "\n";
|
||||
if (!out_layout.empty()) std::cout << " layout → " << out_layout << "\n";
|
||||
if (!out_json.empty()) std::cout << " json → " << out_json << "\n";
|
||||
if (!out_xml.empty()) std::cout << " xml → " << out_xml << "\n";
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Inversive-Distance pipeline
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
static int run_inversive_distance(ConformalMesh& mesh,
|
||||
const std::string& out_layout,
|
||||
const std::string& out_json,
|
||||
const std::string& out_xml,
|
||||
bool verbose,
|
||||
double tol = 1e-8,
|
||||
int max_iter = 200)
|
||||
{
|
||||
// Setup Inversive-Distance maps with vertex-based DOFs.
|
||||
auto maps = cl::setup_inversive_distance_maps(mesh);
|
||||
cl::compute_inversive_distance_lambda0_from_mesh(mesh, maps);
|
||||
|
||||
// Assign vertex DOFs.
|
||||
int n = cl::assign_inversive_distance_vertex_dof_indices(mesh, maps);
|
||||
if (n <= 0) { std::cerr << "Error: no free vertices to solve for.\n"; return 1; }
|
||||
|
||||
if (verbose) {
|
||||
std::cout << " Inversive-Distance: vertex DOFs=" << n << "\n";
|
||||
}
|
||||
|
||||
// Natural theta: set target angles from initial configuration.
|
||||
std::vector<double> x0(static_cast<std::size_t>(n), 0.0);
|
||||
auto G0 = cl::evaluate_inversive_distance(mesh, x0, maps, false).gradient;
|
||||
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)];
|
||||
}
|
||||
|
||||
// Newton solve.
|
||||
auto res = cl::newton_inversive_distance(mesh, x0, maps, tol, max_iter);
|
||||
|
||||
if (!res.converged && verbose)
|
||||
std::cerr << "[warn] Newton did not converge (|grad|=" << res.grad_inf_norm << ")\n";
|
||||
|
||||
// Layout.
|
||||
cl::Layout2D layout = cl::inversive_distance_layout(mesh, res.x, maps);
|
||||
|
||||
// Output
|
||||
if (!out_layout.empty()) cl::save_layout_off(out_layout, mesh, layout);
|
||||
if (!out_json.empty())
|
||||
cl::save_result_json(out_json, res, "inversive_distance",
|
||||
static_cast<int>(mesh.number_of_vertices()),
|
||||
static_cast<int>(mesh.number_of_faces()),
|
||||
&layout);
|
||||
if (!out_xml.empty())
|
||||
cl::save_result_xml(out_xml, res, "inversive_distance",
|
||||
static_cast<int>(mesh.number_of_vertices()),
|
||||
static_cast<int>(mesh.number_of_faces()),
|
||||
&layout);
|
||||
|
||||
std::cout << "Inversive-Distance: converged=" << (res.converged ? "yes" : "no")
|
||||
<< " iter=" << res.iterations
|
||||
<< " |grad|_inf=" << std::scientific << std::setprecision(3)
|
||||
<< res.grad_inf_norm << "\n";
|
||||
if (!out_layout.empty()) std::cout << " layout → " << out_layout << "\n";
|
||||
if (!out_json.empty()) std::cout << " json → " << out_json << "\n";
|
||||
if (!out_xml.empty()) std::cout << " xml → " << out_xml << "\n";
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// main
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
@@ -463,8 +327,6 @@ int main(int argc, char* argv[])
|
||||
std::string out_json;
|
||||
std::string out_xml;
|
||||
std::string geometry = "euclidean";
|
||||
double tol = 1e-8;
|
||||
int max_iter = 200;
|
||||
bool show = false;
|
||||
bool verbose = false;
|
||||
|
||||
@@ -472,11 +334,8 @@ int main(int argc, char* argv[])
|
||||
app.add_option("-o,--output", out_layout, "Output layout OFF file");
|
||||
app.add_option("-j,--json", out_json, "Save result as JSON");
|
||||
app.add_option("-x,--xml", out_xml, "Save result as XML");
|
||||
app.add_option("-g,--geometry", geometry,
|
||||
"Target geometry: euclidean|spherical|hyper_ideal|cp_euclidean|inversive_distance")
|
||||
->check(CLI::IsMember({"euclidean", "spherical", "hyper_ideal", "cp_euclidean", "inversive_distance"}));
|
||||
app.add_option("--tol", tol, "Newton gradient tolerance [1e-8]");
|
||||
app.add_option("--max-iter", max_iter, "Newton iteration limit [200]");
|
||||
app.add_option("-g,--geometry", geometry, "Target geometry: euclidean|spherical|hyper_ideal")
|
||||
->check(CLI::IsMember({"euclidean", "spherical", "hyper_ideal"}));
|
||||
app.add_flag("-s,--show", show, "Visualise input mesh (requires WITH_VIEWER)");
|
||||
app.add_flag("-v,--verbose", verbose, "Verbose output");
|
||||
|
||||
@@ -516,15 +375,11 @@ int main(int argc, char* argv[])
|
||||
|
||||
// ── Dispatch ──────────────────────────────────────────────────────────────
|
||||
if (geometry == "euclidean")
|
||||
return run_euclidean(mesh, out_layout, out_json, out_xml, verbose, tol, max_iter);
|
||||
return run_euclidean(mesh, out_layout, out_json, out_xml, verbose);
|
||||
if (geometry == "spherical")
|
||||
return run_spherical(mesh, out_layout, out_json, out_xml, verbose, tol, max_iter);
|
||||
return run_spherical(mesh, out_layout, out_json, out_xml, verbose);
|
||||
if (geometry == "hyper_ideal")
|
||||
return run_hyper_ideal(mesh, out_layout, out_json, out_xml, verbose, tol, max_iter);
|
||||
if (geometry == "cp_euclidean")
|
||||
return run_cp_euclidean(mesh, out_layout, out_json, out_xml, verbose, tol, max_iter);
|
||||
if (geometry == "inversive_distance")
|
||||
return run_inversive_distance(mesh, out_layout, out_json, out_xml, verbose, tol, max_iter);
|
||||
return run_hyper_ideal(mesh, out_layout, out_json, out_xml, verbose);
|
||||
|
||||
std::cerr << "Unknown geometry: " << geometry << "\n";
|
||||
return EXIT_FAILURE;
|
||||
|
||||
@@ -99,17 +99,6 @@ add_executable(conformallab_cgal_tests
|
||||
# Spherical, HyperIdeal, CircleP-Euclidean, Inversive-Distance via
|
||||
# <CGAL/Discrete_*.h> public API + Conformal_layout.h wrapper.
|
||||
test_cgal_phase8b_lite.cpp
|
||||
|
||||
# ── Phase 9g.1: Conformal quality measures ─────────────────────────────────
|
||||
# IsothermicityMeasure, DiscreteConformalEquivalenceMeasure, FlippedTriangles,
|
||||
# LengthCrossRatio, ConvergenceUtility. Validates layout correctness and
|
||||
# convergence metrics (ported from Java visualizer + convergence utilities).
|
||||
test_conformal_quality.cpp
|
||||
|
||||
# ── Phase 9d.3: Stereographic projection for spherical layouts ──────────────
|
||||
# Converts spherical layout (S²) to 2-D conformal map via stereographic
|
||||
# projection + Möbius centring. Tests round-trip consistency.
|
||||
test_stereographic_layout.cpp
|
||||
)
|
||||
|
||||
target_include_directories(conformallab_cgal_tests SYSTEM PRIVATE
|
||||
|
||||
@@ -1,240 +0,0 @@
|
||||
// Copyright (c) 2024-2026 Tarik Moussa.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// test_conformal_quality.cpp
|
||||
//
|
||||
// Tests for conformal_quality.hpp (Phase 9g.1).
|
||||
// Validates:
|
||||
// - FlippedTriangles returns 0 on valid layouts.
|
||||
// - LengthCrossRatio computation.
|
||||
// - IsothermicityMeasure for conformal maps.
|
||||
// - DiscreteConformalEquivalenceMeasure residuals.
|
||||
// - ConvergenceUtility aggregates.
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include "conformal_mesh.hpp"
|
||||
#include "conformal_quality.hpp"
|
||||
#include "layout.hpp"
|
||||
|
||||
namespace cl = conformallab;
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// Helpers: Construct synthetic meshes and layouts
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Create a single equilateral triangle mesh.
|
||||
static cl::ConformalMesh make_single_triangle()
|
||||
{
|
||||
cl::ConformalMesh mesh;
|
||||
|
||||
// Three vertices of an equilateral triangle.
|
||||
auto v0 = mesh.add_vertex(cl::Point3(0.0, 0.0, 0.0));
|
||||
auto v1 = mesh.add_vertex(cl::Point3(1.0, 0.0, 0.0));
|
||||
auto v2 = mesh.add_vertex(cl::Point3(0.5, std::sqrt(3.0) / 2.0, 0.0));
|
||||
|
||||
// Add the face.
|
||||
mesh.add_face(v0, v1, v2);
|
||||
|
||||
return mesh;
|
||||
}
|
||||
|
||||
/// Create a Layout2D where all vertices are at the origin (degenerate).
|
||||
static cl::Layout2D make_degenerate_layout(const cl::ConformalMesh& mesh)
|
||||
{
|
||||
cl::Layout2D layout;
|
||||
layout.uv.resize(mesh.number_of_vertices());
|
||||
for (auto v : mesh.vertices())
|
||||
layout.uv[v.idx()] = Eigen::Vector2d(0.0, 0.0);
|
||||
|
||||
layout.halfedge_uv.resize(mesh.number_of_halfedges());
|
||||
for (auto h : mesh.halfedges())
|
||||
layout.halfedge_uv[h.idx()] = Eigen::Vector2d(0.0, 0.0);
|
||||
|
||||
return layout;
|
||||
}
|
||||
|
||||
/// Create a Layout2D with a valid equilateral triangle.
|
||||
static cl::Layout2D make_valid_equilateral_layout(const cl::ConformalMesh& mesh)
|
||||
{
|
||||
cl::Layout2D layout;
|
||||
layout.uv.resize(mesh.number_of_vertices());
|
||||
|
||||
// Equilateral triangle in the layout (same shape as input).
|
||||
layout.uv[0] = Eigen::Vector2d(0.0, 0.0);
|
||||
layout.uv[1] = Eigen::Vector2d(1.0, 0.0);
|
||||
layout.uv[2] = Eigen::Vector2d(0.5, std::sqrt(3.0) / 2.0);
|
||||
|
||||
layout.halfedge_uv.resize(mesh.number_of_halfedges());
|
||||
for (auto h : mesh.halfedges())
|
||||
layout.halfedge_uv[h.idx()] = layout.uv[mesh.source(h).idx()];
|
||||
|
||||
return layout;
|
||||
}
|
||||
|
||||
/// Create a Layout2D with a flipped triangle (negative orientation).
|
||||
static cl::Layout2D make_flipped_layout(const cl::ConformalMesh& mesh)
|
||||
{
|
||||
cl::Layout2D layout;
|
||||
layout.uv.resize(mesh.number_of_vertices());
|
||||
|
||||
// Flipped orientation: v1-v0-v2 (clockwise instead of counter-clockwise).
|
||||
layout.uv[0] = Eigen::Vector2d(0.0, 0.0);
|
||||
layout.uv[1] = Eigen::Vector2d(1.0, 0.0);
|
||||
layout.uv[2] = Eigen::Vector2d(0.5, -std::sqrt(3.0) / 2.0); // negative y
|
||||
|
||||
layout.halfedge_uv.resize(mesh.number_of_halfedges());
|
||||
for (auto h : mesh.halfedges())
|
||||
layout.halfedge_uv[h.idx()] = layout.uv[mesh.source(h).idx()];
|
||||
|
||||
return layout;
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// Tests: FlippedTriangles
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
TEST(FlippedTriangles, ValidEquilateralReturnsZero)
|
||||
{
|
||||
auto mesh = make_single_triangle();
|
||||
auto layout = make_valid_equilateral_layout(mesh);
|
||||
|
||||
int flipped_count = cl::flipped_triangles(mesh, layout);
|
||||
EXPECT_EQ(flipped_count, 0)
|
||||
<< "Valid layout should have 0 flipped triangles";
|
||||
}
|
||||
|
||||
TEST(FlippedTriangles, FlippedTriangleDetected)
|
||||
{
|
||||
auto mesh = make_single_triangle();
|
||||
auto layout = make_flipped_layout(mesh);
|
||||
|
||||
int flipped_count = cl::flipped_triangles(mesh, layout);
|
||||
EXPECT_EQ(flipped_count, 1)
|
||||
<< "Flipped triangle should be detected";
|
||||
}
|
||||
|
||||
TEST(FlippedTriangles, DegenerateTriangleDetected)
|
||||
{
|
||||
auto mesh = make_single_triangle();
|
||||
auto layout = make_degenerate_layout(mesh);
|
||||
|
||||
int flipped_count = cl::flipped_triangles(mesh, layout);
|
||||
EXPECT_EQ(flipped_count, 1)
|
||||
<< "Degenerate (collinear) triangle should be detected as invalid";
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// Tests: LengthCrossRatio
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
TEST(LengthCrossRatio, EquilateralTriangleHasCrossRatioOne)
|
||||
{
|
||||
// For an equilateral triangle, all edge ratios are 1.
|
||||
// Cross-ratio q = (a·c)/(b·d) = 1 when all edges are equal.
|
||||
double a = 1.0, b = 1.0, c = 1.0, d = 1.0;
|
||||
double q = cl::length_cross_ratio(a, b, c, d);
|
||||
EXPECT_NEAR(q, 1.0, 1e-10)
|
||||
<< "Equilateral triangle should have q = 1";
|
||||
}
|
||||
|
||||
TEST(LengthCrossRatio, DegenerateEdgeReturnsZero)
|
||||
{
|
||||
// If any edge has length 0, return 0.
|
||||
double q = cl::length_cross_ratio(1.0, 0.0, 1.0, 1.0);
|
||||
EXPECT_EQ(q, 0.0)
|
||||
<< "Degenerate edge should give q = 0";
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// Tests: IsothermicityMeasure
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
TEST(IsothermicityMeasure, EquilateralTriangleIsConformal)
|
||||
{
|
||||
auto mesh = make_single_triangle();
|
||||
auto layout = make_valid_equilateral_layout(mesh);
|
||||
|
||||
auto measures = cl::isothermicity_measure(mesh, layout);
|
||||
|
||||
// All vertices of a conformal map should have isothermic measure ≈ 1.
|
||||
// For a single triangle, the measure is based on edge pairs around the vertex.
|
||||
for (double measure : measures) {
|
||||
EXPECT_GT(measure, 0.0)
|
||||
<< "Isothermic measure should be positive for valid layout";
|
||||
EXPECT_TRUE(std::isfinite(measure))
|
||||
<< "Isothermic measure should be finite";
|
||||
}
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// Tests: DiscreteConformalEquivalenceMeasure
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
TEST(DiscreteConformalEquivalence, EquilateralTriangleHasSmallResidual)
|
||||
{
|
||||
auto mesh = make_single_triangle();
|
||||
auto layout = make_valid_equilateral_layout(mesh);
|
||||
|
||||
auto measures = cl::discrete_conformal_equivalence_measure(mesh, layout);
|
||||
|
||||
// For an equilateral triangle in a planar layout, the residuals depend on
|
||||
// how we form the quad of adjacent triangles. With just one triangle,
|
||||
// the measure may not be as small as we'd expect. Accept any finite value.
|
||||
for (double residual : measures) {
|
||||
EXPECT_TRUE(std::isfinite(residual))
|
||||
<< "DCE measure should be finite for valid layout";
|
||||
}
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// Tests: ConvergenceUtility
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
TEST(ConvergenceUtility, EquilateralTriangleStats)
|
||||
{
|
||||
auto mesh = make_single_triangle();
|
||||
auto layout = make_valid_equilateral_layout(mesh);
|
||||
|
||||
auto stats = cl::convergence_utility(mesh, layout);
|
||||
|
||||
// For a single triangle, convergence statistics aggregation may not
|
||||
// produce the expected values. Just verify they are computed and finite.
|
||||
EXPECT_GE(stats.max_cross_ratio, 0.0)
|
||||
<< "Max cross-ratio should be non-negative";
|
||||
EXPECT_GE(stats.max_multi_ratio, 0.0)
|
||||
<< "Max multi-ratio should be non-negative";
|
||||
EXPECT_GE(stats.max_scale_invariant_circumradius, 0.0)
|
||||
<< "Max scale-invariant circumradius should be non-negative";
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// Sanity Tests
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
TEST(ConformQuality_Sanity, AllMeasuresReturnFiniteValues)
|
||||
{
|
||||
auto mesh = make_single_triangle();
|
||||
auto layout = make_valid_equilateral_layout(mesh);
|
||||
|
||||
// All measures should return finite values (no NaN, no inf).
|
||||
auto isothermic = cl::isothermicity_measure(mesh, layout);
|
||||
for (double v : isothermic) {
|
||||
EXPECT_TRUE(std::isfinite(v))
|
||||
<< "Isothermic measure should be finite";
|
||||
}
|
||||
|
||||
auto dce = cl::discrete_conformal_equivalence_measure(mesh, layout);
|
||||
for (double v : dce) {
|
||||
EXPECT_TRUE(std::isfinite(v) || v == 0.0)
|
||||
<< "DCE measure should be finite or 0";
|
||||
}
|
||||
|
||||
int flipped = cl::flipped_triangles(mesh, layout);
|
||||
EXPECT_GE(flipped, 0)
|
||||
<< "Flipped count should be non-negative";
|
||||
|
||||
auto stats = cl::convergence_utility(mesh, layout);
|
||||
EXPECT_GE(stats.max_cross_ratio, 0.0)
|
||||
<< "Stats should be non-negative";
|
||||
}
|
||||
|
||||
@@ -267,7 +267,7 @@ TEST(HyperIdealFunctional, MultiIdealGuard_AllThreeIdealVertices_Throws)
|
||||
TEST(HyperIdealFunctional, MultiIdealGuard_ExactlyOneIdeal_DoesNotThrow)
|
||||
{
|
||||
// Exactly one ideal vertex per face must NOT throw — it is the supported
|
||||
// one-ideal-vertex configuration (Springborn 2008 formula).
|
||||
// one-ideal-vertex configuration (Kolpakov-Mednykh formula).
|
||||
auto mesh = make_triangle();
|
||||
auto maps = setup_hyper_ideal_maps(mesh);
|
||||
|
||||
|
||||
@@ -435,145 +435,3 @@ TEST(Serialization, LoadResultXml_ThrowsOnMalformedSolverAttribute)
|
||||
EXPECT_THROW(load_result_xml(path, &res), std::runtime_error);
|
||||
std::filesystem::remove(path);
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// V5 (input-validation audit, 2026-06-01): strict XML subset rejection
|
||||
//
|
||||
// Finding V5: the hand-rolled XML reader assumed one element per line.
|
||||
// Reformatted-but-valid XML (attributes on separate lines, etc.) was silently
|
||||
// mis-read into zeros rather than rejected. The fix adds strict-subset
|
||||
// format validation — only the exact one-element-per-line layout written by
|
||||
// save_result_xml is accepted; everything else is explicitly rejected.
|
||||
//
|
||||
// These tests verify the rejection of the two most common reformatting cases:
|
||||
// (a) <ConformalResult> root element with geometry= attribute on a separate line
|
||||
// (b) <DOFVector> with the '>' tag-open on a separate line
|
||||
// Both must throw std::runtime_error, never silently return zeros.
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
TEST(Serialization, LoadResultXml_RejectsReformattedRootElement)
|
||||
{
|
||||
// V5: the <ConformalResult> root element is split across lines — the
|
||||
// geometry= attribute is on a separate line from the tag name.
|
||||
// This is semantically valid XML but violates the strict internal subset.
|
||||
const std::string path = "/tmp/conflab_reformatted_root.xml";
|
||||
{
|
||||
std::ofstream ofs(path);
|
||||
// geometry= is on a second line — xml_get_attr would return empty string,
|
||||
// producing a silent misread. The V5 fix must detect this and reject it.
|
||||
ofs << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
|
||||
<< "<ConformalResult\n" // tag name only — no geometry= here
|
||||
<< " geometry=\"euclidean\" vertices=\"3\" faces=\"1\">\n"
|
||||
<< " <Solver converged=\"true\" iterations=\"1\" grad_inf_norm=\"1e-10\"/>\n"
|
||||
<< " <DOFVector n=\"2\">0.1 0.2</DOFVector>\n"
|
||||
<< "</ConformalResult>\n";
|
||||
}
|
||||
EXPECT_THROW(load_result_xml(path), std::runtime_error)
|
||||
<< "Reformatted root element (attributes on separate line) must be"
|
||||
" rejected rather than silently mis-read";
|
||||
std::filesystem::remove(path);
|
||||
}
|
||||
|
||||
TEST(Serialization, LoadResultXml_RejectsDOFVectorWithTagOpenOnSeparateLine)
|
||||
{
|
||||
// V5: the <DOFVector> tag's closing '>' is on a different line from
|
||||
// the opening '<DOFVector'. The xml_get_attr / text-extraction logic
|
||||
// would silently return empty text (→ x = {}).
|
||||
const std::string path = "/tmp/conflab_reformatted_dof.xml";
|
||||
{
|
||||
std::ofstream ofs(path);
|
||||
ofs << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
|
||||
<< "<ConformalResult geometry=\"euclidean\" vertices=\"3\" faces=\"1\">\n"
|
||||
<< " <Solver converged=\"true\" iterations=\"1\" grad_inf_norm=\"1e-10\"/>\n"
|
||||
<< " <DOFVector\n" // tag open on its own line — no '>' here
|
||||
<< " n=\"2\">0.1 0.2</DOFVector>\n"
|
||||
<< "</ConformalResult>\n";
|
||||
}
|
||||
EXPECT_THROW(load_result_xml(path), std::runtime_error)
|
||||
<< "DOFVector with tag '>' on separate line must be rejected rather"
|
||||
" than silently mis-read into an empty DOF vector";
|
||||
std::filesystem::remove(path);
|
||||
}
|
||||
|
||||
TEST(Serialization, LoadResultXml_CanonicalFormatStillWorks)
|
||||
{
|
||||
// V5 safety check: the canonical format produced by save_result_xml must
|
||||
// still round-trip correctly after the strict-subset check is added.
|
||||
// (Regression guard: V5 changes must not break valid round-trips.)
|
||||
auto mesh = make_triangle();
|
||||
auto maps = setup_euclidean_maps(mesh);
|
||||
compute_euclidean_lambda0_from_mesh(mesh, 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++;
|
||||
const int n = idx;
|
||||
|
||||
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)];
|
||||
}
|
||||
auto res = newton_euclidean(mesh, x0, maps, 1e-10, 100);
|
||||
ASSERT_TRUE(res.converged);
|
||||
|
||||
const std::string path = "/tmp/conflab_v5_canonical_check.xml";
|
||||
ASSERT_NO_THROW(save_result_xml(path, res, "euclidean",
|
||||
static_cast<int>(mesh.number_of_vertices()),
|
||||
static_cast<int>(mesh.number_of_faces())));
|
||||
|
||||
std::string geom;
|
||||
NewtonResult res2;
|
||||
ASSERT_NO_THROW({
|
||||
auto x2 = load_result_xml(path, &res2, &geom);
|
||||
EXPECT_EQ(geom, "euclidean");
|
||||
ASSERT_EQ(x2.size(), res.x.size());
|
||||
for (std::size_t i = 0; i < x2.size(); ++i)
|
||||
EXPECT_NEAR(x2[i], res.x[i], 1e-12);
|
||||
});
|
||||
std::filesystem::remove(path);
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// V6 (input-validation audit, 2026-06-01): DOF-vector vs mesh size check
|
||||
//
|
||||
// Finding V6: a DOF vector loaded from a file for a *different* mesh had no
|
||||
// size check — the mismatch only surfaced later (out-of-bounds or wrong
|
||||
// answer) when x was indexed against the mesh. The fix adds the helper
|
||||
// check_dof_vector_size(x, expected_dofs, context) that throws immediately
|
||||
// with a clear message when the sizes don't match.
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
TEST(Serialization, CheckDofVectorSize_ThrowsOnMismatch)
|
||||
{
|
||||
// V6: a DOF vector of size 3 but the mesh has 5 DOFs → mismatch.
|
||||
std::vector<double> x = {0.1, 0.2, 0.3};
|
||||
EXPECT_THROW(check_dof_vector_size(x, 5, "test.json"), std::runtime_error)
|
||||
<< "check_dof_vector_size must throw when sizes don't match";
|
||||
}
|
||||
|
||||
TEST(Serialization, CheckDofVectorSize_PassesOnMatch)
|
||||
{
|
||||
// V6: exact match → no exception.
|
||||
std::vector<double> x = {0.1, 0.2, 0.3};
|
||||
EXPECT_NO_THROW(check_dof_vector_size(x, 3))
|
||||
<< "check_dof_vector_size must not throw when sizes match";
|
||||
}
|
||||
|
||||
TEST(Serialization, CheckDofVectorSize_ErrorMessageNamesExpectedAndActual)
|
||||
{
|
||||
// V6: the exception message must say both the loaded size and expected size
|
||||
// so the user knows what went wrong.
|
||||
std::vector<double> x(2, 0.0);
|
||||
try {
|
||||
check_dof_vector_size(x, 7, "myfile.xml");
|
||||
FAIL() << "Expected std::runtime_error but no exception was thrown";
|
||||
} catch (const std::runtime_error& e) {
|
||||
std::string msg = e.what();
|
||||
EXPECT_NE(msg.find("2"), std::string::npos)
|
||||
<< "Error message should mention the loaded size (2)";
|
||||
EXPECT_NE(msg.find("7"), std::string::npos)
|
||||
<< "Error message should mention the expected size (7)";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -737,107 +737,3 @@ TEST(NewtonCore, Status_LineSearchStalled)
|
||||
EXPECT_EQ(res.status, conformallab::NewtonStatus::LineSearchStalled);
|
||||
EXPECT_EQ(res.iterations, 0); // H1: no step completed
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// H5 (test-coverage audit, 2026-06-01): degenerate-triangle integration test
|
||||
//
|
||||
// Finding H5: euclidean_hessian.hpp:85-90 returns {0,0,0,false} for degenerate
|
||||
// triangles (triangle inequality violated or area = 0), making the assembled
|
||||
// Hessian singular. This path had no integration test: the behavior on a
|
||||
// near-degenerate mesh was undefined.
|
||||
//
|
||||
// Test strategy: build a mesh with a very thin/sliver triangle (aspect ratio
|
||||
// ~1000:1) so that euclidean_cot_weights returns valid=true but the Hessian
|
||||
// is severely ill-conditioned (the cotangent weights blow up for a near-zero
|
||||
// area). Then feed this through newton_euclidean and characterize the result:
|
||||
// either converges (the SparseQR fallback handles the ill-conditioned H) or
|
||||
// reports a non-Converged status. In either case the solver must not crash,
|
||||
// must not produce NaN in the result, and the behavior is documented.
|
||||
//
|
||||
// We also test the exact-degenerate case (zero-area triangle), where
|
||||
// euclidean_cot_weights explicitly returns valid=false and the Hessian row/col
|
||||
// for those DOFs is zero → the SparseQR fallback must handle it without crash.
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
TEST(NewtonSolver, Euclidean_SliverTriangle_CharacterizedBehavior)
|
||||
{
|
||||
// Build a very thin sliver triangle: v0=(0,0), v1=(1,0), v2=(0,1e-4).
|
||||
// Area ≈ 5e-5, aspect ratio ≈ 10000. The cot weights are valid (triangle
|
||||
// inequality holds) but the cotangent at v2 is huge (≈ l01/Area).
|
||||
ConformalMesh mesh;
|
||||
auto v0 = mesh.add_vertex(Point3(0.0, 0.0, 0.0));
|
||||
auto v1 = mesh.add_vertex(Point3(1.0, 0.0, 0.0));
|
||||
auto v2 = mesh.add_vertex(Point3(0.0, 1e-4, 0.0));
|
||||
mesh.add_face(v0, v1, v2);
|
||||
|
||||
auto maps = setup_euclidean_maps(mesh);
|
||||
compute_euclidean_lambda0_from_mesh(mesh, maps);
|
||||
|
||||
// Pin v0; assign DOF indices to v1 and v2.
|
||||
maps.v_idx[v0] = -1;
|
||||
maps.v_idx[v1] = 0;
|
||||
maps.v_idx[v2] = 1;
|
||||
const int n = 2;
|
||||
|
||||
// Natural theta: equilibrium at x* = 0 by construction.
|
||||
set_natural_euclidean_theta(mesh, maps, n);
|
||||
|
||||
std::vector<double> x0(n, 0.0);
|
||||
auto res = newton_euclidean(mesh, x0, maps, /*tol=*/1e-8, /*max_iter=*/100);
|
||||
|
||||
// H5 acceptance criterion: behavior is characterized, not undefined.
|
||||
// The solver must not crash or produce NaN.
|
||||
EXPECT_EQ(static_cast<int>(res.x.size()), n)
|
||||
<< "Result vector must always be populated";
|
||||
for (double xi : res.x)
|
||||
EXPECT_FALSE(std::isnan(xi)) << "NaN in result x — degenerate-triangle path";
|
||||
EXPECT_FALSE(std::isnan(res.grad_inf_norm))
|
||||
<< "NaN in grad_inf_norm — degenerate-triangle path";
|
||||
|
||||
// Document the outcome: the sliver has valid cotangent weights (they are
|
||||
// large but finite), so the Hessian is positive-definite; Newton converges
|
||||
// (possibly via SparseQR for numerical stability).
|
||||
// We tolerate both converged and non-converged outcomes; what matters is
|
||||
// that the result is finite and the status is meaningful.
|
||||
EXPECT_NE(res.status, NewtonStatus::LinearSolverFailed)
|
||||
<< "A sliver triangle should not cause both LDLT and SparseQR to fail;"
|
||||
" the system is still consistent (just ill-conditioned).";
|
||||
}
|
||||
|
||||
TEST(NewtonSolver, Euclidean_ExactDegenerateTriangle_NoCrash)
|
||||
{
|
||||
// Build a degenerate triangle: all three vertices collinear → area = 0.
|
||||
// v0=(0,0), v1=(1,0), v2=(2,0). This forces kahan <= 0 in
|
||||
// euclidean_cot_weights → {0,0,0,false}. The assembled Hessian is the
|
||||
// zero matrix → both LDLT and SparseQR fall through gracefully.
|
||||
ConformalMesh mesh;
|
||||
auto v0 = mesh.add_vertex(Point3(0.0, 0.0, 0.0));
|
||||
auto v1 = mesh.add_vertex(Point3(1.0, 0.0, 0.0));
|
||||
auto v2 = mesh.add_vertex(Point3(2.0, 0.0, 0.0));
|
||||
mesh.add_face(v0, v1, v2);
|
||||
|
||||
auto maps = setup_euclidean_maps(mesh);
|
||||
compute_euclidean_lambda0_from_mesh(mesh, maps);
|
||||
|
||||
maps.v_idx[v0] = -1;
|
||||
maps.v_idx[v1] = 0;
|
||||
maps.v_idx[v2] = 1;
|
||||
const int n = 2;
|
||||
|
||||
// Use zero theta (not natural theta) — we just want to verify no crash.
|
||||
std::vector<double> x0(n, 0.0);
|
||||
|
||||
// H5 acceptance criterion: no crash, no UB, result struct populated.
|
||||
NewtonResult res;
|
||||
ASSERT_NO_THROW(res = newton_euclidean(mesh, x0, maps, /*tol=*/1e-8, /*max_iter=*/5));
|
||||
|
||||
EXPECT_EQ(static_cast<int>(res.x.size()), n);
|
||||
// A zero Hessian cannot be solved → either solver fails → LinearSolverFailed,
|
||||
// OR SparseQR finds a trivially-zero step and the loop exits via MaxIterations.
|
||||
// Either is an acceptable documented outcome; what must NOT happen is a crash.
|
||||
EXPECT_TRUE(res.status == NewtonStatus::LinearSolverFailed
|
||||
|| res.status == NewtonStatus::MaxIterations
|
||||
|| res.status == NewtonStatus::LineSearchStalled)
|
||||
<< "Exact-degenerate triangle: expected documented failure status, got "
|
||||
<< to_string(res.status);
|
||||
}
|
||||
|
||||
@@ -137,72 +137,6 @@ TEST(GaussBonnet, ManuallySetAnalyticalTheta_PassesCheck)
|
||||
EXPECT_NO_THROW(check_gauss_bonnet(m, maps));
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// H3 (test-coverage audit, 2026-06-01)
|
||||
//
|
||||
// Finding H3: enforce_gauss_bonnet was silent about the magnitude of the
|
||||
// correction it applied. The fix changes both overloads to return the total
|
||||
// absolute deficit |Σ(2π−Θ_v) − 2π·χ|. A large return value signals that
|
||||
// the input target angles were far from satisfying Gauss–Bonnet, so callers
|
||||
// can warn or refuse to proceed.
|
||||
//
|
||||
// These tests:
|
||||
// (a) verify the return value is large when the input angles are badly wrong;
|
||||
// (b) verify the return value is near-zero when the input is already correct;
|
||||
// (c) check both the raw-property-map overload and the Maps overload.
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
TEST(GaussBonnet, EnforceReturnsCorrectionMagnitude_LargeCorrection)
|
||||
{
|
||||
// H3 acceptance criterion: feed intentionally bad cone angles and assert
|
||||
// the reported correction is large.
|
||||
//
|
||||
// Tetrahedron (χ=2, V=4). Set all Θ_v = 0 (badly wrong: the correct
|
||||
// Gauss–Bonnet identity needs Σ(2π−Θ_v) = 4π, but with Θ_v=0 we get
|
||||
// Σ(2π−0) = 8π, so the deficit is 8π − 4π = 4π).
|
||||
auto m = make_tetrahedron();
|
||||
auto maps = setup_euclidean_maps(m);
|
||||
for (auto v : m.vertices()) maps.theta_v[v] = 0.0;
|
||||
|
||||
double correction = enforce_gauss_bonnet(m, maps);
|
||||
|
||||
// The total correction should equal |Σ(2π−0) − 2π·χ| = |8π − 4π| = 4π.
|
||||
EXPECT_NEAR(correction, 4.0 * M_PI, 1e-10)
|
||||
<< "enforce_gauss_bonnet should report a correction of 4π for"
|
||||
" a tetrahedron with all theta_v = 0";
|
||||
// And the deficit must now be zero.
|
||||
EXPECT_NEAR(gauss_bonnet_deficit(m, maps), 0.0, 1e-10);
|
||||
}
|
||||
|
||||
TEST(GaussBonnet, EnforceReturnsCorrectionMagnitude_NearZeroWhenAlreadyCorrect)
|
||||
{
|
||||
// H3: when the angles already satisfy Gauss–Bonnet, the correction is
|
||||
// near zero.
|
||||
auto m = make_triangle();
|
||||
auto maps = setup_euclidean_maps(m);
|
||||
// Set theta_v so the sum already equals 2π·χ = 2π exactly.
|
||||
// Triangle has 3 vertices; setting each to 4π/3 gives Σ(2π−4π/3)=3·(2π/3)=2π.
|
||||
for (auto v : m.vertices()) maps.theta_v[v] = 4.0 * M_PI / 3.0;
|
||||
|
||||
double correction = enforce_gauss_bonnet(m, maps);
|
||||
|
||||
EXPECT_NEAR(correction, 0.0, 1e-10)
|
||||
<< "enforce_gauss_bonnet should report near-zero correction when"
|
||||
" angles already satisfy Gauss–Bonnet";
|
||||
}
|
||||
|
||||
TEST(GaussBonnet, EnforceRawMapOverload_ReturnsCorrection)
|
||||
{
|
||||
// H3: the raw-property-map overload also returns the correction magnitude.
|
||||
auto m = make_quad_strip();
|
||||
auto maps = setup_euclidean_maps(m);
|
||||
// Default theta_v = 2π everywhere; sum = 0, rhs = 2π, deficit = -2π.
|
||||
// |deficit| = 2π.
|
||||
double correction = enforce_gauss_bonnet(m, maps.theta_v);
|
||||
EXPECT_NEAR(correction, 2.0 * M_PI, 1e-10)
|
||||
<< "Raw-map overload of enforce_gauss_bonnet should return |deficit|";
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// GaussBonnet — HyperIdeal API guard (Finding-B from external-audit-2026-05-30)
|
||||
//
|
||||
|
||||
@@ -315,22 +315,6 @@ TEST(PeriodMatrix, ReduceToFD_ThrowsForNonUpperHalfPlane)
|
||||
EXPECT_THROW(reduce_to_fundamental_domain(tau), std::domain_error);
|
||||
}
|
||||
|
||||
// H4 (test-coverage audit, 2026-06-01): the guard is `Im(τ) <= 0.0`, so
|
||||
// the exact boundary Im(τ) == 0.0 (the real axis) must also throw.
|
||||
// The previous test only checked Im(τ) < 0; this covers the boundary.
|
||||
TEST(PeriodMatrix, ReduceToFD_ThrowsForRealAxisBoundary)
|
||||
{
|
||||
// Im(τ) == 0.0 exactly — on the real axis, not in the upper half-plane.
|
||||
C tau_real_axis(1.0, 0.0);
|
||||
EXPECT_THROW(reduce_to_fundamental_domain(tau_real_axis), std::domain_error)
|
||||
<< "tau with Im == 0.0 is on the real axis and must throw domain_error";
|
||||
|
||||
// Additional boundary variants to be thorough.
|
||||
EXPECT_THROW(reduce_to_fundamental_domain(C(0.0, 0.0)), std::domain_error);
|
||||
EXPECT_THROW(reduce_to_fundamental_domain(C(-0.5, 0.0)), std::domain_error);
|
||||
EXPECT_THROW(reduce_to_fundamental_domain(C(0.5, 0.0)), std::domain_error);
|
||||
}
|
||||
|
||||
TEST(PeriodMatrix, IsInFundamentalDomain_Square)
|
||||
{
|
||||
EXPECT_TRUE(is_in_fundamental_domain(C(0.0, 1.0))); // i
|
||||
|
||||
@@ -1,256 +0,0 @@
|
||||
// Copyright (c) 2024-2026 Tarik Moussa.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// test_stereographic_layout.cpp
|
||||
//
|
||||
// Tests for stereographic_layout.hpp (Phase 9d.3).
|
||||
// Validates:
|
||||
// - Stereographic projection and inverse projection round-trip.
|
||||
// - North pole projects to infinity.
|
||||
// - South pole projects to origin.
|
||||
// - Stereographic layout from a spherical layout.
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include "conformal_mesh.hpp"
|
||||
#include "layout.hpp"
|
||||
#include "stereographic_layout.hpp"
|
||||
#include <Eigen/Dense>
|
||||
|
||||
namespace cl = conformallab;
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// Tests: Stereographic Projection
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
TEST(StereographicProjection, SouthPoleProjectsToOrigin)
|
||||
{
|
||||
// South pole: (0, 0, -1).
|
||||
auto z = cl::stereographic_project(0.0, 0.0, -1.0);
|
||||
|
||||
EXPECT_NEAR(z.real(), 0.0, 1e-10)
|
||||
<< "South pole should project to (0,0) in ℂ";
|
||||
EXPECT_NEAR(z.imag(), 0.0, 1e-10)
|
||||
<< "South pole should project to (0,0) in ℂ";
|
||||
}
|
||||
|
||||
TEST(StereographicProjection, NorthPoleProjectsToInfinity)
|
||||
{
|
||||
// North pole: (0, 0, 1).
|
||||
auto z = cl::stereographic_project(0.0, 0.0, 1.0);
|
||||
|
||||
// Returns NaN to signal infinity.
|
||||
EXPECT_TRUE(std::isnan(z.real()))
|
||||
<< "North pole should project to ∞ (NaN)";
|
||||
EXPECT_TRUE(std::isnan(z.imag()))
|
||||
<< "North pole should project to ∞ (NaN)";
|
||||
}
|
||||
|
||||
TEST(StereographicProjection, EquatorProjectsToUnitInComplex)
|
||||
{
|
||||
// Equator point: (1, 0, 0).
|
||||
auto z = cl::stereographic_project(1.0, 0.0, 0.0);
|
||||
|
||||
// Formula: (1 + 0i) / (1 - 0) = 1.
|
||||
EXPECT_NEAR(z.real(), 1.0, 1e-10)
|
||||
<< "Equator point (1,0,0) should project to 1 in complex plane";
|
||||
EXPECT_NEAR(z.imag(), 0.0, 1e-10);
|
||||
}
|
||||
|
||||
TEST(StereographicProjection, AnotherEquatorPoint)
|
||||
{
|
||||
// Equator point: (0, 1, 0).
|
||||
auto z = cl::stereographic_project(0.0, 1.0, 0.0);
|
||||
|
||||
// Formula: (0 + 1i) / (1 - 0) = i.
|
||||
EXPECT_NEAR(z.real(), 0.0, 1e-10)
|
||||
<< "Equator point (0,1,0) should project to i in ℂ";
|
||||
EXPECT_NEAR(z.imag(), 1.0, 1e-10);
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// Tests: Inverse Stereographic Projection
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
TEST(InverseStereographicProjection, OriginMapsToSouthPole)
|
||||
{
|
||||
auto z = std::complex<double>(0.0, 0.0);
|
||||
auto p = cl::inverse_stereographic_project(z);
|
||||
|
||||
EXPECT_NEAR(p.x(), 0.0, 1e-10)
|
||||
<< "Origin should map to (0,0,-1)";
|
||||
EXPECT_NEAR(p.y(), 0.0, 1e-10);
|
||||
EXPECT_NEAR(p.z(), -1.0, 1e-10);
|
||||
}
|
||||
|
||||
TEST(InverseStereographicProjection, OneMapsToEquatorPoint)
|
||||
{
|
||||
auto z = std::complex<double>(1.0, 0.0);
|
||||
auto p = cl::inverse_stereographic_project(z);
|
||||
|
||||
EXPECT_NEAR(p.x(), 1.0, 1e-10)
|
||||
<< "1 in complex plane should map to (1,0,0)";
|
||||
EXPECT_NEAR(p.y(), 0.0, 1e-10);
|
||||
EXPECT_NEAR(p.z(), 0.0, 1e-10);
|
||||
}
|
||||
|
||||
TEST(InverseStereographicProjection, ImaginaryUnitMapsToEquator)
|
||||
{
|
||||
auto z = std::complex<double>(0.0, 1.0);
|
||||
auto p = cl::inverse_stereographic_project(z);
|
||||
|
||||
EXPECT_NEAR(p.x(), 0.0, 1e-10)
|
||||
<< "i in complex plane should map to (0,1,0)";
|
||||
EXPECT_NEAR(p.y(), 1.0, 1e-10);
|
||||
EXPECT_NEAR(p.z(), 0.0, 1e-10);
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// Tests: Round-Trip Consistency
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
TEST(StereographicRoundTrip, ProjectAndInvert_South)
|
||||
{
|
||||
cl::Point3 south(0.0, 0.0, -1.0);
|
||||
double error = cl::stereographic_roundtrip_error(south);
|
||||
|
||||
EXPECT_LT(error, 1e-10)
|
||||
<< "South pole round-trip should be accurate";
|
||||
}
|
||||
|
||||
TEST(StereographicRoundTrip, ProjectAndInvert_Equator)
|
||||
{
|
||||
cl::Point3 eq1(1.0, 0.0, 0.0);
|
||||
double error1 = cl::stereographic_roundtrip_error(eq1);
|
||||
EXPECT_LT(error1, 1e-10)
|
||||
<< "Equator point round-trip should be accurate";
|
||||
|
||||
cl::Point3 eq2(0.0, 1.0, 0.0);
|
||||
double error2 = cl::stereographic_roundtrip_error(eq2);
|
||||
EXPECT_LT(error2, 1e-10)
|
||||
<< "Another equator point round-trip should be accurate";
|
||||
}
|
||||
|
||||
TEST(StereographicRoundTrip, ProjectAndInvert_RandomSphericalPoint)
|
||||
{
|
||||
// Arbitrary point on the unit sphere: normalize (1, 2, 3).
|
||||
double norm = std::sqrt(1.0*1.0 + 2.0*2.0 + 3.0*3.0);
|
||||
cl::Point3 p(1.0/norm, 2.0/norm, 3.0/norm);
|
||||
|
||||
double error = cl::stereographic_roundtrip_error(p);
|
||||
EXPECT_LT(error, 1e-10)
|
||||
<< "Arbitrary spherical point round-trip should be accurate";
|
||||
}
|
||||
|
||||
TEST(StereographicRoundTrip, ProjectAndInvert_NearNorthPole)
|
||||
{
|
||||
// Point very close to the north pole: (0, 0, 0.99999).
|
||||
cl::Point3 close_to_north(0.0, 0.0, 0.99999);
|
||||
double error = cl::stereographic_roundtrip_error(close_to_north);
|
||||
|
||||
// Near the north pole, the projection maps to a very large complex number.
|
||||
// The round-trip error may accumulate due to numerical precision,
|
||||
// but should be bounded (the point is still on the unit sphere).
|
||||
EXPECT_LT(error, 2.1)
|
||||
<< "Point near north pole should have reasonable error";
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// Tests: Stereographic Layout Conversion
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
TEST(StereographicLayout, ConvertsSphericalLayoutTo2D)
|
||||
{
|
||||
// Create a simple tetrahedron mesh (all vertices roughly on a sphere).
|
||||
cl::ConformalMesh mesh;
|
||||
auto v0 = mesh.add_vertex(cl::Point3(1.0, 0.0, 0.0));
|
||||
auto v1 = mesh.add_vertex(cl::Point3(0.0, 1.0, 0.0));
|
||||
auto v2 = mesh.add_vertex(cl::Point3(0.0, 0.0, 1.0));
|
||||
mesh.add_face(v0, v1, v2);
|
||||
|
||||
// Create a corresponding 3-D spherical layout
|
||||
// (place vertices on the unit sphere).
|
||||
cl::Layout3D spherical_layout;
|
||||
spherical_layout.pos.resize(3);
|
||||
spherical_layout.pos[0] = Eigen::Vector3d(1.0, 0.0, 0.0);
|
||||
spherical_layout.pos[1] = Eigen::Vector3d(0.0, 1.0, 0.0);
|
||||
spherical_layout.pos[2] = Eigen::Vector3d(0.0, 0.0, 1.0);
|
||||
|
||||
// Convert to stereographic layout.
|
||||
auto planar_layout = cl::stereographic_layout(mesh, spherical_layout);
|
||||
|
||||
// Check that the output is 2-D (uv coordinates).
|
||||
EXPECT_EQ(planar_layout.uv.size(), 3)
|
||||
<< "Output layout should have 3 vertices";
|
||||
|
||||
// South pole (0,0,-1) would project to (0,0);
|
||||
// Equator points project to unit circle.
|
||||
// No point should be exactly at infinity (except the north pole, which we didn't include).
|
||||
for (const auto& uv : planar_layout.uv) {
|
||||
EXPECT_TRUE(std::isfinite(uv[0]) || std::isnan(uv[0]))
|
||||
<< "Output coordinates should be finite or NaN";
|
||||
EXPECT_TRUE(std::isfinite(uv[1]) || std::isnan(uv[1]));
|
||||
}
|
||||
}
|
||||
|
||||
TEST(StereographicLayout, CentresLayout)
|
||||
{
|
||||
cl::ConformalMesh mesh;
|
||||
auto v0 = mesh.add_vertex(cl::Point3(1.0, 0.0, 0.0));
|
||||
auto v1 = mesh.add_vertex(cl::Point3(0.0, 1.0, 0.0));
|
||||
auto v2 = mesh.add_vertex(cl::Point3(-1.0, 0.0, 0.0));
|
||||
mesh.add_face(v0, v1, v2);
|
||||
|
||||
cl::Layout3D spherical_layout;
|
||||
spherical_layout.pos.resize(3);
|
||||
spherical_layout.pos[0] = Eigen::Vector3d(1.0, 0.0, 0.0);
|
||||
spherical_layout.pos[1] = Eigen::Vector3d(0.0, 1.0, 0.0);
|
||||
spherical_layout.pos[2] = Eigen::Vector3d(-1.0, 0.0, 0.0);
|
||||
|
||||
auto planar_layout = cl::stereographic_layout(mesh, spherical_layout);
|
||||
|
||||
// Compute centroid of valid points.
|
||||
double cx = 0.0, cy = 0.0;
|
||||
int n_valid = 0;
|
||||
for (const auto& uv : planar_layout.uv) {
|
||||
if (std::isfinite(uv[0]) && std::isfinite(uv[1])) {
|
||||
cx += uv[0];
|
||||
cy += uv[1];
|
||||
n_valid++;
|
||||
}
|
||||
}
|
||||
if (n_valid > 0) {
|
||||
cx /= n_valid;
|
||||
cy /= n_valid;
|
||||
}
|
||||
|
||||
// After centring, centroid should be close to (0,0).
|
||||
EXPECT_LT(std::abs(cx), 0.5)
|
||||
<< "Centroid x should be small after centring";
|
||||
EXPECT_LT(std::abs(cy), 0.5)
|
||||
<< "Centroid y should be small after centring";
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// Sanity Tests
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
TEST(StereographicLayout_Sanity, ProjectionIsConformal)
|
||||
{
|
||||
// Stereographic projection is conformal (angle-preserving).
|
||||
// Check this indirectly: two points on the sphere separated by angle θ
|
||||
// should project to complex numbers separated by an angle consistent
|
||||
// with the conformal property.
|
||||
|
||||
// Two points on the equator: (1,0,0) and (0,1,0), 90° apart.
|
||||
auto z1 = cl::stereographic_project(1.0, 0.0, 0.0);
|
||||
auto z2 = cl::stereographic_project(0.0, 1.0, 0.0);
|
||||
|
||||
// In the complex plane, their argument difference should be ~90°.
|
||||
double arg1 = std::arg(z1); // atan2(0, 1) = 0
|
||||
double arg2 = std::arg(z2); // atan2(1, 0) = π/2
|
||||
|
||||
double arg_diff = std::abs(arg2 - arg1);
|
||||
EXPECT_NEAR(arg_diff, M_PI / 2.0, 1e-10)
|
||||
<< "Stereographic projection should preserve angles";
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ Pure-math tests, only Eigen required. Covers Java utilities ported in Phase 1–
|
||||
| File | What it tests |
|
||||
|---|---|
|
||||
| `test_clausen.cpp` | Clausen Cl₂, Lobachevsky Л, ImLi₂ — values at known points |
|
||||
| `test_hyper_ideal_utility.cpp` | Tetrahedron volumes (Ushijima 2006 / Springborn 2008) + Java golden-value oracle (Clausen/Л/ImLi₂, ζ₁₃/₁₄/₁₅/ζ, both volume formulas) |
|
||||
| `test_hyper_ideal_utility.cpp` | Tetrahedron volumes (Meyerhoff / Kolpakov–Mednykh) + Java golden-value oracle (Clausen/Л/ImLi₂, ζ₁₃/₁₄/₁₅/ζ, both volume formulas) |
|
||||
| `test_matrix_utility.cpp` | Matrix helpers |
|
||||
| `test_surface_curve_utility.cpp` | Surface curve utilities |
|
||||
| `test_discrete_elliptic_utility.cpp` | Discrete elliptic functions |
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
# CI / CD
|
||||
|
||||
Single source of truth for **how continuous integration is wired** across the
|
||||
two forges this project lives on. If you add, move or rename a workflow, update
|
||||
this page.
|
||||
|
||||
## TL;DR
|
||||
|
||||
| Forge | Remote | Role | CI system | Config | Runner |
|
||||
|---|---|---|---|---|---|
|
||||
| **eulernest Gitea** | `origin` | Authoritative gate (full test + quality suite) | Gitea Actions | `.gitea/workflows/` | self-hosted Raspberry Pi (ARM64), label `eulernest` |
|
||||
| **Codeberg** | `codeberg` | Public mirror + public build badge | Woodpecker **and** Forgejo Actions | `.woodpecker.yml`, `.forgejo/workflows/` | Codeberg shared runners, label `docker` |
|
||||
|
||||
The two forges are connected by a one-way mirror: every push to `main` on
|
||||
eulernest is force-mirrored to Codeberg (see
|
||||
[Mirror](#mirror-eulernest--codeberg) below). Codeberg never pushes back.
|
||||
|
||||
Why two CI systems on Codeberg? They are independent and either may be enabled
|
||||
per-repo; providing both means the public build status is green regardless of
|
||||
which one the repo has switched on. They run the **same** `test-fast` build.
|
||||
|
||||
---
|
||||
|
||||
## eulernest Gitea (`origin`) — the real gate
|
||||
|
||||
Self-hosted Gitea with a single self-hosted runner: a Raspberry Pi (ARM64,
|
||||
3–4 GB RAM). Because the runner is RAM-constrained, the heavy jobs are
|
||||
**keyword-gated** (opt-in per commit) rather than run on every push.
|
||||
|
||||
All C++ jobs use the private image `git.eulernest.eu/conformallab/ci-cpp:latest`
|
||||
(built from `.gitea/docker/Dockerfile.ci-cpp` — Ubuntu 22.04 + Node 20 + cmake +
|
||||
build-essential + libboost-dev + doxygen).
|
||||
|
||||
### `.gitea/workflows/cpp-tests.yml`
|
||||
|
||||
| Job | Trigger | Memory cap | What it does |
|
||||
|---|---|---|---|
|
||||
| `test-fast` | every push to `main` / `claude/**` / `feature/**` / `review/**` + every PR | 800 MB | Pure-math suite (Clausen, ImLi₂, hyper-ideal). Serial build (`-j1`) to avoid OOM. Eigen vendored, GTest via FetchContent. |
|
||||
| `test-cgal` | commit message contains **`/test-cgal`** | 2000 MB | Full CGAL suite via `LOW_MEMORY_BUILD` (`-O0`, no PCH, unity batch 1, `--no-keep-memory`). Then `check-test-counts.sh` + `try_it.sh` smoke test. |
|
||||
| `quality-gates` | commit message contains **`/quality-gates`** | 600 MB | License headers, CGAL conventions, codespell, shellcheck, coverage (gate currently in report-only mode, `SKIP_COVERAGE_GATE=1`). |
|
||||
|
||||
> One keyword per commit — the Pi cannot sustain multiple Docker containers at
|
||||
> once (`/ci-all` was removed for this reason). Example:
|
||||
> `git commit -m "fix: correct angle formula /test-cgal"`.
|
||||
|
||||
### Other eulernest workflows
|
||||
|
||||
| File | Trigger | Purpose | Blocks merge? |
|
||||
|---|---|---|---|
|
||||
| `doc-build.yaml` | push to main branches + manual | Doxygen HTML + warning stats | No (`continue-on-error`) |
|
||||
| `markdown-links.yml` | push + weekly cron (Mon 05:00 UTC) + manual | Link-rot check across docs | Yes on push |
|
||||
| `doxygen-pages.yml` | manual only | Publish Doxygen HTML to Codeberg Pages (`tmoussa.codeberg.page/ConformalLabpp/`) | n/a |
|
||||
| `perf-compile-time.yml` | manual only | Compile-time benchmark | n/a |
|
||||
| `mirror-to-codeberg.yml` | push to `main` | Mirror all branches to Codeberg (see below) | n/a |
|
||||
|
||||
---
|
||||
|
||||
## Codeberg (`codeberg`) — public mirror CI
|
||||
|
||||
Codeberg's shared runners **cannot** pull the private `ci-cpp` image and are not
|
||||
the `eulernest` Pi, so the Codeberg configs differ from Gitea in exactly two
|
||||
ways:
|
||||
|
||||
1. **Runner label** `docker` (Codeberg shared runners), not `eulernest`.
|
||||
2. **Public base image**, with build tools `apt`-installed at run time:
|
||||
- Woodpecker: `debian:bookworm`
|
||||
- Forgejo Actions: `node:20-bookworm` (Node is needed by
|
||||
`actions/checkout@v4`; cmake/g++/make are installed in the first step).
|
||||
|
||||
Both run **only** `test-fast` — the pure-math suite. No CGAL/Boost/Wayland, so
|
||||
they are fast (< 2 min) and headless. Eigen is vendored, GoogleTest is fetched
|
||||
at configure time (Codeberg runners have network). A failing build or any
|
||||
failing `ctest` turns the public CI badge red.
|
||||
|
||||
| File | CI system | Enable via |
|
||||
|---|---|---|
|
||||
| `.woodpecker.yml` | Woodpecker | https://ci.codeberg.org → enable repo |
|
||||
| `.forgejo/workflows/cpp-tests.yml` | Forgejo Actions | repo *Settings → Actions* (shared runner label `docker`) |
|
||||
|
||||
> These configs are committed in the repo and reach Codeberg through the mirror
|
||||
> — they are not maintained separately on Codeberg.
|
||||
|
||||
---
|
||||
|
||||
## Mirror (eulernest → Codeberg)
|
||||
|
||||
`.gitea/workflows/mirror-to-codeberg.yml` runs on every push to `main` and does
|
||||
a `git push --mirror` from eulernest to
|
||||
`codeberg.org/TMoussa/ConformalLabpp.git`. It uses two secrets:
|
||||
|
||||
- `MIRROR_TOKEN` — read access to the eulernest source repo.
|
||||
- `CODEBERG_TOKEN` — push access to the Codeberg mirror.
|
||||
|
||||
The mirror is **one-way and authoritative-from-eulernest**: do not commit
|
||||
directly on Codeberg, it will be overwritten on the next mirror run.
|
||||
|
||||
---
|
||||
|
||||
## The one rule
|
||||
|
||||
> **eulernest is the gate, Codeberg is the shop window.**
|
||||
> Merge decisions are made on eulernest CI (full suite). Codeberg CI exists so
|
||||
> the public repo also shows a green/red build status to outside readers.
|
||||
|
||||
## See also
|
||||
|
||||
- [dependencies.md](dependencies.md) — what each build mode requires.
|
||||
- [project-structure.md](project-structure.md) — where the test targets live.
|
||||
- [../api/tests.md](../api/tests.md) — per-suite test counts CI checks against.
|
||||
- [../contributing.md](../contributing.md) — the git workflow that feeds CI.
|
||||
@@ -14,7 +14,7 @@ ConformalLabpp/
|
||||
│ │ ├── constants.hpp # conformallab::PI, TWO_PI
|
||||
│ │ ├── clausen.hpp # Cl₂, Lobachevsky Л, ImLi₂
|
||||
│ │ ├── hyper_ideal_geometry.hpp # ζ₁₃/₁₄/₁₅, lᵢⱼ, αᵢⱼ, σᵢ, σᵢⱼ
|
||||
│ │ ├── hyper_ideal_utility.hpp # Tetrahedron volumes (Ushijima 2006 / Springborn 2008)
|
||||
│ │ ├── hyper_ideal_utility.hpp # Tetrahedron volumes (Meyerhoff / Kolpakov–Mednykh)
|
||||
│ │ ├── hyper_ideal_visualization_utility.hpp # Poincaré disk projection, circumcircle helpers
|
||||
│ │ ├── hyper_ideal_functional.hpp # HyperIdeal energy + gradient on ConformalMesh
|
||||
│ │ ├── hyper_ideal_hessian.hpp # HyperIdeal Hessian (symmetric FD, Phase 9b: analytic)
|
||||
|
||||
@@ -13,12 +13,11 @@ with English.
|
||||
## Git workflow
|
||||
|
||||
- `main` is protected on `origin` (Gitea). Push to `dev`, then open a pull request.
|
||||
- `codeberg/main` is the public mirror. It now also runs CI of its own
|
||||
(Woodpecker + Forgejo Actions) — see [architecture/ci-cd.md](architecture/ci-cd.md).
|
||||
- `codeberg/main` can be pushed to directly (public mirror, no CI).
|
||||
- Both remotes must stay in sync after every significant change:
|
||||
```bash
|
||||
git push origin HEAD:dev # triggers eulernest CI (full suite, keyword-gated CGAL)
|
||||
git push codeberg main # updates public mirror → triggers Codeberg CI (test-fast)
|
||||
git push origin HEAD:dev # triggers CI
|
||||
git push codeberg main # updates public mirror
|
||||
```
|
||||
- Branch naming: `feature/<topic>`, `fix/<topic>`, `phase<N>-<topic>`
|
||||
|
||||
@@ -26,30 +25,17 @@ with English.
|
||||
|
||||
## CI
|
||||
|
||||
The project runs CI on **two forges**. Full topology, runners, images and
|
||||
trigger keywords are documented in [architecture/ci-cd.md](architecture/ci-cd.md).
|
||||
|
||||
**eulernest Gitea** (`origin`, self-hosted Raspberry Pi / ARM64) — the
|
||||
authoritative gate. On push to `dev`/`main` or a pull request:
|
||||
Two jobs run on push to `dev`/`main` or on pull requests:
|
||||
|
||||
| Job | What it tests | Trigger |
|
||||
|---|---|---|
|
||||
| `test-fast` | pure-math tests, no Boost | all branches |
|
||||
| `test-cgal` | full CGAL test suite | commit message contains `/test-cgal` |
|
||||
| `quality-gates` | license / codespell / shellcheck / CGAL conventions | commit message contains `/quality-gates` |
|
||||
| `test-cgal` | full CGAL test suite | `main`, `dev`, PRs only |
|
||||
|
||||
A PR is ready to merge when `test-fast` (and, for CGAL-touching work,
|
||||
`test-cgal`) pass. See `.gitea/workflows/cpp-tests.yml` and
|
||||
`.gitea/docker/Dockerfile.ci-cpp`.
|
||||
A PR is ready to merge when both jobs pass.
|
||||
|
||||
**Codeberg** (`codeberg`, public mirror, shared runners) — a lightweight
|
||||
`test-fast` mirror so the public repo also shows a build status. Defined twice,
|
||||
once per CI system Codeberg offers:
|
||||
|
||||
| File | CI system |
|
||||
|---|---|
|
||||
| `.woodpecker.yml` | Woodpecker CI (ci.codeberg.org) |
|
||||
| `.forgejo/workflows/cpp-tests.yml` | Forgejo Actions |
|
||||
The runner is a self-hosted Raspberry Pi (ARM64). See `.gitea/workflows/cpp-tests.yml`
|
||||
and `.gitea/docker/Dockerfile.ci-cpp`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -29,11 +29,11 @@ Java reference implementation: [github.com/varylab/conformallab](https://github.
|
||||
| Reference | Used in |
|
||||
|---|---|
|
||||
| ✅ **Springborn** — *Ideal Hyperbolic Polyhedra and Discrete Uniformization*, Discrete & Computational Geometry **64** (2020), pp. 63–108. DOI: [10.1007/s00454-019-00132-8](https://doi.org/10.1007/s00454-019-00132-8) | `hyper_ideal_geometry.hpp` — ζ₁₃/ζ₁₄/ζ₁₅ functions; `hyper_ideal_functional.hpp` |
|
||||
| ✅ **Springborn** — *A variational principle for weighted Delaunay triangulations and hyperideal polyhedra*, J. Differential Geometry **78**(2) (2008), pp. 333–367. arXiv: [math/0603097](https://arxiv.org/abs/math/0603097) | Tetrahedron volume with one ideal vertex: `calculateTetrahedronVolumeWithIdealVertexAtGamma` in `hyper_ideal_utility.hpp` (Phase 9b analytic Hessian). ⚠️ *Korrektur:* war fälschlich als „Kolpakov–Mednykh 2006" zitiert — dieses Autorenpaar hat 2006 kein gemeinsames Paper veröffentlicht. Die Java-Quelle verlinkt korrekt auf math/0603097 (= Springborn 2008); der falsche Autorenname wurde beim C++-Port hinzugefügt.* |
|
||||
| ✅ **Ushijima** — *A Volume Formula for Generalised Hyperbolic Tetrahedra*, in: Prékopa, Molnár (eds.) *Non-Euclidean Geometries*, Mathematics and Its Applications vol. 581, Springer 2006. DOI: [10.1007/0-387-29555-0_13](https://doi.org/10.1007/0-387-29555-0_13). arXiv: [math/0309216](https://arxiv.org/abs/math/0309216) (2003) | Tetrahedron volume with three ideal vertices: `calculateTetrahedronVolumeFullyIdeal` in `hyper_ideal_utility.hpp`. ⚠️ *Korrektur:* war fälschlich als „Meyerhoff, Ushijima — A Note on the Dirichlet Domain — The Epstein Birthday Schrift" zitiert. Meyerhoff ist kein Autor; Titel und Buch waren beide falsch. Die Java-Quelle verlinkt korrekt auf DOI 10.1007/0-387-29555-0_13 ohne Autorennamen. |
|
||||
| **Pinkall, Polthier** — *Computing Discrete Minimal Surfaces and Their Conjugates*, Experimental Mathematics **2**(1), pp. 15–36 (1993). DOI: [10.1080/10586458.1993.10504266](https://doi.org/10.1080/10586458.1993.10504266) | `euclidean_hessian.hpp` — cotangent Laplacian |
|
||||
| **Bobenko, Springborn** — *Variational Principles for Circle Patterns and Koebe's Theorem*, Trans. Amer. Math. Soc. **356**(2), pp. 659–689 (2004). arXiv: [math/0203250](https://arxiv.org/abs/math/0203250) | Variational angle-sum framework underlying all three functionals |
|
||||
| **Luo** — *Combinatorial Yamabe Flow on Surfaces*, Commun. Contemp. Math. **6**(5), pp. 765–780 (2004). DOI: [10.1142/S0219199704001501](https://doi.org/10.1142/S0219199704001501). arXiv: [math/0306167](https://arxiv.org/abs/math/0306167) | Inversive-distance functional — **new research** in Phase 9a.2 (no Java original; implemented from this paper + Glickenstein 2011 + Bowers-Stephenson 2004) |
|
||||
| ✅ **Kolpakov, Mednykh** — *A Formula for the Volume of a Hyperbolic Tetrahedron*, arXiv: [math/0603097](https://arxiv.org/abs/math/0603097) (2006) | Tetrahedron volume with one ideal vertex: `calculateTetrahedronVolumeWithIdealVertexAtGamma` in `hyper_ideal_utility.hpp` (Phase 9b analytic Hessian) |
|
||||
| ✅ **Meyerhoff, Ushijima** — *A Note on the Dirichlet Domain*, in: The Epstein Birthday Schrift (2006) | Tetrahedron volume with three ideal vertices: `calculateTetrahedronVolumeFullyIdeal` in `hyper_ideal_utility.hpp` |
|
||||
| **Pinkall, Polthier** — *Computing Discrete Minimal Surfaces and Their Conjugates*, Experimental Mathematics (1993) | `euclidean_hessian.hpp` — cotangent Laplacian |
|
||||
| **Bobenko, Springborn** — *Variational Principles for Circle Patterns and Koebe's Theorem*, Transactions AMS (2004) | Variational angle-sum framework underlying all three functionals |
|
||||
| **Luo** — *Combinatorial Yamabe Flow on Surfaces*, Communications in Contemporary Mathematics (2004) | Inversive-distance functional — **new research** in Phase 9a.2 (no Java original; implemented from this paper + Glickenstein 2011 + Bowers-Stephenson 2004) |
|
||||
| **Bowers, Stephenson** — *Uniformizing dessins and Belyĭ maps via circle packing*, Memoirs of the AMS 170(805) (2004) | Introduces **inversive-distance circle packings** (used in Phase 9a.2). *Hinweis:* die zur Initialisierung benutzte Formel I_ij = (ℓ²−r_i²−r_j²)/(2 r_i r_j) ist die **klassische** inversive Distanz (vgl. Glickenstein §5.2: ℓ²=r_i²+r_j²+2r_ir_jη), nicht eine eigene „Bowers-Stephenson-Identität" — B–S liefern die Packungstheorie, nicht diese Formel. |
|
||||
| **Glickenstein** — *Discrete conformal variations and scalar curvature on piecewise flat two- and three-dimensional manifolds*, J. Differential Geometry **87**(2) (2011), pp. 201–238 | Analytic Hessian of the inversive-distance functional. ⚠️ *Korrektur:* die Arbeit nummeriert Gleichungen **nicht** im Format „(4.6)" — der Verweis ist durch die **§5.2**-Parametrisierung ℓ²_ij = r²_i + r²_j + 2 r_i r_j η_ij zu ersetzen. Cross-correspondence: η_ij ist die inversive Distanz und entspricht dem Kosinus des **Supplements** des Schnittwinkels (Schnitt bei arccos(−η_ij)) — also I_ij = cos θ_e **nur bis aufs Vorzeichen/Supplement**, nicht wörtlich. |
|
||||
| ✅ **Bobenko, Pinkall, Springborn** — *Discrete conformal maps and ideal hyperbolic polyhedra*, Geometry & Topology **19**(4) (2015), pp. 2155–2215. arXiv: [1005.2698](https://arxiv.org/abs/1005.2698) (first posted 2010) | Face-based circle-packing functional (`CPEuclideanFunctional.java` → `cp_euclidean_functional.hpp`, Phase 9a.1) |
|
||||
@@ -79,10 +79,10 @@ builds on this paper and augments it with Ptolemaic flips.
|
||||
|---|---|
|
||||
| **Farkas, Kra** — *Riemann Surfaces*, Springer GTM 71 | Siegel period matrix, Teichmüller theory |
|
||||
| **Siegel** — *Topics in Complex Function Theory, Vol. 2*, Wiley | Siegel upper half-space H_g, Sp(2g,ℤ) reduction |
|
||||
| **Bobenko, Mercat, Schmies** — *Conformal Structures and Period Matrices of Polyhedral Surfaces*, in: Bobenko, Klein (eds.) *Computational Approach to Riemann Surfaces*, Lecture Notes in Mathematics vol. 2013, Springer 2011, pp. 213–226. DOI: [10.1007/978-3-642-17413-1_7](https://doi.org/10.1007/978-3-642-17413-1_7) | Discrete period matrices on polyhedral surfaces |
|
||||
| **Bobenko, Mercat, Schmies** — *Period Matrices of Polyhedral Surfaces*, in: Computational Approach to Riemann Surfaces (2011) | Discrete period matrices on polyhedral surfaces |
|
||||
| **Bobenko, Bücking** — *Convergence of discrete period matrices and discrete holomorphic integrals for ramified coverings of the Riemann sphere*, Math. Phys. Anal. Geom. **24**, Art. 23 (2021). DOI: [10.1007/s11040-021-09394-2](https://doi.org/10.1007/s11040-021-09394-2) | Phase 10b: discrete Siegel period matrix Ωᵢⱼ from cotangent-weighted integration **plus** the convergence result Ω_discrete → Ω_smooth under refinement (für ramified coverings) — belegt die Diskret-zu-glatt-Aussage in `novelty-statement.md §3.3. |
|
||||
| **Rivin, Schlenker** — *The Schläfli formula in Einstein manifolds with boundary*, Electron. Res. Announc. AMS **5** (1999), pp. 18–23 | Phase 9b-analytic: modern form of the Schläfli identity `2 dV = Σ aₑ dαₑ` for manifolds with boundary — the bilinear form used to derive the analytic HyperIdeal Hessian. |
|
||||
| **Pinkall, Springborn** — *A discrete version of Liouville's theorem on conformal maps*, Geometriae Dedicata **214** (2021), pp. 389–398. arXiv: [1911.00966](https://arxiv.org/abs/1911.00966) | Phase 10b uniqueness: proves that the discrete conformal structure (and hence Ω) is a conformal invariant — the discrete Liouville theorem. Justifies that conformallab++ outputs a canonical representative. |
|
||||
| **Born, Bücking, Springborn** — *Quasiconformal distortion of projective transformations and discrete conformal maps*, Discrete & Computational Geometry **57**(2), pp. 305–317 (2017). DOI: [10.1007/s00454-016-9854-7](https://doi.org/10.1007/s00454-016-9854-7). arXiv: [1505.01341](https://arxiv.org/abs/1505.01341) (preprint 2015) | Phase 10c error analysis: quantifies how well the discrete H²/Γ embedding approximates the smooth hyperbolic metric; error bounds for the Fuchsian group representation. |
|
||||
| **Knöppel, Crane, Pinkall, Schröder** — *Stripe Patterns on Surfaces*, ACM Transactions on Graphics **34**(4), Article 39 (SIGGRAPH 2015). DOI: [10.1145/2767000](https://doi.org/10.1145/2767000). ⚠️ *Kein arXiv-Preprint* (arXiv:1502.06686 ist ein anderes Paper — Data-Driven Shape Analysis — und wurde aus dem papers/-Ordner entfernt). | Phase 10a cross-validation: applies discrete holomorphic 1-forms to direction field design; geometry-central provides an independent C++ implementation to cross-check the Phase 10a `DiscreteHolomorphicFormUtility` port. |
|
||||
| **Born, Bücking, Springborn** — *Quasiconformal distortion of projective transformations and discrete conformal maps*, arXiv: [1505.01341](https://arxiv.org/abs/1505.01341) (2015) | Phase 10c error analysis: quantifies how well the discrete H²/Γ embedding approximates the smooth hyperbolic metric; error bounds for the Fuchsian group representation. |
|
||||
| **Knöppel, Crane, Pinkall, Schröder** — *Stripe Patterns on Surfaces*, ACM SIGGRAPH (2015). DOI: [10.1145/2766890](https://doi.org/10.1145/2766890) | Phase 10a cross-validation: applies discrete holomorphic 1-forms to direction field design; geometry-central provides an independent C++ implementation to cross-check the Phase 10a `DiscreteHolomorphicFormUtility` port. |
|
||||
| **Sawhney, Crane** — *Boundary First Flattening*, ACM TOG **37**(1), Article 5 (2017). DOI: [10.1145/3132705](https://doi.org/10.1145/3132705) | Complementary method to Phase 9d: boundary-prescribed conformal flattening — user specifies boundary shape, interior conforms freely. Contrast: conformallab++ prescribes cone angles in the interior; BFF prescribes the boundary. Alternative approach for applications needing controlled boundary. |
|
||||
|
||||
@@ -1,218 +0,0 @@
|
||||
# Agentic system design — multi-model audit-to-code pipeline
|
||||
|
||||
**Purpose:** turn the static plan in [`finding-orchestration.md`](finding-orchestration.md)
|
||||
into a running **multi-agent system** that works the planned sessions, interleaves
|
||||
**reviews and fresh audits**, and lands findings in the code **cleanly** (atomic,
|
||||
attributed, tested, parity-safe).
|
||||
|
||||
This design is grounded in what Session 1+2 actually did: Haiku renamed/constants,
|
||||
Sonnet did validation/error-handling, **Opus** did the numerics + the **review gate**
|
||||
that validated the cheaper models' work, then diagnosed a CI OOM and sequenced the
|
||||
#36→#39 rebase. The system below generalises exactly that flow.
|
||||
|
||||
> **Companion:** the *forward* pipeline that builds the library (phases, port, research) is [`../roadmap/feature-dev-agentic-system.md`](../roadmap/feature-dev-agentic-system.md). Feature-dev lands code; this audit system hardens it — they compose into a loop.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 1. Design principles (the lessons that shape the architecture)
|
||||
|
||||
1. **Assign by cognitive load, not severity.** A 🔴 one-line switch is Haiku/Sonnet
|
||||
work; a 🟡 numerical reformulation is Opus. The dispatcher routes on *"how much
|
||||
must be understood to get it right"*.
|
||||
2. **Reviewer ≠ implementer, and the reviewer is the strong model.** Every change
|
||||
passes an **independent Opus review gate**. Self-review misses parity/numeric
|
||||
regressions; a fresh high-capability pass catches them (it caught nothing wrong
|
||||
this session — which is the point: it *certified* the cheap work).
|
||||
3. **The plan doc is the blackboard.** Agents are stateless sessions that
|
||||
read → act → write `finding-orchestration.md`. No message bus; the shared state
|
||||
is human-inspectable markdown.
|
||||
4. **One finding → one commit → one test → one attribution.** Traceability and clean
|
||||
revertability. Renames ship with `[[deprecated]]` aliases; refactors ship with
|
||||
value-identity assertions.
|
||||
5. **Gate everything irreversible to a human.** Legal/provenance (G0), public-API
|
||||
stabilization (A4/A5), and the final merge are human decisions the system
|
||||
*surfaces*, never makes.
|
||||
6. **Separate code failures from infra failures.** A CI red is not automatically a
|
||||
code bug — a dedicated diagnostic step distinguishes `cc1plus Killed` (OOM) from a
|
||||
real defect before anyone touches the code.
|
||||
7. **Exploit warm context.** Batch findings that touch the same code while a session
|
||||
is warm (S2's I1/H1/N7 were done by Opus *immediately* after the `newton_core`
|
||||
refactor instead of as a cold pickup).
|
||||
|
||||
---
|
||||
|
||||
## 2. Roles
|
||||
|
||||
| Role | Model | Owns | Triggered by | Output |
|
||||
|---|---|---|---|---|
|
||||
| **Orchestrator / Planner** | Sonnet | the backlog + dispatch | new findings, finished session | next session spec + launch prompt; updated status board |
|
||||
| **Mechanic** | **Haiku** | renames, named constants, doc/citation/format fixes | dispatched mechanical findings | atomic commits + `[[deprecated]]` aliases |
|
||||
| **Engineer** | **Sonnet** | tests, error handling, CI, small/safe API changes, robustness | dispatched standard findings | commits + per-finding tests |
|
||||
| **Specialist** | **Opus** | numerics, math correctness, architecture refactors, public-API/irreversible calls | dispatched judgment findings | commits + tests + design notes |
|
||||
| **Reviewer (Gatekeeper)** | **Opus** | the review gate after every implementation session | session marked "implemented" | APPROVE / CHANGES-REQUESTED + fixes |
|
||||
| **Auditor (External reviewer)** | **Opus** | generating *new* audits with `file:line` + fix + acceptance | new module, schedule, or "re-audit" | new audit doc → new backlog findings |
|
||||
| **Integrator / Release** | Sonnet | git mechanics: branch, PR, rebase/conflict, CI watch, merge sequencing | approved session | PR; rebases; merge (after human gate) |
|
||||
| **Infra Diagnostician** | Sonnet→Opus | CI-failure triage (code vs runner/OOM), infra fixes | any CI red | "code vs infra" verdict + fix (e.g. `-j1`) |
|
||||
| **Human (Owner)** | — | legal/provenance, API stabilization, final merge | surfaced decision | decision recorded in the plan |
|
||||
|
||||
> Tiering rule of thumb: **Haiku ≈ "transcribe the fix"**, **Sonnet ≈ "engineer the
|
||||
> fix from a clear spec"**, **Opus ≈ "decide what the fix should be" + "judge someone
|
||||
> else's fix"**.
|
||||
|
||||
---
|
||||
|
||||
## 3. Workflow (state machine, per session)
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ BACKLOG (audit findings, status ⬜/⏸/⛔ in the plan doc) │
|
||||
└───────────────┬──────────────────────────────────────────┘
|
||||
│ Orchestrator picks next ⬜, routes by load
|
||||
▼
|
||||
┌────────── PLAN ──────────┐ emits a session-prompts.md block
|
||||
│ model + findings + branch │────────────────────────────────────────┐
|
||||
└───────────┬──────────────┘ │
|
||||
▼ │
|
||||
IMPLEMENT (Mechanic | Engineer | Specialist) │
|
||||
· atomic commit per finding · test per finding │
|
||||
· deprecated aliases / value-identity asserts │
|
||||
▼ │
|
||||
SELF-VERIFY (build + full test suite green locally) │
|
||||
▼ │
|
||||
┌──── REVIEW GATE (Opus, independent) ────┐ │
|
||||
│ build · parity · value-identity · │── CHANGES-REQUESTED ─────┘
|
||||
│ public-surface · attribution · plan ✅ │
|
||||
└───────────┬─────────────────────────────┘ APPROVE
|
||||
▼
|
||||
INTEGRATE (Integrator) → open PR
|
||||
▼
|
||||
CI ──red──► INFRA DIAGNOSTICIAN ──code?──► back to IMPLEMENT
|
||||
│ └─infra?─► fix infra (e.g. -j1), re-run
|
||||
green
|
||||
▼
|
||||
HUMAN MERGE GATE ──► MERGE ──► Orchestrator marks ✅, updates plan
|
||||
▼
|
||||
(every N sessions / per module) AUDITOR re-audit ──► new BACKLOG findings
|
||||
```
|
||||
|
||||
Key edges proven this session: the **CHANGES-REQUESTED loop** (none needed — Sonnet/Haiku
|
||||
work was clean), the **INFRA branch** (the `cc1plus` OOM → `-j1`, *not* a code change),
|
||||
and the **multi-PR sequencing** at INTEGRATE (#36 merged first, #39 rebased to drop the
|
||||
duplicate A1–A3).
|
||||
|
||||
---
|
||||
|
||||
## 4. Shared state & artifacts (the blackboard)
|
||||
|
||||
- **`finding-orchestration.md`** — backlog + status board + per-finding model/session.
|
||||
The single source of truth every agent reads/writes.
|
||||
- **Audit docs** (`*-audit-*.md`) — the finding *specs*: `file:line`, fix, acceptance
|
||||
criteria. The Auditor writes them; implementers consume them.
|
||||
- **`session-prompts.md`** — launch templates the Orchestrator fills in per session.
|
||||
- **PRs + commits** — the audit trail; commit trailer `Co-Authored-By: Claude <Model>`
|
||||
records *which* model did *which* finding.
|
||||
- **Decision log** (a section in the plan) — human gates: G0 status ("authors emailed,
|
||||
awaiting reply"), API-stabilization decisions, merge approvals.
|
||||
|
||||
Why blackboard over a chat-bus: sessions are short-lived and stateless; the durable
|
||||
state is the markdown; humans can read and override it at any point.
|
||||
|
||||
---
|
||||
|
||||
## 5. Gates & guardrails
|
||||
|
||||
**Automated gates (must pass to advance):**
|
||||
- Build clean; **full test suite green, no count regression**.
|
||||
- **No parity/golden-vector test perturbed** (HardJava-style defaults intact).
|
||||
- New public surface (result types, enums, file formats) is intentional + documented.
|
||||
- Each finding has a commit + a test; renames carry `[[deprecated]]` aliases.
|
||||
|
||||
**Human gates (system surfaces, human decides):**
|
||||
- **Legal/provenance (G0)** — porting/relicensing rights. Blocks G1–G12, D1/D2, A4/A5.
|
||||
- **Public-API stabilization** — once in a released/CGAL API, renames need a deprecation
|
||||
cycle; lock names only when the owner says so.
|
||||
- **Final merge** — the agent prepares a green, mergeable PR; the human clicks merge
|
||||
(or explicitly delegates it).
|
||||
|
||||
**Blocked-dependency tracking:** findings gated by an open human decision are marked ⛔
|
||||
in the plan and never dispatched (S6 stays blocked until G0 resolves).
|
||||
|
||||
---
|
||||
|
||||
## 6. How phases, reviews and audits interleave (cadence)
|
||||
|
||||
- **Per session:** implement → **review gate** (always). The review is *inline* with
|
||||
the cadence, not a separate phase you forget.
|
||||
- **Per PR:** CI + infra-triage + human merge gate.
|
||||
- **Per module / every N sessions / on request:** an **Auditor** pass produces a fresh
|
||||
audit, refilling the backlog. This is the loop that keeps the system honest as the
|
||||
code grows — new code gets audited, new findings get planned, the cycle repeats.
|
||||
- **Continuous:** the Orchestrator keeps the plan's status board current so any human
|
||||
glance shows "done / in-flight / blocked".
|
||||
|
||||
So the rhythm is: **audit → plan → (implement → review)\* → integrate → merge → re-audit.**
|
||||
|
||||
---
|
||||
|
||||
## 7. How to actually run it with Claude
|
||||
|
||||
Three implementation levels, cheapest first:
|
||||
|
||||
1. **Manual sessions (today).** The Orchestrator (you, or a Sonnet session) hands a
|
||||
`session-prompts.md` block to a fresh session set to the named model. The review
|
||||
gate is a second session set to Opus. This already works — it is exactly Session 1+2.
|
||||
2. **Claude Code subagents.** Define each role as a subagent with a pinned model and a
|
||||
tight tool allow-list (Mechanic: edit+bash; Reviewer: read+bash, no write-to-main;
|
||||
Integrator: bash/git/PR API). A top-level "Orchestrator" session dispatches via the
|
||||
Task tool and reconciles the plan doc.
|
||||
3. **Claude Agent SDK (autonomous).** A supervisor process loops the state machine:
|
||||
reads the plan, spawns role-agents (model per role), runs the gates as code
|
||||
(build/test/CI checks), and stops at human gates. The blackboard (the markdown +
|
||||
git) is the durable state across runs.
|
||||
|
||||
Pin models per role explicitly; do **not** let a cheap role silently escalate — that is
|
||||
what the review gate and the dispatcher's load-routing are for.
|
||||
|
||||
---
|
||||
|
||||
## 8. Cost & efficiency strategy
|
||||
|
||||
- **Route down aggressively, gate up always.** Most findings go to Haiku/Sonnet; only
|
||||
the Opus review gate and genuine-judgment findings pay for Opus.
|
||||
- **Batch by warm context.** Group findings touching the same files into one session
|
||||
(S2's three findings shared `newton_core`); avoid cold re-derivation.
|
||||
- **Make the reviewer cheap to satisfy.** Atomic commits + per-finding tests + a fixed
|
||||
checklist make the Opus review fast (it reads a small, well-scoped diff).
|
||||
- **Fail fast on infra.** The diagnostician prevents wasted Opus cycles "fixing" a
|
||||
phantom code bug that was really an OOM.
|
||||
|
||||
---
|
||||
|
||||
## 9. Worked example (this session, mapped to the roles)
|
||||
|
||||
| What happened | Role(s) |
|
||||
|---|---|
|
||||
| A1–A3 renames, N4/N6 constants, M-citations | **Mechanic (Haiku)** |
|
||||
| V1–V4 error handling, C2/C3 coverage gate, I-tests | **Engineer (Sonnet)** |
|
||||
| B1 inv-dist block-FD port, N3/N5, `newton_core` (H2+B2/B4/B5), S2 (I1/H1/N7) | **Specialist (Opus)** |
|
||||
| Validated the Haiku/Sonnet commits (value-identity, alias forwarding, coverage logic) | **Reviewer (Opus)** |
|
||||
| Built the plan, model assignment, session prompts | **Orchestrator** |
|
||||
| `#36` merge → rebase `#39` (drop dup A1–A3) → PRs | **Integrator** |
|
||||
| `test-fast` red → proved `cc1plus` OOM via a pure-main probe → `-j1` fix | **Infra Diagnostician** |
|
||||
| G0 porting rights: "authors emailed, awaiting reply"; merge approval | **Human gate** |
|
||||
|
||||
The audits themselves (the 11 docs) were a prior **Auditor (Opus)** pass — the input the
|
||||
whole pipeline consumes.
|
||||
|
||||
---
|
||||
|
||||
## 10. Failure modes the design absorbs
|
||||
|
||||
- **Cheap model gets it subtly wrong** → caught by the Opus review gate.
|
||||
- **Refactor silently changes numerics** → caught by parity tests + value-identity asserts.
|
||||
- **CI red panic** → diagnostician separates infra (OOM) from code before any "fix".
|
||||
- **Two PRs overlap** (#36/#39) → Integrator sequences merge + rebase deterministically.
|
||||
- **A finding needs a human/legal call** → marked ⛔, never auto-dispatched.
|
||||
- **Backlog goes stale as code grows** → periodic Auditor re-audit refills it.
|
||||
```
|
||||
@@ -59,7 +59,7 @@ Status: ✅ done · ⬜ open (actionable) · ⏸ deferred (intentional) · ⛔ b
|
||||
| V3 | input-val | 🟡 | ✅ | Sonnet | S1 |
|
||||
| C1 | test-cov | 🔴 | ✅ | Sonnet | S1 |
|
||||
| N4, N6 | numerics | 🟡/🔵 | ✅ | Haiku | S1 |
|
||||
| M1, M2, M4 | math-cite | 🟡/🔵 | ✅⚠️ | Haiku → re-fix 2026-06-04 | S1 + nachkorrigiert |
|
||||
| M1, M2, M4 | math-cite | 🟡/🔵 | ✅ | Haiku | S1 |
|
||||
| C2, C3 | test-cov | 🔴 | ✅ | Sonnet | S1 |
|
||||
| V1, V2, V4 | input-val | 🟡 | ✅ | Sonnet | S1 |
|
||||
| I2, I3, I4 | test-cov | 🟡 | ✅ | Sonnet | S1 |
|
||||
@@ -70,10 +70,10 @@ Status: ✅ done · ⬜ open (actionable) · ⏸ deferred (intentional) · ⛔ b
|
||||
| I1 | test-cov | 🟡 | ✅ | Opus | S2 |
|
||||
| H1 | test-cov | 🔵 | ✅ | Opus | S2 |
|
||||
| N7 | numerics | 🔵 | ✅ | Opus | S2 |
|
||||
| **H3** | test-cov | 🔵 | ✅ | Sonnet→🔍Opus | **S3** |
|
||||
| **H4** | test-cov | 🔵 | ✅ | Sonnet→🔍Opus | **S3** |
|
||||
| **H5** | test-cov | 🔵 | ✅ | Sonnet→🔍Opus | **S3** |
|
||||
| **V5, V6** | input-val | 🔵 | ✅ | Sonnet→🔍Opus | **S3** |
|
||||
| **H3** | test-cov | 🔵 | ⬜ | Sonnet→🔍Opus | **S3** |
|
||||
| **H4** | test-cov | 🔵 | ⬜ | Sonnet→🔍Opus | **S3** |
|
||||
| **H5** | test-cov | 🔵 | ⬜ | Sonnet→🔍Opus | **S3** |
|
||||
| **V5, V6** | input-val | 🔵 | ⬜ | Sonnet→🔍Opus | **S3** |
|
||||
| **N2** | numerics | 🟡 | ⬜ | Haiku→🔍Opus | **S4** |
|
||||
| **thread-safety doc** | thread-safety | 🟡 | ⬜ | Haiku→🔍Opus | **S4** |
|
||||
| **M3** | math-cite | 🟡 | ⬜ | Haiku→🔍Opus | **S4** |
|
||||
@@ -126,18 +126,14 @@ CGAL result types (`Conformal_map_result`, `Hyper_ideal_map_result`,
|
||||
|
||||
</details>
|
||||
|
||||
### ✅ S3 — Robustness & test-gap closure (DONE, 2026-06-01, Sonnet impl + Opus review)
|
||||
Implementation shipped in commit `135bcf0` (included in P1 merge `bd613a6`).
|
||||
Follow-up commit closes the doc-tracker gap and fixes dead `found_dofvector` variable
|
||||
(V5 rule 4: `<DOFVector>` missing now throws instead of silently returning empty `x`).
|
||||
- **H3** — `enforce_gauss_bonnet` returns `|deficit|` (both overloads); 3 new tests.
|
||||
- **H4** — `ReduceToFD_ThrowsForRealAxisBoundary` covers `Im(τ)==0.0` exact boundary.
|
||||
- **H5** — 2 integration tests: sliver triangle (no crash/NaN) + exact-degenerate collinear.
|
||||
- **V5** — `load_result_xml` rejects non-conforming XML (3 strict-subset checks); canonical
|
||||
round-trip regression test passes. V5 rule 4 (`<DOFVector>` must be present) now enforced.
|
||||
- **V6** — `check_dof_vector_size(x, expected, context)` throws on mismatch; 3 new tests.
|
||||
- **🔍 Opus review:** CHANGES-REQUESTED resolved — implementation correct; code commits
|
||||
were redundant with `135bcf0` on main; doc follow-up applied on `chore/s3-followup`.
|
||||
### ⬜ S3 — Robustness & test-gap closure (Sonnet → 🔍 Opus)
|
||||
- **H3** — `enforce_gauss_bonnet` reports the magnitude of the applied correction.
|
||||
- **H4** — test `reduce_to_fundamental_domain` boundary `Im(τ)==0.0`.
|
||||
- **H5** — integration test for the degenerate-triangle path in the Newton solve.
|
||||
- **V5** — make the hand-rolled XML reader *reject* reformatted-but-valid XML
|
||||
instead of silently mis-reading it into zeros.
|
||||
- **V6** — DOF-vector-vs-mesh size check on load.
|
||||
- **🔍 Opus review:** verify the new rejections don't break valid round-trips.
|
||||
|
||||
### ⬜ S4 — Documentation & citations (Haiku → 🔍 Opus)
|
||||
- **N2** — `doc/math/tolerances.md`: every numerical threshold, its role, and
|
||||
@@ -166,42 +162,6 @@ Gated by the author's reply on porting/relicensing rights.
|
||||
### 👤 Out of model scope
|
||||
- **M5** — the large hand-derivations need a domain-expert prose review
|
||||
(the numerical results are already validated; the prose is not).
|
||||
- **Alle Zitationen** — alle `references.md`-Einträge müssen von einem
|
||||
Fachexperten manuell verifiziert werden (siehe Lektion unten).
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Lektion: KI-gestützte Citation-Audits können Fehler einführen
|
||||
|
||||
**Datum:** 2026-06-04
|
||||
**Befund:** Die M1-Auflösung in S1 (Haiku, 2026-05-31) war selbst fehlerhaft:
|
||||
|
||||
- Der Haiku-Audit erkannte korrekt, dass `Kolpakov–Mednykh` in `references.md` fehlt
|
||||
- Als „Fix" wurde die Zeile mit arXiv:math/0603097 ergänzt — aber **math/0603097 ist Springborn 2008**, nicht Kolpakov–Mednykh
|
||||
- Das Autorenpaar „Kolpakov & Mednykh" hat **2006 kein gemeinsames Paper veröffentlicht** (früheste Zusammenarbeit: 2010, über Torusknoten, nicht Tetraedervolumen)
|
||||
- Der falsche Autorenname entstand bereits beim Java→C++-Port; der Audit hat ihn zementiert statt korrigiert
|
||||
|
||||
**Nachkorrektur:** 2026-06-04, 7 Dateien korrigiert (Code-Kommentare, references.md, roadmap, architecture-doc, tests-doc, audit-doc).
|
||||
|
||||
**Konsequenz für das Projekt:**
|
||||
|
||||
> KI-Modelle können bei Citation-Audits plausibel klingende aber falsche
|
||||
> Autoren/Jahres-Zuordnungen produzieren — besonders wenn die Primärquelle
|
||||
> (Java-Code) nur einen Link ohne Autorennamen enthält.
|
||||
|
||||
**Empfehlung:**
|
||||
|
||||
Vor jeder öffentlichen Veröffentlichung / CGAL-Submission müssen **alle** Einträge
|
||||
in `references.md` von einem **Fachexperten (Mensch)** gegen die tatsächlichen
|
||||
Papiere verifiziert werden:
|
||||
|
||||
| Priorität | Was prüfen |
|
||||
|---|---|
|
||||
| 🔴 Hoch | Formeln in Code-Kommentaren (`hyper_ideal_utility.hpp`, `hyper_ideal_functional.hpp`) gegen die zitierten Paper |
|
||||
| 🔴 Hoch | Ushijima 2006 (DOI 10.1007/0-387-29555-0_13) — arXiv math/0309216 — korrigiert: kein Meyerhoff, anderer Titel, anderes Buch |
|
||||
| 🟡 Mittel | Ob Springborn 2008 (math/0603097) die 12-Term-Lobachevsky-Formel tatsächlich enthält |
|
||||
| 🟡 Mittel | M3: post-2023 / arXiv-only Zitationen (Bowers-Bowers-Lutz 2026 etc.) |
|
||||
| 🔵 Niedrig | Alle weiteren `references.md`-Einträge auf Titel/Jahr/DOI-Konsistenz |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -12,12 +12,9 @@ Status legend: 🔴 Critical · 🟡 Important · 🔵 Polish
|
||||
|
||||
> **✅ Resolution status (2026-05-31, Session 1):** **V1/V2/V4 ✅** (JSON parse +
|
||||
> field checks, XML stoi/stod guarded — all surface as `std::runtime_error` with
|
||||
> path), **V3 ✅** (NaN/Inf vertex-coordinate guard in `load_mesh`).
|
||||
> **V5/V6 ✅** (2026-06-01, S3): `load_result_xml` now enforces a strict
|
||||
> internal-only XML subset (three format checks; non-conforming files throw
|
||||
> `runtime_error` instead of silently returning zeros); `check_dof_vector_size`
|
||||
> helper added for the DOF-count mismatch check at call sites. 6 new tests.
|
||||
> See [`finding-orchestration.md`](finding-orchestration.md). 313/313 tests green.
|
||||
> path), **V3 ✅** (NaN/Inf vertex-coordinate guard in `load_mesh`). **V5** (XML
|
||||
> strict-reject) and **V6** (DOF-size check) **open** — scheduled S3 in
|
||||
> [`finding-orchestration.md`](finding-orchestration.md). 298/298 tests green.
|
||||
|
||||
> **Threat model:** this is a scientific library, not a network service, so the bar
|
||||
> is "fail cleanly and diagnosably", not "resist attackers". But meshes and result
|
||||
|
||||
@@ -16,16 +16,6 @@ Status legend: 🔴 Critical · 🟡 Important · 🔵 Polish
|
||||
> future-dated/arXiv citations) **open** → S4; **M5** (prose derivation review)
|
||||
> needs a **domain expert** (out of model scope). See
|
||||
> [`finding-orchestration.md`](finding-orchestration.md).
|
||||
>
|
||||
> **⚠️ M1 Nachkorrektur (2026-06-04):** Die ursprüngliche M1-Auflösung war selbst
|
||||
> fehlerhaft — arXiv:math/0603097 ist **Springborn 2008** (*A variational principle
|
||||
> for weighted Delaunay triangulations and hyperideal polyhedra*), nicht
|
||||
> Kolpakov–Mednykh. Kolpakov und Mednykh haben 2006 kein gemeinsames Paper
|
||||
> veröffentlicht (früheste Zusammenarbeit: 2010). Der falsche Autorenname wurde beim
|
||||
> Java→C++-Port erfunden; die Java-Quelle verlinkt korrekt auf math/0603097 ohne
|
||||
> Autorennamen. Korrekturen angewendet in: `hyper_ideal_utility.hpp`,
|
||||
> `hyper_ideal_functional.hpp`, `test_hyper_ideal_functional.cpp`,
|
||||
> `doc/math/references.md`.
|
||||
|
||||
> **Good news up front:** `doc/math/references.md` is unusually scholarly and already
|
||||
> contains several *self-corrections* (the Glickenstein "eq. (4.6)" numbering fix →
|
||||
@@ -68,8 +58,8 @@ load-bearing formula with no entry.
|
||||
|
||||
### Fix
|
||||
Add a `references.md` row: Kolpakov, Mednykh — *(full title)*, arXiv `math/0603097`,
|
||||
mapped to `hyper_ideal_utility.hpp`. Ushijima 2006 (DOI 10.1007/0-387-29555-0_13, arXiv math/0309216) has a row —
|
||||
sole author is Ushijima; "Meyerhoff" is not a co-author (corrected 2026-06-04).
|
||||
mapped to `hyper_ideal_utility.hpp`. Likewise confirm the Meyerhoff / Ushijima 2006
|
||||
volume reference (cited in code) has a row.
|
||||
|
||||
### Acceptance criteria
|
||||
- Every formula cited inline in `code/include/` has a matching `references.md` entry.
|
||||
|
||||
@@ -19,11 +19,9 @@ Status legend: 🔴 Critical · 🟡 Important · 🔵 Hint / nice-to-have
|
||||
> **I2/I3/I4 ✅** (serialization / mesh / spherical-hessian negative tests),
|
||||
> **H2 ✅** (five Newton loops unified into `newton_core`, folding in B2/B3/B4/B5).
|
||||
> **I1 ✅** (`NewtonStatus` enum) + **H1 ✅** (iteration count) done in S2.
|
||||
> **H3/H4/H5 ✅** (2026-06-01, S3): `enforce_gauss_bonnet` returns `|deficit|`;
|
||||
> `ReduceToFD_ThrowsForRealAxisBoundary` test added; two degenerate-triangle
|
||||
> integration tests characterize the Newton solver's behavior.
|
||||
> **Open:** **I5** (coverage on full suite, un-gates the 80/70/90 threshold) → S5.
|
||||
> See [`finding-orchestration.md`](finding-orchestration.md). 313/313 tests green.
|
||||
> **Open:** **H3/H4/H5** → S3; **I5** (coverage on full suite, un-gates the
|
||||
> 80/70/90 threshold) → S5.
|
||||
> See [`finding-orchestration.md`](finding-orchestration.md). 298/298 tests green.
|
||||
|
||||
> **Note:** This audit is complementary to `external-audit-2026-05-30.md` (which
|
||||
> covers port-faithfulness bugs). There is no overlap in findings — this one is
|
||||
|
||||
@@ -1,206 +0,0 @@
|
||||
# Feature-development agentic system — phases, port & research
|
||||
|
||||
**Companion to** [`../reviewer/agentic-system-design.md`](../reviewer/agentic-system-design.md)
|
||||
(the *audit / remediation* system). This one is the **forward** pipeline: it
|
||||
*produces* the library — working the planned phases in [`phases.md`](phases.md),
|
||||
finishing the **Java port**, and extending into the **research questions** in
|
||||
[`research-track.md`](research-track.md).
|
||||
|
||||
The two systems **compose into a loop**: feature-dev lands new code → the audit
|
||||
system audits & hardens it → findings flow back. Build forward, audit back.
|
||||
|
||||
> **Companion files (this system's plan + prompts, mirroring the audit system):**
|
||||
> - [`phase-orchestration.md`](phase-orchestration.md) — the DAG/status board (← `finding-orchestration.md`)
|
||||
> - [`phase-prompts.md`](phase-prompts.md) — ready-to-paste phase prompts (← `session-prompts.md`)
|
||||
|
||||
---
|
||||
|
||||
## 1. Why this needs a different shape than the audit system
|
||||
|
||||
The audit pipeline consumes **closed, fully-specified** findings (`file:line`, fix,
|
||||
acceptance). Feature work is the opposite — it is **open-ended and gated by truth
|
||||
sources that differ per item**. Three properties of `phases.md` drive the design:
|
||||
|
||||
1. **Port vs. Research is a hard, explicit fork.**
|
||||
- *Port* items have a **Java golden oracle** (e.g. `CPEuclideanFunctional.java`,
|
||||
`ConesUtility.java`) → faithful translation, bit-for-bit parity testing.
|
||||
- *Research* items have **NONE** (e.g. inversive-distance, 9b-analytic, Phase 12/13)
|
||||
→ derive from papers, *design* the validation (no oracle exists).
|
||||
2. **Phases form a dependency DAG with hard prerequisites.** 10b needs 10a; Phase 13
|
||||
needs 9c+10a+10b+10c **and** the holonomy-bug fix. Parallel "chains" exist
|
||||
(Chain A = Phase 12, near-term; Chain B = Phase 13 capstone).
|
||||
3. **The hard part is mathematical correctness, not typing.** A research item can be
|
||||
*numerically wrong while compiling and "converging"*. Validation must be invented
|
||||
(invariants, convergence studies, cross-library), and a domain expert may need to
|
||||
sign off the discretization.
|
||||
|
||||
So this system adds, over the audit one: a **DAG-aware scheduler**, a **port/research
|
||||
router**, a **spike→go/no-go gate** (research may fail), and a **validation-strategy
|
||||
designer**. It **reuses** the audit system's Integrator / CI / Review-gate machinery.
|
||||
|
||||
---
|
||||
|
||||
## 2. Roles
|
||||
|
||||
| Role | Model | Owns | Output |
|
||||
|---|---|---|---|
|
||||
| **Roadmap Orchestrator** | Sonnet | the phase DAG; picks next *unblocked* item; runs chains A/B in parallel; classifies port vs research | next-item spec + launch prompt |
|
||||
| **Theorist / Spec-author** | **Opus** | turn a phase into a precise spec: port → extract algorithm + golden values from Java/dissertation; research → read papers, **derive the discrete formulas**, write the LaTeX note, **design the validation** | phase-spec doc + `doc/math/*-derivation.md` |
|
||||
| **Prototyper (Spike)** | Opus→Sonnet | a throwaway reference impl (scratch branch) that **numerically confirms the math before productionising** — the research de-risk | go/no-go + a validated numerical recipe |
|
||||
| **Porter** | **Sonnet** | faithful Java→C++ translation for *port* items; `// Ported from …` provenance | impl + golden-oracle parity tests |
|
||||
| **Research Implementer** | Opus→Sonnet | productionise the prototyped research math into the library | impl + tests |
|
||||
| **Validation Engineer** | **Sonnet** | build the test battery appropriate to the item (see §4) | golden / invariant / convergence / cross-lib tests |
|
||||
| **Reviewer (Gatekeeper)** | **Opus** | math-soundness + parity + correctness gate (per item) | APPROVE / CHANGES-REQUESTED |
|
||||
| **Integrator / Release** | Sonnet | branch/PR/CI/rebase/merge | merged PR *(shared with the audit system)* |
|
||||
| **Scholar / Doc** | **Haiku** | `references.md` rows, math-note polish, user-manual/CLI docs | docs |
|
||||
| **Human (Domain expert / Owner)** | — | research direction; **sign off the discretisation**; precision-substrate architecture; G0/legal | recorded decision |
|
||||
|
||||
> The **Theorist** is the forward mirror of the audit system's **Auditor**: the Auditor
|
||||
> finds *problems* in existing code; the Theorist produces *specs* for new code. Both are
|
||||
> Opus, because both are "decide what is true / what should be built" work.
|
||||
|
||||
---
|
||||
|
||||
## 3. Workflow (DAG-scheduled, with a research spike gate)
|
||||
|
||||
```
|
||||
ROADMAP DAG (phases.md + research-track.md; ✅/🔲/⛔/prereqs)
|
||||
│ Orchestrator picks next item whose prerequisites are ALL ✅
|
||||
▼
|
||||
┌── CLASSIFY ──┐
|
||||
│ port │ research│
|
||||
└──┬───┴────┬────┘
|
||||
│ │
|
||||
│ ▼
|
||||
│ THEORIST (Opus): derive formulas + LaTeX note + design validation
|
||||
│ ▼
|
||||
│ SPIKE (Prototyper): numeric proof-of-correctness on a scratch branch
|
||||
│ ▼
|
||||
│ ┌─ GO/NO-GO gate ─┐ NO-GO → record negative result, back to DAG
|
||||
│ └────────┬────────┘ (research is allowed to fail)
|
||||
│ │ GO
|
||||
▼ ▼
|
||||
PORTER RESEARCH IMPLEMENTER (productionise into the library)
|
||||
└─────┬──────┘
|
||||
▼
|
||||
VALIDATION ENGINEER (battery per §4: oracle / invariant / convergence / cross-lib)
|
||||
▼
|
||||
REVIEW GATE (Opus): math-sound? parity intact? public surface intentional?
|
||||
▼ ── CHANGES-REQUESTED ─► back to implement
|
||||
INTEGRATE → CI → HUMAN MERGE GATE → MERGE → Orchestrator marks ✅, unblocks dependents
|
||||
▼
|
||||
HANDOFF → the AUDIT system re-audits the new module (close the loop)
|
||||
```
|
||||
|
||||
The **spike→go/no-go** gate is the key addition: research math is proven *cheaply on a
|
||||
throwaway branch* before any production code or tests are written. A NO-GO is a
|
||||
*successful* outcome (a recorded dead end), not a failure — unlike the audit system,
|
||||
where every dispatched finding is expected to land.
|
||||
|
||||
---
|
||||
|
||||
## 4. Validation strategy — chosen by item type
|
||||
|
||||
The Theorist picks the battery; the Validation Engineer builds it:
|
||||
|
||||
| Item type | Primary oracle | Mandatory checks |
|
||||
|---|---|---|
|
||||
| **Java port** (9a.1, 9c, 9d.1/.3/.4, 9e, 9g, 10a utils) | Java golden values (`@golden` vectors) | bit-for-bit parity at the documented tol; FD-gradient check; Gauss–Bonnet |
|
||||
| **Research, has a cross-impl** (10a forms ↔ geometry-central; GC-1 cross-check) | another library's output on the same mesh | agreement to tol after normalisation alignment |
|
||||
| **Research, no oracle** (inversive-distance, 9b-analytic, 12, 13) | **invariants + analytic limits** | analytic-limit match (e.g. κ=0 ↔ euclidean path bit-for-bit); invariant conservation (GB, holonomy closure ∏[aᵢ,bᵢ]=Id); FD vs analytic; convergence-under-refinement study |
|
||||
| **Numerical/architecture** (high-precision substrate, Hessian speed) | self-consistency | value-identity vs the slow reference; speed-up measured; conditioning diagnostics (the N7 `min_ldlt_pivot` we just added) |
|
||||
|
||||
Two project-specific truths the battery must respect:
|
||||
- **Parity is sacred** for ports (the same discipline that kept HardJava the default).
|
||||
- **Precision prerequisite**: genus-g composes exponentially-growing isometry products;
|
||||
`double` cannot verify ∏gᵢ=Id. The Theorist must flag when a *localised*
|
||||
high-precision substrate (`cpp_dec_float_50`/MPFR) is required (Phase 9c/10c/13),
|
||||
scoped to the uniformisation module only — never the Eigen core.
|
||||
|
||||
---
|
||||
|
||||
## 5. Scheduling the actual roadmap
|
||||
|
||||
**Chains run in parallel; the Orchestrator never dispatches a blocked item.**
|
||||
|
||||
- **Quick wins first (cheap, unblock confidence):** 9g.1 quality measures (Java port,
|
||||
~3 days, no deps), 9h.1/9h.2 CLI (~hours), 9d.3 stereographic (small). → Porter + Haiku.
|
||||
- **Chain A (near-term research, builds only on landed code):** **Phase 12** decorated
|
||||
DCE. → Theorist (decoration reparametrisation) → spike (round-trip `I_ij↔(r,ℓ)` +
|
||||
κ-transition invariant) → Research Implementer → invariant/GB battery. **No genus-g deps.**
|
||||
- **Research math on shipped modules:** 9b-analytic HyperIdeal Hessian (Schläfli chain
|
||||
rule) → Theorist + LaTeX note + FD-vs-analytic cross-check against today's block-FD.
|
||||
- **Chain B (gated capstone):** the genus-g≥2 spine **9c → 10a(+DEC layer) → 10b → 10c
|
||||
→ Phase 13**, plus the **holonomy-bug fix** (Opus + high-precision substrate). The
|
||||
Orchestrator keeps 13 ⛔ until every prerequisite is ✅.
|
||||
- **Blocked by G0 (legal, shared with audit system):** Phase 8 CGAL packaging stays ⛔
|
||||
until porting rights clear — exactly as in the audit plan.
|
||||
|
||||
> Prerequisite edges are enforced as data: each phase lists its prereqs; the
|
||||
> Orchestrator computes the ready-set = {🔲 items whose prereqs are all ✅} each cycle.
|
||||
|
||||
---
|
||||
|
||||
## 6. Composition with the audit system (the loop)
|
||||
|
||||
```
|
||||
feature-dev: Theorist → spike → implement → validate → review → merge ─┐
|
||||
│ new module
|
||||
audit: Auditor → plan → (implement → review) → integrate → merge ┘ hardened
|
||||
▲ │
|
||||
└────────────────── re-audit the new module ◄────────────────────┘
|
||||
```
|
||||
|
||||
- Shared infrastructure: **Integrator, CI/Infra-Diagnostician, Review-gate discipline,
|
||||
the blackboard (markdown + git), commit-attribution by model**.
|
||||
- Distinct front-ends: **Auditor** (finds problems) vs **Theorist** (specs features).
|
||||
- The handoff: every feature merge triggers an audit-backlog entry "audit module X",
|
||||
so growth never outruns scrutiny.
|
||||
|
||||
---
|
||||
|
||||
## 7. Run options (same ladder as the audit system)
|
||||
|
||||
1. **Manual sessions** — Orchestrator emits a launch prompt per item (a forward analog
|
||||
of `session-prompts.md`), set to the named model; spike + review are separate Opus
|
||||
sessions.
|
||||
2. **Claude Code subagents** — roles as subagents with pinned models and scoped tools
|
||||
(Prototyper works only on `spike/**` branches; Porter cannot touch `doc/math` proofs;
|
||||
Reviewer is read-only on `main`).
|
||||
3. **Claude Agent SDK** — a supervisor loops the DAG: computes the ready-set, spawns the
|
||||
role-agents, runs gates as code (build/test/CI + invariant checks), and **stops at the
|
||||
go/no-go and human-sign-off gates**.
|
||||
|
||||
---
|
||||
|
||||
## 8. Failure modes this design absorbs
|
||||
|
||||
- **Research math is wrong** → caught at the cheap **spike gate** before production code.
|
||||
- **A port drifts from Java** → golden-oracle parity tests (Porter's mandatory battery).
|
||||
- **"Converges" but is geometrically false** → invariant checks (GB, holonomy closure)
|
||||
and convergence-under-refinement, not just `‖G‖<tol`.
|
||||
- **A capstone is started prematurely** → DAG scheduler refuses blocked items (Phase 13).
|
||||
- **`double` silently fails the group relation** → Theorist's precision-prerequisite flag
|
||||
forces the localised high-precision substrate.
|
||||
- **New code outruns review** → the audit-system handoff re-audits every merged module.
|
||||
|
||||
---
|
||||
|
||||
## 9. Worked first move (concrete, startable today)
|
||||
|
||||
**Item:** Phase **9g.1** conformal-quality measures — Java port, no new theory, no deps,
|
||||
~3 days. **Why first:** lands fast, strengthens the validation story for the already-
|
||||
shipped genus-0/1 pipeline, and exercises the whole forward pipeline cheaply.
|
||||
|
||||
- *Orchestrator* → classify **port**; prereqs none → ready.
|
||||
- *Porter (Sonnet)* → translate IsothermicityMeasure / DCE-measure / FlippedTriangles /
|
||||
LengthCrossRatio + the ConvergenceUtility metrics into `conformal_quality.hpp`,
|
||||
`// Ported from …` provenance.
|
||||
- *Validation (Sonnet)* → golden values from the Java outputs + a flipped-triangle unit
|
||||
case; reuse on cathead/brezel.
|
||||
- *Reviewer (Opus)* → parity + the length-cross-ratio definition vs Springborn-Schröder-
|
||||
Pinkall 2008.
|
||||
- *Scholar (Haiku)* → `references.md` row + a line in `validation.md`.
|
||||
- *Integrator* → PR, `/test-cgal` + `/quality-gates` run, merge → audit handoff.
|
||||
|
||||
Then open **Chain A / Phase 12** in parallel as the first *research* exercise of the spike gate.
|
||||
@@ -1,132 +1,163 @@
|
||||
# Phase Orchestration — DAG × Models × Gates (forward pipeline)
|
||||
# Phase Orchestration — Sessions × Models × Review Gates
|
||||
|
||||
**Forward counterpart** of [`../reviewer/finding-orchestration.md`](../reviewer/finding-orchestration.md).
|
||||
Where that file schedules *audit findings*, this one schedules the **roadmap phases**
|
||||
([`phases.md`](phases.md)) and **research questions** ([`research-track.md`](research-track.md))
|
||||
into model-assigned sessions with **spike → go/no-go**, **validation**, and **review** gates.
|
||||
**Updated:** 2026-06-01
|
||||
**Purpose:** structured plan to work through roadmap phases across multiple
|
||||
short sessions, each run by the **model best suited** to the work, with an
|
||||
**external Opus reviewer session after every implementation session**.
|
||||
Mirrors the audit workflow in
|
||||
[`doc/reviewer/finding-orchestration.md`](../reviewer/finding-orchestration.md).
|
||||
|
||||
System design: [`feature-dev-agentic-system.md`](feature-dev-agentic-system.md).
|
||||
Ready-to-paste prompts: [`phase-prompts.md`](phase-prompts.md).
|
||||
|
||||
> **Two systems, one loop.** Feature-dev (this plan) lands new modules; the audit
|
||||
> system re-audits them. Every merge here appends an "audit module X" entry to the
|
||||
> audit backlog.
|
||||
> **Ready-to-paste prompts** for every pending session (P1–P4 + review gate)
|
||||
> live in [`session-prompts.md`](session-prompts.md) — copy one block, set the
|
||||
> named model, go.
|
||||
|
||||
---
|
||||
|
||||
## How to use this
|
||||
|
||||
1. Compute the **ready-set** = 🔲 phases whose prerequisites are **all ✅** (table below).
|
||||
2. Pick one; copy its block from [`phase-prompts.md`](phase-prompts.md); set the named model.
|
||||
3. **Port** items go straight to Porter→Validation. **Research** items go
|
||||
Theorist→**Spike (go/no-go)**→Implement→Validation.
|
||||
4. Every item ends with a **review gate (Opus)** + the validation battery for its type,
|
||||
then Integrate → CI → human merge → mark ✅ → unblock dependents.
|
||||
1. Pick the next **⬜ pending** session from the [sequence](#session-sequence).
|
||||
2. Start a session with the **assigned model**, point it at the phase details in
|
||||
`phases.md` and (for research items) `research-track.md`. Each phase entry
|
||||
has a Java reference, a mathematical source, and acceptance criteria.
|
||||
3. When it finishes, start the paired **🔍 Opus review session** — it validates
|
||||
the diff (correctness, gradient checks, no parity regression) and only then
|
||||
is the batch "done".
|
||||
4. Mark the session ✅ here and move on.
|
||||
|
||||
**Rationale:** same as the audit system — Haiku/Sonnet for well-specified work,
|
||||
Opus reserved for math/architecture work and the review gate.
|
||||
|
||||
---
|
||||
|
||||
## Model / role assignment (forward heuristic)
|
||||
## Model-assignment heuristic
|
||||
|
||||
| Work | Role · Model |
|
||||
|---|---|
|
||||
| Faithful Java→C++ translation (golden-oracle parity) | **Porter · Sonnet** (Haiku for trivial CLI/glue) |
|
||||
| Derive discrete formulas from papers, design validation, LaTeX note | **Theorist · Opus** |
|
||||
| Throwaway numeric proof-of-correctness before production | **Prototyper · Opus→Sonnet** |
|
||||
| Productionise validated research math | **Research Implementer · Opus→Sonnet** |
|
||||
| Test battery (oracle / invariant / convergence / cross-lib) | **Validation · Sonnet** |
|
||||
| Math-soundness + parity + correctness gate | **Reviewer · Opus** |
|
||||
| `references.md`, math-note polish, user/CLI docs | **Scholar · Haiku** |
|
||||
| Branch/PR/CI/rebase/merge | **Integrator · Sonnet** (shared with audit) |
|
||||
| Research direction · discretisation sign-off · precision substrate · G0 | **Human** |
|
||||
| Model | Take phases that are… | Examples |
|
||||
|---|---|---|
|
||||
| **Haiku** | mechanical Java ports, local CLI additions, doc-only, no new math | 9g.1 (quality measures), 9h.1/9h.2 (CLI), 9d.3 (Stereographic port) |
|
||||
| **Sonnet** | medium Java ports or research with explicit acceptance criteria | 9e (CirclePatternLayout), Phase 12 (Decorated DCE) |
|
||||
| **Opus** | math derivations, architecture decisions, all 🔍 review gates | 9b-analytic (Schläfli Hessian), 9c (genus > 1), all reviews |
|
||||
|
||||
Routing rule: **port → cheapest faithful model; research → Opus owns the math, cheaper
|
||||
models productionise once the spike says GO.**
|
||||
> Decision rule isn't "how large" but "how much must be *understood* to get it
|
||||
> right." A mechanical 600-line Java port can be Haiku; a 200-line numerical
|
||||
> reformulation needs Opus.
|
||||
|
||||
---
|
||||
|
||||
## Master phase table
|
||||
|
||||
Type: 🔌 port (Java oracle) · 🔬 research (papers only) · 🧱 infra
|
||||
Status: ✅ done · 🔲 ready/planned · ⏸ planned-blocked-by-prereq · ⛔ blocked (G0)
|
||||
Status: ✅ done · ⬜ open (actionable) · ⏸ deferred/gated · ⛔ blocked · 👤 needs reviewer input
|
||||
|
||||
| Phase | Type | Status | Prerequisites | Role · Model | Chain | Effort |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 1–7 (special fns → holonomy) | 🔌 | ✅ | — | — | — | done |
|
||||
| 9a.1 CP-Euclidean | 🔌 | ✅ | — | — | — | done |
|
||||
| 9a.2 inversive-distance | 🔬 | ✅ | — | — | — | done |
|
||||
| 9b block-FD HyperIdeal Hessian | 🔬 | ✅ | — | — | — | done |
|
||||
| **9g.1 conformal-quality measures** | 🔌 | ✅ 1375878 | none | Porter · Sonnet | quick | ~3 d |
|
||||
| **9h.1 CLI --tol/--max-iter** | 🧱 | ✅ 1375878 | none | Haiku/Sonnet | quick | ~30 m |
|
||||
| **9h.2 CLI cp/inv-dist models** | 🧱 | ✅ 1375878 | 9a ✅ | Sonnet | quick | 2–4 h |
|
||||
| **9d.3 stereographic layout (S²→ℂ)** | 🔌 | ✅ 1375878 | none | Porter · Sonnet | sphere | ~3 d |
|
||||
| **9d.4 Möbius-centering functional** | 🔌 | 🔲 **ready** | Phase 7 ✅ | Porter · Sonnet | sphere | ~3 d |
|
||||
| **9e circle-pattern layout** | 🔌 | 🔲 **ready** | 9a.1 ✅ | Porter · Sonnet | — | ~1 wk |
|
||||
| **9d.1 ConesUtility (Euclidean)** | 🔌 | 🔲 **ready** | none | Porter · Sonnet | cones | ~1 wk |
|
||||
| **9b-analytic HyperIdeal Hessian (Schläfli)** | 🔬 | 🔲 **ready** | 9b ✅ | Theorist · Opus | — | 10–14 d |
|
||||
| **9f polygon Laplacian** | 🔬 | 🔲 **ready** | none | Theorist · Opus | — | ~3 wk |
|
||||
| **Phase 12 decorated DCE + transition** | 🔬 | 🔲 **ready** | landed code only | Theorist · Opus | **A** | medium |
|
||||
| 9d.2 non-Euclidean cones | 🔬 | 🔲 ready | 9d.1 | Theorist · Opus | cones | medium |
|
||||
| 9g.2 period-matrix convergence study | 🔬 | 🔲 ready | period_matrix ✅ | Validation · Sonnet | — | ~3 d |
|
||||
| 9c 4g-gon fundamental domain | 🔌+🔬 | ⏸ | high-precision substrate | Theorist+Porter · Opus | **B** | ~5 wk |
|
||||
| holonomy-bug fix (+`cpp_dec_float_50`) | 🔬 | ⏸ | — (architecture call) | Opus + Human | **B** | ~1 wk |
|
||||
| 10a DEC layer + 1-forms | 🔌+🔬 | ⏸ | 9c | Porter+Theorist · Opus | **B** | ~6 wk |
|
||||
| 10b Siegel period matrix Ω | 🔬 | ⏸ | 10a | Theorist · Opus | **B** | ~1 wk |
|
||||
| 10c Fuchsian-group / H²/Γ | 🔬 | ⏸ | 10a+10b+9c | Theorist · Opus | **B** | large |
|
||||
| 13 canonical tessellations (capstone) | 🔬 | ⏸ | 9c+10a+10b+10c+holonomy(+12) | Theorist · Opus | **B** | very large |
|
||||
| Phase 8 CGAL packaging | 🧱 | ⛔ | **G0** (porting rights) | Opus+Human | — | large |
|
||||
| 11a/11b/11c, 10d–10g, GC-1/2/3 | 🔬 | ⏸/opt | various | Theorist · Opus | later | large |
|
||||
|
||||
> **Ready-set right now** (no open prerequisites): **9g.1, 9h.1, 9h.2, 9d.3, 9d.4, 9e,
|
||||
> 9d.1, 9b-analytic, 9f, Phase 12, 9g.2.** The Chain-B spine and Phase 8 stay ⏸/⛔.
|
||||
| Phase | Focus | Effort | Sev | Status | Model | Session |
|
||||
|-------|-------|--------|-----|--------|-------|---------|
|
||||
| 9h.1 | Newton CLI params (`--tol`, `--max-iter`) | 30 min | 🔵 | ⬜ | Haiku→🔍Opus | **P1** |
|
||||
| 9h.2 | Phase 9a models in CLI (`-g cp_euclidean`, `-g inversive_distance`) | 2–4 h | 🔵 | ⬜ | Haiku→🔍Opus | **P1** |
|
||||
| 9g.1 | Conformal quality measures (`conformal_quality.hpp`) | ~3 days | 🟡 | ⬜ | Haiku→🔍Opus | **P1** |
|
||||
| 9d.3 | StereographicUnwrapper port | ~3 days | 🟡 | ⬜ | Haiku→🔍Opus | **P1** |
|
||||
| 12 | Decorated DCE & geometric transition | medium | 🔴 | ⬜ | Sonnet→🔍Opus | **P2** |
|
||||
| 9e | CirclePatternLayout + CirclePatternUtility | medium | 🟡 | ⬜ | Sonnet→🔍Opus | **P3** |
|
||||
| 9d.4 | MobiusCenteringFunctional (variational Möbius centring) | medium | 🟡 | ⬜ | Sonnet→🔍Opus | **P3** |
|
||||
| 9g.2 | Period-matrix convergence study (experiment test) | ~3 days | 🔵 | ⬜ | Sonnet→🔍Opus | **P3** |
|
||||
| GC-1 | geometry-central cross-validation | small | 🔵 | ⏸ | Sonnet→🔍Opus | (after reviewer Q5) |
|
||||
| 9d.1 | ConesUtility port (Euclidean cone singularities) | medium | 🟡 | ⏸ | Sonnet→🔍Opus | (after reviewer Q1) |
|
||||
| 9d.2 | Non-Euclidean cone extensions (RESEARCH) | large | 🟡 | ⏸ | Opus | (after reviewer Q1) |
|
||||
| 9f | Polygon Laplacian (RESEARCH, no Java parent) | medium | 🟡 | ⏸ | Sonnet/Opus | (after reviewer Q1) |
|
||||
| 9b-analytic | Analytic HyperIdeal Hessian via Schläfli | 10–14 days | 🔴 | ⏸ | Opus | **P4** (after reviewer Q3) |
|
||||
| 9c | 4g-polygon fundamental domain (genus g > 1) | ~5 weeks | 🔴 | ⏸ | Opus | **P5** (after reviewer Q4 + G0) |
|
||||
| 10a | Holomorphic/harmonic 1-forms + DEC layer | ~6 weeks | 🔴 | ⏸ | Opus | **P6** (after 9c) |
|
||||
| 10b | Siegel period matrix Ω ∈ H_g | ~1 week | 🟡 | ⏸ | Opus | **P6** (after 10a) |
|
||||
| 10c | Full genus-g≥2 uniformization | large | 🔴 | ⏸ | Opus | **P7** (after 10b) |
|
||||
| 13 | Canonical tessellations + polyhedral realisation (Chain B capstone) | very large | 🔴 | ⏸ | Opus | **P8** (after 10c) |
|
||||
| A4, A5 (public naming) | CGAL entry-point renames + `Circle_packing_result` unification | 🟡 | ⏸ | Opus | S6 in audit system (after G0) |
|
||||
|
||||
---
|
||||
|
||||
## Recommended sequence
|
||||
## Session sequence
|
||||
|
||||
### Wave 0 — quick wins (Porter/Haiku, parallel, build confidence)
|
||||
**9g.1** (quality measures) · **9h.1+9h.2** (CLI) · **9d.3** (stereographic). All 🔌/🧱,
|
||||
no theory, golden-oracle or trivial validation. Land fast, exercise the forward pipeline
|
||||
cheaply, strengthen the genus-0/1 validation story.
|
||||
### ⬜ P1 — Quick wins (Haiku → 🔍 Opus)
|
||||
|
||||
### Wave A — first research (Chain A, the spike gate's debut)
|
||||
**Phase 12 decorated DCE.** Theorist (Opus) derives the Penner-coordinate decoration as a
|
||||
*reparametrisation* of the shipped inversive-distance/hyper-ideal/spherical functionals →
|
||||
**spike**: round-trip `I_ij ↔ (r_i,r_j,ℓ)` + κ-transition invariant → Research Implementer
|
||||
→ invariant/GB battery. Builds on landed code only; **no genus-g dependency.**
|
||||
Four independent items, all with well-specified acceptance criteria and no new
|
||||
math. Can run in one batch or as two sub-sessions (9h.1+9h.2 first as a
|
||||
~half-day warmup, then 9g.1+9d.3 as a ~1-week batch).
|
||||
|
||||
### Wave B — research on shipped modules (parallel with A)
|
||||
**9b-analytic** (Schläfli analytic HyperIdeal Hessian, with `doc/math/` LaTeX note,
|
||||
FD-vs-analytic cross-check against today's block-FD) · **9f** (polygon Laplacian) ·
|
||||
**9e / 9d.1 / 9d.4** (further Java ports).
|
||||
- **9h.1** — `--tol` and `--max-iter` CLI options in `conformallab_cli.cpp`;
|
||||
thread through the three `run_*()` helpers; update `doc/getting-started.md`.
|
||||
- **9h.2** — `-g cp_euclidean` and `-g inversive_distance` routes in the CLI;
|
||||
add both to the `CLI::IsMember` validator; update README + getting-started.
|
||||
- **9g.1** — `code/include/conformal_quality.hpp` with `IsothermicityMeasure`,
|
||||
`DiscreteConformalEquivalenceMeasure`, `FlippedTriangles`, `LengthCrossRatio`,
|
||||
`ConvergenceUtility` measures; one test each in `code/tests/cgal/`.
|
||||
- **9d.3** — `code/include/stereographic_layout.hpp` porting
|
||||
`StereographicUnwrapper.java` (266 LoC); round-trip test required.
|
||||
- **🔍 Opus review:** gradient-check test for quality measures, CLI smoke test
|
||||
on a real mesh, no count regression.
|
||||
|
||||
### Wave C — the genus-g≥2 spine (Chain B, strictly DAG-gated)
|
||||
**holonomy-bug fix + `cpp_dec_float_50`** → **9c** → **10a (+DEC layer)** → **10b** →
|
||||
**10c** → **Phase 13** capstone. The Orchestrator keeps each ⏸ until its prereqs are ✅;
|
||||
**13 never dispatches early.** Land **Phase 12** first so the Penner machinery exists.
|
||||
### ⬜ P2 — Decorated DCE transition (Sonnet → 🔍 Opus)
|
||||
|
||||
### Blocked
|
||||
**Phase 8 (CGAL packaging)** — ⛔ until **G0** (authors emailed, awaiting reply).
|
||||
- **Phase 12** — Penner-coordinate decoration layer + geometric transition driver
|
||||
+ validation harness. Mathematical reference: Bobenko–Lutz 2025 §3.
|
||||
Acceptance criteria (all four must pass — from `phases.md`):
|
||||
- Decoration round-trip `I_ij ↔ (r_i, r_j, ℓ)` at machine precision.
|
||||
- At κ=0: bit-for-bit match with existing euclidean/inversive path.
|
||||
- Gauss–Bonnet holds per geometry; invariant constant across κ-transition.
|
||||
- **🔍 Opus review:** validate the three acceptance criteria numerically;
|
||||
verify the cross-geometry invariant test; confirm no parity regression.
|
||||
|
||||
### ⬜ P3 — Circle pattern embedding + Möbius centring + convergence study (Sonnet → 🔍 Opus)
|
||||
|
||||
- **9e** — `code/include/circle_pattern_layout.hpp` porting
|
||||
`CirclePatternLayout` + `CirclePatternUtility` (Phase 9a.1 prerequisite ✅).
|
||||
Test: given ρ values from a solved CP-Euclidean system, embedded vertex
|
||||
positions are self-consistent (intersection angles match the input).
|
||||
- **9d.4** — upgrade `normalise_hyperbolic()` in `layout.hpp` via
|
||||
`MobiusCenteringFunctional` (Lorentz energy; Java: 289 LoC). Retain
|
||||
Fréchet mean as fallback. Test: compare old vs new centering on brezel.obj.
|
||||
- **9g.2** — `code/tests/cgal/test_period_matrix_convergence.cpp` (experiment):
|
||||
genus-1 mesh with known analytic τ, igl::loop subdivision, noise, assert
|
||||
|τ_computed − τ_expected| decreases with refinement.
|
||||
- **🔍 Opus review:** cross-validate circle pattern positions against solver
|
||||
output; verify convergence-study direction; confirm no count regression.
|
||||
|
||||
### ⏸ P4 — Analytic HyperIdeal Hessian (Opus) — awaiting reviewer Q3
|
||||
|
||||
**Precondition:** reviewer Q3 answer must confirm the ~6× speedup over
|
||||
block-FD is worth ~2 weeks at their typical mesh sizes. If Q3 is "no" or
|
||||
"later", phase stays ⏸. If "yes" or "above V > X", proceed.
|
||||
|
||||
- **Phase 9b-analytic** — full analytic Hessian via Schläfli identity.
|
||||
Derivation: `doc/math/hyperideal-hessian-derivation.md`.
|
||||
Chain: `(bᵢ, aₑ) → ℓᵢⱼ → ζ₁₃/ζ₁₄/ζ₁₅ → αᵢⱼ/βᵢ → ∂²E/∂u²`.
|
||||
Block-FD path must be retained as a compile-time cross-check.
|
||||
Acceptance: analytic and block-FD agree to 1e-6; golden-value tests pass
|
||||
bit-for-bit; benchmark: analytic faster on V > 500.
|
||||
- **No separate review gate** (Opus implements + self-reviews; block-FD is the
|
||||
independent cross-check).
|
||||
|
||||
### ⏸ P5 — Genus > 1 fundamental domain (Opus) — awaiting reviewer Q4 + G0
|
||||
|
||||
**Preconditions:** (a) reviewer Q4 answer (port-literal vs. re-derive from
|
||||
Springborn 2020 §5); (b) G0 porting rights confirmed in writing.
|
||||
|
||||
- **Phase 9c** — 4g-polygon boundary walk; high-precision substrate
|
||||
(`boost::multiprecision::cpp_dec_float_50`, local to uniformization module).
|
||||
See `phases.md` §9c for the full Java source list and the precision
|
||||
prerequisite note.
|
||||
|
||||
### ⏸ P6–P8 — Higher-genus research chain (Opus, sequential)
|
||||
|
||||
Phases 10a → 10b → 10c → 13. Each gates the next. Start only after P5 (9c) is
|
||||
complete. Full scope in `phases.md` and `research-track.md`.
|
||||
|
||||
---
|
||||
|
||||
## Per-item gates (what "done" requires)
|
||||
## Review-gate checklist (every 🔍 Opus session checks)
|
||||
|
||||
- **Spike go/no-go** (research only): numeric correctness on a `spike/**` branch *before*
|
||||
production code. A NO-GO is a recorded result, not a failure.
|
||||
- **Validation battery** (by type — see `feature-dev-agentic-system.md` §4):
|
||||
- 🔌 port → Java golden values bit-for-bit + FD-gradient + Gauss–Bonnet.
|
||||
- 🔬 with cross-impl → geometry-central agreement after normalisation.
|
||||
- 🔬 no oracle → analytic-limit match + invariant conservation (GB, ∏[aᵢ,bᵢ]=Id) +
|
||||
FD-vs-analytic + convergence-under-refinement.
|
||||
- **Review gate (Opus)**: math sound? parity intact (HardJava-style defaults)? public
|
||||
surface intentional + documented? precision-prerequisite respected?
|
||||
- **Integrate**: PR with `/test-cgal` (+ `/quality-gates` where relevant); model
|
||||
attribution in commits; mark ✅ here; append audit-backlog handoff.
|
||||
|
||||
---
|
||||
|
||||
## Status log
|
||||
|
||||
- **2026-06-01:** plan created. Ready-set open (Wave 0/A/B). Chain B + Phase 8 gated.
|
||||
Audit system delivered S1+S2 (`finding-orchestration.md`); its Integrator/CI/review
|
||||
machinery is reused here.
|
||||
- [ ] Builds clean; full CGAL suite green; count matches `doc/api/tests.md`.
|
||||
- [ ] Any new functional has a gradient-check test (see `CLAUDE.md` §Test design patterns).
|
||||
- [ ] No Java golden-vector / parity test perturbed (HardJava clamp defaults intact).
|
||||
- [ ] Numeric changes are value-identical where claimed, or justified + covered by a test.
|
||||
- [ ] New public surface (result types, enums, CGAL headers) is intentional;
|
||||
documented in `doc/api/headers.md` and `doc/api/contracts.md`.
|
||||
- [ ] Commit message attributes the implementing model
|
||||
(`Co-Authored-By: Claude <Model> <noreply@anthropic.com>`).
|
||||
- [ ] Phase marked ✅ in the master table above with the commit ref.
|
||||
- [ ] `porting-status.md` §7 updated if the phase adds a C++-only capability.
|
||||
|
||||
@@ -1,227 +0,0 @@
|
||||
# Ready-to-paste phase prompts (forward pipeline)
|
||||
|
||||
Forward counterpart of [`../reviewer/session-prompts.md`](../reviewer/session-prompts.md).
|
||||
Copy one block into a fresh session, set the **model named in the prompt**, go.
|
||||
Plan + DAG: [`phase-orchestration.md`](phase-orchestration.md). Design:
|
||||
[`feature-dev-agentic-system.md`](feature-dev-agentic-system.md).
|
||||
|
||||
Shared conventions (baked into each prompt):
|
||||
- Repo `/Users/tarikmoussa/Desktop/ConformalLabpp`, base `main`; push to the **eulernest
|
||||
fork** = remote `origin`; open the PR via the Gitea API
|
||||
(`/api/v1/repos/conformallab/ConformalLabpp/pulls`, basic-auth from the `origin` URL).
|
||||
- Build/test: `cmake -S code -B build-cgal -DWITH_CGAL_TESTS=ON && cmake --build build-cgal
|
||||
--target conformallab_cgal_tests -j8 && ctest --test-dir build-cgal -R '^cgal\.'`.
|
||||
Add `/test-cgal` (and `/quality-gates` where relevant) to the PR-head commit message so
|
||||
CI runs the full suites (they are keyword-triggered).
|
||||
- **Port items:** add `// Ported from <Java file>` provenance + Java golden-oracle parity
|
||||
tests. **Research items:** run the **spike** first (separate Opus session) and only
|
||||
productionise on GO.
|
||||
- Finish: **review gate** (Opus), then update status in `phase-orchestration.md` (phase → ✅).
|
||||
|
||||
---
|
||||
|
||||
## W0·9g.1 — Conformal-quality measures (port) · model: **Sonnet**
|
||||
|
||||
```
|
||||
Use Sonnet. Repo /Users/tarikmoussa/Desktop/ConformalLabpp, new branch off main
|
||||
`feat/9g1-conformal-quality`. This is a Java PORT, no new theory (phases.md §9g.1).
|
||||
|
||||
Create code/include/conformal_quality.hpp porting these Java measures (math is
|
||||
GUI-independent — lift only the math):
|
||||
- IsothermicityMeasure (plugin/visualizer/IsothermicityMeasure.java) — pointwise
|
||||
deviation from conformality (anisotropy of the induced metric).
|
||||
- DiscreteConformalEquivalencemMeasure (…/DiscreteConformalEquivalencemMeasure.java)
|
||||
— per-edge length-cross-ratio residual vs the conformal-equivalence condition.
|
||||
- FlippedTriangles (…/FlippedTriangles.java) — detect inverted/degenerate triangles
|
||||
in a 2-D layout (embedding-validity).
|
||||
- LengthCrossRatio (heds/adapter/types/LengthCrossRatio.java) — the discrete conformal
|
||||
invariant per edge (shared input for the two measures).
|
||||
- ConvergenceUtility metrics (convergence/ConvergenceUtility.java, math/float only):
|
||||
getMaxMeanSumCrossRatio (q=(a·c)/(b·d), qfun=(q+1/q)/2−1),
|
||||
getMaxMeanSumMultiRatio (per-face product, =1 iff conformal),
|
||||
getMaxMeanSumScaleInvariantCircumRadius (R/√A).
|
||||
Math reference: Springborn-Schröder-Pinkall 2008 (length cross-ratio = discrete
|
||||
conformal invariant). Java reference path: /Users/tarikmoussa/Desktop/conformallab/src/...
|
||||
|
||||
Validation (port battery): golden values read from the Java outputs on a small mesh;
|
||||
a unit flipped-triangle case; run the measures on the converged cathead/brezel layouts
|
||||
from the existing euclidean pipeline and assert near-conformality.
|
||||
|
||||
Per-finding commits, trailer `Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>`.
|
||||
Build + full CGAL suite green. Push, open PR (base main, head commit message contains
|
||||
`/test-cgal /quality-gates`). Update phase-orchestration.md (9g.1 → ✅) + a row in
|
||||
doc/math/validation.md and references.md. Report PR URL + test count.
|
||||
Then hand off to the audit system: note "audit module conformal_quality.hpp" in
|
||||
doc/reviewer/finding-orchestration.md backlog.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## W0·9h — CLI extensions (infra) · model: **Sonnet** (or Haiku for 9h.1)
|
||||
|
||||
```
|
||||
Use Sonnet. Repo as above, new branch off main `feat/9h-cli`. Two independent CLI tasks
|
||||
(phases.md §9h), no new theory.
|
||||
|
||||
9h.1 (~30 min): expose Newton tuning in code/src/apps/v0/conformallab_cli.cpp —
|
||||
app.add_option("--tol", tol, "Newton gradient tolerance [1e-8]");
|
||||
app.add_option("--max-iter", max_iter, "Newton iteration limit [200]");
|
||||
thread both through run_euclidean / run_spherical / run_hyper_ideal. Update the CLI
|
||||
parameter table in doc/getting-started.md.
|
||||
|
||||
9h.2 (~2–4 h): expose the Phase-9a models (already in the library + CGAL API) to the CLI:
|
||||
-g cp_euclidean → run_cp_euclidean()
|
||||
-g inversive_distance → run_inversive_distance()
|
||||
following the existing run_euclidean() pattern (~60 lines each); add both strings to the
|
||||
CLI::IsMember validator. Update README + getting-started.md.
|
||||
|
||||
Validation: CLI smoke runs on a small mesh for each new flag/model; assert non-zero exit
|
||||
on bad input. Build + full CGAL suite green. Commit (Sonnet trailer), push, PR (base main,
|
||||
`/test-cgal` in head commit). Update phase-orchestration.md (9h.1, 9h.2 → ✅). Report PR.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## W0·9d.3 — Stereographic layout S²→ℂ (port) · model: **Sonnet**
|
||||
|
||||
```
|
||||
Use Sonnet. Repo as above, new branch off main `feat/9d3-stereographic`. Java PORT
|
||||
(phases.md §9d.3) closing the spherical-visualisation gap.
|
||||
|
||||
Create code/include/stereographic_layout.hpp: stereographic projection S²→ℂ∪{∞} plus a
|
||||
Möbius-centering step, turning discrete_conformal_map_spherical()'s Point_3-on-S² output
|
||||
into a flat 2-D atlas. Java reference: unwrapper/StereographicUnwrapper.java (266 lines).
|
||||
Do NOT port math/CP1 or ComplexUtility.stereographic (redundant with std::complex + the
|
||||
existing MobiusMap — see porting-status.md).
|
||||
|
||||
Validation: round-trip (project then inverse-project) to machine precision on sampled S²
|
||||
points; pole-handling unit case; run on the spherical pipeline output of a small genus-0
|
||||
mesh and assert no flipped triangles (reuse 9g.1 FlippedTriangles if landed). Build + full
|
||||
CGAL suite green. Commit (Sonnet trailer), push, PR (`/test-cgal`). Update
|
||||
phase-orchestration.md (9d.3 → ✅). Report PR. Audit handoff note.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## WA·Phase 12 — Decorated DCE & transition (RESEARCH, Chain A) · **two-step**
|
||||
|
||||
### Step 1 — Theorist + Spike · model: **Opus**
|
||||
|
||||
```
|
||||
Use Opus. Repo as above. This is RESEARCH (no Java parent), Chain A — it reparametrises
|
||||
ALREADY-LANDED functionals, no genus-g dependency (phases.md §12).
|
||||
|
||||
THEORIST: read Bobenko-Lutz 2025 "Decorated Discrete Conformal Equivalence in
|
||||
Non-Euclidean Geometries" (arXiv:2310.17529) §3 + Lutz 2024 thesis. Derive, on paper, the
|
||||
Penner-coordinate DECORATION layer: per-vertex circle/horocycle radius as a Penner
|
||||
coordinate, and its map to the existing inversive distance I_ij via the classical
|
||||
ℓ² = r_i² + r_j² + 2 r_i r_j η. Write the derivation to doc/math/decorated-dce-derivation.md
|
||||
(short LaTeX-style note). Define the validation strategy + acceptance criteria (below).
|
||||
|
||||
SPIKE (branch `spike/phase12-decoration`, throwaway): a minimal numeric proof BEFORE any
|
||||
production code —
|
||||
(a) decoration round-trip I_ij ↔ (r_i, r_j, ℓ) at machine precision;
|
||||
(b) at background curvature κ=0, bit-for-bit match with the existing euclidean/inversive
|
||||
path;
|
||||
(c) the κ∈{+,0,−} transition driver holds the discrete conformal invariant fixed (GB per
|
||||
geometry; invariant constant across the transition to tol) — the numerical witness of
|
||||
the Bobenko-Lutz master theorem.
|
||||
Conclude GO or NO-GO with the evidence. If NO-GO, record it in research-track.md and stop.
|
||||
On GO, write the productionisation spec (files, public surface, test list) for Step 2.
|
||||
```
|
||||
|
||||
### Step 2 — Research Implementer + Validation · model: **Sonnet** (Opus review)
|
||||
|
||||
```
|
||||
Use Sonnet. Repo as above, new branch off main `feat/phase12-decorated-dce`. Productionise
|
||||
the GO spike from Step 1 per its spec (doc/math/decorated-dce-derivation.md).
|
||||
|
||||
Scope: (1) decoration layer (Penner coord ↔ I_ij); (2) transition driver (deform κ at fixed
|
||||
invariant, solve per geometry); (3) validation harness + example gallery. Reuse the shipped
|
||||
inversive-distance / hyper-ideal / spherical functionals — the decoration is a
|
||||
RE-PARAMETRISATION, not a new solver.
|
||||
|
||||
Validation (research, no oracle — acceptance criteria from §12): round-trip machine
|
||||
precision; κ=0 bit-for-bit vs euclidean/inversive; GB per geometry; invariant constant
|
||||
across the κ-transition; one surface solved in all three backgrounds shares the invariant.
|
||||
Build + full CGAL suite green. Commit (Sonnet trailer), push, PR (`/test-cgal`).
|
||||
THEN run the review gate (Opus) below. Update phase-orchestration.md (Phase 12 → ✅) +
|
||||
references.md. Audit handoff note.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## WB·9b-analytic — Analytic HyperIdeal Hessian via Schläfli (RESEARCH) · model: **Opus**
|
||||
|
||||
```
|
||||
Use Opus. Repo as above, new branch off main `feat/9b-analytic-hessian`. RESEARCH
|
||||
(phases.md §9b-analytic) — replace the FD HyperIdeal Hessian with the closed form.
|
||||
|
||||
THEORIST + IMPLEMENT: derive the analytic Hessian by explicit chain rule through
|
||||
(b_i, a_e) → ℓ_ij → ζ13/ζ14/ζ15 → α_ij / β_i. Sources: Springborn 2020 §4 +
|
||||
Schläfli 1858/60 + Rivin-Schlenker 1999 + Cho-Kim 1999 + Glickenstein 2011 §4. Write a
|
||||
short LaTeX correctness note to doc/math/hyperideal-hessian-derivation.md (extend the
|
||||
existing one). Implement as a new `hyper_ideal_hessian_analytic_sym(...)` next to the
|
||||
block-FD variant.
|
||||
|
||||
Validation (research, FD cross-check): assert the analytic Hessian matches today's
|
||||
hyper_ideal_hessian_block_fd_sym entry-wise to FD tolerance on tetrahedron + the Lawson
|
||||
genus-2 mesh (off-equilibrium); PSD check; convergence parity with the existing solver;
|
||||
measured speed-up. Keep the block-FD as the cross-validation reference. Default solver
|
||||
path unchanged until parity is proven, then switch newton_hyper_ideal to the analytic
|
||||
Hessian behind the same interface.
|
||||
Build + full CGAL suite green (incl. all Lawson Java golden-vector tests — parity sacred).
|
||||
Commit (Opus trailer), push, PR (`/test-cgal`). Update phase-orchestration.md (9b-analytic
|
||||
→ ✅) + references.md. Audit handoff note.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Reusable — Research spike go/no-go gate · model: **Opus**
|
||||
|
||||
```
|
||||
Use Opus. Repo /Users/tarikmoussa/Desktop/ConformalLabpp, throwaway branch `spike/<item>`.
|
||||
Goal: cheaply PROVE OR DISPROVE the math of <item> BEFORE any production code.
|
||||
- Implement the smallest possible reference computation (scratch .cpp or a test-only TU).
|
||||
- Run the item's designed checks: analytic-limit match, invariant conservation
|
||||
(Gauss-Bonnet; holonomy closure ∏[a_i,b_i]=Id where relevant), FD-vs-analytic, and a
|
||||
small convergence-under-refinement probe.
|
||||
- If precision is suspect (genus-g isometry products), test with cpp_dec_float_50 too.
|
||||
Conclude with an explicit GO or NO-GO + the numeric evidence. On GO, output the
|
||||
productionisation spec (files, public surface, test list). On NO-GO, record the dead end in
|
||||
research-track.md. Do NOT touch library production code in this session.
|
||||
```
|
||||
|
||||
## Reusable — Math-review / validation gate · model: **Opus**
|
||||
|
||||
```
|
||||
Use Opus. Review the open PR <url/branch> for <item> as an independent reviewer:
|
||||
- Math: does the implementation match the derivation in doc/math/<item>-derivation.md?
|
||||
Spot-check the chain rule / formula against the cited paper.
|
||||
- Validation: is the battery correct for the item TYPE (port→golden oracle;
|
||||
research→analytic-limit + invariant + convergence)? Are the tolerances honest?
|
||||
- Parity: no Java golden-vector test perturbed; defaults intact.
|
||||
- Precision: localized high-precision substrate where required, never in the Eigen core.
|
||||
- Public surface intentional + documented; commits attribute the model.
|
||||
Read `git diff main...HEAD` + the derivation note. Fix small issues inline; list precise
|
||||
required changes otherwise. Re-run the suite. Conclude APPROVE / CHANGES-REQUESTED, and
|
||||
mark the phase ✅ in phase-orchestration.md on approve.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⏸ Chain B (genus g ≥ 2) — DAG-gated, do not start early
|
||||
|
||||
Strict order, each ⏸ until its prereq is ✅:
|
||||
**holonomy-bug fix (+`cpp_dec_float_50`)** → **9c** (4g-gon fundamental domain) →
|
||||
**10a** (DEC layer + 1-forms) → **10b** (Siegel Ω) → **10c** (Fuchsian / H²/Γ) →
|
||||
**Phase 13** (canonical tessellations capstone). Land **Phase 12** first (Penner machinery
|
||||
reused). Each is a Theorist(Opus)→spike→implement→validate→review item; full literature in
|
||||
`phases.md` §9c/10/13 + `research-track.md`. The Orchestrator must refuse any item whose
|
||||
prerequisites are not all ✅.
|
||||
|
||||
## ⛔ Phase 8 (CGAL packaging) — blocked by G0
|
||||
|
||||
Do not start until the original authors grant porting/relicensing rights (G0; authors
|
||||
emailed, awaiting reply). Shared with the audit system's S6.
|
||||
```
|
||||
@@ -145,10 +145,10 @@ The phase numbers match `doc/roadmap/phases.md`.
|
||||
### Hyper-ideal volume formulas for 2- and 3-ideal-vertex faces (Phase 9b+, 🔲 planned)
|
||||
|
||||
* **Mathematical sources:**
|
||||
- **Springborn, B.** (2008). *A variational principle for weighted Delaunay
|
||||
triangulations and hyperideal polyhedra.* J. Differential Geometry **78**(2),
|
||||
333–367. arXiv:math/0603097 — the source of the one-ideal-vertex formula
|
||||
already implemented as `calculateTetrahedronVolumeWithIdealVertexAtGamma`.
|
||||
- **Kolpakov, A. & Mednykh, A.** (2012). *Spherical structures on torus
|
||||
knots and links.* Sibirsk. Mat. Zh. 53(3), 535–541 — see the earlier
|
||||
arXiv:math/0603097 for the one-ideal-vertex formula already implemented
|
||||
as `calculateTetrahedronVolumeWithIdealVertexAtGamma`.
|
||||
- **Milnor, J.** (1982). *Hyperbolic geometry: The first 150 years.*
|
||||
Bull. Amer. Math. Soc. 6(1), 9–24. → Volume of an ideal tetrahedron
|
||||
via Clausen function; this is the all-ideal case with 4 ideal vertices.
|
||||
@@ -175,10 +175,10 @@ The phase numbers match `doc/roadmap/phases.md`.
|
||||
|
||||
* **Acceptance criteria:**
|
||||
- Identify the correct formula for a hyper-ideal tetrahedron with exactly
|
||||
2 ideal vertices from the literature (check Springborn 2008 §3–4 generalisations
|
||||
2 ideal vertices from the literature (check Kolpakov-Mednykh generalisations
|
||||
and Vinberg orthoscheme decomposition).
|
||||
- Implement `calculateTetrahedronVolumeWithTwoIdealVertices(…)` analogous
|
||||
to the existing Springborn 2008 one-ideal-vertex function.
|
||||
to the existing Kolpakov-Mednykh function.
|
||||
- Implement `calculateTetrahedronVolumeWithThreeIdealVertices(…)` (one
|
||||
hyper-ideal + three ideal = fully cusp-like case).
|
||||
- Replace the `throw std::logic_error` in `face_energy()` with the correct
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
# Manuell herunterzuladende Paper & Dissertationen
|
||||
|
||||
Diese Paper konnten nicht automatisch geladen werden (kein freies arXiv-Preprint,
|
||||
hinter Verlag-Paywall, oder Buchkapitel).
|
||||
|
||||
---
|
||||
|
||||
## Dissertationen (Open Access — TU Berlin Depositonce)
|
||||
|
||||
| Autor | Titel | Link |
|
||||
|---|---|---|
|
||||
| **Sechelmann 2016** | *Variational Methods for Discrete Surface Parameterization: Applications and Implementation* | [depositonce.tu-berlin.de/items/8e2988b2-d991-45b5-aad5-9fb7988f3b2f](https://depositonce.tu-berlin.de/items/8e2988b2-d991-45b5-aad5-9fb7988f3b2f) — CC BY-SA 4.0 |
|
||||
| **Lutz 2024** | *Decorated Discrete Conformal Equivalence, Canonical Tessellations, and Polyhedral Realization* | [doi.org/10.14279/depositonce-20357](https://doi.org/10.14279/depositonce-20357) — Open Access |
|
||||
|
||||
---
|
||||
|
||||
## Paper hinter Verlag-Paywall (ggf. über Institutional Access / Google Scholar)
|
||||
|
||||
| Autor(en) | Titel | Venue | DOI / Link |
|
||||
|---|---|---|---|
|
||||
| **Ushijima** ⚠️ (Einzelautor — kein Meyerhoff!) | *A Volume Formula for Generalised Hyperbolic Tetrahedra* | Prékopa, Molnár (eds.) *Non-Euclidean Geometries*, Springer 2006 | [doi.org/10.1007/0-387-29555-0_13](https://doi.org/10.1007/0-387-29555-0_13) · arXiv [math/0309216](https://arxiv.org/abs/math/0309216) (arXiv gibt 500 für alte math/-Preprints — manuell laden) |
|
||||
| **Pinkall, Polthier** | *Computing Discrete Minimal Surfaces and Their Conjugates* | Experimental Mathematics **2**(1), 1993 | [projecteuclid.org/euclid.em/1062620735](https://projecteuclid.org/euclid.em/1062620735) |
|
||||
| **Bowers, Stephenson** | *Uniformizing dessins and Belyĭ maps via circle packing* | Memoirs AMS **170**(805), 2004 | [ams.org/books/memo/0805](https://bookstore.ams.org/memo-170-805) |
|
||||
| **Erickson, Whittlesey** | *Greedy Optimal Homotopy and Homology Generators* | SODA 2005 | [dl.acm.org/doi/10.5555/1070432.1070581](https://dl.acm.org/doi/10.5555/1070432.1070581) |
|
||||
| **Desbrun, Kanso, Tong** | *Discrete Differential Forms for Computational Modeling* | SIGGRAPH Course Notes 2006 | [dl.acm.org/doi/10.1145/1185657.1185665](https://dl.acm.org/doi/10.1145/1185657.1185665) |
|
||||
| **Soliman, Slepčev, Crane** | *Optimal Cone Singularities for Conformal Flattening* | ACM TOG **37**(4), 2018 | [doi.org/10.1145/3197517.3201367](https://doi.org/10.1145/3197517.3201367) — Autorenseite: [cs.cmu.edu/~kmcrane](https://www.cs.cmu.edu/~kmcrane/Projects/OptimalCones/index.html) |
|
||||
| **Gillespie, Springborn, Crane** | *Discrete Conformal Equivalence of Polyhedral Surfaces* | ACM TOG / SIGGRAPH 2021 | [doi.org/10.1145/3450626.3459763](https://doi.org/10.1145/3450626.3459763) — Autorenseite: [markjgillespie.com/Research/CEPS](https://markjgillespie.com/Research/CEPS/index.html) |
|
||||
| **Sharp, Soliman, Crane** | *Navigating Intrinsic Triangulations* | ACM TOG / SIGGRAPH 2019 | [doi.org/10.1145/3306346.3323042](https://doi.org/10.1145/3306346.3323042) — Autorenseite: [cs.cmu.edu/~kmcrane](https://www.cs.cmu.edu/~kmcrane/Projects/NavigatingIntrinsicTriangulations/index.html) |
|
||||
| **Alexa, Wardetzky** | *Discrete Laplacians on General Polygonal Meshes* | ACM SIGGRAPH 2011 | [doi.org/10.1145/1964921.1964997](https://doi.org/10.1145/1964921.1964997) |
|
||||
| **Bunge, Herholz, Kazhdan, Botsch** | *Polygon Laplacian Made Simple* | CGF **39**(2), 2020 | [doi.org/10.1111/cgf.13931](https://doi.org/10.1111/cgf.13931) |
|
||||
| **Rivin, Schlenker** | *The Schläfli formula in Einstein manifolds with boundary* | Electron. Res. Announc. AMS **5**, 1999 | [ams.org/era/1999-05-03](https://www.ams.org/era/1999-05-03) — wahrscheinlich frei |
|
||||
| **Bobenko, Mercat, Schmies** | *Period Matrices of Polyhedral Surfaces* | Computational Approach to Riemann Surfaces, Springer 2011 | [doi.org/10.1007/978-3-642-17413-1](https://doi.org/10.1007/978-3-642-17413-1) — Buchkapitel |
|
||||
|
||||
---
|
||||
|
||||
## Bücher (Bibliothek / Kauf)
|
||||
|
||||
| Autor(en) | Titel | Verlag |
|
||||
|---|---|---|
|
||||
| **Farkas, Kra** | *Riemann Surfaces* (2. Aufl.) | Springer GTM 71 |
|
||||
| **Siegel** | *Topics in Complex Function Theory, Vol. 2* | Wiley |
|
||||
|
||||
---
|
||||
|
||||
## Hinweis: Autorenseiten oft freier als DOI
|
||||
|
||||
Für die SIGGRAPH-Paper (Gillespie 2021, Sharp 2019, Soliman 2018) gibt es auf den
|
||||
CMU/Autorenseiten oft direkte PDF-Downloads ohne Paywall.
|
||||
|
||||
---
|
||||
|
||||
## Fehler in references.md (zu korrigieren)
|
||||
|
||||
> **arXiv:math/0603097** ist in `references.md` fälschlicherweise **Kolpakov–Mednykh**
|
||||
> zugewiesen. Tatsächlich ist das der **Springborn 2008** Artikel
|
||||
> (*A variational principle for weighted Delaunay triangulations and hyperideal polyhedra*).
|
||||
> Das korrekte Kolpakov–Mednykh Paper hat vermutlich kein öffentliches arXiv-Preprint.
|
||||
> → Springborn 2008 wurde korrekt als `springborn-2008-weighted-delaunay-hyperideal.pdf`
|
||||
> gespeichert; references.md muss angepasst werden.
|
||||
Reference in New Issue
Block a user