Skip to content

Commit 38d495d

Browse files
hyperpolymathclaude
andcommitted
feat(quandledb): v0.3.0 extended invariants — determinant, signature, Alexander, HOMFLY-PT
Server: - format_alexander: sparse "exp:coeff" store → t^n human-readable display - format_homfly: two-variable polynomial Dict → l^a·m^b display string - compute_homfly: on-demand HOMFLY-PT via KnotTheory.jl (≤12 crossings, silently skips on error or missing pd_code) - handle_knot_detail: passes computed HOMFLY into response - knot_to_dict: adds alexander_display and homfly_polynomial fields Frontend: - Types.res: determinant, signature, alexanderPolynomial, alexanderDisplay, homflyPolynomial added to knot type - Decoders.res: decode all five new optional fields in decodeKnot - Page_KnotDetail.res: Determinant, Signature, Alexander Polynomial, HOMFLY-PT rows added to the Invariants table - Page_Query.res: "By determinant" example query added ROADMAP.adoc: v0.2.0 and v0.3.0 marked complete Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent c5a577e commit 38d495d

6 files changed

Lines changed: 169 additions & 25 deletions

File tree

quandledb/ROADMAP.adoc

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,23 @@
1919

2020
== v0.2.0 - Resolution Language (KRL)
2121

22-
* [ ] KRL (Knot Resolution Language) parser
23-
* [ ] Query DSL for topological predicates
24-
* [ ] Query editor in frontend with syntax highlighting
25-
* [ ] Composable predicates via text interface
22+
* [x] KRL (Knot Resolution Language) parser (recursive-descent, 9 precedence levels)
23+
* [x] SQL→KRL frontend (SELECT/WHERE/ORDER BY/LIMIT desugaring)
24+
* [x] KRL evaluator (filter, sort, take, skip, return, group_by, aggregate, find_equivalent)
25+
* [x] Predicate pushdown to Skein.jl (crossing_number, writhe, genus, determinant, signature)
26+
* [x] Query editor in frontend with Ctrl+Enter shortcut and example quick-load buttons
27+
* [x] Composable predicates via text interface (AND/OR/NOT, nested parentheses)
28+
* [x] Per-stage execution trace panel (rows_in/out, elapsed_ms, note)
29+
* [x] Circuit-breaker fault isolation (Skein DB + semantic sidecar)
30+
* [x] `/health` and `/metrics` (Prometheus) endpoints
31+
* [x] DB-free seam test suite (8 seams, MockDataProvider + MockSemProvider)
2632

2733
== v0.3.0 - Extended Invariants
2834

29-
* [ ] Alexander polynomial computation and display
30-
* [ ] HOMFLY-PT polynomial
31-
* [ ] Knot signature
32-
* [ ] Knot determinant
35+
* [x] Alexander polynomial display (`format_alexander`: sparse coeff map → t^n notation)
36+
* [x] HOMFLY-PT polynomial (on-demand via KnotTheory.jl, bounded to ≤12 crossings)
37+
* [x] Knot signature (surfaced from Skein.jl schema to API and frontend)
38+
* [x] Knot determinant (surfaced to frontend; filterable in KRL and SQL)
3339

3440
== v0.4.0 - Links and Tangles
3541

quandledb/frontend/src/Decoders.res

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,13 @@ let decodeKnot = (json: Js.Json.t): result<Types.knot, string> => {
9393
writhe: wr,
9494
genus: optInt("genus"),
9595
seifertCircleCount: optInt("seifert_circle_count"),
96+
determinant: optInt("determinant"),
97+
signature: optInt("signature"),
98+
alexanderPolynomial: optStr("alexander_polynomial"),
99+
alexanderDisplay: optStr("alexander_display"),
96100
jonesPolynomial: optStr("jones_polynomial"),
97101
jonesDisplay: optStr("jones_display"),
102+
homflyPolynomial: optStr("homfly_polynomial"),
98103
metadata,
99104
createdAt: optStr("created_at")->Belt.Option.getWithDefault(""),
100105
updatedAt: optStr("updated_at")->Belt.Option.getWithDefault(""),

quandledb/frontend/src/Page_KnotDetail.res

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,17 @@ let make = (
5454
"Seifert Circles",
5555
k.seifertCircleCount->Belt.Option.map(Belt.Int.toString),
5656
)}
57+
{renderOptField(
58+
"Determinant",
59+
k.determinant->Belt.Option.map(Belt.Int.toString),
60+
)}
61+
{renderOptField(
62+
"Signature",
63+
k.signature->Belt.Option.map(Belt.Int.toString),
64+
)}
65+
{renderOptField("Alexander Polynomial", k.alexanderDisplay)}
5766
{renderOptField("Jones Polynomial", k.jonesDisplay)}
67+
{renderOptField("HOMFLY-PT", k.homflyPolynomial)}
5868
</tbody>
5969
</table>
6070
</div>

quandledb/frontend/src/Page_Query.res

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ let examples = [
1818
"SQL: figure-eight",
1919
"SELECT name, crossing_number, writhe\nFROM knots\nWHERE crossing_number = 4\nORDER BY name ASC",
2020
),
21+
(
22+
"By determinant",
23+
"from knots\n| filter determinant == 3\n| return name, crossing_number, determinant, signature, alexander_polynomial",
24+
),
2125
(
2226
"All invariants",
2327
"from invariants",

quandledb/frontend/src/Types.res

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,13 @@ type knot = {
1717
writhe: int,
1818
genus: option<int>,
1919
seifertCircleCount: option<int>,
20+
determinant: option<int>,
21+
signature: option<int>,
22+
alexanderPolynomial: option<string>,
23+
alexanderDisplay: option<string>,
2024
jonesPolynomial: option<string>,
2125
jonesDisplay: option<string>,
26+
homflyPolynomial: option<string>,
2227
metadata: Js.Dict.t<string>,
2328
createdAt: string,
2429
updatedAt: string,

quandledb/server/serve.jl

Lines changed: 131 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -475,25 +475,136 @@ function format_jones(jp::String)
475475
replace(s, "q^" => "q^")
476476
end
477477

478-
function knot_to_dict(record::KnotRecord; semantic=nothing)
478+
"""
479+
format_alexander(poly) -> Union{String, Nothing}
480+
481+
Pretty-print a stored Alexander polynomial. The serialised form is
482+
`"exp:coeff,exp:coeff,..."` (produced by `_serialise_int_poly` in KnotTheoryExt).
483+
Renders using the variable `t`, e.g. `"-1:1,0:-3,1:1"` → `"t⁻¹ - 3 + t"`.
484+
Returns `nothing` for a nothing/missing input.
485+
"""
486+
function format_alexander(poly::Nothing)
487+
nothing
488+
end
489+
490+
function format_alexander(poly::String)
491+
try
492+
terms = String[]
493+
for pair in split(poly, ",")
494+
parts = split(pair, ":")
495+
length(parts) == 2 || continue
496+
exp = parse(Int, parts[1])
497+
coef = parse(Int, parts[2])
498+
coef == 0 && continue
499+
base = exp == 0 ? "" :
500+
exp == 1 ? "t" :
501+
exp == -1 ? "t⁻¹" :
502+
exp > 0 ? "t^$(exp)" : "t^($(exp))"
503+
if coef == 1
504+
push!(terms, isempty(base) ? "1" : base)
505+
elseif coef == -1
506+
push!(terms, isempty(base) ? "-1" : "-$(base)")
507+
else
508+
push!(terms, isempty(base) ? string(coef) : "$(coef)$(base)")
509+
end
510+
end
511+
isempty(terms) && return "0"
512+
# Join with sign-aware spacing
513+
result = terms[1]
514+
for t in terms[2:end]
515+
if startswith(t, "-")
516+
result *= " - " * t[2:end]
517+
else
518+
result *= " + " * t
519+
end
520+
end
521+
result
522+
catch
523+
poly # fall back to raw string on any parse failure
524+
end
525+
end
526+
527+
"""
528+
format_homfly(poly) -> Union{String, Nothing}
529+
530+
Render a HOMFLY-PT polynomial (Dict{Tuple{Int,Int},Int} keyed by (l-exp, m-exp))
531+
as a human-readable string, e.g. `"-l⁻¹m² + lm²"`.
532+
"""
533+
function format_homfly(poly::Nothing)
534+
nothing
535+
end
536+
537+
function format_homfly(poly::Dict)
538+
isempty(poly) && return "0"
539+
var_str = (exp::Int, letter::String) ->
540+
exp == 0 ? "" :
541+
exp == 1 ? letter :
542+
exp == -1 ? "$(letter)⁻¹" :
543+
exp > 0 ? "$(letter)^$(exp)" : "$(letter)^($(exp))"
544+
terms = String[]
545+
for (le, me) in sort(collect(keys(poly)))
546+
c = poly[(le, me)]
547+
c == 0 && continue
548+
lpart = var_str(le, "l")
549+
mpart = var_str(me, "m")
550+
mono = lpart * mpart
551+
isempty(mono) && (mono = "1")
552+
if c == 1
553+
push!(terms, mono)
554+
elseif c == -1
555+
push!(terms, "-$(mono)")
556+
else
557+
push!(terms, "$(c)$(mono)")
558+
end
559+
end
560+
isempty(terms) && return "0"
561+
result = terms[1]
562+
for t in terms[2:end]
563+
result *= startswith(t, "-") ? " - " * t[2:end] : " + " * t
564+
end
565+
result
566+
end
567+
568+
"""
569+
compute_homfly(record) -> Union{String, Nothing}
570+
571+
Attempt on-demand HOMFLY-PT computation via KnotTheory.jl. Skips knots with
572+
more than 12 crossings (state-sum is exponential) and silently returns `nothing`
573+
on any error.
574+
"""
575+
function compute_homfly(record::KnotRecord)
576+
record.crossing_number > 12 && return nothing
577+
isnothing(record.pd_code) && return nothing
578+
try
579+
pd = to_planardiagram(record) # requires KnotTheory.jl loaded
580+
raw = KnotTheory.homfly_polynomial(pd)
581+
format_homfly(raw)
582+
catch
583+
nothing
584+
end
585+
end
586+
587+
function knot_to_dict(record::KnotRecord; semantic=nothing, homfly=nothing)
479588
Dict{String, Any}(
480-
"id" => record.id,
481-
"name" => record.name,
482-
"gauss_code" => record.gauss_code.crossings,
483-
"diagram_format" => record.diagram_format,
484-
"crossing_number" => record.crossing_number,
485-
"writhe" => record.writhe,
486-
"genus" => record.genus,
589+
"id" => record.id,
590+
"name" => record.name,
591+
"gauss_code" => record.gauss_code.crossings,
592+
"diagram_format" => record.diagram_format,
593+
"crossing_number" => record.crossing_number,
594+
"writhe" => record.writhe,
595+
"genus" => record.genus,
487596
"seifert_circle_count" => record.seifert_circle_count,
488-
"determinant" => record.determinant,
489-
"signature" => record.signature,
597+
"determinant" => record.determinant,
598+
"signature" => record.signature,
490599
"alexander_polynomial" => record.alexander_polynomial,
491-
"jones_polynomial" => record.jones_polynomial,
492-
"jones_display" => format_jones(record.jones_polynomial),
493-
"metadata" => record.metadata,
494-
"semantic" => semantic,
495-
"created_at" => string(record.created_at),
496-
"updated_at" => string(record.updated_at),
600+
"alexander_display" => format_alexander(record.alexander_polynomial),
601+
"jones_polynomial" => record.jones_polynomial,
602+
"jones_display" => format_jones(record.jones_polynomial),
603+
"homfly_polynomial" => homfly,
604+
"metadata" => record.metadata,
605+
"semantic" => semantic,
606+
"created_at" => string(record.created_at),
607+
"updated_at" => string(record.updated_at),
497608
)
498609
end
499610

@@ -610,7 +721,10 @@ end
610721
function handle_knot_detail(db::SkeinDB, sdb::SemanticIndexDB, name::String)
611722
record = fetch_knot(db, name)
612723
isnothing(record) && return error_response("Knot '$name' not found"; status = 404)
613-
json_response(knot_to_dict(record; semantic = semantic_summary_by_name(sdb, record.name)))
724+
homfly = compute_homfly(record)
725+
json_response(knot_to_dict(record;
726+
semantic = semantic_summary_by_name(sdb, record.name),
727+
homfly = homfly))
614728
end
615729

616730
function handle_semantic_detail(db::SkeinDB, sdb::SemanticIndexDB, name::String)

0 commit comments

Comments
 (0)