Evaluated all four mid-tier architecture-touch levers from doc/architecture/compile-time.md. Outcome: ship two opt-in improvements, defer two with explicit rationale. #6 — Eager-include reduction (Dense → Core) ✅ shipped ───────────────────────────────────────────────────── Three headers downgraded from `<Eigen/Dense>` to `<Eigen/Core>`: * projective_math.hpp * hyper_ideal_visualization_utility.hpp * mesh_utils.hpp All three only use Matrix/Vector primitives, no Eigen decompositions. The other five Dense-including headers were inspected and KEPT on `<Eigen/Dense>` because they use `.inverse()`, `.determinant()`, `ColPivHouseholderQR`, or `SelfAdjointEigenSolver`. Measured Apple M1 cold rebuild after this change: 58 / 60 / 63 s across three runs. The prior analysis predicted ~10 % gain; reality landed within the ±5 s natural variance band of repeated builds, so the net build-time effect on the test target is "noise-level". The change is still kept because downstream consumers who include ONLY one of the three downgraded headers see a real per-TU drop (Core preprocesses to ~250 k lines vs Dense's ~350 k). #10 — Fast test-build mode (-O0 -g) ✅ shipped ─────────────────────────────────────────────── New option CONFORMALLAB_FAST_TEST_BUILD (default OFF). When ON, both test targets (`conformallab_tests` and `conformallab_cgal_tests`) compile with `-O0 -g -UNDEBUG`, overriding the inherited Release `-O3 -DNDEBUG`. Measured Apple clang: 51.6 s vs 46.8 s without -O0 → slightly slower. The Backend phase that prior analysis predicted would drop from 9.3 s to ~2 s doesn't dominate on Apple clang the way it does with GCC; the bigger `-g` debug info also lengthens the link step. Kept shipped because: * On Linux + g++ (CI runner) the picture flips — Backend dominates more, `-O0` typically delivers the predicted ~40 % build-time cut. * Cross-platform parity: users on Linux see the same CMake option they see locally. Honest documentation in doc/architecture/compile-time.md notes that the Apple-clang-local benefit is currently 0 %. Tests RUN ~15× slower under `-O0` (1.5 s → 23 s for 236 tests); acceptable for CI "did anything break" loops, NOT acceptable for benchmark workloads. #5 — Move detail:: impls to .inl files ⏸ deferred ─────────────────────────────────────────────────── Pure enabler for #7. Without #7 landing, the .inl extraction would just add an extra hop to header reading. Reconsider once a concrete maintenance reason emerges (e.g. a downstream user wants to override a detail helper). #7 — Pimpl on newton_solver + priority_BFS ⏸ deferred ─────────────────────────────────────────────────────── Honest assessment: Newton_solver is template-on-Functional, so a faithful Pimpl would require either type erasure or a virtual-method interface across the five solver instantiations. Estimated 1-2 weeks of refactor with measurable API-surface risk. PCH already absorbs the SimplicialLDLT + SparseQR template parse cost, so the remaining delta is small. Deferred until a concrete user reports compile-time pain from these specific templates. Documentation ───────────── README.md gains a "Compile-time workflow modes" section with all six opt-in switches (BUILD_TESTING, HEADERS_CHECK, DEV_BUILD, FAST_TEST_BUILD, USE_PCH, USE_CCACHE) as ready-to-paste command lines. doc/architecture/compile-time.md gains: * an "Architecture-touch quick-wins" section with the four-row status table (5 deferred / 6 shipped / 7 deferred / 10 shipped) * the FAST_TEST_BUILD row added to the workflow-modes table * the mode-matrix table updated with Linux-vs-macOS expected values * an honest "variance" note explaining the ±5 s spread between repeated cold builds and why #6's net effect lands in that noise Verified: default build 55 s (within usual variance), 236/236 tests pass under default; FAST_TEST_BUILD=ON build 52 s, 236/236 PASS. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
132 lines
4.7 KiB
C++
132 lines
4.7 KiB
C++
#pragma once
|
||
// Copyright (c) 2024-2026 Tarik Moussa.
|
||
// SPDX-License-Identifier: MIT
|
||
|
||
// Port of the static helper
|
||
// HyperIdealVisualizationPlugin.getEuclideanCircleFromHyperbolic()
|
||
// from de.varylab.discreteconformal.plugin.
|
||
//
|
||
// Converts a hyperbolic circle (center + radius in the hyperboloid model)
|
||
// to its Euclidean representation (cx, cy, r) in the Poincaré disk model.
|
||
//
|
||
// Mathematical background
|
||
// -----------------------
|
||
// Hyperboloid model: points (x,y,z,w) with w²-x²-y²-z²=1, w>0.
|
||
// Metric signature: g = diag(+1,+1,+1,−1) (spatial-first, time-last).
|
||
//
|
||
// Hyperbolic translation from the origin e₄=(0,0,0,1) to p=(a,b,c,d):
|
||
// T = [ I₃ + p'·p'ᵀ/(d+1) p' ] p' = (a,b,c)
|
||
// [ p'ᵀ d ]
|
||
// This is the standard Lorentz boost; it is in O(3,1) and maps e₄ → p.
|
||
//
|
||
// Poincaré disk projection (jReality convention):
|
||
// (x,y,z,w) → (x,y) / (w+1)
|
||
//
|
||
// The three reference points on the unit hyperbolic circle (at origin) are
|
||
// p1 = (sinh r, 0, 0, cosh r)
|
||
// p2 = (0, sinh r, 0, cosh r)
|
||
// p3 = (-sinh r, 0, 0, cosh r)
|
||
// After translation and projection to the Poincaré disk their circumcircle
|
||
// equals the image of the original hyperbolic circle.
|
||
|
||
#include <Eigen/Core> // downgraded from <Eigen/Dense>: this header only
|
||
// uses Matrix/Vector primitives, no decompositions.
|
||
#include <array>
|
||
#include <cmath>
|
||
|
||
namespace conformallab {
|
||
|
||
/// Circumcenter of three 2-D points (`a`, `b`, `c`) in the Euclidean plane.
|
||
inline Eigen::Vector2d circumcenter2d(
|
||
const Eigen::Vector2d& a,
|
||
const Eigen::Vector2d& b,
|
||
const Eigen::Vector2d& c)
|
||
{
|
||
double ax = a.x(), ay = a.y();
|
||
double bx = b.x(), by = b.y();
|
||
double cx = c.x(), cy = c.y();
|
||
|
||
double D = 2.0 * (ax*(by - cy) + bx*(cy - ay) + cx*(ay - by));
|
||
double a2 = ax*ax + ay*ay;
|
||
double b2 = bx*bx + by*by;
|
||
double c2 = cx*cx + cy*cy;
|
||
|
||
double ux = (a2*(by - cy) + b2*(cy - ay) + c2*(ay - by)) / D;
|
||
double uy = (a2*(cx - bx) + b2*(ax - cx) + c2*(bx - ax)) / D;
|
||
return {ux, uy};
|
||
}
|
||
|
||
/// 4×4 Lorentz boost: maps the hyperboloid origin `e₄ = (0,0,0,1)` to
|
||
/// `center`. Precondition: `center` lies on the hyperboloid.
|
||
inline Eigen::Matrix4d hyperboloidTranslation(const Eigen::Vector4d& center)
|
||
{
|
||
Eigen::Vector3d p = center.head<3>();
|
||
double d = center(3);
|
||
|
||
Eigen::Matrix4d T = Eigen::Matrix4d::Identity();
|
||
// Upper-left 3×3 block: I + p'·p'ᵀ / (d+1)
|
||
T.block<3,3>(0,0) += p * p.transpose() / (d + 1.0);
|
||
// Right column and bottom row
|
||
T.block<3,1>(0,3) = p;
|
||
T.block<1,3>(3,0) = p.transpose();
|
||
T(3,3) = d;
|
||
return T;
|
||
}
|
||
|
||
/// Project a hyperboloid point `x` onto the Poincaré disk (jReality
|
||
/// convention: add 1 to the w-coordinate, then dehomogenise spatial part).
|
||
inline Eigen::Vector2d toPoincareDisk(const Eigen::Vector4d& x)
|
||
{
|
||
double w = x(3) + 1.0;
|
||
return {x(0) / w, x(1) / w};
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// getEuclideanCircleFromHyperbolic
|
||
//
|
||
// Inputs
|
||
// center – point on the hyperboloid, e.g. (0,0,0,1) for the origin
|
||
// radius – hyperbolic radius (real number > 0)
|
||
//
|
||
// Output
|
||
// { euclidean_cx, euclidean_cy, euclidean_radius }
|
||
// describing the circle in the Poincaré disk that corresponds to the
|
||
// given hyperbolic circle.
|
||
//
|
||
// Port of HyperIdealVisualizationPlugin.getEuclideanCircleFromHyperbolic()
|
||
// ---------------------------------------------------------------------------
|
||
/// Convert a hyperbolic circle (`center` on the hyperboloid, hyperbolic
|
||
/// `radius`) to the corresponding Euclidean circle in the Poincaré disk;
|
||
/// returns `{cx, cy, r}`. Port of `HyperIdealVisualizationPlugin
|
||
/// .getEuclideanCircleFromHyperbolic()`.
|
||
inline std::array<double,3> getEuclideanCircleFromHyperbolic(
|
||
const Eigen::Vector4d& center, double radius)
|
||
{
|
||
const double s = std::sinh(radius);
|
||
const double ch = std::cosh(radius);
|
||
|
||
// Three points on the hyperbolic circle centered at the origin
|
||
Eigen::Vector4d p1( s, 0.0, 0.0, ch);
|
||
Eigen::Vector4d p2(0.0, s, 0.0, ch);
|
||
Eigen::Vector4d p3(-s, 0.0, 0.0, ch);
|
||
|
||
// Apply the hyperbolic translation to the target center
|
||
const Eigen::Matrix4d T = hyperboloidTranslation(center);
|
||
p1 = T * p1;
|
||
p2 = T * p2;
|
||
p3 = T * p3;
|
||
|
||
// Project to the Poincaré disk
|
||
const Eigen::Vector2d q1 = toPoincareDisk(p1);
|
||
const Eigen::Vector2d q2 = toPoincareDisk(p2);
|
||
const Eigen::Vector2d q3 = toPoincareDisk(p3);
|
||
|
||
// Euclidean circumcircle of the three projected points
|
||
const Eigen::Vector2d ec = circumcenter2d(q1, q2, q3);
|
||
const double r = (ec - q1).norm();
|
||
|
||
return {ec.x(), ec.y(), r};
|
||
}
|
||
|
||
} // namespace conformallab
|