// Copyright (c) 2024-2026 Tarik Moussa. // SPDX-License-Identifier: MIT // test_phase7.cpp // // Phase 7 — Tests for Java-parity layout features: // - MobiusMap : identity, inverse, compose, from_three, is_identity // - best_root_face : selects a valid face; interior bonus // - halfedge_uv : size, non-seam consistency, seam divergence // - Priority BFS : vertex ordering / depth correctness // - normalise_euclidean : halfedge_uv centroid at origin // - period_matrix.hpp : τ in upper half-plane, SL(2,ℤ) reduction // - fundamental_domain.hpp: parallelogram CCW, generators, tiling_copy #include "conformal_mesh.hpp" #include "mesh_builder.hpp" #include "euclidean_functional.hpp" #include "hyper_ideal_functional.hpp" #include "newton_solver.hpp" #include "layout.hpp" #include "period_matrix.hpp" #include "fundamental_domain.hpp" #include "mesh_io.hpp" #include "cut_graph.hpp" #include "gauss_bonnet.hpp" #include #include #include #include #include using namespace conformallab; using C = std::complex; // ════════════════════════════════════════════════════════════════════════════ // MobiusMap // ════════════════════════════════════════════════════════════════════════════ TEST(MobiusMap, Identity_AppliesAsIdentity) { MobiusMap id = MobiusMap::identity(); C z(0.3, 0.7); C w = id.apply(z); EXPECT_NEAR(w.real(), z.real(), 1e-12); EXPECT_NEAR(w.imag(), z.imag(), 1e-12); } TEST(MobiusMap, Identity_IsIdentity) { EXPECT_TRUE(MobiusMap::identity().is_identity()); } TEST(MobiusMap, NonIdentity_IsNotIdentity) { // T(z) = z + 1 — translation, clearly not identity MobiusMap T{ C(1), C(1), C(0), C(1) }; EXPECT_FALSE(T.is_identity()); } TEST(MobiusMap, Inverse_ComposeIsIdentity) { // T(z) = (2z + 1) / (z + 3) MobiusMap T{ C(2), C(1), C(1), C(3) }; MobiusMap TinvT = T.inverse().compose(T); EXPECT_TRUE(TinvT.is_identity(1e-9)); } TEST(MobiusMap, Compose_OrderCorrect) { // S: z ↦ z + 1, T: z ↦ 2z // S.compose(T) means S applied after T: z ↦ 2z + 1 MobiusMap S{ C(1), C(1), C(0), C(1) }; // z + 1 MobiusMap T{ C(2), C(0), C(0), C(1) }; // 2z MobiusMap ST = S.compose(T); C z(1.0, 0.0); // S(T(z)) = S(2) = 3 EXPECT_NEAR(ST.apply(z).real(), 3.0, 1e-12); EXPECT_NEAR(ST.apply(z).imag(), 0.0, 1e-12); } TEST(MobiusMap, FromThree_RecoversMap) { // Known map T(z) = (z + i) / (1 + 0·z) — translation by i C w1 = C(0, 1) + C(0, 1); // T(i) = 2i C w2 = C(1, 0) + C(0, 1); // T(1) = 1 + i C w3 = C(-1, 0) + C(0, 1); // T(-1) = -1 + i MobiusMap T = MobiusMap::from_three(C(0, 1), w1, C(1, 0), w2, C(-1, 0), w3); // Verify T maps a fourth point correctly: T(0) = i C result = T.apply(C(0, 0)); EXPECT_NEAR(result.real(), 0.0, 1e-9); EXPECT_NEAR(result.imag(), 1.0, 1e-9); } TEST(MobiusMap, FromThree_DegenerateReturnsIdentity) { // Three coincident points → singular system → identity fallback C z(0.5, 0.5); MobiusMap T = MobiusMap::from_three(z, z, z, z, z, z); // Should not crash; returns identity (or at least a valid map) // We just check the result is finite C w = T.apply(C(0.1, 0.2)); EXPECT_FALSE(std::isnan(w.real())); EXPECT_FALSE(std::isnan(w.imag())); } TEST(MobiusMap, Apply_Vector2d) { MobiusMap id = MobiusMap::identity(); Eigen::Vector2d p(0.4, 0.6); Eigen::Vector2d q = id.apply(p); EXPECT_NEAR(q.x(), p.x(), 1e-12); EXPECT_NEAR(q.y(), p.y(), 1e-12); } // ════════════════════════════════════════════════════════════════════════════ // best_root_face // ════════════════════════════════════════════════════════════════════════════ TEST(BestRootFace, ReturnsValidFace_Triangle) { auto mesh = make_triangle(); Face_index f = detail::best_root_face(mesh); EXPECT_NE(f, Face_index()); EXPECT_GE(f.idx(), 0); } TEST(BestRootFace, ReturnsValidFace_Tetrahedron) { auto mesh = make_tetrahedron(); Face_index f = detail::best_root_face(mesh); EXPECT_NE(f, Face_index()); // Tetrahedron has 4 faces — best is one of them EXPECT_LT(static_cast(f.idx()), mesh.number_of_faces()); } // ════════════════════════════════════════════════════════════════════════════ // halfedge_uv — size and non-seam consistency // ════════════════════════════════════════════════════════════════════════════ // Helper: build equilibrium Euclidean layout for a given mesh. // Uses x = 0 (identity scale factor) which is the equilibrium for natural edge lengths. static Layout2D make_euclidean_layout(ConformalMesh& mesh) { EuclideanMaps maps = setup_euclidean_maps(mesh); compute_euclidean_lambda0_from_mesh(mesh, maps); // Pin first vertex (DOF = -1); assign sequential indices to the rest. auto vit = mesh.vertices().begin(); maps.v_idx[*vit++] = -1; int idx = 0; for (; vit != mesh.vertices().end(); ++vit) maps.v_idx[*vit] = idx++; std::vector x(static_cast(idx), 0.0); return euclidean_layout(mesh, x, maps); } TEST(HalfedgeUV, Size_EqualsNumberOfHalfedges_Triangle) { auto mesh = make_triangle(); auto lay = make_euclidean_layout(mesh); EXPECT_EQ(lay.halfedge_uv.size(), mesh.number_of_halfedges()); } TEST(HalfedgeUV, Size_EqualsNumberOfHalfedges_QuadStrip) { auto mesh = make_quad_strip(); auto lay = make_euclidean_layout(mesh); EXPECT_EQ(lay.halfedge_uv.size(), mesh.number_of_halfedges()); } TEST(HalfedgeUV, NonBorderHalfedges_MatchUV) { // For an open mesh with no cut graph the layout has no seams. // Every non-border halfedge h must satisfy: // halfedge_uv[h] == uv[source(h)] auto mesh = make_quad_strip(); auto lay = make_euclidean_layout(mesh); for (auto h : mesh.halfedges()) { if (mesh.is_border(h)) continue; std::size_t hi = static_cast(h.idx()); std::size_t vi = static_cast(mesh.source(h).idx()); EXPECT_NEAR(lay.halfedge_uv[hi].x(), lay.uv[vi].x(), 1e-10) << "halfedge " << hi << " source vertex " << vi; EXPECT_NEAR(lay.halfedge_uv[hi].y(), lay.uv[vi].y(), 1e-10) << "halfedge " << hi << " source vertex " << vi; } } TEST(HalfedgeUV, BorderHalfedges_AreZero) { auto mesh = make_triangle(); auto lay = make_euclidean_layout(mesh); bool found_border = false; for (auto h : mesh.halfedges()) { if (!mesh.is_border(h)) continue; std::size_t hi = static_cast(h.idx()); EXPECT_NEAR(lay.halfedge_uv[hi].x(), 0.0, 1e-12); EXPECT_NEAR(lay.halfedge_uv[hi].y(), 0.0, 1e-12); found_border = true; } EXPECT_TRUE(found_border); } // ════════════════════════════════════════════════════════════════════════════ // Priority BFS — depth ordering // ════════════════════════════════════════════════════════════════════════════ TEST(PriorityBFS, Layout_SucceedsOnOpenMesh) { auto mesh = make_quad_strip(); auto lay = make_euclidean_layout(mesh); EXPECT_TRUE(lay.success); } TEST(PriorityBFS, Layout_NoSeamOnOpenMesh) { auto mesh = make_quad_strip(); auto lay = make_euclidean_layout(mesh); EXPECT_FALSE(lay.has_seam); } TEST(PriorityBFS, AllVerticesPlaced) { auto mesh = make_tetrahedron(); // Tetrahedron is closed; layout without cut graph will have a seam EuclideanMaps 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++; std::vector x(static_cast(idx), 0.0); auto lay = euclidean_layout(mesh, x, maps); EXPECT_TRUE(lay.success); // All UVs must be finite for (auto& p : lay.uv) { EXPECT_FALSE(std::isnan(p.x())); EXPECT_FALSE(std::isnan(p.y())); } } // ════════════════════════════════════════════════════════════════════════════ // normalise_euclidean — centroid + PCA applied to both uv and halfedge_uv // ════════════════════════════════════════════════════════════════════════════ TEST(NormaliseEuclidean, UVCentroidAtOrigin) { auto mesh = make_quad_strip(); auto lay = make_euclidean_layout(mesh); normalise_euclidean(lay); Eigen::Vector2d mean = Eigen::Vector2d::Zero(); for (auto& p : lay.uv) mean += p; mean /= static_cast(lay.uv.size()); EXPECT_NEAR(mean.x(), 0.0, 1e-10); EXPECT_NEAR(mean.y(), 0.0, 1e-10); } TEST(NormaliseEuclidean, HalfedgeUVCentroidAlsoShifted) { // After normalisation: the non-border halfedge_uv entries should also be // centred (since they are shifted by the same mean as uv). // We verify that the mean of non-border halfedge_uv is near (0,0). auto mesh = make_quad_strip(); auto lay = make_euclidean_layout(mesh); normalise_euclidean(lay); Eigen::Vector2d mean = Eigen::Vector2d::Zero(); int count = 0; for (auto h : mesh.halfedges()) { if (mesh.is_border(h)) continue; mean += lay.halfedge_uv[static_cast(h.idx())]; ++count; } if (count > 0) mean /= static_cast(count); EXPECT_NEAR(mean.x(), 0.0, 1e-9); EXPECT_NEAR(mean.y(), 0.0, 1e-9); } // ════════════════════════════════════════════════════════════════════════════ // PeriodMatrix — reduce_to_fundamental_domain // ════════════════════════════════════════════════════════════════════════════ TEST(PeriodMatrix, ReduceToFD_AlreadyInFD) { // τ = i is in F (|i|=1, Re(i)=0, Im(i)=1>0) C tau(0.0, 1.0); C reduced = reduce_to_fundamental_domain(tau); EXPECT_TRUE(is_in_fundamental_domain(reduced)); EXPECT_NEAR(reduced.real(), 0.0, 1e-10); EXPECT_NEAR(reduced.imag(), 1.0, 1e-10); } TEST(PeriodMatrix, ReduceToFD_ShiftsRealPart) { // τ = 2 + 3i → T step: τ -= 2 → 3i (|3i|=3≥1, Re=0) C tau(2.0, 3.0); C reduced = reduce_to_fundamental_domain(tau); EXPECT_TRUE(is_in_fundamental_domain(reduced, 1e-9)); EXPECT_NEAR(reduced.real(), 0.0, 1e-10); EXPECT_NEAR(reduced.imag(), 3.0, 1e-10); } TEST(PeriodMatrix, ReduceToFD_InvertsSmallTau) { // τ = 0.5i → |0.5i|=0.5<1 → S: τ↦-1/(0.5i) = 2i C tau(0.0, 0.5); C reduced = reduce_to_fundamental_domain(tau); EXPECT_TRUE(is_in_fundamental_domain(reduced, 1e-9)); EXPECT_NEAR(reduced.real(), 0.0, 1e-10); EXPECT_NEAR(reduced.imag(), 2.0, 1e-10); } TEST(PeriodMatrix, ReduceToFD_ThrowsForNonUpperHalfPlane) { C tau(0.5, -1.0); // Im < 0 → not in upper half-plane 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 EXPECT_TRUE(is_in_fundamental_domain(C(0.3, 1.5))); // inside EXPECT_FALSE(is_in_fundamental_domain(C(0.6, 1.5))); // Re > 1/2 EXPECT_FALSE(is_in_fundamental_domain(C(0.0, 0.5))); // |τ| < 1 } TEST(PeriodMatrix, ComputePeriodMatrix_UnitSquare) { // ω_1 = (1, 0), ω_2 = (0, 1) → τ = i HolonomyData hol; hol.translations = { Eigen::Vector2d(1.0, 0.0), Eigen::Vector2d(0.0, 1.0) }; PeriodData pd = compute_period_matrix(hol, /*reduce=*/false); EXPECT_EQ(pd.genus(), 1); EXPECT_GT(pd.tau.imag(), 0.0); EXPECT_NEAR(pd.tau.real(), 0.0, 1e-10); EXPECT_NEAR(pd.tau.imag(), 1.0, 1e-10); } TEST(PeriodMatrix, ComputePeriodMatrix_ReducedTau_InFD) { // ω_1 = (1, 0), ω_2 = (0.5, 0.25) → τ = 0.5 + 0.25i // |τ| = sqrt(0.25 + 0.0625) ≈ 0.559 < 1 → needs S step. // compute_period_matrix reduces with normalizeModulus (Finding 6), whose // mirror-folded target domain is { 0 ≤ Re ≤ ½, Im > 0, |τ| ≥ 1 } — note the // RIGHT boundary Re = +½ is CLOSED here. This is NOT the half-open SL(2,ℤ) // domain of is_in_fundamental_domain (−½ ≤ Re < ½), which excludes Re = +½ // because +½ ≡ −½ under T. For this input normalizeModulus lands exactly on // τ = ½ + i, so we must check the normalizeModulus domain, not the SL(2,ℤ) // one (asserting is_in_fundamental_domain here would wrongly fail on +½). HolonomyData hol; hol.translations = { Eigen::Vector2d(1.0, 0.0), Eigen::Vector2d(0.5, 0.25) }; PeriodData pd = compute_period_matrix(hol, /*reduce=*/true); EXPECT_TRUE(pd.in_fundamental_domain); const double tol = 1e-9; EXPECT_GT(pd.tau.imag(), 0.0); EXPECT_GE(pd.tau.real(), 0.0 - tol); // 0 ≤ Re (mirror fold) EXPECT_LE(pd.tau.real(), 0.5 + tol); // Re ≤ ½ (closed right edge) EXPECT_GE(std::abs(pd.tau), 1.0 - tol); // |τ| ≥ 1 // Concretely: τ = ½ + i. EXPECT_NEAR(pd.tau.real(), 0.5, 1e-12); EXPECT_NEAR(pd.tau.imag(), 1.0, 1e-12); } // ───────────────────────────────────────────────────────────────────────────── // Golden-value oracle — pin normalizeModulus (the τ reduction used by // compute_period_matrix, Finding 6) bit-for-bit against the upstream Java // reference (de.varylab.discreteconformal.util.DiscreteEllipticUtility. // normalizeModulus), captured by calling the compiled Java method (openjdk 17) // on these exact τ. This locks the sign/fold conventions of the SL(2,ℤ)+mirror // reduction (0 ≤ Re ≤ ½, Im ≥ 0, |τ| ≥ 1) against an independent implementation, // catching drift the existing in-FD membership checks cannot (they only assert // the result lies in F, not that it is the SAME representative Java picks). // // To regenerate: /tmp/oracle/TauOracle.java. Values are Java printf %.17g. // ───────────────────────────────────────────────────────────────────────────── TEST(PeriodMatrix, NormalizeModulus_GoldenJava) { auto chk = [](double re, double im, double re_g, double im_g) { C n = conformallab::normalizeModulus(C(re, im)); EXPECT_NEAR(n.real(), re_g, 1e-12); EXPECT_NEAR(n.imag(), im_g, 1e-12); }; chk(0.3, 0.5, 0.11764705882352933, 1.4705882352941178); // |τ|<1 → S + folds chk(-0.4, 1.3, 0.40000000000000000, 1.3000000000000000); // Re<0 mirror fold chk(2.7, 0.8, 0.41095890410958880, 1.0958904109589043); // large Re → T chk(0.1, 2.0, 0.10000000000000000, 2.0000000000000000); // already in F chk(-1.6, 0.9, 0.41237113402061850, 0.92783505154639180); // T + S + mirror } // ════════════════════════════════════════════════════════════════════════════ // End-to-end holonomy → τ on real genus-1 torus meshes // // Regression test for the holonomy-extraction bug: euclidean_holonomy() developed // the cut surface along a BFS dual tree that crossed the primal-tree edges freely. // Relative to that tree the cut graph's 2g generator edges were NOT generators — // some were null-homotopic — so the two developed copies of a "cut" edge landed // on top of each other and compute_period_matrix() got ω ≈ 0 (→ τ = 0 / NaN / // huge). The fix develops across the cut graph's OWN dual spanning tree T* only // (CutGraph::is_dual_tree), unfolding the surface onto a true disk so the cut // edges become the boundary identifications that carry the lattice generators. // // Analytic target. The bundled meshes are tori of REVOLUTION (major radius R, // minor radius r, R > r), not abstract square/hexagonal flat tori. Their // conformal modulus is purely imaginary, // // τ = i · √(R² − r²) / r (reduced so |τ| ≥ 1) // // derived from the flat-conformal change of variable dψ = r/(R + r cos φ) dφ on // the induced metric ds² = (R + r cos φ)² dθ² + r² dφ²; the ψ-period is // 2πr/√(R²−r²), giving the rectangular lattice ratio above. Re(τ) = 0 follows // from the meridian ⟂ longitude reflection symmetry. The coarse polygonal cross // sections (square/hex/octagon) approximate the circular value from above; the // gap shrinks as the cross section gains sides. // ════════════════════════════════════════════════════════════════════════════ namespace { // Run the full pipeline solve → cut → layout → period matrix on a torus mesh and // return the reduced τ together with the two raw holonomy generators. struct TorusTau { std::complex tau; std::vector omega; bool converged = false; }; TorusTau run_torus_pipeline(const std::string& file) { const std::string path = std::string(CONFORMALLAB_DATA_DIR) + "/off/" + file; ConformalMesh mesh = load_mesh(path); EuclideanMaps maps = setup_euclidean_maps(mesh); // Θ_v = 2π (flat target) compute_euclidean_lambda0_from_mesh(mesh, maps); int idx = 0; bool pinned = false; for (auto v : mesh.vertices()) { if (!pinned) { maps.v_idx[v] = -1; pinned = true; } else maps.v_idx[v] = idx++; } enforce_gauss_bonnet(mesh, maps); std::vector x0(static_cast(idx), 0.0); auto res = newton_euclidean(mesh, x0, maps); CutGraph cg = compute_cut_graph(mesh); HolonomyData hol; euclidean_layout(mesh, res.x, maps, &cg, &hol, /*normalise=*/false); PeriodData pd = compute_period_matrix(hol, /*reduce=*/true); return TorusTau{pd.tau, hol.translations, res.converged}; } // Reduced conformal modulus of a torus of revolution (major R, minor r). double revolution_tau_imag(double R, double r) { return std::sqrt(R * R - r * r) / r; // ≥ 1 form (|τ| ≥ 1) } void check_torus(const std::string& file, double R, double r, double rel_tol) { TorusTau t = run_torus_pipeline(file); ASSERT_TRUE(t.converged) << file << ": Newton did not converge"; // Generators must be non-degenerate (the bug collapsed them to ~0). ASSERT_EQ(t.omega.size(), 2u); EXPECT_GT(t.omega[0].norm(), 1e-3) << file << ": ω₁ degenerate"; EXPECT_GT(t.omega[1].norm(), 1e-3) << file << ": ω₂ degenerate"; EXPECT_TRUE(std::isfinite(t.tau.real()) && std::isfinite(t.tau.imag())) << file << ": τ is not finite (" << t.tau.real() << "+" << t.tau.imag() << "i)"; EXPECT_GT(t.tau.imag(), 0.0) << file << ": τ must lie in the upper half-plane"; EXPECT_TRUE(is_in_fundamental_domain(t.tau, 1e-6)) << file << ": τ = " << t.tau.real() << "+" << t.tau.imag() << "i not in F"; // Re(τ) = 0 by the meridian ⟂ longitude reflection symmetry. EXPECT_NEAR(t.tau.real(), 0.0, 0.05) << file << ": Re(τ) should vanish for a torus of revolution"; const double expected = revolution_tau_imag(R, r); EXPECT_NEAR(t.tau.imag(), expected, rel_tol * expected) << file << ": Im(τ) = " << t.tau.imag() << " vs analytic i·√(R²−r²)/r = " << expected; } } // namespace // 4×4 torus of revolution: R = 2, r = 1 → τ = i√3 ≈ 1.732i. // Square (4-gon) cross section → coarsest circle approximation, looser tolerance. TEST(HolonomyEndToEnd, Torus4x4_TauMatchesRevolutionModulus) { check_torus("torus_4x4.off", /*R=*/2.0, /*r=*/1.0, /*rel_tol=*/0.10); } // Hexagonal 6×6 torus of revolution: R = 3, r = 1 → τ = i√8 ≈ 2.828i. TEST(HolonomyEndToEnd, TorusHex6x6_TauMatchesRevolutionModulus) { check_torus("torus_hex_6x6.off", /*R=*/3.0, /*r=*/1.0, /*rel_tol=*/0.05); } // Octagonal 8×8 torus of revolution: R = 3, r = 1 → τ = i√8 ≈ 2.828i. TEST(HolonomyEndToEnd, Torus8x8_TauMatchesRevolutionModulus) { check_torus("torus_8x8.off", /*R=*/3.0, /*r=*/1.0, /*rel_tol=*/0.05); } // ════════════════════════════════════════════════════════════════════════════ // Finding-H (java-port-audit item 7, external-audit-2026-05-30): // End-to-end torus with Re(τ) < 0 before normalizeModulus // // torus_skewed_4x4.off is a flat torus on a parallelogram lattice // ω₁ = (4, 0) ω₂ = (−1, 4) // The raw τ = ω₂/ω₁ = (−0.25 + i), Re < 0. // After normalizeModulus the mirror fold gives τ = (0.25 + i), Re ≥ 0. // // This guards against a regression where compute_period_matrix uses // reduce_to_fundamental_domain (old code, no mirror fold) instead of // normalizeModulus (Java-faithful, finding 6 fix) — in that case the // pipeline would silently report τ with Re < 0 instead of Re ≥ 0. // ════════════════════════════════════════════════════════════════════════════ TEST(HolonomyEndToEnd, SkewedTorus_ReTauNegativeBeforeNorm_FoldedToPositive) { // ── Load the skewed flat torus ──────────────────────────────────────── const std::string path = std::string(CONFORMALLAB_DATA_DIR) + "/off/torus_skewed_4x4.off"; ConformalMesh mesh = load_mesh(path); ASSERT_GT(mesh.number_of_vertices(), 0u) << "Failed to load torus_skewed_4x4.off"; ASSERT_EQ(conformallab::euler_characteristic(mesh), 0) << "Mesh must be a torus (χ=0)"; // ── Run the full pipeline ───────────────────────────────────────────── EuclideanMaps maps = setup_euclidean_maps(mesh); compute_euclidean_lambda0_from_mesh(mesh, maps); int idx = 0; bool pinned = false; for (auto v : mesh.vertices()) { if (!pinned) { maps.v_idx[v] = -1; pinned = true; } else maps.v_idx[v] = idx++; } enforce_gauss_bonnet(mesh, maps); std::vector x0(static_cast(idx), 0.0); auto res = newton_euclidean(mesh, x0, maps); ASSERT_TRUE(res.converged) << "Newton did not converge on skewed flat torus"; CutGraph cg = compute_cut_graph(mesh); HolonomyData hol; euclidean_layout(mesh, res.x, maps, &cg, &hol, /*normalise=*/false); ASSERT_EQ(hol.translations.size(), 2u) << "Expected exactly 2 holonomy generators"; // ── Raw τ (no normalization) must have Re < 0 ───────────────────────── // This confirms the mesh geometry does produce a τ with negative real // part, making the normalizeModulus step non-trivial. PeriodData pd_raw = compute_period_matrix(hol, /*reduce=*/false); EXPECT_LT(pd_raw.tau.real(), 0.0) << "Raw τ must have Re < 0 for this skewed lattice" << " (got Re = " << pd_raw.tau.real() << ")"; // ── Normalized τ must have Re ≥ 0 (normalizeModulus was applied) ───── PeriodData pd = compute_period_matrix(hol, /*reduce=*/true); EXPECT_GE(pd.tau.real(), -1e-10) << "Normalized τ must have Re ≥ 0 (normalizeModulus mirror fold)" << " (got Re = " << pd.tau.real() << ")"; EXPECT_GT(pd.tau.imag(), 0.0) << "τ must lie in the upper half-plane"; EXPECT_GE(std::abs(pd.tau), 1.0 - 1e-9) << "|τ| ≥ 1 (fundamental domain condition)"; // ── Additional fundamental-domain conditions ─────────────────────────── // These are the normalizeModulus guarantees (Finding 6 / java-port-audit). EXPECT_LE(pd.tau.real(), 0.5 + 1e-9) << "normalizeModulus must produce Re(τ) ≤ ½"; // The exact value depends on which generators tree-cotree finds; // we do NOT assert a specific numeric value here (generator choice is // an implementation detail of the tree-cotree algorithm, not of // normalizeModulus). The assertions above are sufficient to confirm // that the mirror fold was applied. } // ════════════════════════════════════════════════════════════════════════════ // Finding-H synthetic sanity: compute_period_matrix with explicit Re(τ)<0 // holonomy verifies the mirror fold numerically (no mesh, no tree-cotree). // ════════════════════════════════════════════════════════════════════════════ TEST(HolonomyEndToEnd, SyntheticHolonomy_NegativeReTau_NormalizedToPositive) { // Lattice: ω₁=(4,0), ω₂=(-1,4) → τ_raw = (-1+4i)/4 = -0.25+i // normalizeModulus: Re=-0.25 < 0 → mirror: τ = -conj(τ) = +0.25+i HolonomyData hol; hol.translations = { Eigen::Vector2d(4.0, 0.0), Eigen::Vector2d(-1.0, 4.0) }; PeriodData pd_raw = compute_period_matrix(hol, /*reduce=*/false); EXPECT_NEAR(pd_raw.tau.real(), -0.25, 1e-10) << "Raw Re(τ) must be -0.25"; EXPECT_NEAR(pd_raw.tau.imag(), 1.0, 1e-10) << "Raw Im(τ) must be 1.0"; PeriodData pd = compute_period_matrix(hol, /*reduce=*/true); EXPECT_GE(pd.tau.real(), 0.0 - 1e-9) << "Normalized Re(τ) ≥ 0"; EXPECT_LE(pd.tau.real(), 0.5 + 1e-9) << "Normalized Re(τ) ≤ ½"; EXPECT_NEAR(pd.tau.real(), 0.25, 1e-9) << "Mirror fold: Re = -0.25 → +0.25"; EXPECT_NEAR(pd.tau.imag(), 1.0, 1e-9) << "Im(τ) preserved by mirror fold"; EXPECT_GE(std::abs(pd.tau), 1.0 - 1e-9) << "|τ| ≥ 1"; } // ════════════════════════════════════════════════════════════════════════════ // FundamentalDomain — genus-1 parallelogram // ════════════════════════════════════════════════════════════════════════════ TEST(FundamentalDomain, Genus1_HasFourVertices) { HolonomyData hol; hol.translations = { Eigen::Vector2d(1.0, 0.0), Eigen::Vector2d(0.0, 1.0) }; FundamentalDomain fd = compute_fundamental_domain_genus1(hol); EXPECT_EQ(fd.vertices.size(), 4u); EXPECT_TRUE(fd.is_valid()); } TEST(FundamentalDomain, Genus1_VerticesMatchGenerators_UnitSquare) { Eigen::Vector2d w1(1.0, 0.0), w2(0.0, 1.0); HolonomyData hol; hol.translations = { w1, w2 }; FundamentalDomain fd = compute_fundamental_domain_genus1(hol); // Expected (CCW): origin, w1, w1+w2, w2 EXPECT_NEAR(fd.vertices[0].x(), 0.0, 1e-12); EXPECT_NEAR(fd.vertices[0].y(), 0.0, 1e-12); EXPECT_NEAR(fd.vertices[1].x(), w1.x(), 1e-12); EXPECT_NEAR(fd.vertices[1].y(), w1.y(), 1e-12); EXPECT_NEAR(fd.vertices[2].x(), (w1 + w2).x(), 1e-12); EXPECT_NEAR(fd.vertices[2].y(), (w1 + w2).y(), 1e-12); EXPECT_NEAR(fd.vertices[3].x(), w2.x(), 1e-12); EXPECT_NEAR(fd.vertices[3].y(), w2.y(), 1e-12); } TEST(FundamentalDomain, Genus1_CCWOrientation) { // After possible swap, the signed area = cross(v1-v0, v3-v0) > 0 (CCW) HolonomyData hol; hol.translations = { Eigen::Vector2d(1.0, 0.0), Eigen::Vector2d(0.0, 1.0) }; FundamentalDomain fd = compute_fundamental_domain_genus1(hol); Eigen::Vector2d v0 = fd.vertices[0], v1 = fd.vertices[1], v3 = fd.vertices[3]; double cross = (v1 - v0).x() * (v3 - v0).y() - (v1 - v0).y() * (v3 - v0).x(); EXPECT_GT(cross, 0.0); } TEST(FundamentalDomain, Genus1_CCWEnforced_WhenInputIsCW) { // If we give CW generators (w2 × w1 < 0), the polygon must still be CCW. // w1 = (0,1), w2 = (1,0): cross w1×w2 = 0*0 - 1*1 = -1 < 0 → should swap HolonomyData hol; hol.translations = { Eigen::Vector2d(0.0, 1.0), Eigen::Vector2d(1.0, 0.0) }; FundamentalDomain fd = compute_fundamental_domain_genus1(hol); Eigen::Vector2d v0 = fd.vertices[0], v1 = fd.vertices[1], v3 = fd.vertices[3]; double cross = (v1 - v0).x() * (v3 - v0).y() - (v1 - v0).y() * (v3 - v0).x(); EXPECT_GT(cross, 0.0); } TEST(FundamentalDomain, Genus1_EdgeIdentifications) { HolonomyData hol; hol.translations = { Eigen::Vector2d(1.0, 0.0), Eigen::Vector2d(0.0, 1.0) }; FundamentalDomain fd = compute_fundamental_domain_genus1(hol); EXPECT_EQ(fd.edge_identifications.size(), 2u); // bottom ≡ top: (0,2) EXPECT_EQ(fd.edge_identifications[0].first, 0); EXPECT_EQ(fd.edge_identifications[0].second, 2); // right ≡ left: (1,3) EXPECT_EQ(fd.edge_identifications[1].first, 1); EXPECT_EQ(fd.edge_identifications[1].second, 3); } TEST(FundamentalDomain, Genus1_GeneratorsStored) { Eigen::Vector2d w1(2.0, 1.0), w2(-1.0, 3.0); HolonomyData hol; hol.translations = { w1, w2 }; FundamentalDomain fd = compute_fundamental_domain_genus1(hol); EXPECT_EQ(fd.generators.size(), 2u); // Generators are w1 and w2 (possibly swapped to ensure CCW) // Their sum of norms matches the originals double norm_gen = fd.generators[0].norm() + fd.generators[1].norm(); double norm_in = w1.norm() + w2.norm(); EXPECT_NEAR(norm_gen, norm_in, 1e-10); } TEST(FundamentalDomain, HigherGenus_ReturnsEmpty) { HolonomyData hol; hol.translations = { Eigen::Vector2d(1, 0), Eigen::Vector2d(0, 1), Eigen::Vector2d(2, 0), Eigen::Vector2d(0, 2) // g=2, 4 generators }; FundamentalDomain fd = compute_fundamental_domain(hol); // g > 1 returns empty (TODO Phase 8) EXPECT_FALSE(fd.is_valid()); } // ════════════════════════════════════════════════════════════════════════════ // tiling_copy / tiling_neighbourhood // ════════════════════════════════════════════════════════════════════════════ TEST(TilingCopy, ShiftAppliedToAllUV) { auto mesh = make_quad_strip(); auto lay = make_euclidean_layout(mesh); Eigen::Vector2d w1(3.0, 0.0), w2(0.0, 2.0); // m=1, n=2 → expected shift = w1 + 2*w2 = (3, 4) Layout2D copy = tiling_copy(lay, w1, w2, 1, 2); Eigen::Vector2d expected_shift(3.0, 4.0); for (std::size_t i = 0; i < lay.uv.size(); ++i) { EXPECT_NEAR(copy.uv[i].x(), lay.uv[i].x() + expected_shift.x(), 1e-12); EXPECT_NEAR(copy.uv[i].y(), lay.uv[i].y() + expected_shift.y(), 1e-12); } } TEST(TilingCopy, ZeroShift_IsSameAsCopy) { auto mesh = make_quad_strip(); auto lay = make_euclidean_layout(mesh); Eigen::Vector2d w1(1, 0), w2(0, 1); Layout2D copy = tiling_copy(lay, w1, w2, 0, 0); for (std::size_t i = 0; i < lay.uv.size(); ++i) { EXPECT_NEAR(copy.uv[i].x(), lay.uv[i].x(), 1e-12); EXPECT_NEAR(copy.uv[i].y(), lay.uv[i].y(), 1e-12); } } TEST(TilingNeighbourhood, CorrectCount) { auto mesh = make_quad_strip(); auto lay = make_euclidean_layout(mesh); HolonomyData hol; hol.translations = { Eigen::Vector2d(1, 0), Eigen::Vector2d(0, 1) }; // m_max=1, n_max=1 → (2*1+1) * (2*1+1) = 9 tiles auto tiles = tiling_neighbourhood(lay, hol, 1, 1); EXPECT_EQ(tiles.size(), 9u); } TEST(TilingNeighbourhood, EmptyHolonomy_ReturnsSingleTile) { auto mesh = make_quad_strip(); auto lay = make_euclidean_layout(mesh); HolonomyData hol; // no translations auto tiles = tiling_neighbourhood(lay, hol); EXPECT_EQ(tiles.size(), 1u); }