Skip to content

CeresCanonicalVerification.kt

src/jvmMain/kotlin/afield/examples/ceres/CeresCanonicalVerification.kt · 127 lines

package afield.examples.ceres

import afield.geometry.polytope.CeresRelaxationOptions
import afield.geometry.polytope.PolyhedralObject
import afield.geometry.polytope.RelaxationPlan
import afield.geometry.polytope.canonicalDiagnostics
import afield.geometry.polytope.canonicalize
import afield.geometry.polytope.conway
import afield.geometry.polytope.relaxWithCeres
import kotlin.math.abs

Does the Ceres realizer produce a canonical form, or merely a small residual?

A low cost is not the claim — the canonical objective turns out to have a spurious zero-cost minimum, so it cannot be. The claim is the three defining conditions measured independently by canonicalDiagnostics: every edge tangent to one common sphere, the centroid of those tangency points at the origin, every face planar — plus the result still being a solid rather than a folded lump.

The kk family is the interesting part. dev_docs/POLYTOPE_ALGEBRA.md records kkC as the standing counter-example that neither existing iteration solves; it is now exact. The rest of the family divides into cases that reach machine precision and cases that reach roughly 1e-3 — much better than canonicalize manages, and honestly not exact.

private data class CanonicalCase(
    val expression: String,
    /** Tangency-radius spread this input is expected to reach. */
    val spread: Double,
)

private val CANONICAL_CASES = listOf(
    // Exact.
    CanonicalCase("kC", 1e-5),
    CanonicalCase("kD", 1e-5),
    CanonicalCase("tI", 1e-5),
    CanonicalCase("kkC", 1e-5),
    CanonicalCase("kkD", 1e-5),
    // Good, not exact. See the class comment.
    CanonicalCase("kkT", 5e-3),
    CanonicalCase("kkO", 5e-3),
    CanonicalCase("kkI", 5e-3),
)

private fun describe(label: String, p: PolyhedralObject): String {
    val d = p.canonicalDiagnostics()
    return "  %-24s spread=%.3e centroid=%.3e planarity=%.3e dihedral=[%.3f..%.3f]%s".format(
        label,
        d.tangencyRadiusSpread,
        d.tangencyCentroidOffset,
        d.maxFacePlanarityError,
        d.dihedralAngleMin,
        d.dihedralAngleMax,
        if (d.degenerateFaces.isEmpty()) "" else "  DEGENERATE x${d.degenerateFaces.size}",
    )
}

fun main()

body — 69 lines, from line 58
fun main() {
    val failures = mutableListOf<String>()

    for (case in CANONICAL_CASES) {
        val seed = conway(case.expression).polyhedron
        println(
            "\n${case.expression}  V=${seed.topology.elements(0).size} " +
                "E=${seed.topology.elements(1).size} F=${seed.topology.elements(2).size}",
        )
        println(describe("seed", seed))

        val iterated = seed.canonicalize().polyhedron
        println(describe("canonicalize()", iterated))

        val result = seed.relaxWithCeres(
            RelaxationPlan.canonicalConditions(),
            CeresRelaxationOptions(maxIterations = 500),
        )
        val solved = result.realization.polyhedron
        println(describe("ceres", solved))
        println("      ${result.summary}")

        val d = solved.canonicalDiagnostics()
        fun require(name: String, ok: Boolean) {
            if (!ok) failures += "${case.expression}: $name"
        }

        require("tangency radii common (< ${case.spread})", d.tangencyRadiusSpread < case.spread)
        require("tangency centroid at origin", d.tangencyCentroidOffset < 1e-4)
        require("faces planar", d.maxFacePlanarityError < 1e-6)
        require("no degenerate faces", d.degenerateFaces.isEmpty())
        require("midsphere radius sane", d.tangencyRadiusMean.isFinite() && d.tangencyRadiusMean > 1e-3)
        require("coordinates finite and bounded", solved.coordinatesSane())
        // No collapsed edges: the spurious minimum this objective admits. The barrier in
        // CeresRelaxationOptions.minEdgeLengthFraction is what keeps this true.
        require("no collapsed edges", solved.shortestEdge() > 1e-4)
        // Strictly better than the iteration it is meant to replace.
        require(
            "beats canonicalize()",
            d.tangencyRadiusSpread <= iterated.canonicalDiagnostics().tangencyRadiusSpread + 1e-12,
        )
    }

    if (failures.isNotEmpty()) {
        failures.forEach { println("FAILED: $it") }
        error("${failures.size} canonical checks failed")
    }
    println("\nok — ${CANONICAL_CASES.size} solids reach a genuine canonical form, kkC and kkD exactly")
}

private fun PolyhedralObject.coordinatesSane(): Boolean {
    val r = realization ?: return false
    return (0 until r.size).all {
        r.x(it).isFinite() && r.y(it).isFinite() && r.z(it).isFinite() &&
            abs(r.x(it)) < 1e6 && abs(r.y(it)) < 1e6 && abs(r.z(it)) < 1e6
    }
}

private fun PolyhedralObject.shortestEdge(): Double {
    val r = realization ?: return 0.0
    val index = topology.elements(0).mapIndexed { at, v -> v.id.value to at }.toMap()
    return topology.elements(1).minOfOrNull { edge ->
        val (a, b) = topology.incidentDown(edge.id).map { index.getValue(it) }
        val dx = (r.x(a) - r.x(b)).toDouble()
        val dy = (r.y(a) - r.y(b)).toDouble()
        val dz = (r.z(a) - r.z(b)).toDouble()
        kotlin.math.sqrt(dx * dx + dy * dy + dz * dz)
    } ?: 0.0
}