From 58ef431c94f4d117d220b0794caba3753f2b0b6a Mon Sep 17 00:00:00 2001 From: Pieter Belmans Date: Thu, 9 Jul 2026 08:21:18 +0200 Subject: [PATCH 01/20] feat: strongly connected components --- docs/src/methods/quivers.md | 1 + src/QuiverTools.jl | 2 +- src/Quivers.jl | 48 +++++++++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/docs/src/methods/quivers.md b/docs/src/methods/quivers.md index c75cc59..3052a3f 100644 --- a/docs/src/methods/quivers.md +++ b/docs/src/methods/quivers.md @@ -22,6 +22,7 @@ indegree outdegree is_acyclic is_connected +strongly_connected_components is_sink is_source underlying_graph diff --git a/src/QuiverTools.jl b/src/QuiverTools.jl index 7186e68..e2aa427 100644 --- a/src/QuiverTools.jl +++ b/src/QuiverTools.jl @@ -40,7 +40,7 @@ export Quiver, HNType, LunaType, QuiverModuli, QuiverModuliSpace, QuiverModuliSt # Quivers export n_vertices, n_arrows, arrows, indegree, outdegree, is_acyclic, is_connected, is_sink, is_source, - underlying_graph, first_hochschild_cohomology + strongly_connected_components, underlying_graph, first_hochschild_cohomology # Constructors export kronecker_quiver, loop_quiver, jordan_quiver, subspace_quiver, star_quiver, diff --git a/src/Quivers.jl b/src/Quivers.jl index 5ed7182..efb365b 100644 --- a/src/Quivers.jl +++ b/src/Quivers.jl @@ -104,6 +104,54 @@ function is_connected(Q::Quiver) return all(p -> p > 0, paths) end +""" + strongly_connected_components(Q::Quiver) + +Compute the strongly connected components of `Q`. + +Two vertices belong to the same strongly connected component if and only if +they are connected by paths in both directions. The reachability relation is +computed as the reflexive-transitive closure of the adjacency relation, using the +Floyd--Warshall algorithm in its original, Boolean, form +[[Warshall](https://doi.org/10.1145/321105.321107)]; its ``O(n^3)`` running time +is not an issue for the quivers we consider. + +# Input + +- `Q::Quiver`: a quiver. + +# Output + +- a list of the strongly connected components, each given as the list of its vertices. + +# Examples + +```jldoctest +julia> strongly_connected_components(cyclic_quiver(3)) +1-element Vector{Vector{Int64}}: + [1, 2, 3] + +julia> strongly_connected_components(kronecker_quiver(3)) +2-element Vector{Vector{Int64}}: + [1] + [2] + +julia> strongly_connected_components(Quiver("1-2,2-1,2-3")) +2-element Vector{Vector{Int64}}: + [1, 2] + [3] +``` +""" +function strongly_connected_components(Q::Quiver) + n = n_vertices(Q) + # reflexive-transitive closure by Floyd--Warshall [doi:10.1145/321105.321107] + reachable = [i == j || Q.adjacency[i, j] > 0 for i in 1:n, j in 1:n] + for k in 1:n, i in 1:n, j in 1:n + reachable[i, j] |= reachable[i, k] && reachable[k, j] + end + return unique([findall(j -> reachable[i, j] && reachable[j, i], 1:n) for i in 1:n]) +end + """ indegree(Q::Quiver, j::Int) From 72ea9e966b1fcfd8c33ca775326b40d0aa433381 Mon Sep 17 00:00:00 2001 From: Pieter Belmans Date: Thu, 9 Jul 2026 08:21:45 +0200 Subject: [PATCH 02/20] feat: bocklandt reduction and coregularity of quiver settings --- docs/src/methods/representation-theory.md | 2 + src/QuiverTools.jl | 3 +- src/RepresentationTheory.jl | 212 ++++++++++++++++++++++ test/runtests.jl | 35 ++++ 4 files changed, 251 insertions(+), 1 deletion(-) diff --git a/docs/src/methods/representation-theory.md b/docs/src/methods/representation-theory.md index 77697f9..4c7e2a7 100644 --- a/docs/src/methods/representation-theory.md +++ b/docs/src/methods/representation-theory.md @@ -16,5 +16,7 @@ all_general_subdimension_vectors is_general_subdimension_vector canonical_decomposition in_fundamental_domain +bocklandt_reduction +is_coregular first_hochschild_cohomology ``` diff --git a/src/QuiverTools.jl b/src/QuiverTools.jl index e2aa427..2d7f55c 100644 --- a/src/QuiverTools.jl +++ b/src/QuiverTools.jl @@ -60,7 +60,8 @@ export is_general_subdimension_vector, all_general_subdimension_vectors # Representation theory export euler_form, euler_matrix, is_root, is_schur_root, is_real_root, is_imaginary_root, is_isotropic_root, - general_ext, general_hom, canonical_decomposition, in_fundamental_domain + general_ext, general_hom, canonical_decomposition, in_fundamental_domain, + bocklandt_reduction, is_coregular # Moduli export all_luna_types, is_luna_type, dimension_of_luna_stratum diff --git a/src/RepresentationTheory.jl b/src/RepresentationTheory.jl index 8872212..894dcaa 100644 --- a/src/RepresentationTheory.jl +++ b/src/RepresentationTheory.jl @@ -271,3 +271,215 @@ function in_fundamental_domain(Q::Quiver, d::AbstractVector{Int}; interior::Bool simple -> euler_form(Q, d, simple) + euler_form(Q, simple, d) <= bound, simples ) end + +######################################################################################## +# Bocklandt's reduction algorithm +######################################################################################## + +# One pass of the reduction steps R_I, R_II, R_III of [MR1929191] on a strongly +# connected quiver setting. Returns the new setting, or `nothing` if no step applies, +# i.e., if the setting is reduced in the sense of [Definition 3.1, MR1929191]. +# +# The setting is given by a plain adjacency matrix and dimension vector rather than a +# Quiver: the adjacency of a Quiver is an immutable static matrix whose size is a type +# parameter, so the repeated resizing done here would allocate a new type at every +# step, and calling the memoized euler_form on such throwaway quivers would pollute +# its cache. +function __bocklandt_step(A::Matrix{Int}, d::Vector{Int}) + n = length(d) + # \chi(d, e_v) and \chi(e_v, d), for e_v the unit vector at the vertex v + chi_in(v) = d[v] - sum(d[w] * A[w, v] for w in 1:n) + chi_out(v) = d[v] - sum(A[v, w] * d[w] for w in 1:n) + for v in 1:n + # R_I [Lemma 3.2, MR1929191]: remove a loopless vertex whose incoming or outgoing + # paths carry at most d[v] dimensions, shortcutting every path through it; a lone + # vertex is kept so that the reduced coregular settings are the three settings of + # [Theorem 1.1, MR1929191] + if A[v, v] == 0 && n > 1 && (chi_in(v) >= 0 || chi_out(v) >= 0) + keep = setdiff(1:n, v) + return A[keep, keep] + A[keep, v] * A[v, keep]', d[keep] + end + # R_II [Lemma 3.3, MR1929191]: remove all loops on a vertex of dimension 1 + if A[v, v] > 0 && d[v] == 1 + B = copy(A) + B[v, v] = 0 + return B, d + end + # R_III [Lemma 3.4, MR1929191]: on a vertex of dimension k >= 2 carrying a single + # loop and, besides the loop, a single incoming (resp. outgoing) arrow from + # (resp. to) a vertex of dimension 1, remove the loop and thicken that arrow to + # k parallel arrows + if A[v, v] == 1 && d[v] >= 2 && (chi_in(v) == -1 || chi_out(v) == -1) + B = copy(A) + B[v, v] = 0 + if chi_in(v) == -1 + u = findfirst(w -> w != v && A[w, v] > 0, 1:n) + B[u, v] = d[v] + else + u = findfirst(w -> w != v && A[v, w] > 0, 1:n) + B[v, u] = d[v] + end + return B, d + end + end + return nothing +end + +# fully reduce a strongly connected quiver setting, i.e., apply reduction steps until +# the setting is reduced in the sense of [Definition 3.1, MR1929191] +function __bocklandt_reduce(A::Matrix{Int}, d::Vector{Int}) + while (step = __bocklandt_step(A, d)) !== nothing + A, d = step + end + return A, d +end + +""" + bocklandt_reduction(Q::Quiver, d::AbstractVector{Int}) + +Reduce the quiver setting `(Q, d)` using the reduction steps of +[[MR1929191](https://mathscinet.ams.org/mathscinet/relay-station?mr=1929191)]. + +The ring of invariants of a quiver setting is the tensor product of those of its +strongly connected components, and vertices of dimension `0` do not contribute, so +these are discarded first, by [Lemma 2.4, MR1929191]. Each component is then +simplified using the three reduction steps of [Section 3, MR1929191], each of which +preserves the ring of invariants up to a polynomial factor: + +- ``R_I`` [Lemma 3.2, MR1929191]: a vertex ``v`` without loops with + ``\\chi(d, e_v) \\geq 0`` or ``\\chi(e_v, d) \\geq 0`` is removed, and every pair of + arrows ``u \\to v \\to w`` is replaced by an arrow ``u \\to w``; +- ``R_{II}`` [Lemma 3.3, MR1929191]: the loops on a vertex of dimension `1` are + removed; +- ``R_{III}`` [Lemma 3.4, MR1929191]: the unique loop on a vertex ``v`` of dimension + ``k \\geq 2`` with ``\\chi(d, e_v) = -1`` (resp. ``\\chi(e_v, d) = -1``) is removed, + and the unique incoming (resp. outgoing) non-loop arrow is replaced by ``k`` + parallel arrows. + +The result, to which no further reduction step applies, is *reduced* in the sense of +[Definition 3.1, MR1929191]; it is returned as the disjoint union of the reduced +components. By [Theorem 3.5, MR1929191] the input setting is coregular if and only if +the reduced setting is, which is what [`is_coregular`](@ref) exploits. + +# Input + +- `Q::Quiver`: a quiver. +- `d::AbstractVector{Int}`: a dimension vector. + +# Output + +- a dictionary with the reduced quiver `Q` and dimension vector `d`. + +# Examples + +The setting below is reduced by applying ``R_{III}``, ``R_I``, and ``R_{II}``, +in this order: + +```jldoctest +julia> Q = Quiver("1-2, 2-2, 2-1"); + +julia> setting = bocklandt_reduction(Q, [1, 2]); + +julia> setting["Q"] +Quiver with adjacency matrix [0;;] + +julia> setting["d"] +1-element Vector{Int64}: + 1 +``` + +A reduced setting is returned unchanged: + +```jldoctest +julia> setting = bocklandt_reduction(Quiver("1--2, 2--1"), [1, 1]); + +julia> setting["Q"] +Quiver with adjacency matrix [0 2; 2 0] + +julia> setting["d"] +2-element Vector{Int64}: + 1 + 1 +``` +""" +function bocklandt_reduction(Q::Quiver, d::AbstractVector{Int}) + length(d) == n_vertices(Q) || + throw(ArgumentError("dimension vector must have length $(n_vertices(Q))")) + all(di >= 0 for di in d) || + throw(ArgumentError("dimension vector must be non-negative")) + + # vertices of dimension 0 and arrows between different strongly connected components + # play no role in the invariant theory [Lemma 2.4, MR1929191] + A = Matrix{Int}(Q.adjacency) + vertices = support(d) + components = strongly_connected_components(Quiver(A[vertices, vertices])) + + reduced = [ + __bocklandt_reduce(A[vertices[c], vertices[c]], Vector{Int}(d[vertices[c]])) for + c in components + ] + return Dict( + "Q" => reduce( + disjoint_union, [Quiver(B) for (B, _) in reduced]; init=Quiver(zeros(Int, 0, 0)) + ), + "d" => reduce(vcat, [e for (_, e) in reduced]; init=Int[]), + ) +end + +""" + is_coregular(Q::Quiver, d::AbstractVector{Int}) + +Check whether the quiver setting `(Q, d)` is coregular, i.e., whether the ring of +invariants of the `d`-dimensional representation variety of `Q` is a polynomial ring. + +Equivalently, this checks whether the affine quotient variety parametrizing +`d`-dimensional semisimple representations of `Q` is smooth, in which case it is an +affine space, by [Theorem 2.1, MR1929191]. + +By [[Theorem 1.1, MR1929191](https://mathscinet.ams.org/mathscinet/relay-station?mr=1929191)] +this is the case if and only if every strongly connected component of the +[`bocklandt_reduction`](@ref) of `(Q, d)` is one of + +- a vertex without loops, +- a vertex with one loop, +- a vertex of dimension `2` with two loops. + +# Input + +- `Q::Quiver`: a quiver. +- `d::AbstractVector{Int}`: a dimension vector. + +# Output + +- whether the ring of invariants of the setting `(Q, d)` is a polynomial ring. + +# Examples + +The invariants of pairs of ``2 \\times 2`` matrices form a polynomial ring, but those +of pairs of ``3 \\times 3`` matrices do not, by +[[Procesi](https://mathscinet.ams.org/mathscinet/relay-station?mr=419491)]; +the former is the third reduced coregular setting of [Theorem 1.1, MR1929191]: + +```jldoctest +julia> is_coregular(jordan_quiver(2), [2]) +true + +julia> is_coregular(jordan_quiver(2), [3]) +false +``` + +For an acyclic quiver the quotient variety is a point, so the setting is coregular: + +```jldoctest +julia> is_coregular(kronecker_quiver(3), [2, 3]) +true +``` +""" +function is_coregular(Q::Quiver, d::AbstractVector{Int}) + setting = bocklandt_reduction(Q, d) + A, e = setting["Q"].adjacency, setting["d"] + return all( + length(c) == 1 && (A[c[1], c[1]] <= 1 || (A[c[1], c[1]], e[c[1]]) == (2, 2)) for + c in strongly_connected_components(setting["Q"]) + ) +end diff --git a/test/runtests.jl b/test/runtests.jl index baacaa5..db1c4ca 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -180,3 +180,38 @@ end; @test string(poincare_polynomial(M)) == "L^6 + L^5 + 3*L^4 + 3*L^3 + 3*L^2 + L + 1" end; + +@testset "Bocklandt reduction" begin + # invariants of pairs of 2x2 matrices form a polynomial ring, of 3x3 they do not, + # and neither do those of triples of 2x2 matrices; a single matrix always does + @test is_coregular(jordan_quiver(2), [2]) + @test !is_coregular(jordan_quiver(2), [3]) + @test !is_coregular(jordan_quiver(3), [2]) + @test all(is_coregular(jordan_quiver(1), [n]) for n in 1:5) + + # for acyclic quivers the quotient variety is a point + @test is_coregular(kronecker_quiver(3), [2, 3]) + @test is_coregular(subspace_quiver(4), [1, 1, 1, 1, 2]) + + # settings I, II and IV of [Theorem 4.4, MR1929191] are coregular + @test is_coregular(Quiver("1-2, 2-1"), [4, 5]) # I + @test is_coregular(Quiver("1--2, 2--1"), [1, 2]) # II with k = 2 <= n = 2 + @test !is_coregular(Quiver("1--2, 2--1"), [1, 1]) # II fails for k = 2 > n = 1 + @test is_coregular(Quiver("1-2, 2-1, 2-3, 3-2"), [3, 2, 3]) # IV + + # the reduction combines R_III, R_I and R_II to a lone vertex of dimension 1 + setting = bocklandt_reduction(Quiver("1-2, 2-2, 2-1"), [1, 2]) + @test n_vertices(setting["Q"]) == 1 + @test n_arrows(setting["Q"]) == 0 + @test setting["d"] == [1] + + # a reduced setting is returned unchanged + setting = bocklandt_reduction(Quiver("1--2, 2--1"), [1, 1]) + @test Matrix(setting["Q"].adjacency) == [0 2; 2 0] + @test setting["d"] == [1, 1] + + # vertices of dimension 0 and arrows between strongly connected components are dropped + @test bocklandt_reduction(kronecker_quiver(3), [2, 0])["d"] == [2] + @test is_coregular(Quiver("1-1, 1-2, 2-2"), [2, 2]) + @test is_coregular(kronecker_quiver(3), [0, 0]) +end; From 093593eb52629b8790cce1cf81cb4360ec94473b Mon Sep 17 00:00:00 2001 From: Pieter Belmans Date: Thu, 9 Jul 2026 08:22:11 +0200 Subject: [PATCH 03/20] feat: smoothness of moduli spaces via local quivers and coregularity --- src/Moduli.jl | 38 +++++++++++++++++++++++++++++++++++++- test/runtests.jl | 10 ++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/Moduli.jl b/src/Moduli.jl index 4dc9f19..84e2625 100644 --- a/src/Moduli.jl +++ b/src/Moduli.jl @@ -762,6 +762,15 @@ end Checks if the moduli space is smooth. +In the presence of properly semistable representations, the moduli space is +étale-locally isomorphic, around a polystable representation, to the affine quotient of +the corresponding local quiver setting near the zero representation, by +[[MR1972892](https://mathscinet.ams.org/mathscinet/relay-station?mr=1972892)]. +Following the strategy of +[[Theorem 4.2, MR1929191](https://mathscinet.ams.org/mathscinet/relay-station?mr=1929191)], +the moduli space is thus smooth if and only if the local quiver setting of every Luna +type is coregular, which is checked using [`is_coregular`](@ref). + # Input - `M::QuiverModuliSpace`: a moduli space of representations of a quiver. @@ -776,6 +785,26 @@ Setups with `d` `theta`-coprime are smooth: ```jldoctest julia> Q = kronecker_quiver(3); M = QuiverModuliSpace(Q, [2, 3]); +julia> is_smooth(M) +true +``` + +For the 3-Kronecker quiver and `d = (3, 3)` the moduli space is singular, whereas for +`d = (2, 2)` and `d = (2, 4)` one gets ``\\mathbb{P}^5``, despite the presence of +properly semistable representations: +```jldoctest +julia> M = QuiverModuliSpace(kronecker_quiver(3), [3, 3]); + +julia> is_smooth(M) +false + +julia> M = QuiverModuliSpace(kronecker_quiver(3), [2, 2]); + +julia> is_smooth(M) +true + +julia> M = QuiverModuliSpace(kronecker_quiver(3), [2, 4]); + julia> is_smooth(M) true ``` @@ -787,7 +816,14 @@ function is_smooth(M::QuiverModuliSpace) return true end - throw(NotImplementedError("Not implemented for properly semistable cases.")) + # smoothness at the polystable points of a Luna stratum is equivalent to + # coregularity of its local quiver setting, by combining the étale-local description + # of [MR1972892] with [Theorem 2.1, MR1929191]; this is the globalization of + # [Theorem 4.2, MR1929191] to arbitrary stability parameters + return all(all_luna_types(M)) do tau + setting = local_quiver_setting(M, tau) + is_coregular(setting["Q"], setting["d"]) + end end """ diff --git a/test/runtests.jl b/test/runtests.jl index db1c4ca..8c6d83f 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -214,4 +214,14 @@ end; @test bocklandt_reduction(kronecker_quiver(3), [2, 0])["d"] == [2] @test is_coregular(Quiver("1-1, 1-2, 2-2"), [2, 2]) @test is_coregular(kronecker_quiver(3), [0, 0]) + + # smoothness of moduli spaces with properly semistable representations: for the + # 2-Kronecker quiver and d = (2, 2) one gets P^2, and for the 3-Kronecker quiver + # both d = (2, 2) and d = (2, 4) give P^5: the deepest local quiver setting is two + # loops on a vertex of dimension 2, the reduced coregular setting C1 of [MR1929191]; + # for d = (3, 3) that setting has dimension 3 instead, so the space is singular + @test is_smooth(QuiverModuliSpace(kronecker_quiver(2), [2, 2])) + @test is_smooth(QuiverModuliSpace(kronecker_quiver(3), [2, 2])) + @test is_smooth(QuiverModuliSpace(kronecker_quiver(3), [2, 4])) + @test !is_smooth(QuiverModuliSpace(kronecker_quiver(3), [3, 3])) end; From abaf3d5a7a65e60713788a580c95ebc607edc5db Mon Sep 17 00:00:00 2001 From: Pieter Belmans Date: Thu, 9 Jul 2026 08:33:26 +0200 Subject: [PATCH 04/20] test: smoothness on walls for the 6-subspace quiver --- test/runtests.jl | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/runtests.jl b/test/runtests.jl index 8c6d83f..0966495 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -224,4 +224,16 @@ end; @test is_smooth(QuiverModuliSpace(kronecker_quiver(3), [2, 2])) @test is_smooth(QuiverModuliSpace(kronecker_quiver(3), [2, 4])) @test !is_smooth(QuiverModuliSpace(kronecker_quiver(3), [3, 3])) + + # the 6-subspace quiver with d = (1^5, 2; 3) and stability parameters on a wall: + # for theta = (1^5, 2; -3) the moduli space is accidentally isomorphic to Gr(2, 4), + # hence smooth despite the eleven Luna strata, whereas for theta = (2^5, 1; -4) + # there are ten isolated singular points, one for each two-element subset of the + # five thin subspace vertices + S = subspace_quiver(6) + d = [1, 1, 1, 1, 1, 2, 3] + @test is_smooth(QuiverModuliSpace(S, d, [1, 1, 1, 1, 1, 2, -3])) + @test !is_smooth(QuiverModuliSpace(S, d, [2, 2, 2, 2, 2, 1, -4])) + # for d = (1^4, 2^2; 3) the analogous first wall crossing has smooth target too + @test is_smooth(QuiverModuliSpace(S, [1, 1, 1, 1, 2, 2, 3], [2, 2, 2, 2, 1, 1, -4])) end; From c5dd247e340a46a0db52946f0d33d79d21d28e5e Mon Sep 17 00:00:00 2001 From: Pieter Belmans Date: Thu, 9 Jul 2026 08:36:18 +0200 Subject: [PATCH 05/20] test: the Segre cubic is singular --- test/runtests.jl | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/runtests.jl b/test/runtests.jl index 0966495..05e8d2c 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -236,4 +236,9 @@ end; @test !is_smooth(QuiverModuliSpace(S, d, [2, 2, 2, 2, 2, 1, -4])) # for d = (1^4, 2^2; 3) the analogous first wall crossing has smooth target too @test is_smooth(QuiverModuliSpace(S, [1, 1, 1, 1, 2, 2, 3], [2, 2, 2, 2, 1, 1, -4])) + + # the Segre cubic threefold, as the moduli space for the 6-subspace quiver with + # d = (1^6; 2) and canonical stability: it has ten nodes, one for each splitting + # of the six thin subspace vertices into complementary triples + @test !is_smooth(QuiverModuliSpace(S, [1, 1, 1, 1, 1, 1, 2])) end; From 368f1334cf672cf60842bc0c715cb67ce5abb1b9 Mon Sep 17 00:00:00 2001 From: Pieter Belmans Date: Thu, 9 Jul 2026 08:46:00 +0200 Subject: [PATCH 06/20] feat: codimension of the singular locus --- docs/src/methods/quiver-moduli.md | 1 + src/Moduli.jl | 52 +++++++++++++++++++++++++++++++ src/QuiverTools.jl | 3 +- test/runtests.jl | 8 +++++ 4 files changed, 63 insertions(+), 1 deletion(-) diff --git a/docs/src/methods/quiver-moduli.md b/docs/src/methods/quiver-moduli.md index 4ee95d4..0a7f255 100644 --- a/docs/src/methods/quiver-moduli.md +++ b/docs/src/methods/quiver-moduli.md @@ -26,6 +26,7 @@ Black-box methods are provided to study some of their properties. is_nonempty dimension is_smooth +codimension_singular_locus is_projective index motive diff --git a/src/Moduli.jl b/src/Moduli.jl index 84e2625..56cc7ce 100644 --- a/src/Moduli.jl +++ b/src/Moduli.jl @@ -826,6 +826,58 @@ function is_smooth(M::QuiverModuliSpace) end end +""" + codimension_singular_locus(M::QuiverModuliSpace) + +Computes the codimension of the singular locus of the moduli space. + +The singular locus is a union of Luna strata: all points of the stratum of a Luna type +are singular if the corresponding local quiver setting is not coregular, and smooth +otherwise, as in [`is_smooth`](@ref). Unlike for moduli of vector bundles on a curve, +the singular locus can be strictly smaller than the locus of properly semistable +representations, whose codimension is bounded by that of the singular locus. + +# Input + +- `M::QuiverModuliSpace`: a moduli space of representations of a quiver. + +# Output + +- the codimension of the singular locus, or `Inf` if the moduli space is smooth. + +# Examples + +The Segre cubic threefold, with its ten singular points: +```jldoctest +julia> M = QuiverModuliSpace(subspace_quiver(6), [1, 1, 1, 1, 1, 1, 2]); + +julia> codimension_singular_locus(M) +3 +``` + +For the 3-Kronecker quiver and `d = (2, 2)` the properly semistable locus is non-empty +yet the moduli space is smooth, whilst for `d = (3, 3)` there are singularities: +```jldoctest +julia> codimension_singular_locus(QuiverModuliSpace(kronecker_quiver(3), [2, 2])) +Inf + +julia> codimension_singular_locus(QuiverModuliSpace(kronecker_quiver(3), [3, 3])) +3 +``` +""" +function codimension_singular_locus(M::QuiverModuliSpace) + M.condition == "stable" && return Inf + + # the stratum of a Luna type consists of singular points if and only if its local + # quiver setting is not coregular; the stable stratum is always smooth + singular = filter(all_luna_types(M)) do tau + setting = local_quiver_setting(M, tau) + !is_coregular(setting["Q"], setting["d"]) + end + isempty(singular) && return Inf + return dimension(M) - maximum(dimension_of_luna_stratum(M, tau) for tau in singular) +end + """ is_smooth(M::QuiverModuliStack) diff --git a/src/QuiverTools.jl b/src/QuiverTools.jl index 2d7f55c..ec43e13 100644 --- a/src/QuiverTools.jl +++ b/src/QuiverTools.jl @@ -65,7 +65,8 @@ export euler_form, euler_matrix, is_root, is_schur_root, is_real_root, is_imagin # Moduli export all_luna_types, is_luna_type, dimension_of_luna_stratum -export is_nonempty, codimension_unstable_locus, dimension, is_smooth, +export is_nonempty, codimension_unstable_locus, codimension_singular_locus, dimension, + is_smooth, is_projective, is_strongly_amply_stable, semistable_equals_stable, semisimple_moduli_space # Hodge diff --git a/test/runtests.jl b/test/runtests.jl index 05e8d2c..9ac5474 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -241,4 +241,12 @@ end; # d = (1^6; 2) and canonical stability: it has ten nodes, one for each splitting # of the six thin subspace vertices into complementary triples @test !is_smooth(QuiverModuliSpace(S, [1, 1, 1, 1, 1, 1, 2])) + + # codimension of the singular locus: the ten nodes of the Segre cubic; for the + # 3-Kronecker quiver and d = (2, 2) the properly semistable locus is non-empty + # while the singular locus is empty, and for d = (3, 3) the largest singular + # Luna stratum has codimension 3 in the 10-dimensional moduli space + @test codimension_singular_locus(QuiverModuliSpace(S, [1, 1, 1, 1, 1, 1, 2])) == 3 + @test codimension_singular_locus(QuiverModuliSpace(kronecker_quiver(3), [2, 2])) == Inf + @test codimension_singular_locus(QuiverModuliSpace(kronecker_quiver(3), [3, 3])) == 3 end; From d0b683e27e9083588988a3618346a9bff658bcb5 Mon Sep 17 00:00:00 2001 From: Pieter Belmans Date: Thu, 9 Jul 2026 09:35:28 +0200 Subject: [PATCH 07/20] feat: cofree quiver settings --- docs/src/methods/representation-theory.md | 1 + src/QuiverTools.jl | 2 +- src/RepresentationTheory.jl | 258 ++++++++++++++++++++++ test/runtests.jl | 44 ++++ 4 files changed, 304 insertions(+), 1 deletion(-) diff --git a/docs/src/methods/representation-theory.md b/docs/src/methods/representation-theory.md index 4c7e2a7..078f312 100644 --- a/docs/src/methods/representation-theory.md +++ b/docs/src/methods/representation-theory.md @@ -18,5 +18,6 @@ canonical_decomposition in_fundamental_domain bocklandt_reduction is_coregular +is_cofree first_hochschild_cohomology ``` diff --git a/src/QuiverTools.jl b/src/QuiverTools.jl index ec43e13..11d2129 100644 --- a/src/QuiverTools.jl +++ b/src/QuiverTools.jl @@ -61,7 +61,7 @@ export is_general_subdimension_vector, all_general_subdimension_vectors export euler_form, euler_matrix, is_root, is_schur_root, is_real_root, is_imaginary_root, is_isotropic_root, general_ext, general_hom, canonical_decomposition, in_fundamental_domain, - bocklandt_reduction, is_coregular + bocklandt_reduction, is_coregular, is_cofree # Moduli export all_luna_types, is_luna_type, dimension_of_luna_stratum diff --git a/src/RepresentationTheory.jl b/src/RepresentationTheory.jl index 894dcaa..32c95ca 100644 --- a/src/RepresentationTheory.jl +++ b/src/RepresentationTheory.jl @@ -483,3 +483,261 @@ function is_coregular(Q::Quiver, d::AbstractVector{Int}) c in strongly_connected_components(setting["Q"]) ) end + +######################################################################################## +# Cofree quiver settings +######################################################################################## + +# Everything below implements the classification of cofree quiver settings of +# Bocklandt--Van de Weyer [doi:10.1016/j.jalgebra.2007.08.019]: wedge away vertices +# using their reduction step W, split into prime components, and compare against the +# list of Theorem 1, whose members are recognized by the criteria of Theorems 5, 6, 8 +# and 9. Paths and cycles are quasiprimitive throughout: they use every vertex w as a +# source of at most d[w] arrows. + +# The number of quasiprimitive cycles through v, counted with arrow multiplicities and +# capped at cap + 1 to bound the enumeration; only used when every such cycle passes +# through v exactly once (it also passes through a vertex of dimension 1), so that +# counting closed walks anchored at v is correct. +function __n_quasiprimitive_cycles(A::Matrix{Int}, d::Vector{Int}, v::Int, cap::Int) + n = length(d) + budget = copy(d) + total = Ref(0) + function walk(x::Int, mult::Int) + (total[] > cap || budget[x] == 0) && return nothing + budget[x] -= 1 + for y in 1:n + A[x, y] == 0 && continue + y == v ? (total[] += mult * A[x, y]) : walk(y, mult * A[x, y]) + end + budget[x] += 1 + return nothing + end + walk(v, 1) + return total[] +end + +# One application of the wedging step W of [doi:10.1016/j.jalgebra.2007.08.019] to a +# vertex of dimension at least 2: a vertex whose unique outgoing (resp. incoming) +# arrow ends (resp. starts) at a vertex of dimension 1 is removed, redirecting its +# other arrows to that vertex, provided its dimension is at least the number of +# quasiprimitive cycles through it. Returns the new setting, or `nothing`. +# Wedging preserves cofreeness in both directions [Lemma 3]. +function __wedge_step(A::Matrix{Int}, d::Vector{Int}) + n = length(d) + for v in 1:n + (d[v] >= 2 && A[v, v] == 0) || continue + outs, ins = findall(>(0), A[v, :]), findall(>(0), A[:, v]) + wedge_out = length(outs) == 1 && A[v, outs[1]] == 1 && d[outs[1]] == 1 + wedge_in = length(ins) == 1 && A[ins[1], v] == 1 && d[ins[1]] == 1 + (wedge_out || wedge_in) || continue + __n_quasiprimitive_cycles(A, d, v, d[v]) <= d[v] || continue + B = copy(A) + wedge_out ? (B[:, outs[1]] .+= A[:, v]) : (B[ins[1], :] .+= A[v, :]) + keep = setdiff(1:n, v) + return B[keep, keep], d[keep] + end + return nothing +end + +# Split a strongly connected quiver setting into its prime components, i.e., the +# summands of its decomposition as an iterated connected sum at vertices of +# dimension 1; a setting is cofree iff its prime components are [Lemma 3]. +function __prime_components(A::Matrix{Int}, d::Vector{Int}) + n = length(d) + for v in 1:n + (d[v] == 1 && n + A[v, v] >= 2) || continue + # weakly connected components of the quiver minus v; each one, together with v and + # the arrows between them, is a summand, as is every loop at v + others = setdiff(1:n, v) + reachable = [ + i == j || A[others[i], others[j]] + A[others[j], others[i]] > 0 + for i in eachindex(others), j in eachindex(others) + ] + for k in eachindex(others), i in eachindex(others), j in eachindex(others) + reachable[i, j] |= reachable[i, k] && reachable[k, j] + end + pieces = unique([findall(reachable[i, :]) for i in eachindex(others)]) + length(pieces) + A[v, v] >= 2 || continue + out = Vector{Tuple{Matrix{Int},Vector{Int}}}() + for piece in pieces + keep = sort(vcat(others[piece], v)) + B = A[keep, keep] + B[findfirst(==(v), keep), findfirst(==(v), keep)] = 0 + append!(out, __prime_components(B, d[keep])) + end + append!(out, (fill(1, 1, 1), [1]) for _ in 1:A[v, v]) + return out + end + return [(A, d)] +end + +# [Theorem 6]: a strongly connected setting with a vertex v of dimension 1 through +# which all cycles run is cofree iff every other vertex w satisfies +# d[w] >= #{quasiprimitive paths v -> w} + #{quasiprimitive paths w -> v} - 1. +# The quiver minus v is acyclic here, so these paths are counted by powers of the +# adjacency matrix with v deleted, and quasiprimitivity is automatic. +function __is_cofree_through_vertex(A::Matrix{Int}, d::Vector{Int}, v::Int) + n = length(d) + B = copy(A) + B[v, :] .= 0 + B[:, v] .= 0 + S = sum(B^k for k in 0:(n - 1)) + return all( + d[w] >= + sum(A[v, x] * S[x, w] for x in 1:n) + sum(S[w, x] * A[x, v] for x in 1:n) - 1 for + w in 1:n if w != v + ) +end + +# Decide cofreeness of a prime strongly connected setting by recognizing the members +# of the list of [Theorem 1, doi:10.1016/j.jalgebra.2007.08.019]. +function __is_cofree_prime(A::Matrix{Int}, d::Vector{Int}) + n = length(d) + ins, outs = [sum(A[:, i]) for i in 1:n], [sum(A[i, :]) for i in 1:n] + + # a single vertex: no arrows, a cyclic quiver (one loop), any number of loops on a + # vertex of dimension 1, or the setting Q_2 (two loops on a vertex of dimension 2) + n == 1 && return A[1, 1] <= 1 || d[1] == 1 || (A[1, 1], d[1]) == (2, 2) + + # (iii) cyclic quiver settings are always cofree [Theorem 5] + all(ins[i] == 1 && outs[i] == 1 for i in 1:n) && return true + + # (i) all cycles run through a vertex of dimension 1 [Theorem 6] + for v in filter(v -> d[v] == 1, 1:n) + B = copy(A) + B[v, :] .= 0 + B[:, v] .= 0 + all(==(0), B^n) && return __is_cofree_through_vertex(A, d, v) + end + + # the remaining members of the list, (ii) and (iv), consist of two cycles sharing a + # path of s >= 1 vertices: n + 1 arrows in total, a unique vertex x of out-degree 2 + # and a unique vertex y of in-degree 2 (possibly equal), all other degrees 1 + sum(outs) == n + 1 || return false + x, y = findfirst(==(2), outs), findfirst(==(2), ins) + (isnothing(x) || isnothing(y)) && return false + + # the shared path runs from y to x; the two branches lead from x back to y + shared = [y] + while shared[end] != x + length(shared) > n && return false + push!(shared, findfirst(>(0), A[shared[end], :])) + end + function branch(start::Int) + b = Int[] + cur = start + while cur != y + (cur == x || cur in shared || cur in b || length(b) > n) && return nothing + push!(b, cur) + cur = findfirst(>(0), A[cur, :]) + end + return b + end + targets = findall(>(0), A[x, :]) + b1 = branch(targets[1]) + b2 = A[x, targets[1]] == 2 ? b1 : branch(targets[end]) + (isnothing(b1) || isnothing(b2)) && return false + length(shared) + length(b1) + length(b2) == n || return false + + # (ii) one branch is a single vertex of dimension 1: cofree iff the minimal + # dimension along the other cycle is attained exactly once in the shared path, or + # not there but exactly once in the other branch [Theorem 8] + for (c, rest) in ((b1, b2), (b2, b1)) + if length(c) == 1 && d[c[1]] == 1 + m = minimum(d[w] for w in vcat(shared, rest)) + count(w -> d[w] == m, shared) == 1 && return true + count(w -> d[w] == m, shared) == 0 && + count(w -> d[w] == m, rest) == 1 && + return true + end + end + any(length(b) == 1 && d[b[1]] == 1 for b in (b1, b2)) && return false + + # (iv) two cycles sharing a path, all branch dimensions at least 2, exactly one + # shared dimension equal to 2 and the others at least 4 [Theorem 9] + all(d[w] >= 2 for w in vcat(b1, b2)) || return false + return count(w -> d[w] == 2, shared) == 1 && + all(d[w] == 2 || d[w] >= 4 for w in shared) +end + +""" + is_cofree(Q::Quiver, d::AbstractVector{Int}) + +Check whether the quiver setting `(Q, d)` is cofree, i.e., whether the coordinate ring +of the `d`-dimensional representation variety of `Q` is a graded free module over its +ring of invariants. + +By a criterion of Popov this is the case if and only if the setting is coregular (see +[`is_coregular`](@ref)) and its nullcone is equidimensional. The implementation follows +the classification of +[[Bocklandt--Van de Weyer](https://doi.org/10.1016/j.jalgebra.2007.08.019)]: +the setting is cofree if and only if all its strongly connected components are, which +is decided by wedging away vertices (their reduction step ``W``), splitting into prime +components (the summands of the decomposition as an iterated connected sum at vertices +of dimension `1`), and comparing against the list of [Theorem 1, loc. cit.]. + +Cofreeness is stronger than coregularity: it moreover makes the quotient map from the +representation variety to the affine quotient flat. + +# Input + +- `Q::Quiver`: a quiver. +- `d::AbstractVector{Int}`: a dimension vector. + +# Output + +- whether the coordinate ring of the setting `(Q, d)` is a graded free module over the + ring of invariants. + +# Examples + +Pairs of ``2 \\times 2`` matrices are cofree, pairs of ``3 \\times 3`` matrices are +not even coregular, and cyclic quiver settings are always cofree: + +```jldoctest +julia> is_cofree(jordan_quiver(2), [2]) +true + +julia> is_cofree(jordan_quiver(2), [3]) +false + +julia> is_cofree(cyclic_quiver(3), [1, 2, 3]) +true +``` + +A coregular setting need not be cofree: + +```jldoctest +julia> Q = Quiver("1--2, 2-1"); + +julia> is_coregular(Q, [2, 2]), is_cofree(Q, [2, 2]) +(true, false) + +julia> is_coregular(Q, [2, 4]), is_cofree(Q, [2, 4]) +(true, true) +``` +""" +function is_cofree(Q::Quiver, d::AbstractVector{Int}) + length(d) == n_vertices(Q) || + throw(ArgumentError("dimension vector must have length $(n_vertices(Q))")) + all(di >= 0 for di in d) || + throw(ArgumentError("dimension vector must be non-negative")) + + # vertices of dimension 0 do not contribute, and arrows between different strongly + # connected components only contribute a free matrix factor [Lemma 3] + A = Matrix{Int}(Q.adjacency) + vertices = support(d) + for c in strongly_connected_components(Quiver(A[vertices, vertices])) + Ac, dc = A[vertices[c], vertices[c]], Vector{Int}(d[vertices[c]]) + # wedge the vertices of dimension at least 2 first, then split into prime + # components; wedges at vertices of dimension 1 only occur for cyclic quivers, + # which are cofree anyway [Remark 2] + while (step = __wedge_step(Ac, dc)) !== nothing + Ac, dc = step + end + all(__is_cofree_prime(B, e) for (B, e) in __prime_components(Ac, dc)) || + return false + end + return true +end diff --git a/test/runtests.jl b/test/runtests.jl index 9ac5474..d67ab8a 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -250,3 +250,47 @@ end; @test codimension_singular_locus(QuiverModuliSpace(kronecker_quiver(3), [2, 2])) == Inf @test codimension_singular_locus(QuiverModuliSpace(kronecker_quiver(3), [3, 3])) == 3 end; + +@testset "cofree quiver settings" begin + # cyclic quiver settings and matrix invariants: pairs of 2x2 matrices are cofree, + # pairs of 3x3 matrices and triples of 2x2 matrices are not; any number of loops on + # a vertex of dimension 1 is cofree + @test all(is_cofree(cyclic_quiver(n), fill(k, n)) for n in 1:3, k in 1:3) + @test is_cofree(cyclic_quiver(3), [1, 2, 3]) + @test is_cofree(jordan_quiver(2), [2]) + @test !is_cofree(jordan_quiver(2), [3]) + @test !is_cofree(jordan_quiver(3), [2]) + @test is_cofree(jordan_quiver(3), [1]) + + # acyclic settings are trivially cofree, as the invariants are constants + @test is_cofree(kronecker_quiver(3), [2, 3]) + @test is_cofree(subspace_quiver(4), [1, 1, 1, 1, 2]) + + # settings with all cycles through a vertex of dimension 1: the k arrows back and + # forth give 2k - 1 as the bound on the other dimension + @test is_cofree(Quiver("1-2, 2-1"), [1, 5]) + @test is_cofree(Quiver("1--2, 2--1"), [1, 3]) + @test !is_cofree(Quiver("1--2, 2--1"), [1, 2]) + + # two cycles sharing a path: cofree iff exactly one shared dimension is 2 and the + # others are at least 4, so coregularity does not suffice + @test is_coregular(Quiver("1--2, 2-1"), [2, 2]) + @test !is_cofree(Quiver("1--2, 2-1"), [2, 2]) + @test !is_cofree(Quiver("1--2, 2-1"), [2, 3]) + @test is_cofree(Quiver("1--2, 2-1"), [2, 4]) + + # two cycles sharing a path through a vertex of dimension 1: cofree iff the minimal + # dimension along the big cycle is attained exactly once in the shared path, or not + # there but exactly once in the other branch + theta_quiver = Quiver("1-2, 2-3, 3-1, 2-4, 4-1") + @test is_cofree(theta_quiver, [2, 3, 4, 1]) + @test is_cofree(theta_quiver, [3, 3, 2, 1]) + @test !is_cofree(theta_quiver, [2, 2, 3, 1]) + + # wedging removes the vertex of dimension 3 on the path to the central vertex, + # reducing to the setting [2, 3, 4, 1] above; with dimension 1 instead there are two + # vertices of dimension 1 on a common cycle, which is never cofree + wedged = Quiver("1-2, 2-3, 3-1, 2-5, 5-4, 4-1") + @test is_cofree(wedged, [2, 3, 4, 1, 3]) + @test !is_cofree(wedged, [2, 3, 4, 1, 1]) +end; From ed61d74acffe0a734762bbcc847c12894a0db626 Mon Sep 17 00:00:00 2001 From: Pieter Belmans Date: Thu, 9 Jul 2026 12:34:45 +0200 Subject: [PATCH 08/20] refactor: tighten the cofree classification code --- src/RepresentationTheory.jl | 138 ++++++++++++++---------------------- 1 file changed, 52 insertions(+), 86 deletions(-) diff --git a/src/RepresentationTheory.jl b/src/RepresentationTheory.jl index 32c95ca..98fe52a 100644 --- a/src/RepresentationTheory.jl +++ b/src/RepresentationTheory.jl @@ -496,18 +496,14 @@ end # source of at most d[w] arrows. # The number of quasiprimitive cycles through v, counted with arrow multiplicities and -# capped at cap + 1 to bound the enumeration; only used when every such cycle passes -# through v exactly once (it also passes through a vertex of dimension 1), so that -# counting closed walks anchored at v is correct. +# capped to bound the enumeration; only used when every such cycle passes through v +# exactly once, so that counting closed walks anchored at v is correct. function __n_quasiprimitive_cycles(A::Matrix{Int}, d::Vector{Int}, v::Int, cap::Int) - n = length(d) - budget = copy(d) - total = Ref(0) + total, budget = Ref(0), copy(d) function walk(x::Int, mult::Int) (total[] > cap || budget[x] == 0) && return nothing budget[x] -= 1 - for y in 1:n - A[x, y] == 0 && continue + for y in findall(>(0), A[x, :]) y == v ? (total[] += mult * A[x, y]) : walk(y, mult * A[x, y]) end budget[x] += 1 @@ -518,22 +514,19 @@ function __n_quasiprimitive_cycles(A::Matrix{Int}, d::Vector{Int}, v::Int, cap:: end # One application of the wedging step W of [doi:10.1016/j.jalgebra.2007.08.019] to a -# vertex of dimension at least 2: a vertex whose unique outgoing (resp. incoming) -# arrow ends (resp. starts) at a vertex of dimension 1 is removed, redirecting its -# other arrows to that vertex, provided its dimension is at least the number of -# quasiprimitive cycles through it. Returns the new setting, or `nothing`. -# Wedging preserves cofreeness in both directions [Lemma 3]. +# vertex v of dimension at least 2 whose unique outgoing (resp. incoming) arrow ends +# (resp. starts) at a vertex of dimension 1: v is removed and its other arrows are +# redirected to that vertex, provided d[v] is at least the number of quasiprimitive +# cycles through v. Wedging preserves cofreeness in both directions [Lemma 3]. function __wedge_step(A::Matrix{Int}, d::Vector{Int}) n = length(d) - for v in 1:n - (d[v] >= 2 && A[v, v] == 0) || continue + for v in findall(v -> d[v] >= 2 && A[v, v] == 0, 1:n) outs, ins = findall(>(0), A[v, :]), findall(>(0), A[:, v]) - wedge_out = length(outs) == 1 && A[v, outs[1]] == 1 && d[outs[1]] == 1 - wedge_in = length(ins) == 1 && A[ins[1], v] == 1 && d[ins[1]] == 1 - (wedge_out || wedge_in) || continue - __n_quasiprimitive_cycles(A, d, v, d[v]) <= d[v] || continue + out = length(outs) == 1 && A[v, outs[1]] == 1 && d[outs[1]] == 1 + into = length(ins) == 1 && A[ins[1], v] == 1 && d[ins[1]] == 1 + ((out || into) && __n_quasiprimitive_cycles(A, d, v, d[v]) <= d[v]) || continue B = copy(A) - wedge_out ? (B[:, outs[1]] .+= A[:, v]) : (B[ins[1], :] .+= A[v, :]) + out ? (B[:, outs[1]] .+= A[:, v]) : (B[ins[1], :] .+= A[v, :]) keep = setdiff(1:n, v) return B[keep, keep], d[keep] end @@ -545,88 +538,65 @@ end # dimension 1; a setting is cofree iff its prime components are [Lemma 3]. function __prime_components(A::Matrix{Int}, d::Vector{Int}) n = length(d) - for v in 1:n - (d[v] == 1 && n + A[v, v] >= 2) || continue - # weakly connected components of the quiver minus v; each one, together with v and - # the arrows between them, is a summand, as is every loop at v + for v in findall(==(1), d) + # the summands at v are the weakly connected components of the quiver minus v, + # each taken together with v and the arrows between them, and every loop at v others = setdiff(1:n, v) - reachable = [ - i == j || A[others[i], others[j]] + A[others[j], others[i]] > 0 - for i in eachindex(others), j in eachindex(others) - ] - for k in eachindex(others), i in eachindex(others), j in eachindex(others) - reachable[i, j] |= reachable[i, k] && reachable[k, j] - end - pieces = unique([findall(reachable[i, :]) for i in eachindex(others)]) + U = A[others, others] + pieces = strongly_connected_components(Quiver(U + U')) length(pieces) + A[v, v] >= 2 || continue - out = Vector{Tuple{Matrix{Int},Vector{Int}}}() + out = [(fill(1, 1, 1), [1]) for _ in 1:A[v, v]] for piece in pieces - keep = sort(vcat(others[piece], v)) + keep = sort!(vcat(others[piece], v)) B = A[keep, keep] - B[findfirst(==(v), keep), findfirst(==(v), keep)] = 0 + w = findfirst(==(v), keep) + B[w, w] = 0 append!(out, __prime_components(B, d[keep])) end - append!(out, (fill(1, 1, 1), [1]) for _ in 1:A[v, v]) return out end return [(A, d)] end -# [Theorem 6]: a strongly connected setting with a vertex v of dimension 1 through -# which all cycles run is cofree iff every other vertex w satisfies -# d[w] >= #{quasiprimitive paths v -> w} + #{quasiprimitive paths w -> v} - 1. -# The quiver minus v is acyclic here, so these paths are counted by powers of the -# adjacency matrix with v deleted, and quasiprimitivity is automatic. -function __is_cofree_through_vertex(A::Matrix{Int}, d::Vector{Int}, v::Int) - n = length(d) - B = copy(A) - B[v, :] .= 0 - B[:, v] .= 0 - S = sum(B^k for k in 0:(n - 1)) - return all( - d[w] >= - sum(A[v, x] * S[x, w] for x in 1:n) + sum(S[w, x] * A[x, v] for x in 1:n) - 1 for - w in 1:n if w != v - ) -end - # Decide cofreeness of a prime strongly connected setting by recognizing the members # of the list of [Theorem 1, doi:10.1016/j.jalgebra.2007.08.019]. function __is_cofree_prime(A::Matrix{Int}, d::Vector{Int}) n = length(d) - ins, outs = [sum(A[:, i]) for i in 1:n], [sum(A[i, :]) for i in 1:n] - # a single vertex: no arrows, a cyclic quiver (one loop), any number of loops on a # vertex of dimension 1, or the setting Q_2 (two loops on a vertex of dimension 2) n == 1 && return A[1, 1] <= 1 || d[1] == 1 || (A[1, 1], d[1]) == (2, 2) # (iii) cyclic quiver settings are always cofree [Theorem 5] - all(ins[i] == 1 && outs[i] == 1 for i in 1:n) && return true + ins, outs = vec(sum(A; dims=1)), vec(sum(A; dims=2)) + all(ins .== 1) && all(outs .== 1) && return true - # (i) all cycles run through a vertex of dimension 1 [Theorem 6] - for v in filter(v -> d[v] == 1, 1:n) + # (i) all cycles run through a vertex v of dimension 1 [Theorem 6]: cofree iff + # d[w] >= #{quasiprimitive paths v -> w} + #{quasiprimitive paths w -> v} - 1 for + # all other w; the quiver minus v is acyclic, so its adjacency powers count paths + for v in findall(==(1), d) B = copy(A) B[v, :] .= 0 B[:, v] .= 0 - all(==(0), B^n) && return __is_cofree_through_vertex(A, d, v) + any(!=(0), B^n) && continue + S = sum(B^k for k in 0:(n - 1)) + return all( + d[w] >= A[v, :]' * S[:, w] + S[w, :]' * A[:, v] - 1 for w in 1:n if w != v + ) end - # the remaining members of the list, (ii) and (iv), consist of two cycles sharing a - # path of s >= 1 vertices: n + 1 arrows in total, a unique vertex x of out-degree 2 - # and a unique vertex y of in-degree 2 (possibly equal), all other degrees 1 + # (ii) and (iv) are two cycles sharing a path of s >= 1 vertices: n + 1 arrows, a + # unique vertex x of out-degree 2, a unique y of in-degree 2, all other degrees 1; + # the shared path runs from y to x, the two branches lead from x back to y sum(outs) == n + 1 || return false x, y = findfirst(==(2), outs), findfirst(==(2), ins) (isnothing(x) || isnothing(y)) && return false - - # the shared path runs from y to x; the two branches lead from x back to y shared = [y] while shared[end] != x length(shared) > n && return false push!(shared, findfirst(>(0), A[shared[end], :])) end - function branch(start::Int) + function branch(cur::Int) b = Int[] - cur = start while cur != y (cur == x || cur in shared || cur in b || length(b) > n) && return nothing push!(b, cur) @@ -635,30 +605,26 @@ function __is_cofree_prime(A::Matrix{Int}, d::Vector{Int}) return b end targets = findall(>(0), A[x, :]) - b1 = branch(targets[1]) - b2 = A[x, targets[1]] == 2 ? b1 : branch(targets[end]) - (isnothing(b1) || isnothing(b2)) && return false - length(shared) + length(b1) + length(b2) == n || return false - - # (ii) one branch is a single vertex of dimension 1: cofree iff the minimal - # dimension along the other cycle is attained exactly once in the shared path, or - # not there but exactly once in the other branch [Theorem 8] + b1, b2 = branch(targets[1]), branch(targets[end]) + (isnothing(b1) || isnothing(b2) || length(shared) + length(b1) + length(b2) != n) && + return false + + # (ii) a branch is a single vertex of dimension 1: cofree iff the minimal dimension + # along the other cycle is attained exactly once in the shared path, or not there + # but exactly once in the other branch [Theorem 8] for (c, rest) in ((b1, b2), (b2, b1)) if length(c) == 1 && d[c[1]] == 1 - m = minimum(d[w] for w in vcat(shared, rest)) - count(w -> d[w] == m, shared) == 1 && return true - count(w -> d[w] == m, shared) == 0 && - count(w -> d[w] == m, rest) == 1 && - return true + m = minimum(d[vcat(shared, rest)]) + return count(==(m), d[shared]) == 1 || + (count(==(m), d[shared]) == 0 && count(==(m), d[rest]) == 1) end end - any(length(b) == 1 && d[b[1]] == 1 for b in (b1, b2)) && return false - # (iv) two cycles sharing a path, all branch dimensions at least 2, exactly one - # shared dimension equal to 2 and the others at least 4 [Theorem 9] - all(d[w] >= 2 for w in vcat(b1, b2)) || return false - return count(w -> d[w] == 2, shared) == 1 && - all(d[w] == 2 || d[w] >= 4 for w in shared) + # (iv) all branch dimensions at least 2, exactly one shared dimension equal to 2, + # and the other shared dimensions at least 4 [Theorem 9] + return all(d[vcat(b1, b2)] .>= 2) && + count(==(2), d[shared]) == 1 && + all(w -> w == 2 || w >= 4, d[shared]) end """ From abf1cc9359036c8582dafeeb15ff52d73eaa723c Mon Sep 17 00:00:00 2001 From: Pieter Belmans Date: Thu, 9 Jul 2026 12:35:24 +0200 Subject: [PATCH 09/20] docs: explain coregular and cofree quiver settings --- docs/src/methods/representation-theory.md | 30 ++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/docs/src/methods/representation-theory.md b/docs/src/methods/representation-theory.md index 078f312..169065f 100644 --- a/docs/src/methods/representation-theory.md +++ b/docs/src/methods/representation-theory.md @@ -16,8 +16,36 @@ all_general_subdimension_vectors is_general_subdimension_vector canonical_decomposition in_fundamental_domain +first_hochschild_cohomology +``` + +## Invariant theory of quiver representations + +The affine quotient of the representation variety by the base change group +parametrizes semisimple representations of the quiver, and its ring of functions is +the ring of invariants, generated by traces along oriented cycles. + +A quiver setting is *coregular* if this ring of invariants is a polynomial ring, or +equivalently if the affine quotient is smooth (in which case it is an affine space). +This is decided by the reduction algorithm of +[[Bocklandt](https://mathscinet.ams.org/mathscinet/relay-station?mr=1929191)], +which simplifies a quiver setting without changing its invariant theory and then +compares the result against a short list. + +A stronger property is *cofreeness*: the coordinate ring of the representation +variety is a graded free module over the ring of invariants, which by a criterion of +Popov amounts to coregularity together with equidimensionality of the nullcone. +Cofree quiver settings are classified by +[[Bocklandt--Van de Weyer](https://doi.org/10.1016/j.jalgebra.2007.08.019)]. + +Beyond deciding smoothness of the affine quotient itself, these notions drive the +study of moduli spaces of quiver representations: étale-locally around a polystable +representation, a moduli space is the affine quotient of a *local quiver setting*, so +coregularity of local quiver settings decides smoothness of moduli spaces; see +[`is_smooth`](@ref) and [`codimension_singular_locus`](@ref). + +```@docs bocklandt_reduction is_coregular is_cofree -first_hochschild_cohomology ``` From 6244ba3cccd39884c8732297a56aa7f97f2c0bcc Mon Sep 17 00:00:00 2001 From: Pieter Belmans Date: Tue, 25 Aug 2026 18:26:05 +0200 Subject: [PATCH 10/20] refactor: precompute Bocklandt Euler vectors --- src/RepresentationTheory.jl | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/RepresentationTheory.jl b/src/RepresentationTheory.jl index 98fe52a..e35489c 100644 --- a/src/RepresentationTheory.jl +++ b/src/RepresentationTheory.jl @@ -287,15 +287,14 @@ end # its cache. function __bocklandt_step(A::Matrix{Int}, d::Vector{Int}) n = length(d) - # \chi(d, e_v) and \chi(e_v, d), for e_v the unit vector at the vertex v - chi_in(v) = d[v] - sum(d[w] * A[w, v] for w in 1:n) - chi_out(v) = d[v] - sum(A[v, w] * d[w] for w in 1:n) + # the vectors of \chi(d, e_v) and \chi(e_v, d), for e_v the unit vector at v + chi_in, chi_out = d - A' * d, d - A * d for v in 1:n # R_I [Lemma 3.2, MR1929191]: remove a loopless vertex whose incoming or outgoing # paths carry at most d[v] dimensions, shortcutting every path through it; a lone # vertex is kept so that the reduced coregular settings are the three settings of # [Theorem 1.1, MR1929191] - if A[v, v] == 0 && n > 1 && (chi_in(v) >= 0 || chi_out(v) >= 0) + if A[v, v] == 0 && n > 1 && (chi_in[v] >= 0 || chi_out[v] >= 0) keep = setdiff(1:n, v) return A[keep, keep] + A[keep, v] * A[v, keep]', d[keep] end @@ -309,10 +308,10 @@ function __bocklandt_step(A::Matrix{Int}, d::Vector{Int}) # loop and, besides the loop, a single incoming (resp. outgoing) arrow from # (resp. to) a vertex of dimension 1, remove the loop and thicken that arrow to # k parallel arrows - if A[v, v] == 1 && d[v] >= 2 && (chi_in(v) == -1 || chi_out(v) == -1) + if A[v, v] == 1 && d[v] >= 2 && (chi_in[v] == -1 || chi_out[v] == -1) B = copy(A) B[v, v] = 0 - if chi_in(v) == -1 + if chi_in[v] == -1 u = findfirst(w -> w != v && A[w, v] > 0, 1:n) B[u, v] = d[v] else From a06b502f4f77ba89138f1e0a715873c2c8c7ec0a Mon Sep 17 00:00:00 2001 From: Pieter Belmans Date: Tue, 25 Aug 2026 18:26:25 +0200 Subject: [PATCH 11/20] refactor: share local smoothness plumbing --- src/Moduli.jl | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/Moduli.jl b/src/Moduli.jl index 56cc7ce..7c0ab48 100644 --- a/src/Moduli.jl +++ b/src/Moduli.jl @@ -549,7 +549,14 @@ function local_quiver_setting(M::QuiverModuli, tau) Qloc = Quiver(A) dloc = [m for e in keys(tau) for m in tau[e]] - return Dict("Q" => Qloc, "d" => dloc) + return Dict("Q" => Qloc, "d" => dloc, "summands" => summands) +end + +# whether the local quiver setting of the Luna type is coregular, i.e., whether the +# moduli space is smooth along the corresponding stratum +function __is_smooth_stratum(M::QuiverModuli, tau) + setting = local_quiver_setting(M, tau) + return is_coregular(setting["Q"], setting["d"]) end """ @@ -820,10 +827,7 @@ function is_smooth(M::QuiverModuliSpace) # coregularity of its local quiver setting, by combining the étale-local description # of [MR1972892] with [Theorem 2.1, MR1929191]; this is the globalization of # [Theorem 4.2, MR1929191] to arbitrary stability parameters - return all(all_luna_types(M)) do tau - setting = local_quiver_setting(M, tau) - is_coregular(setting["Q"], setting["d"]) - end + return all(tau -> __is_smooth_stratum(M, tau), all_luna_types(M)) end """ @@ -870,10 +874,7 @@ function codimension_singular_locus(M::QuiverModuliSpace) # the stratum of a Luna type consists of singular points if and only if its local # quiver setting is not coregular; the stable stratum is always smooth - singular = filter(all_luna_types(M)) do tau - setting = local_quiver_setting(M, tau) - !is_coregular(setting["Q"], setting["d"]) - end + singular = filter(tau -> !__is_smooth_stratum(M, tau), all_luna_types(M)) isempty(singular) && return Inf return dimension(M) - maximum(dimension_of_luna_stratum(M, tau) for tau in singular) end From 7b3c4c07022b453389305459f4252ee547d78249 Mon Sep 17 00:00:00 2001 From: Pieter Belmans Date: Tue, 25 Aug 2026 18:26:59 +0200 Subject: [PATCH 12/20] refactor: name weakly connected components --- src/RepresentationTheory.jl | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/RepresentationTheory.jl b/src/RepresentationTheory.jl index e35489c..e7f021f 100644 --- a/src/RepresentationTheory.jl +++ b/src/RepresentationTheory.jl @@ -532,6 +532,11 @@ function __wedge_step(A::Matrix{Int}, d::Vector{Int}) return nothing end +# the weakly connected components of the quiver with adjacency matrix A, as the +# strongly connected components of its double +__weakly_connected_components(A::Matrix{Int}) = + strongly_connected_components(Quiver(A + A')) + # Split a strongly connected quiver setting into its prime components, i.e., the # summands of its decomposition as an iterated connected sum at vertices of # dimension 1; a setting is cofree iff its prime components are [Lemma 3]. @@ -541,8 +546,7 @@ function __prime_components(A::Matrix{Int}, d::Vector{Int}) # the summands at v are the weakly connected components of the quiver minus v, # each taken together with v and the arrows between them, and every loop at v others = setdiff(1:n, v) - U = A[others, others] - pieces = strongly_connected_components(Quiver(U + U')) + pieces = __weakly_connected_components(A[others, others]) length(pieces) + A[v, v] >= 2 || continue out = [(fill(1, 1, 1), [1]) for _ in 1:A[v, v]] for piece in pieces From 7530430c213dc6dabc7ef560693b470a58b46b74 Mon Sep 17 00:00:00 2001 From: Pieter Belmans Date: Fri, 10 Jul 2026 15:22:08 +0200 Subject: [PATCH 13/20] docs: document the summands of a local quiver setting --- src/Moduli.jl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Moduli.jl b/src/Moduli.jl index 7c0ab48..cd6804a 100644 --- a/src/Moduli.jl +++ b/src/Moduli.jl @@ -530,7 +530,9 @@ Returns the local quiver and dimension vector for the given Luna type. # Output -- a dictionary with the local quiver `Q` and dimension vector `d` for the given Luna type. +- a dictionary with the local quiver `Q`, its dimension vector `d`, and the list + `summands` of the dimension vectors of the stable summands, one for each vertex of + the local quiver, ordered compatibly with `d`. """ function local_quiver_setting(M::QuiverModuli, tau) if !is_luna_type(M, tau) From 3682b6d4f494eddb2fea3103ac61bd1b18394189 Mon Sep 17 00:00:00 2001 From: Pieter Belmans Date: Tue, 25 Aug 2026 18:29:22 +0200 Subject: [PATCH 14/20] fix: harden core quiver-setting APIs --- src/Moduli.jl | 36 ++++++++++++++++++++--------- src/RepresentationTheory.jl | 38 +++++++++++++++--------------- test/runtests.jl | 46 +++++++++++++++++++++++++++++-------- 3 files changed, 81 insertions(+), 39 deletions(-) diff --git a/src/Moduli.jl b/src/Moduli.jl index cd6804a..77c890e 100644 --- a/src/Moduli.jl +++ b/src/Moduli.jl @@ -461,6 +461,16 @@ function is_luna_type(M::QuiverModuli, tau) end ks = collect(keys(tau)) + isempty(ks) && return false + if !all( + e -> length(e) == n_vertices(M.Q) && all(>=(0), e) && any(>(0), e), + ks, + ) + return false + end + if !all(e -> !isempty(tau[e]) && all(>(0), tau[e]), ks) + return false + end # each key `e` contributes `sum(tau[e])` copies of `e` (one per multiplicity in its list) if sum(sum(tau[e]) * e for e in ks) != M.d return false @@ -469,10 +479,12 @@ function is_luna_type(M::QuiverModuli, tau) return false end - if !all(has_semistables(M.Q, e, M.theta, M.denom) for e in ks) + if !all(has_stables(M.Q, e, M.theta, M.denom) for e in ks) return false end - return true + # A rigid stable representation is unique up to isomorphism, so its dimension + # vector cannot encode several distinct stable summands in one Luna type. + return all(e -> length(tau[e]) == 1 || euler_form(M.Q, e, e) <= 0, ks) end """ @@ -513,6 +525,8 @@ julia> dimension_of_luna_stratum(M, Dict([0, 0] => [1])) ``` """ function dimension_of_luna_stratum(M::QuiverModuli, tau) + is_luna_type(M, tau) || + throw(DomainError(tau, "not a Luna type for the given moduli problem")) # the formula below would give 1 for the zero dimension vector sum(M.d) == 0 && return 0 return sum(length(tau[e]) * (1 - euler_form(M.Q, e, e)) for e in collect(keys(tau))) @@ -530,13 +544,13 @@ Returns the local quiver and dimension vector for the given Luna type. # Output -- a dictionary with the local quiver `Q`, its dimension vector `d`, and the list - `summands` of the dimension vectors of the stable summands, one for each vertex of - the local quiver, ordered compatibly with `d`. +- a named tuple `(Q, d, summands)` containing the local quiver, its dimension vector, + and the dimension vectors of the stable summands, one for each vertex of the local + quiver, ordered compatibly with `d`. """ function local_quiver_setting(M::QuiverModuli, tau) if !is_luna_type(M, tau) - throw(DomainError("Not a Luna type")) + throw(DomainError(tau, "not a Luna type for the given moduli problem")) end # one local vertex per distinct stable summand, i.e. per entry of each multiplicity list; @@ -551,14 +565,14 @@ function local_quiver_setting(M::QuiverModuli, tau) Qloc = Quiver(A) dloc = [m for e in keys(tau) for m in tau[e]] - return Dict("Q" => Qloc, "d" => dloc, "summands" => summands) + return (Q=Qloc, d=dloc, summands=summands) end # whether the local quiver setting of the Luna type is coregular, i.e., whether the # moduli space is smooth along the corresponding stratum function __is_smooth_stratum(M::QuiverModuli, tau) setting = local_quiver_setting(M, tau) - return is_coregular(setting["Q"], setting["d"]) + return is_coregular(setting.Q, setting.d) end """ @@ -735,7 +749,7 @@ end function _dimension(M::QuiverModuliSpace) # the zero representation is semistable, but not stable, for d = 0 !is_connected(M.Q) && - raise(ArgumentError("Q is not connected, M has disjoint connected components.")) + throw(ArgumentError("Q is not connected, M has disjoint connected components.")) if all(M.d .== 0) if M.condition == "semistable" @@ -755,10 +769,10 @@ function _dimension(M::QuiverModuliSpace) if M.condition == "stable" return -Inf elseif M.condition == "semistable" - if has_semistables(M.Q, M.d, M.theta) + if has_semistables(M.Q, M.d, M.theta, M.denom) return maximum( dimension_of_luna_stratum(M, tau) for - tau in all_luna_types(M.Q, M.d, M.theta) + tau in all_luna_types(M.Q, M.d, M.theta, M.denom) ) end end diff --git a/src/RepresentationTheory.jl b/src/RepresentationTheory.jl index e7f021f..34141ee 100644 --- a/src/RepresentationTheory.jl +++ b/src/RepresentationTheory.jl @@ -68,6 +68,14 @@ euler_form(Q::Quiver, x::AbstractVector{Int}, y::AbstractVector{Int}) = # this inlines x' * (I - adjacency) * y: retrieving the memoized Euler matrix # costs more than recomputing the two products +# Validate the common public contract for a dimension vector on `Q`. +function __check_dimension_vector(Q::Quiver, d::AbstractVector{Int}) + length(d) == n_vertices(Q) || + throw(ArgumentError("dimension vector must have length $(n_vertices(Q))")) + all(>=(0), d) || throw(ArgumentError("dimension vector must be non-negative")) + return nothing +end + ######################################################################################## # Canonical decomposition ######################################################################################## @@ -367,7 +375,7 @@ the reduced setting is, which is what [`is_coregular`](@ref) exploits. # Output -- a dictionary with the reduced quiver `Q` and dimension vector `d`. +- a named tuple `(Q, d)` containing the reduced quiver and dimension vector. # Examples @@ -379,10 +387,10 @@ julia> Q = Quiver("1-2, 2-2, 2-1"); julia> setting = bocklandt_reduction(Q, [1, 2]); -julia> setting["Q"] +julia> setting.Q Quiver with adjacency matrix [0;;] -julia> setting["d"] +julia> setting.d 1-element Vector{Int64}: 1 ``` @@ -392,20 +400,17 @@ A reduced setting is returned unchanged: ```jldoctest julia> setting = bocklandt_reduction(Quiver("1--2, 2--1"), [1, 1]); -julia> setting["Q"] +julia> setting.Q Quiver with adjacency matrix [0 2; 2 0] -julia> setting["d"] +julia> setting.d 2-element Vector{Int64}: 1 1 ``` """ function bocklandt_reduction(Q::Quiver, d::AbstractVector{Int}) - length(d) == n_vertices(Q) || - throw(ArgumentError("dimension vector must have length $(n_vertices(Q))")) - all(di >= 0 for di in d) || - throw(ArgumentError("dimension vector must be non-negative")) + __check_dimension_vector(Q, d) # vertices of dimension 0 and arrows between different strongly connected components # play no role in the invariant theory [Lemma 2.4, MR1929191] @@ -417,11 +422,11 @@ function bocklandt_reduction(Q::Quiver, d::AbstractVector{Int}) __bocklandt_reduce(A[vertices[c], vertices[c]], Vector{Int}(d[vertices[c]])) for c in components ] - return Dict( - "Q" => reduce( + return ( + Q=reduce( disjoint_union, [Quiver(B) for (B, _) in reduced]; init=Quiver(zeros(Int, 0, 0)) ), - "d" => reduce(vcat, [e for (_, e) in reduced]; init=Int[]), + d=reduce(vcat, [e for (_, e) in reduced]; init=Int[]), ) end @@ -476,10 +481,10 @@ true """ function is_coregular(Q::Quiver, d::AbstractVector{Int}) setting = bocklandt_reduction(Q, d) - A, e = setting["Q"].adjacency, setting["d"] + A, e = setting.Q.adjacency, setting.d return all( length(c) == 1 && (A[c[1], c[1]] <= 1 || (A[c[1], c[1]], e[c[1]]) == (2, 2)) for - c in strongly_connected_components(setting["Q"]) + c in strongly_connected_components(setting.Q) ) end @@ -688,10 +693,7 @@ julia> is_coregular(Q, [2, 4]), is_cofree(Q, [2, 4]) ``` """ function is_cofree(Q::Quiver, d::AbstractVector{Int}) - length(d) == n_vertices(Q) || - throw(ArgumentError("dimension vector must have length $(n_vertices(Q))")) - all(di >= 0 for di in d) || - throw(ArgumentError("dimension vector must be non-negative")) + __check_dimension_vector(Q, d) # vertices of dimension 0 do not contribute, and arrows between different strongly # connected components only contribute a free matrix factor [Lemma 3] diff --git a/test/runtests.jl b/test/runtests.jl index d67ab8a..fc79524 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -102,14 +102,29 @@ end; @test is_luna_type(M, Dict([1, 1] => [1], [2, 2] => [1])) # [1,1] + [2,2] = [3,3] @test !is_luna_type(M, Dict([1, 1] => [2])) # 2*[1,1] = [2,2] != [3,3] + # The encoding requires nonzero dimension vectors, nonempty lists of positive + # multiplicities, and stable (not merely semistable) summands. + @test !is_luna_type(M, Dict([1, 1] => Int[])) + @test !is_luna_type(M, Dict([1, 1] => [-1], [2, 2] => [2])) + @test !is_luna_type(M, Dict([1, 1, 0] => [3])) + @test !is_luna_type(QuiverModuliSpace(kronecker_quiver(2), [2, 2]), Dict([2, 2] => [1])) + + # A rigid stable summand can occur with higher multiplicity, but there cannot be + # two distinct stable summands of that dimension vector. + R = QuiverModuliSpace(Q, [2, 0]) + @test is_luna_type(R, Dict([1, 0] => [2])) + @test !is_luna_type(R, Dict([1, 0] => [1, 1])) + @test_throws DomainError dimension_of_luna_stratum(R, Dict([1, 0] => [1, 1])) + # Local quiver at a stable point is the g-loop quiver on one vertex with # g = 1 - = dim M^s. For the 3-Kronecker quiver and d = (2,2) this is g = 5. # (This is the value from the definition in MR1972892; it intentionally differs from # QuiverTools/Sage, which returns 4 via general_ext and undercounts the diagonal.) X = QuiverModuliSpace(Q, [2, 2]) loc = QuiverTools.local_quiver_setting(X, Dict([2, 2] => [1])) - @test loc["d"] == [1] - @test Matrix(loc["Q"].adjacency) == fill(5, 1, 1) + @test propertynames(loc) == (:Q, :d, :summands) + @test loc.d == [1] + @test Matrix(loc.Q.adjacency) == fill(5, 1, 1) # Luna strata of M(2d) ≅ P^2 for the subspace quiver Q^(4) = affine D4 with # d = (1,1,1,1;2). There are five polystable types; we check their local quivers @@ -122,9 +137,9 @@ end; # whether the local quiver is symmetric (i.e. only loops and 2-cycles). function fingerprint(tau) s = QuiverTools.local_quiver_setting(N, tau) - A = Matrix(s["Q"].adjacency) + A = Matrix(s.Q.adjacency) loops = [A[i, i] for i in 1:size(A, 1)] - (sort(s["d"]), sort(loops), sort(vec(A)), A == permutedims(A)) + (sort(s.d), sort(loops), sort(vec(A)), A == permutedims(A)) end eK, eKb, eL, eLb = [1, 1, 0, 0, 1], [0, 0, 1, 1, 1], [1, 0, 1, 0, 1], [0, 1, 0, 1, 1] # ξ1 = (d, d): two vertices, a loop on each, no arrows between them @@ -182,6 +197,16 @@ end; end; @testset "Bocklandt reduction" begin + # The public quiver-setting routines enforce the dimension-vector contract. + for f in (bocklandt_reduction, is_coregular, is_cofree) + @test_throws ArgumentError f(jordan_quiver(1), [1, 1]) + @test_throws ArgumentError f(jordan_quiver(1), [-1]) + end + + # The dimension API rejects disconnected quivers with the documented exception. + disconnected = disjoint_union(kronecker_quiver(1), kronecker_quiver(1)) + @test_throws ArgumentError dimension(QuiverModuliSpace(disconnected, [1, 1, 1, 1])) + # invariants of pairs of 2x2 matrices form a polynomial ring, of 3x3 they do not, # and neither do those of triples of 2x2 matrices; a single matrix always does @test is_coregular(jordan_quiver(2), [2]) @@ -201,17 +226,18 @@ end; # the reduction combines R_III, R_I and R_II to a lone vertex of dimension 1 setting = bocklandt_reduction(Quiver("1-2, 2-2, 2-1"), [1, 2]) - @test n_vertices(setting["Q"]) == 1 - @test n_arrows(setting["Q"]) == 0 - @test setting["d"] == [1] + @test propertynames(setting) == (:Q, :d) + @test n_vertices(setting.Q) == 1 + @test n_arrows(setting.Q) == 0 + @test setting.d == [1] # a reduced setting is returned unchanged setting = bocklandt_reduction(Quiver("1--2, 2--1"), [1, 1]) - @test Matrix(setting["Q"].adjacency) == [0 2; 2 0] - @test setting["d"] == [1, 1] + @test Matrix(setting.Q.adjacency) == [0 2; 2 0] + @test setting.d == [1, 1] # vertices of dimension 0 and arrows between strongly connected components are dropped - @test bocklandt_reduction(kronecker_quiver(3), [2, 0])["d"] == [2] + @test bocklandt_reduction(kronecker_quiver(3), [2, 0]).d == [2] @test is_coregular(Quiver("1-1, 1-2, 2-2"), [2, 2]) @test is_coregular(kronecker_quiver(3), [0, 0]) From a85d3feeeb6b02f011a274fa2aa041cb8c4fedca Mon Sep 17 00:00:00 2001 From: Pieter Belmans Date: Tue, 25 Aug 2026 13:44:29 +0200 Subject: [PATCH 15/20] test: cover longer reachability paths --- src/Quivers.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Quivers.jl b/src/Quivers.jl index efb365b..aab91e2 100644 --- a/src/Quivers.jl +++ b/src/Quivers.jl @@ -127,9 +127,9 @@ is not an issue for the quivers we consider. # Examples ```jldoctest -julia> strongly_connected_components(cyclic_quiver(3)) +julia> strongly_connected_components(cyclic_quiver(4)) # 1 → 4 needs a path of length 3 1-element Vector{Vector{Int64}}: - [1, 2, 3] + [1, 2, 3, 4] julia> strongly_connected_components(kronecker_quiver(3)) 2-element Vector{Vector{Int64}}: @@ -146,6 +146,7 @@ function strongly_connected_components(Q::Quiver) n = n_vertices(Q) # reflexive-transitive closure by Floyd--Warshall [doi:10.1145/321105.321107] reachable = [i == j || Q.adjacency[i, j] > 0 for i in 1:n, j in 1:n] + # After the kth outer pass, paths may use any intermediate vertex in 1:k. for k in 1:n, i in 1:n, j in 1:n reachable[i, j] |= reachable[i, k] && reachable[k, j] end From 257b5103e40d4c0714194c0bcc5719ae7d7f4301 Mon Sep 17 00:00:00 2001 From: Pieter Belmans Date: Tue, 25 Aug 2026 13:44:30 +0200 Subject: [PATCH 16/20] refactor: relocate dimension validation --- src/Misc.jl | 8 ++++++++ src/QuiverTools.jl | 2 +- src/RepresentationTheory.jl | 8 -------- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Misc.jl b/src/Misc.jl index e4a5cc7..c0b9b80 100644 --- a/src/Misc.jl +++ b/src/Misc.jl @@ -2,6 +2,14 @@ # Misc ###### +# Validate the common public contract for a dimension vector on `Q`. +function __check_dimension_vector(Q::Quiver, d::AbstractVector{Int}) + length(d) == n_vertices(Q) || + throw(ArgumentError("dimension vector must have length $(n_vertices(Q))")) + all(>=(0), d) || throw(ArgumentError("dimension vector must be non-negative")) + return nothing +end + """ identity_matrix(n::Int) diff --git a/src/QuiverTools.jl b/src/QuiverTools.jl index 11d2129..21944b9 100644 --- a/src/QuiverTools.jl +++ b/src/QuiverTools.jl @@ -135,9 +135,9 @@ end include("Types.jl") include("Quivers.jl") +include("Misc.jl") include("Stability.jl") include("RepresentationTheory.jl") -include("Misc.jl") include("Constructors.jl") include("Moduli.jl") include("Hodge.jl") diff --git a/src/RepresentationTheory.jl b/src/RepresentationTheory.jl index 34141ee..451209e 100644 --- a/src/RepresentationTheory.jl +++ b/src/RepresentationTheory.jl @@ -68,14 +68,6 @@ euler_form(Q::Quiver, x::AbstractVector{Int}, y::AbstractVector{Int}) = # this inlines x' * (I - adjacency) * y: retrieving the memoized Euler matrix # costs more than recomputing the two products -# Validate the common public contract for a dimension vector on `Q`. -function __check_dimension_vector(Q::Quiver, d::AbstractVector{Int}) - length(d) == n_vertices(Q) || - throw(ArgumentError("dimension vector must have length $(n_vertices(Q))")) - all(>=(0), d) || throw(ArgumentError("dimension vector must be non-negative")) - return nothing -end - ######################################################################################## # Canonical decomposition ######################################################################################## From a0131531bc54d601e0ca630746aa66eed42ea4e6 Mon Sep 17 00:00:00 2001 From: Pieter Belmans Date: Tue, 25 Aug 2026 13:47:04 +0200 Subject: [PATCH 17/20] docs: clarify Boolean reachability --- src/Quivers.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Quivers.jl b/src/Quivers.jl index aab91e2..7be361c 100644 --- a/src/Quivers.jl +++ b/src/Quivers.jl @@ -111,8 +111,8 @@ Compute the strongly connected components of `Q`. Two vertices belong to the same strongly connected component if and only if they are connected by paths in both directions. The reachability relation is -computed as the reflexive-transitive closure of the adjacency relation, using the -Floyd--Warshall algorithm in its original, Boolean, form +computed as the reflexive-transitive closure of the adjacency relation, using +Warshall's Boolean transitive-closure algorithm [[Warshall](https://doi.org/10.1145/321105.321107)]; its ``O(n^3)`` running time is not an issue for the quivers we consider. @@ -144,9 +144,9 @@ julia> strongly_connected_components(Quiver("1-2,2-1,2-3")) """ function strongly_connected_components(Q::Quiver) n = n_vertices(Q) - # reflexive-transitive closure by Floyd--Warshall [doi:10.1145/321105.321107] + # `reachable[i, j]` records existence of a path, not the number of paths. reachable = [i == j || Q.adjacency[i, j] > 0 for i in 1:n, j in 1:n] - # After the kth outer pass, paths may use any intermediate vertex in 1:k. + # After the kth outer Warshall pass, paths may use any intermediate vertex in 1:k. for k in 1:n, i in 1:n, j in 1:n reachable[i, j] |= reachable[i, k] && reachable[k, j] end From d58f2ff623732cce20410f61df0326bc8d1a4cb2 Mon Sep 17 00:00:00 2001 From: Pieter Belmans Date: Tue, 25 Aug 2026 14:53:48 +0200 Subject: [PATCH 18/20] docs: clarify validation for Luna types --- src/Misc.jl | 2 +- src/Moduli.jl | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Misc.jl b/src/Misc.jl index c0b9b80..43a24e6 100644 --- a/src/Misc.jl +++ b/src/Misc.jl @@ -2,7 +2,7 @@ # Misc ###### -# Validate the common public contract for a dimension vector on `Q`. +# Shared validator for public quiver-setting functions that accept `(Q, d)` directly. function __check_dimension_vector(Q::Quiver, d::AbstractVector{Int}) length(d) == n_vertices(Q) || throw(ArgumentError("dimension vector must have length $(n_vertices(Q))")) diff --git a/src/Moduli.jl b/src/Moduli.jl index 77c890e..cd5b475 100644 --- a/src/Moduli.jl +++ b/src/Moduli.jl @@ -460,6 +460,8 @@ function is_luna_type(M::QuiverModuli, tau) return tau == Dict(M.d => [1]) end + # A nonzero Luna type has at least one nonzero dimension vector of the correct length, + # and every dimension vector has a nonempty list of positive multiplicities. ks = collect(keys(tau)) isempty(ks) && return false if !all( From 435d53b9d9b021848355b9e52330d7cfa8c16615 Mon Sep 17 00:00:00 2001 From: Pieter Belmans Date: Tue, 25 Aug 2026 15:19:09 +0200 Subject: [PATCH 19/20] refactor: inline Bocklandt reduction loop --- src/RepresentationTheory.jl | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/src/RepresentationTheory.jl b/src/RepresentationTheory.jl index 451209e..cd19952 100644 --- a/src/RepresentationTheory.jl +++ b/src/RepresentationTheory.jl @@ -289,6 +289,8 @@ function __bocklandt_step(A::Matrix{Int}, d::Vector{Int}) n = length(d) # the vectors of \chi(d, e_v) and \chi(e_v, d), for e_v the unit vector at v chi_in, chi_out = d - A' * d, d - A * d + # The three conditions are mutually exclusive at a fixed vertex, and the final + # reduced setting is independent of the chosen moves [Theorem 6, arXiv:math/0207250]. for v in 1:n # R_I [Lemma 3.2, MR1929191]: remove a loopless vertex whose incoming or outgoing # paths carry at most d[v] dimensions, shortcutting every path through it; a lone @@ -324,15 +326,6 @@ function __bocklandt_step(A::Matrix{Int}, d::Vector{Int}) return nothing end -# fully reduce a strongly connected quiver setting, i.e., apply reduction steps until -# the setting is reduced in the sense of [Definition 3.1, MR1929191] -function __bocklandt_reduce(A::Matrix{Int}, d::Vector{Int}) - while (step = __bocklandt_step(A, d)) !== nothing - A, d = step - end - return A, d -end - """ bocklandt_reduction(Q::Quiver, d::AbstractVector{Int}) @@ -410,10 +403,13 @@ function bocklandt_reduction(Q::Quiver, d::AbstractVector{Int}) vertices = support(d) components = strongly_connected_components(Quiver(A[vertices, vertices])) - reduced = [ - __bocklandt_reduce(A[vertices[c], vertices[c]], Vector{Int}(d[vertices[c]])) for - c in components - ] + reduced = map(components) do c + B, e = A[vertices[c], vertices[c]], Vector{Int}(d[vertices[c]]) + while (step = __bocklandt_step(B, e)) !== nothing + B, e = step + end + return B, e + end return ( Q=reduce( disjoint_union, [Quiver(B) for (B, _) in reduced]; init=Quiver(zeros(Int, 0, 0)) From 8f0fd0142a0bac75a7470b2538e7f00123ee58a4 Mon Sep 17 00:00:00 2001 From: Pieter Belmans Date: Tue, 25 Aug 2026 18:29:49 +0200 Subject: [PATCH 20/20] docs: fix moduli stack smoothness typo --- src/Moduli.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Moduli.jl b/src/Moduli.jl index cd5b475..5a40a2c 100644 --- a/src/Moduli.jl +++ b/src/Moduli.jl @@ -902,7 +902,7 @@ end Checks if the moduli stack is smooth. -This is always trus, as the quotient stack of a smooth variety is smooth. +This is always true, as the quotient stack of a smooth variety is smooth. # Input