low-vertex-flat-tori
View on GitHubNumerically discovering low complexity flat tori in 3 space, following Rich Schwartz's vertex-minimal construction.
with Fabian Lander
- Built with TypeScript / three.js
- Status Active
Readme
Numerically discovering flat, embedded 8-vertex tori, following Rich Schwartz’s
vertex-minimal construction (rich/).
A flat torus is intrinsically a flat sheet ℝ²/Λ. The challenge is realizing one with only 8 vertices as a straight-edge polyhedron in ℝ³ that is both flat (every vertex has cone angle exactly 2π) and embedded (no two triangles cross). There are 7 combinatorial types of 8-vertex torus triangulation (each V=8, E=24, F=16); only type #7 is degree-6-regular — the other six mix degree 5/7, so nothing in the pipeline assumes degree 6.
The mathematics, in layers
The repository is organized so the code reads like the mathematics. Each layer’s home is a
folder under src/core/, documented in that folder’s README.
- The torus — the topology (a genus-1 surface). Never an object, just the fact that
V − E + F = 0. - A triangulation — the discrete topology: a combinatorial structure realizing the
torus. Pure combinatorics, all derived from a triangle list. (
topology/,triangulations/) - A fundamental domain — a developing chart: how to cut the torus open along a minimal
graph and unroll it into the plane. A presentation choice. (
topology/) - A marking — a basis of
H₁(T²,ℤ), two oriented loops. The Teichmüller marking. (topology/) - The developing map — unfold a flat realization → read the holonomy of the marking → its
modulus τ ∈ ℍ (its point in Teichmüller space). Forget the marking (the
SL(2,ℤ)quotient,reduceModulus) → moduli space. (moduli/)
These five are intrinsic — true independent of any embedding. Realizing the flat torus as a polyhedron in ℝ³, and searching for flat embedded ones, is the extrinsic half — the search system, below.
The search system
The extrinsic problem is sharp: find configurations in ℝ³ⱽ that lie on a high-codimension submanifold (flat: every cone angle 2π, codim V−1; optionally a fixed modulus) and inside a tiny open set (embedded). Because the manifold is thin and the open set is small, you cannot one-shot a far/tiny target — you must move along the manifold while staying inside the open set.
The kit models exactly that, as a few modular objects close to the math (each lives in its
src/core/ folder, documented in that folder’s README):
- a configuration space
ConfigSpace = (T, φ)— a triangulation with an embeddingφ: ℝⁿ → ℝ³ⱽ(coordinate system: full, pinned, symmetry, Doyle–Schwartz). It pulls a condition to a real function on ℝⁿ, pushes a point up to ℝ³ⱽ, and gives the boundarypaperTorus(x). (configuration/,coordinates/) - conditions of two kinds — closed submanifolds
{fn = target}(flat, collinear, modulus) you project onto (constraints/), and open regions (embedded) you stay inside (embedding/); - three operations, all on one QR kernel (the constraint Jacobian’s
Jᵀ = QR): project (corrector onto the manifold), minimize (Riemannian gradient descent along it, into the region), continuation (tracks the manifold∩region as a target moves) — the tool for the tiny far set — plus certify to record the result (raw τ and reduced τ̂, margin, embeddedness). (solvers/,search/)
The solvers run entirely on the problem’s space ℝⁿ — they take pulled Fns and a gate predicate,
never a Triangulation, a coordinate system, or a chart.
Layout
src/ is three rings by purity — core/ (pure, headless, tsx-runnable), display/ (torus →
three.js), app/ (scene/path-trace harness). Imports cross folders via the aliases @core/*,
@display/*, @app/*.
src/core/ THE PURE, HEADLESS CORE — no three.js, no DOM; every folder runs under tsx.
topology/ the intrinsic torus, COMBINATORIAL — works on ANY triangulation:
triangulation.ts deriveCombinatorics + makeTriangulation(data, marking) + types (Euler-checked)
trees.ts shared spanning-tree primitives (primal/dual trees, tree–cotree, LCA path)
marking.ts canonicalMarking: H₁ generators + cut + develop order — EXPENSIVE, run OFFLINE
fundamentalDomain.ts the developing chart — exact minimal cut + centered-spiral unroll order
harmonicLayout.ts flat-torus harmonic (Tutte) embedding — the SCRATCH layout that picks the marking
(depends only on geometry/; the geometric measurement of a metric torus lives in moduli/, below)
moduli/ the modulus: develop a metric torus → τ, and the space it lives in:
develop.ts the developing map via per-triangle frames (no circle–circle): developNet + frames
modulus.ts τ ∈ ℍ directly from the frames + tauJacobian (the exact analytic ∂τ/∂p)
reduce.ts the space ℍ/SL(2,ℤ): applyMobius, reduceModulus, SQUARE/HEXAGONAL (torus-blind)
(depends on topology + geometry; consumed by constraints/modulus and search/certify)
triangulations/ the specific triangulations we study, as DATA — scales to many:
{seven,eight,nine,ten}Vertex.ts the triangle lists per vertex count (TriangulationData;
ids v7-1, v8-1..7, v9-1..112, v10-1..2109 — 2229 tori from Lutz's census)
<census>.markings.generated.ts per-census precomputed markings, keyed by id — GENERATED
by `npm run compute-markings` (1:1 with the data files)
index.ts the registry: ALL_TORI, RICH, byId('v8-7') — joins each data ⊕ markings via
makeTriangulation (eager; building LOADS the marking, never computes it)
(depends on topology)
THE SEARCH STACK — a modular, problem-agnostic kit for constrained search (see per-folder READMEs).
Dependency-ordered, each layer using only the ones below:
geometry → functions → {configuration, coordinates, constraints, embedding} → solvers → sampling → search
geometry/ torus-blind ℝ²/ℝ³ kernels: point/segment/triangle distances, the tri–tri chord
functions/ the generic toolkit ONLY: the `Fn` contract (a map ℝⁿ→ℝᵏ with inDim/outDim +
value/jacobian; `ScalarFn` = outDim 1) + the algebra (fdFn/scalarFn/affine; the
one chain-rule `compose` = both pullback g∘φ and post-map locus∘τ; `stack` to
combine conditions, `leastSquares` to soften one into a flow energy). ONE map type —
a constraint is an `Fn` + a `target`, an energy a `ScalarFn`, a coordinate map an `Fn`.
configuration/ the configuration-space MACHINERY: `ConfigSpace = (T, φ)` with pull/push/coords/
metric (space.ts); `paperTorus` (the {triang, positions} boundary bundle); csv.
coordinates/ the coordinate-system INSTANCES (each a map (`Fn`) φ, both push + coords):
full · pin (pinCoords/pinVertices) · symmetry · normalized (the gauge-fixed
section of C → C/Sim, 3V−7 free coords; the mirror of moduli/reduce).
constraints/ the CLOSED conditions {fn=target} you project onto (+ types.ts: Constraint = {fn, target?}):
flat (fn emits V−1 rows), collinear, modulus — the point/line/circle × Teichmüller/moduli
grid (pinTeichmuller/pinModuli × point/verticalLine/circle, each locus a Constraint on ℍ;
named: fixedModulus, modulusWall).
embedding/ the OPEN condition Ω you stay inside (its own home): embedded (isEmbedded gate +
clearance) · separation (minSeparation + the fatten cell-gap substrate) ·
energies/ (overlap + fatten) · cells (cellTables). Defines the `Region` contract
({contains, margin?}) the solvers stay inside.
solvers/ the engine, on ℝⁿ over one QR kernel (qr.ts): project (corrector), minimize
(Riemannian descent, region-gated), continuation (tracks a family); types.ts holds Family.
sampling/ producing seeds: rng · perturb · seeds (random + deterministic gridSeeds) ·
reference (RICH_REFERENCE).
search/ composition & measurement: certify (raw τ AND reduced τ̂), collect, the recipes,
pull (pull conditions into a coordinate system).
src/display/ TORUS → THREE.JS (the impure presentation library):
mesh/ the triangulation's k-cells realized in ℝ³ (one `Part`) + section/obj
viewer/ the one subject `makeTorusView` (factory, streams positions) + decorations + look
src/app/ THE SCENE/PRESENT HARNESS:
render/ the path-traced `Studio` (WebGL ↔ three-gpu-pathtracer) + stage/controls
demos/ renders/ browser entry points (interactive demos; path-traced figures), each a <name>/main.ts
scripts/ headless CLI runners (the search drivers); scripts/legacy/ is a read-only archive
data/ CSV result sets we keep (one torus per row, 24 floats); samples/ is the gitignored dump
The dependency rule: all of src/core/ never imports three.js or touches the DOM — so every
algorithm runs headless under tsx; display//app/ may, and depend on core/, never the reverse
(a @display/@app import inside src/core/ is a glaring purity violation). Within core/: topology
(combinatorial) depends only on geometry/ (its harmonic scratch layout uses geometry/vec2);
moduli/ (the modulus measurement + space) depends on topology + geometry. Arrows are one-way
folder→folder: geometry → topology → triangulations and geometry/topology → moduli; the search
stack geometry → functions → {configuration, coordinates, constraints, embedding} → solvers → sampling → search builds on top (with constraints/search also consuming moduli); display/app sit on top
of all of it. geometry/ is the single bottom everything rests on. Machinery and its instances are flat
siblings — topology↔triangulations, functions↔constraints, configuration↔coordinates — never
nested (the arrow is dependency, not containment).
Searches
A search is three small pieces wired together — there is no framework, just composition:
- a seed source —
() => Float64Array | null, the next starting configuration (src/core/sampling/seeds.ts:perturbedSeeds,poolSeeds,uniformSeedsrandom;gridSeedsdeterministic, returningnullwhen exhausted — all built onsampling/perturb+ an RNG); - an
attemptrecipe —(seed) => Certificate | null: runproject/minimizeon the seed andcertify, returning the certificate to accept it ornullto reject; - the
collectdriver —collect(drawSeed, attempt, {maxTries, maxAccepts, onAccept}): the rejection-sampling loop (stops on anullseed), with all IO in theonAccept/onTrycallbacks.
The three built-in searches (thin runners in scripts/, logic in src/core/search/):
npm run discover -- [opts] # any flat embedded torus
npm run wall -- --c 0 # flat embedded tori on a modulus wall |Re τ̂|=c (0 rect, ½ rhombic)
npm run semi-solutions -- [opts] # Doyle–Schwartz semi-solution scan (flat immersions; embeddedness recorded)
discover and wall share one recipe (flattenFlowEmbed), differing only in held:
seed → project(held) → minimize(held, energy, region = embedded) → certify
held = [flat] → discover
held = [flat, modulusWall(c)] → wall (descends to embedded along the wall)
To write a new search, write its attempt (pick the coordinate system, the held conditions,
whether to minimize, and the accept predicate) and hand it to collect with a seed source — e.g.
semiSolution runs in a pinCoords coordinate system with held = [flat, collinear, collinear] and
no descent (a flat-immersion search). A ~15-line src/core/search/<name>.ts plus a thin scripts/<name>.mjs
is a whole new search.
Validate the develop → τ pipeline (covolume = area, rotational defect ≈ 0, cone deficit ≈ 0):
npx tsx scripts/legacy/develop-check.mjs [path/to.csv]
Running
Install once: npm install.
npm run dev <name> # serve a demo/render (vite). Omit <name> to list them.
npm run build <name> # self-contained build → dist/<name>/
npm test # vitest
npx tsc --noEmit # the typecheck — there is no separate linter
npm run dev <name>(a demo underdemos/or a render underrenders/) writes a gitignored.dev/<name>.htmland serves it on a stable per-name port (a hash of the name, 5200–5599), so entries never collide and run in parallel.buildrewrites the trackedindex.html, sogit statusshows it modified after a build.
Adding a triangulation
- Add a
{ id, triangles }entry (TriangulationData) to a data file (src/core/triangulations/eightVertex.ts, or a new per-vertex-count file likenineVertex.ts). - Run
npm run compute-markingsto (re)generate the per-census*.markings.generated.tsfiles (the expensive harmonic-layout + min-cut pass, run once, committed; 1:1 with the data files). - Add the
build(data, markings)pair toALL_TORIinsrc/core/triangulations/index.ts.
The builder derives all combinatorics and validates V − E + F = 0 (+ manifold edges, single-cycle
links, coherent orientation); no vertex/edge/face count is hard-wired. The marking is a deterministic
function of the triangle list, so the generated table is a pure on-disk cache — recomputed only when
you re-run the generator, never at load.
Data format
CSV result files (data/): one torus per line, 24 comma-separated full-precision floats in
x0,y0,z0, …, x7,y7,z7 order — exactly a PaperTorus’s positions. A row records only points;
interpret it by pairing with a chosen triangulation. .bin files are the same 24-float packing
in Float32.
Normalization convention
src/core/coordinates/normalized.ts puts a torus into a canonical pose under the similarity group of
ℝ³ (translation ⊕ rotation ⊕ uniform scale = 7 DOF): vertex 0 at the origin, vertex 1 at (1,0,0),
vertex 2 in the xy-plane with y₂ ≥ 0. That removes the 7 similarity DOF, leaving 3V − 7 free
numbers. Only proper rotations are used, so chirality is preserved, not quotiented. It is a real
coordinate system (the section of C → C/Sim): normalizePose is the projection onto the slice
(coords), the linear scatter back is push. Use it to search in a gauge-fixed chart (full-rank
constraints, deduplicated up to similarity), or for storage/dedup; off it, the gauge is handled
implicitly by the solvers’ minimum-norm step.