Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions KnotDiagrams/Project.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name = "KnotDiagrams"
uuid = "a7e3c1f0-5d4b-4e8a-9f1c-2b3d4e5f6a7b"
version = "0.1.0"

[deps]
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"

[extras]
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0"

[targets]
test = ["Test"]

[compat]
DataFrames = "1"
julia = "1.9"
105 changes: 105 additions & 0 deletions KnotDiagrams/docs/narrative.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
Fraying-Model Fragment-Count Distribution Predictions
=====================================================

This document states testable predictions about the fragment-count
distribution L_D(k) for knot diagrams, derived from the combinatorics
of Kauffman bracket state sums.

Definitions
-----------

For an n-crossing knot diagram D, the 2^n global resolution states are
obtained by choosing one of two smoothings at each crossing. Each state
yields a collection of k disjoint simple closed curves ("fragments").

L_D(k) = |{ states s : s produces exactly k fragments }|

The tropical summary is:
support(L_D) = { k : L_D(k) > 0 }
min, max = extrema of support
|support| = cardinality
class = "spread" if |support| > 1, "collapse" if |support| = 1

Prediction 1 — No Collapse for Nontrivial Knots
------------------------------------------------

Every prime knot with crossing number >= 3 has |support| > 1 (is "spread").

Rationale: For an alternating prime knot, the all-A state (Seifert state)
produces a number of circles related to the genus, while the all-B state
produces a different count. The minimum and maximum circle counts in
the state sum always differ for nontrivial knots.

Prediction 2 — Trefoil (3_1) Has Support {1, 2, 3}
---------------------------------------------------

The trefoil should produce states with 1, 2, or 3 circles.

L_{3_1}(1) = 3 (three states yield 1 circle)
L_{3_1}(2) = 4 (four states yield 2 circles)
L_{3_1}(3) = 1 (one state yields 3 circles)

Verification: the all-0 state (smoothing 0 at every crossing) gives 2
circles, and the all-1 state gives 3 circles. Three mixed states give
1 circle each. This matches the Kauffman bracket structure for the
trefoil.

Prediction 3 — Topology Predicts the Distribution
--------------------------------------------------

Diagrams of different knot types at the same crossing number should
produce different fragment-count histograms L_D(k). Specifically:

5_1 vs 5_2 — different histograms, likely different supports
6_1 vs 6_2 — different histograms
6_1 vs 6_3 — different histograms
6_2 vs 6_3 — different histograms
7_i vs 7_j — different histograms for i ≠ j (all 21 pairs)

This is the central testable claim: the fragment-count distribution is a
topological invariant of the diagram (up to diagram equivalence), and
distinct knot types generally have distinct distributions.

Prediction 4 — Torus Knots Have Wider Support
----------------------------------------------

Torus knots T(2, 2k+1) have wider support (larger |support|) than
non-torus knots at the same crossing number.

5_1 (torus) — wider support than 5_2 (twist knot)
7_1 (torus) — wider support than 7_2, ..., 7_7

Rationale: the Seifert state of a torus knot produces the minimum
number of Seifert circles (k+1 for T(2,2k+1)), while the all-opposite
state produces a larger count. The spread between min and max is
maximised for torus knots among alternating knots at a given crossing
number.

Prediction 5 — Support Range Grows with Crossing Number
--------------------------------------------------------

For the torus knot series T(2, 2k+1):
3_1: |support| around 3
5_1: |support| around 4-5
7_1: |support| around 5-6

The support range (max - min + 1) grows roughly linearly with crossing
number for torus knots.

Prediction 6 — The Distribution Refines the Jones Polynomial
-------------------------------------------------------------

Two knots can share the same Jones polynomial but have different
fragment-count distributions. The L_D(k) histogram retains state-level
information that the polynomial (which sums over states with signs and
weights) discards. This makes L_D(k) a finer invariant in some cases.

Expected Outcomes
-----------------

Running the analysis on all prime knots up to 7 crossings should show:
- 14 diagrams, all classified as "spread"
- 25 pairwise comparisons (1 + 3 + 21 for n=5,6,7)
- Majority of pairs have different supports
- All pairs have different histograms (stronger claim)
- Torus knots (3_1, 5_1, 7_1) have the widest support at each cn
92 changes: 92 additions & 0 deletions KnotDiagrams/scripts/plot_distributions.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
#!/usr/bin/env julia
# SPDX-License-Identifier: PMPL-1.0-or-later
#
# Generate histogram plots for fragment-count distributions.
# Requires CairoMakie.
#
# Usage:
# julia --project=. scripts/plot_distributions.jl

using KnotDiagrams
using CairoMakie

function plot_all(fds::AbstractVector{FragmentDistribution};
output_dir::String = joinpath(@__DIR__, "..", "plots"))
mkpath(output_dir)

# Group by crossing number
groups = Dict{Int,Vector{FragmentDistribution}}()
for fd in fds
cn = fd.diagram.crossing_number
push!(get!(Vector{FragmentDistribution}, groups, cn), fd)
end

# Per-crossing-number overlaid histograms
for cn in sort(collect(keys(groups)))
group = groups[cn]
plot_group(group, cn, output_dir)
end

# Per-diagram individual plots
for fd in fds
plot_single(fd, output_dir)
end

n_group = length(groups)
n_single = length(fds)
println("Saved $(n_group + n_single) plots to $output_dir")
end

function plot_group(group::Vector{FragmentDistribution}, cn::Int, output_dir::String)
all_k = Set{Int}()
for fd in group
union!(all_k, keys(fd.histogram))
end
k_range = minimum(all_k):maximum(all_k)

fig = Figure(size = (800, 500))
ax = Axis(fig[1, 1],
title = "Fragment-count distribution — $(cn)-crossing knots",
xlabel = "Fragment count k",
ylabel = "Number of states",
xticks = collect(k_range),
)

n_diagrams = length(group)
bar_width = 0.8 / n_diagrams
offsets = range(-0.4 + bar_width / 2, step = bar_width, length = n_diagrams)

for (i, fd) in enumerate(group)
ks = collect(k_range)
counts = [get(fd.histogram, k, 0) for k in ks]
barplot!(ax, ks .+ offsets[i], counts,
width = bar_width,
label = fd.diagram.name,
color = Cycled(i),
)
end

axislegend(ax, position = :rt)
save(joinpath(output_dir, "distribution_n$(cn).png"), fig, px_per_unit = 2)
end

function plot_single(fd::FragmentDistribution, output_dir::String)
k_range = fd.summary.min_k:fd.summary.max_k
ks = collect(k_range)
counts = [get(fd.histogram, k, 0) for k in ks]

fig = Figure(size = (600, 400))
ax = Axis(fig[1, 1],
title = "L_D(k) — $(fd.diagram.name)",
xlabel = "Fragment count k",
ylabel = "Number of states",
xticks = ks,
)
barplot!(ax, ks, counts, color = :steelblue)

save(joinpath(output_dir, "distribution_$(fd.diagram.name).png"), fig, px_per_unit = 2)
end

# Main
results = analyse_all(verbose = false)
plot_all(results.distributions)
142 changes: 142 additions & 0 deletions KnotDiagrams/scripts/run_analysis.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
#!/usr/bin/env julia
# SPDX-License-Identifier: PMPL-1.0-or-later
#
# Full analysis pipeline: compute fragment-count distributions for all
# prime knots up to 7 crossings and compare within crossing numbers.
#
# Usage:
# julia --project=. scripts/run_analysis.jl
#
# For plots, run separately:
# julia --project=. scripts/plot_distributions.jl

using KnotDiagrams
using DataFrames

function main()
println("=" ^ 72)
println(" Knot Diagram Fragment-Count Distribution Analysis")
println("=" ^ 72)
println()

# Run full analysis
results = analyse_all(verbose = true)
println()

# Print summary table
println("=" ^ 72)
println("SUMMARY TABLE")
println("=" ^ 72)
show(stdout, results.summary_table; allrows = true, allcols = true)
println("\n")

# Print comparison table
println("=" ^ 72)
println("COMPARISON TABLE")
println("=" ^ 72)
show(stdout, results.comparison_table; allrows = true, allcols = true)
println("\n")

# Validate against narrative predictions
validate_narrative(results)

return results
end

"""
validate_narrative(results)

Check results against the narrative predictions in docs/narrative.txt.
"""
function validate_narrative(results)
println("=" ^ 72)
println("NARRATIVE VALIDATION")
println("=" ^ 72)

passed = 0
failed = 0

# Prediction 1: All prime knots are "spread"
print(" [1] All prime knots have |support| > 1 (spread): ")
all_spread = all(fd -> fd.summary.classification == :spread, results.distributions)
if all_spread
println("PASS")
passed += 1
else
collapsed = filter(fd -> fd.summary.classification == :collapse, results.distributions)
println("FAIL — collapsed: $(join([fd.diagram.name for fd in collapsed], ", "))")
failed += 1
end

# Prediction 2: Trefoil support includes at least {1,2} or {2,3}
print(" [2] Trefoil support contains multiple values: ")
trefoil = first(filter(fd -> fd.diagram.name == "3_1", results.distributions))
if trefoil.summary.support_size >= 2
println("PASS (support = $(trefoil.summary.support))")
passed += 1
else
println("FAIL (support = $(trefoil.summary.support))")
failed += 1
end

# Prediction 3: Figure-eight has different distribution from trefoil
print(" [3] Figure-eight distribution differs from trefoil: ")
fig8 = first(filter(fd -> fd.diagram.name == "4_1", results.distributions))
if fig8.histogram != trefoil.histogram
println("PASS")
passed += 1
else
println("FAIL")
failed += 1
end

# Prediction 4: 5_1 and 5_2 have different distributions
print(" [4] 5_1 vs 5_2 histograms differ: ")
cn5 = filter(r -> r.crossing_number == 5, results.comparisons)
if !isempty(cn5) && first(cn5).histograms_differ
println("PASS")
passed += 1
else
println("FAIL")
failed += 1
end

# Prediction 5: Most pairs at same crossing number have differing supports
print(" [5] Majority of same-cn pairs have differing supports: ")
n_total = length(results.comparisons)
n_differ = count(r -> r.supports_differ, results.comparisons)
ratio = n_total > 0 ? n_differ / n_total : 0.0
if ratio > 0.5
println("PASS ($n_differ / $n_total = $(round(ratio * 100, digits=1))%)")
passed += 1
else
println("FAIL ($n_differ / $n_total = $(round(ratio * 100, digits=1))%)")
failed += 1
end

# Prediction 6: Torus knots have wider support
print(" [6] Torus knots tend to have wider support: ")
fd51 = first(filter(fd -> fd.diagram.name == "5_1", results.distributions))
fd52 = first(filter(fd -> fd.diagram.name == "5_2", results.distributions))
fd71 = first(filter(fd -> fd.diagram.name == "7_1", results.distributions))
other_7 = filter(fd -> fd.diagram.crossing_number == 7 && fd.diagram.name != "7_1",
results.distributions)
avg_other = isempty(other_7) ? 0.0 :
sum(fd.summary.support_size for fd in other_7) / length(other_7)

torus_wider = fd51.summary.support_size >= fd52.summary.support_size &&
fd71.summary.support_size >= avg_other
if torus_wider
println("PASS (5_1: $(fd51.summary.support_size) vs 5_2: $(fd52.summary.support_size), " *
"7_1: $(fd71.summary.support_size) vs avg others: $(round(avg_other, digits=1)))")
passed += 1
else
println("INCONCLUSIVE (5_1: $(fd51.summary.support_size) vs 5_2: $(fd52.summary.support_size), " *
"7_1: $(fd71.summary.support_size) vs avg others: $(round(avg_other, digits=1)))")
end

println()
println(" Passed: $passed / $(passed + failed)")
end

main()
Loading
Loading