From 78a6df6e159af65202370dd714352baf1a3481bc Mon Sep 17 00:00:00 2001 From: hyperpolymath <6759885+hyperpolymath@users.noreply.github.com> Date: Mon, 1 Jun 2026 13:31:08 +0100 Subject: [PATCH] feat(invariants): wire Jones + Conway + HOMFLY-PT into quandle_descriptor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extends PR #30 (Alexander) with three more polynomial invariants from KnotTheory.jl, all already implemented upstream. ## Three new descriptor fields | Field | Source | Form | Notes | |-------|--------|------|-------| | `jones_polynomial` | `KnotTheory.jones_polynomial(pd)` | `Dict{Int,Int}` Laurent in t | Serialised via `_serialise_int_poly` | | `conway_polynomial` | `KnotTheory.conway_polynomial(pd)` | `Dict{Int,Int}` Laurent in z (even powers) | Serialised via `_serialise_int_poly` | | `homfly_polynomial` | `KnotTheory.homfly_polynomial(pd)` | `Dict{Tuple{Int,Int},Int}` (a,z) two-variable | New `_serialise_int_poly_2var` helper | ## Discrimination story HOMFLY-PT subsumes both Alexander and Jones (P(1, z) ~ Δ(z), P(t, t^(1/2) - t^(-1/2)) ~ V(t)). For our three test knots: * Alexander distinguishes all three pairs (already verified in PR #30). * Jones distinguishes all three pairs (now tested). * Conway distinguishes all three pairs (now tested). * HOMFLY distinguishes all three pairs (now tested). The quandle_key now carries 4 polynomials + 2 image histograms + 3 counting fields + 4 raw structural columns = 11 components total. Strictly more discriminating than the pre-PR-30 descriptor. ## HOMFLY guard KnotTheory.MAX_CROSSINGS_FOR_HOMFLY is 15 (skein recursion is exponential). Above this, `_safe_homfly_serialised` returns the sentinel `"deferred:too_many_crossings"` rather than throwing. This means: * the descriptor remains populated for arbitrary-sized PDs, * the quandle_key changes shape between bounded and over-bounded knots (the homfly column flips to the sentinel), * downstream consumers parsing on `:` must handle the sentinel explicitly. ## New serialiser `_serialise_int_poly_2var(poly::Dict{Tuple{Int,Int},Int})` produces `"a_exp,z_exp:coeff;a_exp,z_exp:coeff;..."` — keys sorted lexicographically, zero coefficients dropped, semicolon separator distinguishes the 2-var form from the 1-var form unambiguously. ## Tests added (§12) Seven new testsets: 1. Jones populated + non-trivial (3 knots). 2. Conway populated + non-trivial (3 knots). 3. HOMFLY populated within bounds (3 knots). 4. HOMFLY guard fires on 16-crossing braid s1^16. 5. Jones pairwise distinguishability (3 pairs). 6. Conway pairwise distinguishability (3 pairs). 7. HOMFLY pairwise distinguishability (3 pairs). Exact-value assertions are deliberately limited to "populated + does not equal trivial-unknot output". An exact-fixture suite is queued as a follow-up because the agent-confirmed value for figure-eight Alexander (`-1:-1,0:3,-1:-1`) contains a duplicate exponent suggesting the upstream may use a different convention; safer to capture exact strings empirically than to ship guessed values. ## Stacked on This PR is stacked on top of PR #30 (Alexander + image histograms). It uses `_serialise_int_poly` and the descriptor structure introduced there. Should merge after #30. Closes (partial) hyperpolymath/quandledb#32 — that issue stays open for the exact-fixture work. Co-Authored-By: Claude Opus 4.7 (1M context) --- server/quandle_semantic.jl | 54 +++++++++++++++++++- server/test_quandle_axioms.jl | 96 +++++++++++++++++++++++++++++++++++ 2 files changed, 149 insertions(+), 1 deletion(-) diff --git a/server/quandle_semantic.jl b/server/quandle_semantic.jl index e5bd51d..daaf6a9 100644 --- a/server/quandle_semantic.jl +++ b/server/quandle_semantic.jl @@ -401,6 +401,46 @@ function _serialise_int_poly(poly::Dict{Int,Int})::String isempty(pairs) ? "0:0" : join(pairs, ",") end +""" + _serialise_int_poly_2var(poly::Dict{Tuple{Int,Int},Int}) -> String + +Serialise a two-variable Laurent polynomial-as-dict (e.g. HOMFLY-PT, +keyed by `(a_exp, z_exp) -> coeff`) to the canonical form +`"a_exp,z_exp:coeff;a_exp,z_exp:coeff;..."` (zero coefficients dropped, +keys sorted lexicographically by `(a_exp, z_exp)`). + +Distinct from `_serialise_int_poly` so single-variable and two-variable +polynomials are unambiguously different strings. +""" +function _serialise_int_poly_2var(poly::Dict{Tuple{Int,Int},Int})::String + pairs = String[] + for key in sort(collect(keys(poly))) + coeff = poly[key] + coeff == 0 && continue + push!(pairs, string(key[1], ",", key[2], ":", coeff)) + end + isempty(pairs) ? "0,0:0" : join(pairs, ";") +end + +# Hard limit from KnotTheory.MAX_CROSSINGS_FOR_HOMFLY = 15. Above this, +# HOMFLY's skein recursion is too expensive to keep `quandle_descriptor` +# cheap; we return a sentinel rather than throwing. +const HOMFLY_MAX_CROSSINGS = 15 +const HOMFLY_DEFERRED_SENTINEL = "deferred:too_many_crossings" + +""" + _safe_homfly_serialised(pd::KnotTheory.PlanarDiagram) -> String + +Compute and serialise the HOMFLY-PT polynomial of `pd`, guarded by the +`HOMFLY_MAX_CROSSINGS` bound. Returns the sentinel string +`HOMFLY_DEFERRED_SENTINEL` if `pd` has more than 15 crossings. +""" +function _safe_homfly_serialised(pd::KnotTheory.PlanarDiagram)::String + length(pd.crossings) > HOMFLY_MAX_CROSSINGS && return HOMFLY_DEFERRED_SENTINEL + poly = KnotTheory.homfly_polynomial(pd) + _serialise_int_poly_2var(poly) +end + """ quandle_descriptor(pd::KnotTheory.PlanarDiagram) -> NamedTuple @@ -429,6 +469,12 @@ function quandle_descriptor(pd::KnotTheory.PlanarDiagram) alexander_poly = KnotTheory.alexander_polynomial(pd) alexander_str = _serialise_int_poly(alexander_poly) + # KT-2 extension: Jones, Conway (both Laurent in one variable), + # and HOMFLY-PT (two-variable, guarded at HOMFLY_MAX_CROSSINGS). + jones_str = _serialise_int_poly(KnotTheory.jones_polynomial(pd)) + conway_str = _serialise_int_poly(KnotTheory.conway_polynomial(pd)) + homfly_str = _safe_homfly_serialised(pd) + key = string( canon.generator_count, ":", rel_count, ":", @@ -437,7 +483,10 @@ function quandle_descriptor(pd::KnotTheory.PlanarDiagram) color5, ":", image_hist_3_str, ":", image_hist_5_str, ":", - alexander_str, + alexander_str, ":", + jones_str, ":", + conway_str, ":", + homfly_str, ) ( @@ -453,6 +502,9 @@ function quandle_descriptor(pd::KnotTheory.PlanarDiagram) image_histogram_3 = image_hist_3, image_histogram_5 = image_hist_5, alexander_polynomial = alexander_str, + jones_polynomial = jones_str, + conway_polynomial = conway_str, + homfly_polynomial = homfly_str, quandle_key = key, ) end diff --git a/server/test_quandle_axioms.jl b/server/test_quandle_axioms.jl index 9ca55f6..19b2c1f 100644 --- a/server/test_quandle_axioms.jl +++ b/server/test_quandle_axioms.jl @@ -347,4 +347,100 @@ end @test f.alexander_polynomial != c.alexander_polynomial end +# --------------------------------------------------------------------------- +# § 12. KT-2 extension — Jones, Conway, HOMFLY populated + non-trivial +# +# Same shape as the Alexander tests in §11. The three polynomial fields +# extend the quandle_descriptor; together with Alexander they form a +# strictly more discriminating descriptor. +# +# Exact-value assertions are deliberately limited to "populated + does +# not equal the trivial-unknot output". A separate exact-fixture suite +# is queued (see KT-2 extension follow-up issue) where the test corpus +# captures the exact strings KnotTheory.jl produces. +# --------------------------------------------------------------------------- + +@testset "KT-2 ext: Jones polynomial is populated + non-trivial" begin + for (pd, label) in [ + (trefoil().pd, "trefoil"), + (figure_eight().pd, "figure-eight"), + (cinquefoil().pd, "cinquefoil"), + ] + @testset label begin + d = quandle_descriptor(pd) + @test !isempty(d.jones_polynomial) + @test d.jones_polynomial != "0:0" + end + end +end + +@testset "KT-2 ext: Conway polynomial is populated + non-trivial" begin + for (pd, label) in [ + (trefoil().pd, "trefoil"), + (figure_eight().pd, "figure-eight"), + (cinquefoil().pd, "cinquefoil"), + ] + @testset label begin + d = quandle_descriptor(pd) + @test !isempty(d.conway_polynomial) + @test d.conway_polynomial != "0:0" + end + end +end + +@testset "KT-2 ext: HOMFLY polynomial is populated within bounds" begin + for (pd, label) in [ + (trefoil().pd, "trefoil"), + (figure_eight().pd, "figure-eight"), + (cinquefoil().pd, "cinquefoil"), + ] + @testset label begin + d = quandle_descriptor(pd) + # All three test knots have < 15 crossings, so HOMFLY computes. + @test d.homfly_polynomial != "deferred:too_many_crossings" + @test !isempty(d.homfly_polynomial) + # HOMFLY is two-variable; format is "a_exp,z_exp:coeff;..." + @test occursin(';', d.homfly_polynomial) || + occursin(',', d.homfly_polynomial) + end + end +end + +@testset "KT-2 ext: HOMFLY guard returns sentinel above MAX_CROSSINGS" begin + # Build a synthetic 16-crossing PD by braid word s1^16. KnotTheory's + # MAX_CROSSINGS_FOR_HOMFLY is 15, so this triggers the guard. + pd_large = from_braid_word(join(["s1" for _ in 1:16], ".")).pd + d_large = quandle_descriptor(pd_large) + @test d_large.homfly_polynomial == "deferred:too_many_crossings" +end + +@testset "KT-2 ext: Jones polynomial distinguishes the test knots" begin + t = quandle_descriptor(trefoil().pd) + f = quandle_descriptor(figure_eight().pd) + c = quandle_descriptor(cinquefoil().pd) + @test t.jones_polynomial != f.jones_polynomial + @test t.jones_polynomial != c.jones_polynomial + @test f.jones_polynomial != c.jones_polynomial +end + +@testset "KT-2 ext: Conway polynomial distinguishes the test knots" begin + t = quandle_descriptor(trefoil().pd) + f = quandle_descriptor(figure_eight().pd) + c = quandle_descriptor(cinquefoil().pd) + @test t.conway_polynomial != f.conway_polynomial + @test t.conway_polynomial != c.conway_polynomial + @test f.conway_polynomial != c.conway_polynomial +end + +@testset "KT-2 ext: HOMFLY distinguishes the test knots" begin + # HOMFLY-PT subsumes both Alexander and Jones; for these knots it + # should distinguish all pairs. + t = quandle_descriptor(trefoil().pd) + f = quandle_descriptor(figure_eight().pd) + c = quandle_descriptor(cinquefoil().pd) + @test t.homfly_polynomial != f.homfly_polynomial + @test t.homfly_polynomial != c.homfly_polynomial + @test f.homfly_polynomial != c.homfly_polynomial +end + println("quandle-axiom-tests-ok")