Adds a comprehensive 'External audit documents' section to the reviewer index:
- groups all 12 audits (correctness, API/perf/packaging, robustness)
- surfaces G0 (porting rights) as the cross-cutting release blocker
- records the decisions taken: coverage gate 80/70/90, API naming A1-A5
(A1-A3 implemented in PR #36, A4-A5 deferred pending G0/G1)
- points a new session at the low-risk quick-win findings
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Checks code/docs against the published mathematics (vs java-port-audit which
checks code vs Java):
- M1: Kolpakov-Mednykh volume formula used in code but missing from references.md
- M2: BPS circle-packing paper cited as '2010' in code vs '2015' in references.md
- M3: post-2023/preprint citations back unimplemented phases — re-verify + clarify
scope before submission
- M4: no canonical citation-key convention
- M5: large derivations numerically validated but prose unreviewed — expert sign-off
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Closes the dimensions the prior audits did not cover:
- numerical-stability: tolerance hierarchy (FD-step vs Newton tol), discontinuous
clamps, cancellation in cotangent/area, magic constants.
- input-validation: malformed JSON/XML deserialization, NaN/Inf vertex coords on
load, schema-drift handling.
- thread-safety: no mutable static state (good); undocumented same-mesh concurrency
contract; Eigen threading.
- dependency-license: existing THIRD-PARTY-LICENSES matrix is sound but predicated
on the MIT premise that G0 undermines; test meshes have no provenance/license.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Every container now has an explicit --memory limit so the OOM killer
has clean boundaries to work with instead of letting any single job
consume all available RAM:
test-fast: 800m (build Eigen+GTest, run 26 tests)
test-cgal: 2000m (LOW_MEMORY_BUILD, was already set)
quality-gates: 600m (apt install + 4 script checks)
doc-build: 800m (Doxygen HTML generation)
markdown-links: 400m (pure Python, very lightweight)
memory-swap = 1.5× memory in each case (150% total) so the OOM
killer fires cleanly without exhausting the SD card.
⚠ This is workflow-side protection only. The runner-side fix
(capacity: 1 in act-runner config) prevents parallel containers
entirely and is the more reliable protection — see CLAUDE.md.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Pi runner (3-4 GB RAM, swap heavily loaded) OOMs when /ci-all
triggers test-cgal + quality-gates + doc-build + links simultaneously:
four Docker containers start at once after test-fast completes, each
consuming 150-400 MB, exhausting available RAM.
Fix: /ci-all removed from all three workflow files. Each keyword
now triggers exactly one job — no parallel container competition.
Safe workflow: one keyword per commit.
Typical sequence:
git commit -m 'fix: ... /test-cgal' → CGAL tests (~5 min)
git commit -m 'chore: /quality-gates' → style checks (~30 s)
CLAUDE.md: CI table updated with Pi-runner warning note.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Finding-U6 from doc/reviewer/usability-audit-2026-05-31.md.
The Finding-D fix (external-audit-2026-05-30) added a clean one-call
gauge-vertex overload:
assign_euclidean_vertex_dof_indices(mesh, maps, gauge_vertex)
But all user-facing code still showed the old verbose manual loop:
auto vit = mesh.vertices().begin();
maps.v_idx[*vit++] = -1;
int idx = 0;
for (; vit != mesh.vertices().end(); ++vit) maps.v_idx[*vit] = idx++;
Replaced in three places:
README.md 'Minimal usage' code block
example_euclidean.cpp Step 3
example_layout.cpp pin_first() helper
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
U4 (README.md:17) — status line: v0.9.0 → v0.10.0, test count 0 → 277
U5 (Discrete_conformal_map.h:6-16) — \file Doxygen block rewritten:
was: 'provides a single function … Spherical/hyperbolic scheduled for Phase 8b.2'
now: lists all three discrete_conformal_map_* functions already present,
plus pointers to the circle-packing companion headers
U7 (getting-started.md) — new 'Mode 4 — Low-memory build' section added
after Mode 3; shows CONFORMALLAB_LOW_MEMORY_BUILD=ON with -j1 and explains
the -O0 / no PCH / batch-1 tradeoffs for Raspberry Pi / ≤ 4 GB runners
U8 (README.md compile-time modes) — LOW_MEMORY_BUILD entry added to the
compile-time workflow code block with a one-line explanation and the
mandatory -j1 note
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Finding-U3 from doc/reviewer/usability-audit-2026-05-31.md.
Three concrete errors corrected:
1. check_gauss_bonnet() row (was: implies all map types work)
→ now: 'EuclideanMaps/SphericalMaps only' — HyperIdealMaps overload
is = delete (compile error since external-audit Finding-B fix)
2. enforce_gauss_bonnet() row (same issue)
→ same restriction note added
3. newton_hyper_ideal() preconditions (was: 'lambda0 initialised')
→ WRONG: HyperIdeal computes lengths from b_v, a_e via zeta functions
internally; lambda0 plays no role. Correct precondition:
'no lambda0 needed · no GB pre-check'
→ Added explanation: strictly convex energy (Springborn 2020 Thm 1.3)
→ Newton converges for any targets without a pre-check
Gauss-Bonnet prose section also updated:
- Examples now show EuclideanMaps/SphericalMaps explicitly
- HyperIdeal compile-error note added with the correct hyperbolic
identity Σ(2π-Θ_v) - Area = 2π·χ and why no pre-check is needed
- Open-mesh note added (GB identity does not hold for boundary meshes)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
U10: CLI has no parameter reference in README or getting-started.md.
Documents all 7 current flags with defaults in the audit file so
a future session can add the table to getting-started.md.
U11: Three useful CLI gaps documented as a roadmap item:
- --tol / --max-iter: Newton solver tuning, currently hardcoded
- -g cp_euclidean / -g inversive_distance: Phase-9a models not
reachable from CLI despite being fully implemented in the library
Each item has an effort estimate (~30 min / ~2 h) and concrete
acceptance criteria so a future session can pick it up cold.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Finding-U1 and Finding-U2 from doc/reviewer/usability-audit-2026-05-31.md.
The existing examples (example_euclidean, example_layout, example_hyper_ideal)
all used the "natural theta" pattern which makes x*=0 trivially the
equilibrium — u_v ≈ 0 everywhere, no deformation. A new user following
these examples saw solver output but not conformal geometry.
New: example_flatten.cpp
- PRIMARY USE CASE: conformally flatten a mesh to the plane
- Sets Θ_v = 2π for all interior vertices (flat target)
- Pins boundary vertices (no Gauss-Bonnet check for open meshes)
- Demonstrates non-trivial u_v (cathead.obj: range ≈ 2.96, 5 Newton iters)
- Documents the difference from "natural theta" explicitly
New: example_cgal_api.cpp
- Demonstrates CGAL::discrete_conformal_map_euclidean (Discrete_conformal_map.h)
- First runnable CGAL public API example; contrast with internal API
- Documents the "natural theta" default behaviour and explains why u_v=0
- Explains when to use CGAL API vs internal API
Both examples registered in code/examples/CMakeLists.txt and compile
cleanly with -DWITH_CGAL=ON.
Updated:
- example_euclidean.cpp: prominent "TESTING CONVENTION" warning
- example_layout.cpp: same warning on set_natural_theta helper
- doc/getting-started.md: example_flatten is now the recommended
"start here" example; note on natural-theta behaviour added
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
External documentation and usability review of v0.10.0.
9 findings covering documentation correctness, API usability,
and new-user experience.
Critical:
U1 — All examples show trivial identity map (natural theta),
not real conformal flattening — new users see no-op output
U2 — CGAL public API has zero runnable examples
Medium:
U3 — contracts.md incorrect: check_gauss_bonnet row missing
HyperIdeal restriction (deleted overload after Finding-B)
U4 — README still shows v0.9.0 (current: v0.10.0, 277 tests)
U5 — Discrete_conformal_map.h header comment describes Phase-8a
state; all 5 entry functions already implemented
U6 — New gauge-vertex overload (Finding-D) not reflected in
README or examples — old verbose loop still shown
Minor:
U7 — CONFORMALLAB_LOW_MEMORY_BUILD absent from getting-started.md
U8 — CONFORMALLAB_LOW_MEMORY_BUILD absent from README table
U9 — Layout2D.uv index semantics undocumented (v.idx() contract)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
issue_comment triggers had two problems: Gitea did not reliably fire
them, and the refs/pull/N/head checkout was fragile. Commit-message
keywords are simpler and guaranteed to work on any push event.
Trigger keywords (add anywhere in the commit message):
/test-cgal → CGAL test suite (277 tests, LOW_MEMORY_BUILD)
/quality-gates → license/codespell/shellcheck/cgal-conventions
/docs → Doxygen build + warning summary
/links → Markdown internal link check
test-fast still runs on every push (no keyword needed).
All `issue_comment` event handlers and `refs/pull/N/head` checkouts
removed from all three workflow files. review/** added to push branch
filters so this PR branch triggers normally.
CLAUDE.md CI table updated.
/test-cgal /quality-gates
Every CI job except test-fast and mirror-to-codeberg now runs only
when explicitly requested via a PR comment, instead of on every push
or PR sync. This keeps the Pi runner idle during WIP commits and lets
the author decide when to pay each job's cost.
Trigger commands:
/test-cgal → CGAL test suite (277 tests, ~5 min build + 31 s run)
/quality-gates → license/codespell/shellcheck/cgal-conventions (~30 s)
/docs → Doxygen build + warning summary (~2 min)
/links → Markdown internal link check (~10 s)
All comment-triggered jobs check:
event is issue_comment
AND comment is on a PR (issue.pull_request != null)
AND comment body contains the trigger word
AND checkout uses refs/pull/N/head (not the default branch)
Jobs that stay automatic:
test-fast — runs on every push (26 pure-math tests, < 5 s)
mirror-to-codeberg — unchanged
Jobs that keep additional triggers:
markdown-links — weekly cron (Mon 05:00 UTC) + workflow_dispatch
doc-build — workflow_dispatch (for manual runs outside a PR)
quality-gates drops `needs: test-fast` — it now runs independently
when comment-triggered (caller decides whether test-fast passed first).
CLAUDE.md CI pipeline table updated with all five jobs and their new
trigger descriptions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Instead of triggering on every pull_request push, test-cgal now fires
only when a PR comment contains "/test-cgal". This prevents the Pi
runner (3-4 GB RAM, swap constantly loaded) from queuing CGAL builds
faster than it can drain them on every WIP commit.
Workflow changes:
- Add `issue_comment: types: [created]` to the top-level `on:` block
- test-cgal `if:` condition:
event is issue_comment
AND comment is on a PR (issue.pull_request != null)
AND comment body contains "/test-cgal"
- Checkout uses `refs/pull/N/head` so the PR branch is checked out
correctly (issue_comment sets GITHUB_REF to the default branch, not
the PR branch)
Usage: write "/test-cgal" as a PR comment to trigger the 277-test
CGAL suite. The build uses CONFORMALLAB_LOW_MEMORY_BUILD=ON (-O0,
no PCH, unity batch 1) and runs in a 2000 MB container.
CLAUDE.md: CI table + status paragraph updated accordingly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Finding-I from doc/reviewer/external-audit-2026-05-30.md.
Root cause: CGAL+Eigen at -O3 drives cc1plus peak RAM to ~700 MB per
Unity compilation unit (batch size 4) on ARM64. With the 1600 MB
container limit the 2nd or 3rd TU reliably triggered OOM-kill, so
test-cgal was gated off via `if: false` since 2026-05-26.
Fix: new CMake option CONFORMALLAB_LOW_MEMORY_BUILD=ON applies four
orthogonal memory-saving measures to conformallab_cgal_tests:
1. -O0 (no debug info): optimizer passes entirely skipped → cc1plus
peak drops from ~700 MB to ~150-200 MB per TU on ARM64.
Omitting -g avoids the additional object-file / linker RAM cost.
2. CONFORMALLAB_USE_PCH=OFF: saves the one-time ~200 MB PCH
compilation cost; each TU re-parses CGAL headers (fast at -O0).
3. UNITY_BUILD_BATCH_SIZE=1: one source file per cc1plus invocation,
removing the "4-file template-explosion" per-unit multiplier.
4. -Wl,--no-keep-memory (GNU ld): linker releases symbol tables after
each input file → ~15-25 % less linker RSS.
Verified locally with cmake -DCONFORMALLAB_LOW_MEMORY_BUILD=ON:
277/277 CGAL tests pass, 31 s runtime (vs 2 s at -O3 — expected;
tests run 15× slower without optimizer but all correct).
CI workflow changes (cpp-tests.yml):
- test-cgal re-enabled: `if: github.event_name == 'pull_request'`
- Configure step adds -DCONFORMALLAB_LOW_MEMORY_BUILD=ON
- Container memory: 1600m → 2000m (--memory-swap=3000m for 1 GB swap
headroom), using ~half of the Pi's 3-4 GB while leaving OS margin.
CLAUDE.md updated: new flag added to compile-time options table; CI
status row corrected from DISABLED to active.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
MINOR-1 (spherical_functional.hpp:426)
Wrong comment said "second derivative < 0 for a convex functional".
The spherical energy is *concave* (NSD Hessian); the monotone-f argument
applies to both convex and concave functionals equally. Comment rewritten
to explain the actual physics: increasing scale increases all angles and
thus reduces Σ G_v.
MINOR-2 (spherical_functional.hpp:494-495)
Forward finite difference O(ε) → central finite difference O(ε²):
old: dft = (sum_Gv(t + fd_eps) - ft) / fd_eps
new: dft = (sum_Gv(t + fd_eps) - sum_Gv(t - fd_eps)) / (2*fd_eps)
Same cost when the extra sum_Gv(t - fd_eps) replaces the cached ft.
MINOR-3 (euclidean_functional.hpp, spherical_functional.hpp,
inversive_distance_functional.hpp)
New header gauss_legendre.hpp centralises the 10-point Gauss-Legendre
nodes and weights (gl10_nodes() / gl10_weights()). The three energy
functions now use the shared accessors instead of duplicated local
static arrays.
MINOR-4 (euclidean_functional.hpp, spherical_functional.hpp,
hyper_ideal_functional.hpp, inversive_distance_functional.hpp)
halfedge_to_index() centralised in conformal_mesh.hpp. All four local
aliases (eucl_hidx, spher_hidx, hidx, id_detail::hidx) now delegate to
it as one-line wrappers; the aliases are kept for now to avoid a larger
call-site churn, clearly documented as thin wrappers.
MINOR-5 (clausen.hpp:33-38)
Added a comment above inits() explaining the intentional off-by-one
return value and how it interacts with csevl() — matching the Java
Clausen.inits() / csevl() contract.
277/277 CGAL + 26/26 pure-math tests pass, 0 failed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Finding-E from doc/reviewer/external-audit-2026-05-30.md.
Scan also uncovered the same issue in inversive_distance_functional.hpp.
Three functions used absolute error `|analytic - fd| > tol` while the
rest of the library (euclidean_functional, spherical_functional,
euclidean_hessian, hyper_ideal_functional) all use relative error
`|analytic - fd| / max(1, |analytic|) > tol`.
Absolute error is too strict for large gradients (false failures) and
too lenient for small gradients.
Fixed:
cp_euclidean_functional.hpp gradient_check_cp_euclidean()
cp_euclidean_functional.hpp hessian_check_cp_euclidean()
inversive_distance_functional.hpp gradient_check_inversive_distance()
All three now use the relative criterion and accumulate all failures
before returning (ok=false instead of early return on first mismatch).
Default tol updated from 1e-6/1e-5 to 1e-4, matching the Java
FunctionalTest convention used by all other checks in the library.
Error message updated to print rel-err instead of raw diff.
266/266 CGAL tests pass, 0 failed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Finding-D from doc/reviewer/external-audit-2026-05-30.md.
All three DOF-assignment functions iterated over every vertex
unconditionally, overwriting any v_idx set before the call. The
documentation said "pin before OR after" — the "before" option was
silently wrong (the pin would be overwritten). For the
inversive-distance variant the doc explicitly said the pre-call pin
would make the function "a no-op for that vertex", which was false.
Changes (three headers):
- euclidean_functional.hpp assign_euclidean_vertex_dof_indices()
- spherical_functional.hpp assign_vertex_dof_indices()
- inversive_distance_functional.hpp assign_inversive_distance_vertex_dof_indices()
For each:
1. Single-arg overload: doc corrected to "pin AFTER, not before;
pre-call pins are overwritten"
2. New two-arg overload accepting a Vertex_index gauge: pins the
requested vertex (v_idx=-1) in a single pass, preventing the
user error entirely
Three new GTests in test_euclidean_functional.cpp:
SingleArg_PinBeforeHasNoEffect — documents the old pitfall
TwoArg_GaugeIsPinnedOthersAreSequential — verifies the new overload
TwoArg_NewtonConvergesWithGaugeOverload — end-to-end correctness
266/266 CGAL tests pass, 0 failed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Finding-C from doc/reviewer/external-audit-2026-05-30.md.
The box comment at the top and the function-level comment above
euclidean_cot_weights() both stated:
cot_k = (t_adj1·l123 − t_adj2·t_opp) / denom2 ← WRONG
The correct formula (verified numerically on a 3-4-5 right triangle,
expected cot1=4/3, cot2=3/4, cot3=0) is:
cot_k = (t_opp · l123 − t_a · t_b) / denom2
where t_opp is the t-value of the edge OPPOSITE vertex k, and t_a/t_b
are the t-values of the two edges ADJACENT to vertex k.
The implementation in euclidean_cot_weights() was already correct;
only the documentation was wrong.
Changes (documentation only, zero code changes):
- Box comment: rewritten with correct formula and explicit per-vertex
assignment (cot1: t_opp=t23, t_a=t12, t_b=t31; etc.)
- Box comment: added missing ½ factor to Hessian contribution lines
- Function-level comment: corrected to (t_opp·l123 − t_a·t_b)/(8·Area)
with a pointer to the box comment for the full assignment
- Inline return comment in euclidean_cot_weights(): now shows the
mapping (cot1: t_opp=t23, t_a=t12, t_b=t31) directly at the formula
263/263 CGAL tests pass, 0 failed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Finding-B from doc/reviewer/external-audit-2026-05-30.md.
The Euclidean Gauss–Bonnet identity Σ(2π−Θ_v) = 2π·χ is ONLY valid for
flat/Euclidean and spherical metrics. For hyperbolic metrics (HyperIdeal)
the correct identity is Σ(2π−Θ_v) − Area(M) = 2π·χ. The previously
provided gauss_bonnet_sum(HyperIdealMaps) overload would silently pass the
wrong LHS to check_gauss_bonnet, which would always throw "deficit = ±Area"
for valid hyperbolic targets.
Fix:
- gauss_bonnet_sum(mesh, HyperIdealMaps) → = delete + explanation comment
- enforce_gauss_bonnet(mesh, HyperIdealMaps&) → = delete + explanation comment
- Header box comment rewritten with the correct hyperbolic Gauss–Bonnet
identity and a clear "HyperIdeal: NOT SUPPORTED" section
New test in test_phase6.cpp:
- HyperIdeal_EuclideanSumDiscrepancy_DocumentsWhyCheckIsDeleted
verifies numerically that the Euclidean sum = 0 but 2π·χ = 4π for a
regular tetrahedron, documenting the −4π discrepancy that motivated
the deletion
- Three compile-time static_asserts (SFINAE) confirm the overload is not
invocable with HyperIdealMaps but remains so with Euclidean/SphericalMaps
263/263 CGAL tests pass, 0 failed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Follow-up to the Finding-A fix (face_energy guard). The throw is the
correct safe behaviour for now; the mathematically complete solution
requires formulas for semi-ideal and fully-ideal tetrahedra that are
absent from the Java reference.
Adds a structured research entry to research-track.md (Phase 9b+):
- primary references: Kolpakov-Mednykh arXiv:math/0603097, Milnor 1982,
Vinberg 1985
- acceptance criteria: two new volume functions + gradient-check tests +
limiting-behaviour continuity witness
- explicit note that the throw in face_energy() must not be removed
without implementing and testing the replacement formulas
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Finding-A from doc/reviewer/external-audit-2026-05-30.md.
Root cause: both the Java reference (HyperIdealFunctional.java:222-231)
and the C++ port silently applied the one-ideal-vertex volume formula
to the first ideal vertex found in a face, ignoring any additional ideal
vertices. For two or three ideal vertices this produces a wrong energy
value with no diagnostic.
Fix: add an ideal_count guard at the top of face_energy() that throws
std::logic_error for ideal_count >= 2. The one-ideal (Kolpakov-Mednykh)
and zero-ideal (Meyerhoff/Ushijima) paths are unchanged and correct.
Three new GTests cover the three guard cases:
MultiIdealGuard_TwoIdealVertices_Throws (two ideal → throw)
MultiIdealGuard_AllThreeIdealVertices_Throws (all ideal → throw)
MultiIdealGuard_ExactlyOneIdeal_DoesNotThrow (one ideal → no throw)
262/262 CGAL tests pass, 0 failed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Full external code review of v0.10.0 against all math-critical headers
(direct source read, not automated scan). Documents 5 open bugs/API errors,
3 test gaps inherited from java-port-audit.md, 1 architectural CI risk,
and 5 minor findings. Each finding is self-contained with file+line
references, a concrete fix proposal, and acceptance criteria so a new
session can pick up any single finding without prior context.
Key findings:
- FINDING-A (critical): face_energy() wrong for mixed ideal/hyper-ideal configs
- FINDING-B (medium): Gauss–Bonnet API conceptually wrong for HyperIdeal
- FINDING-C (medium): cotangent formula in euclidean_hessian.hpp header is wrong
- FINDING-D (medium): DOF-assignment doc falsely claims "pin before" works
- FINDING-E (medium): cp_euclidean gradient_check uses absolute not relative error
- FINDING-F/G/H: three open test gaps from java-port-audit.md
- FINDING-I (arch): 246/272 CGAL tests not gated in CI
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Upgrades the cyclic Euclidean Hessian from block-FD to closed-form, satisfying
the novelty-statement §3.2 "analytic Hessians, not finite difference" claim for
the Euclidean path.
- euclidean_hessian.hpp: `euclidean_hessian_analytic` — closed-form cyclic
Hessian from the law-of-cosines angle derivatives
∂α_i/∂s_i = ℓ_i²/4A, ∂α_i/∂s_j = ½cot α_i − ℓ_j²/4A (Σ_j = 0),
chained to (u, λ_e) and sign-mapped to the gradient outputs (−α vertex,
+α_opp edge). Reuses euclidean_cot_weights. Block-FD kept as cross-check.
- newton_solver.hpp: newton_euclidean cyclic path now uses the analytic Hessian.
- tests: CyclicHessian_Analytic_MatchesBlockFD_Tetrahedron — analytic == block-FD
(1e-6), == gradient FD (1e-5), symmetric (1e-9). Existing cyclic convergence
oracle still GREEN with the analytic Hessian routed in.
Tier-2 (Wente) finding: wente_torus02.obj is a QUAD mesh (1240 quads) and the
Java golden comes from cyclic (quad-net) uniformization; the C++ period-matrix
pipeline is triangle-based, so a faithful bit-vs-Java τ comparison needs a
quad/cyclic pipeline (Phase 9f). Deferred and documented; golden τ = ½+i√3/2.
244/244 cgal tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Build-verified attempt to port the Java EuclideanCyclicConvergenceTest
(cathead, "circular hole edge" φ = π−0.1).
- ✅ GREEN `CyclicCircularEdge_PhiEntersGradient_CatHead`: solver-free
cross-validation that the circular-edge φ target enters the cyclic edge
gradient exactly (ΔG_e = −Δφ_e at 1e-12; no other component moves).
- ⏸️ DISABLED `CyclicCircularEdge_CatHead_JavaXVal`: the full Java convergence
assertion (α_opp+α_opp = π−0.1), kept with golden semantics. Auto-activates
once the edge-DOF Hessian lands.
Build-verification finding: `newton_euclidean` -> `euclidean_hessian` throws
"edge DOFs are not supported" — the cyclic full solve is blocked by a missing
edge-DOF Euclidean Hessian (gradient supports edge DOFs, analytic Hessian does
not). Spherical convergence (Tier-1 #2) is already covered by test_newton_solver.
Documented in doc/reviewer/java-ignore-crossvalidation.md.
All 13 EuclideanFunctional tests pass (1 disabled).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Classifies the 22 Java @Ignore'd test files by root cause (ARM64 PETSc
native lib vs logic/known-issue) and by cross-validation value for the
C++ port.
- Notes what PR #27 already locks (math cores + functional-eval oracles)
to avoid duplication.
- Priority ranking: HIGH (HyperIdeal Lawson convergence golden vector —
records the exact u*), MEDIUM (genus-1 Wente uniformization, branch-point
variant), LATER (genus-2/hyperelliptic/Mobius/quasi-isothermic/Schottky),
NON-ORACLE (Java gaps: testHessian, testDoLayout; ARM64-flaky known issues).
- Documents why the Lawson mesh needs a low-level half-edge generator
(4 vertices / 12 edges => multi-edges; CGAL add_face cannot build it).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Infrastructure table: add Matrix (→ Eigen inline, done) and P2 rows.
P2 marked "port inline with Phase 9c" — almost exclusively consumed by
FundamentalPolygon/CanonicalForm/SurfaceCurve (all Phase 9c), so building
p2_geometry.hpp as a standalone ahead of 9c would be dead weight.
- Phase 9c rows in the utility table: ⚠️ annotation listing the exact P2.*
methods needed (makeDirectIsometryFromFrames, projectP3ToP2,
imbedMatrixP2InP3) plus the P2Big/cpp_dec_float_50 high-precision
prerequisite for the group-relation product ∏gᵢ = Id.
Ensures a future Phase 9c session starts with full context and does not
discover the jReality/precision dependencies mid-port.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ports the small but pervasive de.jreality.math.Pn surface that the Java
algorithmic core uses across every not-yet-ported geometric phase:
HyperbolicLayout, SphericalLayout, FundamentalPolygon (9c), KoebePolyhedron
(10c'), quasi-isothermic (10e), hyperelliptic theta, CircleDomain (11c).
Porting it once unblocks all downstream consumers instead of re-deriving
the metric ad hoc per phase.
pn_geometry.hpp — header-only, Eigen, ~120 LOC:
PnMetric { EUCLIDEAN=0, ELLIPTIC=+1, HYPERBOLIC=-1 } matching jReality.
pn_inner_product — bilinear form per signature.
pn_norm / pn_dehomogenize / pn_set_to_length / pn_normalize.
pn_distance_between — Euclidean (spatial), elliptic (acos), hyperbolic (acosh).
pn_linear_interpolation — affine (E) and slerp (S/H constant-speed geodesic).
HYPERBOLIC uses the timelike-positive ("upper-sheet") convention, identical to
the already-verified projective_math.hpp::hyperbolicDistance; pn_distance_between
is regression-anchored against it in the tests.
test_pn_geometry.cpp — 6 tests, all GREEN:
InnerProductSignatures, EuclideanDistance, EllipticDistanceIsAngle,
HyperbolicDistanceClosedFormAndAnchor (+ projective_math.hpp anchor),
NormAndScaling, LinearInterpolationGeodesic.
doc/roadmap/java-parity.md — new "Infrastructure / support layers" section:
de.jreality.math.Pn → pn_geometry.hpp (partial, 6 fns covering core usage)
de.jreality.math.Rn → Eigen (no separate port needed)
MatrixBuilder → not yet (only needed for hyperelliptic theta)
conformallab XML types → not planned (GUI persistence, not algorithmic)
246/246 cgal tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Second Lawson variant from HyperIdealConvergenceTest...WithBranchPoints.
- make_lawson_branch_points(): base + STELLAR subdivision (Java StellarLinear)
via CGAL Euler::add_center_vertex — a centre vertex per quad fan-connected to
its 4 corners (6 centres, 24 triangles, 36 edges). The 6 centres are IDEAL
vertices (b=0, v_idx=-1); θ_e=π on the 12 base edges, θ_e=π/2 on the 24 spokes.
- BranchPointsGoldenVector_JavaXVal: newton_hyper_ideal converges to the Java
golden (per symmetry class @1e-4):
original vertices → 1.3169579
base edges → 2.2924317
spoke edges → 0 (the π/2 spokes collapse to ideal)
Key insight: Java's index-based θ split (first 12 edges π, rest π/2) is
geometric — base edges vs stellar spokes — so the symmetry shortcut applies.
createLawsonHyperelliptic() NOT ported: needs a reader for the Java
conformal-data XML (lawson_curve_source.xml) + a port of
HyperIdealHyperellipticUtility, and its golden vector is not class-symmetric.
Documented as a deferred task.
243/243 cgal tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ports the Java HyperIdealConvergenceTest (Lawson square-tiled) — the strongest
remaining @Ignore'd oracle (a hard-coded converged solution from a historical
x86 PETSc run).
- make_lawson_square_tiled(): builds the genus-2 base (4 vertices, 12 edges,
6 quads) via the low-level CGAL Surface_mesh half-edge API (add_edge +
set_target/set_next/set_face/set_halfedge), since the multi-edges (≥2 edges
per vertex pair) make add_face / OFF / polygon-soup impossible. Then
triangulate_faces → 12 triangles, 18 edges (12 original + 6 diagonals).
BuildsValidGenus2Mesh: is_valid + V=4/F=12/E=18 + χ=−2.
- ConvergenceGoldenVector_JavaXVal: Θ_v=2π, θ_e=π/2 (12 original edges),
θ_e=π (6 diagonals); newton_hyper_ideal converges (from x0=1.0, unconstrained)
to the Java golden vector:
vertices → 1.1462158341786262
original → 1.7627471737467797
aux → 2.633915794495759
asserted per symmetry class @1e-5 (robust to DOF ordering).
The perfect symmetry of the golden vector means any consistent one-diagonal
triangulation reproduces the three values, so the external jtem Triangulator
choice need not be replicated. Resolves the Tier-3 item from PR #29's analysis.
242/242 cgal tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>