11 Commits

Author SHA1 Message Date
Tarik Moussa
449c5899c0 ci: re-enable CGAL tests with LOW_MEMORY_BUILD for Raspberry Pi runner
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>
2026-05-31 01:17:27 +02:00
Tarik Moussa
59a26123c8 fix(minor): all five MINOR findings — doc, accuracy, and DRY
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>
2026-05-31 01:17:27 +02:00
Tarik Moussa
2325328f77 test: add end-to-end skewed-torus and synthetic holonomy Re(τ)<0 tests
Finding-H from doc/reviewer/external-audit-2026-05-30.md
(java-port-audit missing-test item 7).

Guards against a regression where compute_period_matrix reverts to
reduce_to_fundamental_domain (no mirror fold) instead of normalizeModulus
(Finding 6 fix, Java-faithful): such a revert would silently produce
Re(τ) < 0 for lattices where the natural generators give a negative
real part.

Two new tests in test_phase7.cpp:

1. SkewedTorus_ReTauNegativeBeforeNorm_FoldedToPositive
   - New mesh: code/data/off/torus_skewed_4x4.off
     Flat 4×4 torus on parallelogram lattice ω₁=(4,0) ω₂=(-1,4);
     16 vertices, 32 triangles, χ=0 (genus 1).
   - Full pipeline: newton_euclidean → cut_graph → euclidean_layout
     → compute_period_matrix(reduce=false) + compute_period_matrix(reduce=true)
   - Asserts: raw Re(τ) < 0, normalized Re(τ) ∈ [0,½], Im(τ) > 0, |τ| ≥ 1

2. SyntheticHolonomy_NegativeReTau_NormalizedToPositive
   - Bypasses mesh; supplies explicit ω₁=(4,0) ω₂=(-1,4) directly
   - Asserts raw τ = -0.25+i (to 1e-10), normalized τ = 0.25+i (to 1e-9)
   - Pin-points the mirror fold: Re(-0.25) → Re(+0.25)

277/277 CGAL tests pass, 0 failed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 01:17:27 +02:00
Tarik Moussa
adbf682f0f test: add degenerate-triangle limiting-angle and edge-DOF guard tests
Finding-F and Finding-G from doc/reviewer/external-audit-2026-05-30.md
(java-port-audit missing-test items 1 and 2).

Finding-F — Degenerate triangle: limiting angles (item 1)
  test_euclidean_functional.cpp:
    DegenerateTriangle_LimitingAngles_L{12,23,31}TooLong
      — verifies α_opposite = π, other two = 0, valid = false
      for all three edge-over-long cases
    DegenerateTriangle_GradientPicksUpPiCorner
      — end-to-end mesh test: forces effective l12 >> l23+l31 via
        lambda0, evaluates gradient, asserts G_v3 = π (not 2π from
        a skipped degenerate face) and G_v1=G_v2 = 2π

  test_spherical_functional.cpp:
    DegenerateTriangle_LimitingAngles_S{12,23,31}TooLong
      — same coverage for spherical_angles()

Finding-G — euclidean_hessian edge-DOF guard (item 2)
  test_euclidean_hessian.cpp:
    EdgeDOFGuard_Throws
      — assign_euclidean_all_dof_indices + euclidean_hessian → throw
    EdgeDOFGuard_VertexOnlyDoesNotThrow
      — vertex-only layout → no throw (regression guard)

275/275 CGAL tests pass, 0 failed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 01:17:27 +02:00
Tarik Moussa
7534c62c3d fix(gradient-checks): use relative error in all FD check functions
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>
2026-05-31 01:17:27 +02:00
Tarik Moussa
cfbbc1b21f fix(dof-assign): correct "pin before" docs + add gauge-vertex overloads
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>
2026-05-31 01:17:27 +02:00
Tarik Moussa
bef5a0ceb7 docs(euclidean-hessian): fix wrong cotangent formula in two comment blocks
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>
2026-05-31 01:17:27 +02:00
Tarik Moussa
46c0b63de8 fix(gauss-bonnet): delete HyperIdeal overloads — wrong identity for hyperbolic metrics
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>
2026-05-31 01:17:27 +02:00
Tarik Moussa
faf96ee28c doc(roadmap): add research item for 2/3-ideal hyper-ideal volume formulas
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>
2026-05-31 01:17:27 +02:00
Tarik Moussa
9afefcbb7b fix(hyper-ideal): guard face_energy() against unsupported multi-ideal faces
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>
2026-05-31 01:17:27 +02:00
Tarik Moussa
7f1a077269 doc(reviewer): add external audit 2026-05-30
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>
2026-05-31 01:17:27 +02:00