# Java → C++ Port Audit — Anomalies & Missing Test Cases

Systematic comparison of the C++ port (`code/include/*.hpp`) against the original
Java reference (`~/Desktop/conformallab`,
`de.varylab.discreteconformal.functional.*`). Focus: math correctness, logic, and
faithfulness to the reference.

Status legend: ✅ fixed · ⚠️ open / needs decision · ℹ️ note (no action)

---

## Files compared (math-critical)

| Java | C++ | Verdict |
|------|-----|---------|
| `Clausen.java` | `clausen.hpp` | ✅ faithful (incl. `inits` return, `clausen2`, `Л`, `ImLi2`) |
| `EuclideanCyclicFunctional.java` | `euclidean_functional.hpp`, `euclidean_geometry.hpp` | ✅ angles + gradient assembly match; degenerate handling fixed |
| — Hessian (`triangleHessian`/`conformalHessian`) | `euclidean_hessian.hpp` | cotangent + vertex block match; edge block **not implemented** (now guarded) |
| `SphericalFunctional.java` | `spherical_functional.hpp`, `spherical_geometry.hpp` | ✅ vertex-mode + edge-DOF replacement parameterization now match (Finding 3) |
| `HyperIdealFunctional.java` | `hyper_ideal_functional.hpp`, `hyper_ideal_geometry.hpp` | ✅ faithful (incl. degenerate (π,0,0) angles) |
| `HyperIdealUtility.java` | `hyper_ideal_utility.hpp` | ✅ ζ, ζ₁₃, ζ₁₄, ζ₁₅, both tetrahedron-volume formulas match |
| `CPEuclideanFunctional.java` | `cp_euclidean_functional.hpp` | ✅ `p()`, gradient, energy match |
| `DiscreteEllipticUtility.java` | `discrete_elliptic_utility.hpp`, `period_matrix.hpp` | ✅ `normalizeModulus` faithful and now wired into `compute_period_matrix` (Finding 6) |
| `HomologyUtility.java`, `CanonicalBasisUtility.java`, `SpanningTreeUtility.java` | `cut_graph.hpp` | tree-cotree only; no canonical/symplectic basis ℹ️ (Finding 7) |
| `EuclideanLayout.java` (holonomy) | `layout.hpp` (`detail::euclidean_holonomy`) | translation via midpoint displacement; ignores residual rotation ℹ️ (Finding 8) |

---

## Findings

### 1. ✅ FIXED — Degenerate triangle handling (Euclidean + Spherical gradient)
The Java reference assigns the **limiting** corner angles to a degenerate
(triangle-inequality-violating) face — the corner opposite the over-long edge is
**π**, the other two **0**. This is the convex C¹ extension that keeps the BPS
energy well-defined on the infeasible region, so Newton can pass through a flip
without stalling.

The C++ port returned `{0,0,0}` and **skipped the whole face** in the gradient.
Tell-tale: the hyper-ideal port *already* mirrors Java's (π,0,0) fallback —
euclidean/spherical were inconsistent oversights.

**Fix:** `euclidean_geometry.hpp` / `spherical_geometry.hpp` degenerate branches
now return the limiting angles (still `valid=false`, so the cotangent Hessian
keeps skipping — matching Java, whose `triangleHessian` zeroes cotangents on
degenerate faces). `euclidean_functional.hpp` / `spherical_functional.hpp`
gradients no longer skip. All 237 tests pass; solution unchanged (only affects
evaluations that previously contributed zero).

### 2. ✅ FIXED — `euclidean_hessian` missing edge-DOF guard
Doc comment claimed "the function asserts that no edge DOF is variable" — but
**no such guard existed**. With edge DOFs assigned
(`assign_euclidean_all_dof_indices`), the Java `conformalHessian`'s edge-edge and
vertex-edge blocks are absent, so the C++ would silently build a Hessian with
all-zero edge rows/cols (singular → LDLT fails → QR least-squares garbage).
**Fix:** added an always-compiled `throw std::logic_error` guard.

### 3. ✅ FIXED — Spherical edge-DOF parameterization now matches Java
Java's spherical functional **drops** the `u_i + u_j` vertex terms when an edge is
a variable (replacement parameterization, `SphericalFunctional.java:398–400`),
whereas the C++ previously used additive `Λ = λ⁰ + u_i + u_j + λ_e` always. That
made the C++ spherical edge gradient carry an extra `−(S_f⁺+S_f⁻)/2` term absent
from Java's `G.add(i, αk + αl − π)` (`SphericalFunctional.java:283–292`).

**Fix:** added helper `spher_eff_lambda` implementing the replacement convention
(`Λ_ij = λ_e` when the edge carries a DOF, else `λ⁰ + u_i + u_j`); `spherical_gradient`
Pass 1 now uses it, and the edge gradient is `α_opp⁺ + α_opp⁻ − θ_e` (θ_e default
π), exactly matching Java. The energy is the path integral of this gradient, so it
follows automatically. The **vertex-only path is bit-for-bit unchanged** (no edge
DOFs → identical to before). All 237 tests pass.

### 4. ✅ FIXED — Spherical Hessian edge-DOF guard added
`spherical_hessian.hpp` builds only the vertex block; Java's spherical
`conformalHessian` includes edge terms. Same limitation as the Euclidean Hessian
but previously **without** the edge-DOF guard added in Finding 2.
**Fix:** added an always-compiled `throw std::logic_error` guard at the top of
`spherical_hessian` (mirrors Finding 2), so any attempt to use edge DOFs with the
spherical Hessian fails loudly instead of silently building a singular matrix.
All 237 tests pass.

### 5. ℹ️ NOTE — CP-Euclidean energy uses `clausen2`, Java uses `clausen`
`CPEuclideanFunctional.java:226` calls `Clausen.clausen` (the BORDERLINE-2.0944
polynomial); the C++ energy calls `clausen2` (the Chebyshev variant). Both compute
the same Cl₂ integral to ~machine precision, and the term appears only in the
energy (not the Newton-driving gradient). No functional impact. The C++ never
ported the `clausen()` variant at all — harmless.

### 6. ✅ FIXED — Period-matrix reduction now uses the faithful Java port
Two reductions coexist:
- `discrete_elliptic_utility.hpp::normalizeModulus` is a **faithful** line-by-line
  port of Java `DiscreteEllipticUtility.normalizeModulus` — it folds τ into
  **`0 ≤ Re(τ) ≤ ½`, `Im(τ) ≥ 0`, `|τ| ≥ 1`** (the extra `Re ≥ 0` fold uses the
  mirror symmetry τ ≅ −τ̄).
- `period_matrix.hpp::reduce_to_fundamental_domain` reduces to the **standard**
  SL(2,ℤ) domain **`−½ ≤ Re(τ) < ½`, `|τ| ≥ 1`** (no `Re ≥ 0` fold).

`compute_period_matrix` (the production path, and the one the `torus_8x8` τ test
exercises) calls **`reduce_to_fundamental_domain`**, so `normalizeModulus` is
**never used outside its own unit test** (verified by grep). Consequence: the C++
production τ can land with `Re(τ) < 0` where Java would report the mirrored
`+|Re(τ)|`. Same conformal type up to orientation, but **not equal to the Java
oracle value** — this will bite any golden-value cross-check (missing-test item 5).

**Fix (option a):** `compute_period_matrix` now calls `normalizeModulus`, so the
production τ matches the Java oracle exactly (folds into `0 ≤ Re ≤ ½`, `Im ≥ 0`,
`|τ| ≥ 1` via the mirror symmetry). `normalizeModulus` is no longer dead code. The
existing `torus_8x8` τ test still passes (`Re ∈ [0,½] ⊂ [−½,½]` so it satisfies
`is_in_fundamental_domain` too). `reduce_to_fundamental_domain` is retained for
callers that explicitly want the canonical (non-mirror-folded) SL(2,ℤ) domain.

### 7. ℹ️ NOTE — Cut graph is tree-cotree, not a canonical homology basis
`cut_graph.hpp` (`compute_cut_graph`) runs the Erickson–Whittlesey tree-cotree
algorithm: primal BFS tree, dual BFS cotree, the remaining `2g` edges generate
H₁. Java's `CanonicalBasisUtility.getCanonicalHomologyBasis` goes further — it
builds the intersection form on the generators and solves for a **canonical
symplectic basis** `{a₁..a_g, b₁..b_g}` (and uses *weighted* shortest-path
cycles, not unweighted BFS).

- **Genus 1:** harmless — there is only one generator pair, and
  `compute_period_matrix` reduces τ to the fundamental domain, so the basis choice
  washes out. The common case matches.
- **Genus > 1:** the C++ `omega[]` ordering is arbitrary (edge-iteration order),
  so `τ = ω₂/ω₁` is **not** the canonical Riemann period matrix. This is already
  flagged in the `period_matrix.hpp` header ("Computing Ω from holonomy data
  requires integration of holomorphic differentials — not implemented") so it is
  consistent with the documented contract, not a regression.

### 8. ✅ FIXED — Holonomy now fits the full deck isometry (was: midpoint only)
`detail::euclidean_holonomy` (`layout.hpp`) develops each face along a dual tree
that never crosses a cut edge.  It **previously** read each generator's
translation as the **midpoint displacement** `ω = midA − midB` of the shared cut
edge between its two independent developments — exact **only** when the two
developments differ by a pure translation (linear part = identity), i.e. a
perfectly flat cone metric (Θ ≡ 2π).  Under residual cone-angle defect that was
a first-order approximation.

**Fix (2026-05-29):** it now fits the full orientation-preserving deck isometry
`g(z) = a·z + b` to the correspondence `(Bs,Bt) ↦ (As,At)` (`a = (At−As)/(Bt−Bs)`,
`b = As − a·Bs`).  Because both faces use the same edge length, `|a| = 1` exactly,
so the fit is an exact isometry; `ω = b` is its translation part and `|arg(a)|`
is exposed per cut edge as `HolonomyData::residual_rotation` (a convergence
diagnostic).  For a flat cone metric `a = 1` and `b` reduces to `midA − midB`, so
converged flat tori are **bit-for-bit unchanged** — all 240 CGAL tests still pass,
including `HolonomyEndToEnd.{Torus4x4,TorusHex6x6,Torus8x8}`.  Note: when
`|arg(a)|` is non-negligible the holonomy is a genuine rotation and *no* lattice
translation is well-defined; the diagnostic now surfaces that instead of silently
averaging.

---

## Findings in non-ported (conformallab++-only) math modules

These modules have **no Java reference** — they were ported from the literature
(Luo 2004, Glickenstein 2011, Bowers–Stephenson 2004) or are original additions.
They were audited against the cited formulas / first principles rather than Java.

### 9. ✅ FIXED — Inversive-distance gradient now uses limiting angles (consistent with Finding 1)
`inversive_distance_functional.hpp::inversive_distance_gradient` previously detected
a triangle-inequality-violating face two ways and **skipped it** in both:
- `if (l12sq <= 0 || l23sq <= 0 || l31sq <= 0) continue;` (non-real circle config)
- `if (!fa.valid) continue;` (degenerate-but-real triangle)

The second skip was the *same* situation Finding 1 fixed for Euclidean/Spherical:
`euclidean_angles` returns the **limiting angles** (π opposite the over-long edge,
0/0 for the others) with `valid=false`, but this code threw them away.

**Fix:** removed the `if (!fa.valid) continue;` skip so the limiting angles flow
into the gradient (the BPS-style convex C¹ extension), mirroring Finding 1; added a
comment explaining the rationale. The first skip (`l*sq <= 0`, a genuinely non-real
circle configuration with no limiting angle) is **kept**. All 237 tests pass.

### 10. ℹ️ NOTE — Inversive-distance edge length has no overflow centring
`id_detail::edge_length_squared` computes `ℓ² = rᵢ² + rⱼ² + 2·I·rᵢ·rⱼ` with
`r = exp(u)` directly — no log-centring like `euclidean_angles`'s `μ`. The *angle*
computation is still safe (it re-centres internally on `log ℓ²`), but the raw `ℓ²`
can overflow for extreme `|u|`. Cosmetic robustness only; not hit in practice.

### 11. ℹ️ NOTE — Verified correct (no action)
Audited against the literature and found faithful:
- **`trilaterate_2d / _sph / _hyp`** (`layout.hpp`) — circle-circle intersection
  (ℝ²), spherical law of cosines via `α·pa+β·pb+γ·(pa×pb)`, and hyperbolic law of
  cosines + Poincaré-disk Möbius placement. All three exact; left/CCW side correct.
- **`newton_solver.hpp`** — merit `f = ½‖G‖²`; Newton dir is always a descent dir
  (`∇f·dx = −‖G‖²`), Armijo tests algebraically correct in both phases; spherical
  solver factorises `−H` (PSD) and `d_sd = −H·G = −∇f` uses the un-negated H — all
  consistent. SparseQR fallback handles the gauge null space (valid because
  Gauss–Bonnet makes `G ⟂ 𝟙`).
- **`gauss_bonnet.hpp`** — `χ=V−E+F`, `g=(2−χ)/2`, `enforce` shift
  `δ=(lhs−rhs)/V` makes the new sum equal `2π·χ` exactly.
- **Normalisations** — Euclidean PCA, spherical Rodrigues (mean→north),
  hyperbolic weighted Fréchet-mean Möbius centring: all correct (spherical has a
  benign antipodal-mean edge case that no-ops).

---

## Missing test cases (gaps observed during the audit)

1. **Degenerate / flipped-triangle gradient** — no test drives a face through the
   triangle-inequality boundary to lock in Finding 1. Add: build a triangle with
   one edge length > sum of the others, assert the gradient picks up the π corner
   (not 0). Cover both Euclidean and Spherical.

2. **`euclidean_hessian` edge-DOF guard** — no test asserts the new `throw` fires
   when `assign_euclidean_all_dof_indices` is used. Add an `EXPECT_THROW`.

3. ✅ **DONE (2026-05-29)** — Holonomy / period matrix end-to-end now covers
   three tori of revolution (`HolonomyEndToEnd.{Torus4x4,TorusHex6x6,Torus8x8}`),
   each asserting τ against the analytic modulus `i·√(R²−r²)/r` and `Re(τ) ≈ 0`.
   (This also fixed the Euclidean holonomy extraction — develop across the dual
   tree only — see `layout.hpp` / `cut_graph.hpp`.)

4. ✅ **DONE (2026-05-29)** — the Finding-3 spherical edge-DOF gradient is now
   locked at two levels: (a) `EdgeGradient_RegularTetClosedForm` — an independent
   *closed-form* oracle pinning each edge-DOF gradient to `π/3` (= 2·(2π/3) − π)
   on the regular spherical tetrahedron; and (b)
   `SphericalGoldenJava.FullMeshEdgeDofGradient_Tetrahedron` — a *live-Java*
   full-mesh oracle (item 5) on a generic asymmetric configuration that pins
   BOTH the vertex gradient AND the edge gradient `α_opp⁺ + α_opp⁻ − θ_e` to
   1e-12 vs the real raw `conformalEnergyAndGradient`.  The vertex-DOF spherical
   path is likewise locked by `SphericalGoldenJava.FullMeshGradientAndEnergy_…`.
   **Still open (solver feature, not a correctness gap):** Newton-to-convergence
   *with* edge DOFs is blocked by the Finding-4 spherical-Hessian guard (`throw`
   on edge DOFs), so a converged-metric test needs an FD-Hessian or guard
   relaxation first.

5. ✅ **DONE (2026-05-29)** — golden-value oracles now span both levels:
   • *pure-math / per-triangle* (1e-12 vs compiled upstream, openjdk 17):
   `HyperIdealGoldenJava` (Clausen/Л/ImLi₂, ζ₁₃/₁₄/₁₅/ζ, both tetrahedron-volume
   formulas), `EuclideanGoldenJava` (angle formula + 2·Л energy),
   `SphericalGoldenJava` (law-of-cosines angles + β relations + Л energy),
   `PeriodMatrix.NormalizeModulus_GoldenJava` (item 8).
   • *full-mesh energy + gradient at a known `x`* — the originally-fragile part,
   now solved via a shared OBJ tetrahedron loaded identically in both languages:
   `EuclideanGoldenJava.FullMeshGradientAndEnergy_Tetrahedron` and
   `SphericalGoldenJava.FullMeshGradientAndEnergy_Tetrahedron` drive the REAL
   `EuclideanCyclicFunctional` / `SphericalFunctional` and pin every per-vertex
   gradient `Θ−Σα` (matched by vertex position) AND `ΔE = E(x)−E(0)` to 1e-12.
   The ΔE check is doubly independent: C++ computes energy as a Gauss-Legendre
   PATH INTEGRAL of its gradient, Java as a CLOSED-FORM sum — the initialEnergy
   constant and the φ·λ⁰ term cancel in the difference (no edge DOFs here).
   • *full-mesh EDGE-DOF gradient* —
   `SphericalGoldenJava.FullMeshEdgeDofGradient_Tetrahedron` makes two opposite
   tetrahedron edges variable (replacement parameterization λ_e = x[e_idx]) and
   pins BOTH the vertex gradient AND the edge gradient `α_opp⁺+α_opp⁻−θ_e` to
   1e-12 vs the real raw `conformalEnergyAndGradient`, closing the Finding-3
   solution-level gap (item 4).  It is a pure gradient oracle (no ΔE): with an
   edge DOF, x=0 ⇒ λ_e=0 ⇒ arc length π is degenerate, so the path-integral-from-
   origin energy reference is ill-defined; the fixed-x gradient is the
   unambiguous quantity.  It never touches the Hessian, so the Finding-4 edge-DOF
   Hessian guard does not block it.  Harness: `/tmp/oracle/SphereEdgeOracle.java`.
   **Subtlety surfaced:** the spherical oracle must call Java's raw
   `conformalEnergyAndGradient`, not `evaluate()`; `evaluate()` first runs a 1-D
   Brent maximization over the global-scale gauge (`maximizeInNegativeDirection`),
   which the C++ `spherical_gradient` deliberately omits (C++ factors that gauge
   into the Newton solver's `spherical_gauge_shift`).  Harnesses:
   `/tmp/oracle/{tet.obj,EucMeshOracle.java,SphereMeshOracle.java,SphereEdgeOracle.java}`.
   The *edge-DOF* full-mesh gradient oracle is now also done (item 4,
   `SphereEdgeOracle.java`).

6. **CP-Euclidean `clausen` vs `clausen2`** — a single test asserting
   `|clausen(x) − clausen2(x)| < 1e-12` across a sweep would document Finding 5
   and guard against an approximation regression.

7. ⚠️ **PARTLY DONE (2026-05-29)** — the `Re ≥ 0` fold convention is now locked at
   the function level by item 8 (`NormalizeModulus_GoldenJava`, including `Re < 0`
   inputs).  **Still open:** an *end-to-end* torus whose true τ has `Re(τ) < 0`
   before reduction, asserting the production pipeline yields `Re(τ) ≥ 0` (so any
   future switch back to `reduce_to_fundamental_domain` is caught at the mesh
   level, not just the function level).

8. ✅ **DONE (2026-05-29)** — `PeriodMatrix.NormalizeModulus_GoldenJava` pins the
   C++ `normalizeModulus` against five recorded outputs of the real Java
   `DiscreteEllipticUtility.normalizeModulus` (1e-12), including `Re < 0` mirror
   folds, large-`Re` `T`-steps, and `|τ| < 1` `S`-inversions — locking the
   `0 ≤ Re ≤ ½` fold convention.  (Generated via `/tmp/oracle/TauOracle.java`.)
   This also surfaced a real interaction: `compute_period_matrix` can land
   exactly on `Re = +½` (the closed right edge of `normalizeModulus`'s mirror-
   folded domain), which the half-open SL(2,ℤ) `is_in_fundamental_domain`
   (`−½ ≤ Re < ½`, P1-3) correctly *excludes*; `ComputePeriodMatrix_ReducedTau_InFD`
   was updated to assert the `normalizeModulus` domain rather than the SL(2,ℤ) one.

9. **Higher-genus cut graph (Finding 7)** — only genus-1 is exercised end-to-end.
   Add a genus-2 mesh and assert `compute_cut_graph` returns exactly `2g = 4` cut
   edges and `genus == 2`, documenting that the basis is *not* canonical (so no τ
   correctness is claimed there).

10. **Inversive-distance degenerate face (Finding 9)** — no test drives an
    inversive-distance triangle through the triangle-inequality boundary. Now that
    Finding 9 mirrors Finding 1 (limiting angles used, not skipped), add the
    analogue of missing-test item 1: assert the corner opposite the over-long edge
    is picked up as π rather than silently skipped.
