From f373fe66fc88ca04bb39e847680c64b8d0aa5952 Mon Sep 17 00:00:00 2001 From: Pieter Belmans Date: Fri, 31 Jul 2026 15:45:24 +0200 Subject: [PATCH 1/7] feat: intersection cohomology of singular quiver moduli Meinhardt--Reineke [MR4000572] identify the Donaldson--Thomas invariants of a quiver with the intersection cohomology of its moduli space of semistable representations. The Harder--Narasimhan recursion behind motive() already gives the class of the semistable stack for any dimension vector, so only the plethystic logarithm is left to add. --- src/Hodge.jl | 321 +++++++++++++++++++++++++++++++++++++++++++++ src/QuiverTools.jl | 5 + 2 files changed, 326 insertions(+) diff --git a/src/Hodge.jl b/src/Hodge.jl index 8a903ee..132fdc3 100644 --- a/src/Hodge.jl +++ b/src/Hodge.jl @@ -596,3 +596,324 @@ function motive( # this branch agrees with the trivial-stability branch above (see issue #36) return -solve(T, y)[1] end + +######################################################################################## +# Intersection cohomology of quiver moduli spaces. +# +# When `d` and `theta` admit properly semistable representations the moduli space is +# singular and its ordinary cohomology is not determined by the Harder--Narasimhan +# recursion. Meinhardt--Reineke +# [[MR4000572](https://mathscinet.ams.org/mathscinet-getitem?mr=4000572)] identify the +# Donaldson--Thomas invariants of the quiver with the intersection cohomology of the +# moduli space, which makes it computable from the motives of the semistable stacks. +# +# With ``\Lambda_\mu`` the monoid of dimension vectors of the slope of ``d``, their +# Lemma in §3.1 and Theorem 3.4 read +# +# \sum_{e \in \Lambda_\mu} L^{(e, e)/2} [\mathfrak{M}^{ss}_e] t^e +# = Exp((\sum_{0 \neq e \in \Lambda_\mu} DT_e t^e)/(L^{1/2} - L^{-1/2})), +# E(IH^*(M^{ss}_d)) = L^{\dim/2} DT_d, \dim M^{ss}_d = 1 - (d, d), +# +# where ``(-, -)`` is the Euler form and ``Exp`` is the plethystic exponential. +# The motive of the stack ``\mathfrak{M}^{ss}_e`` is what `motive` computes. +# +# Quiver moduli have Hodge structures concentrated on the diagonal, so everything in +# sight is a rational function in the Lefschetz class alone, and the half powers only +# need a square root `w` of it, with ``L^{1/2} = -w`` because ``L^{1/2}`` sits in odd +# degree. The Adams operations are then the substitutions ``w \mapsto w^n``. +# +# For a dimension vector which is primitive in ``\Lambda_\mu`` the plethystic logarithm +# is its own leading term, and the answer is the ordinary Poincaré polynomial again. +######################################################################################## + +""" + _mobius(n::Int) + +Return the Möbius function ``\\mu(n)``. + +This is an internal method, only used in the plethystic logarithm computing +intersection cohomology. + +# Examples + +```jldoctest +julia> QuiverTools._mobius.(1:10) +10-element Vector{Int64}: + 1 + -1 + -1 + 0 + -1 + 1 + -1 + 0 + 0 + 1 +``` +""" +function _mobius(n::Int) + result = 1 + for p in 2:n # the range is fixed before the loop divides `n` down + if n % p == 0 + n = n ÷ p + n % p == 0 && return 0 + result = -result + end + end + return result +end + +""" + intersection_poincare_polynomial(M::QuiverModuliSpace) + +Compute the Poincaré polynomial of the intersection cohomology of the moduli space `M`. + +The algorithm is the one of +[[MR4000572](https://mathscinet.ams.org/mathscinet-getitem?mr=4000572)], which identifies +the Donaldson--Thomas invariants of the quiver with the intersection cohomology of +``M^{ss}_\\theta(Q, \\mathbf{d})``. It needs `M.theta` to be generic for the slope of +`M.d`, meaning that the antisymmetrized Euler form vanishes on the dimension vectors of +that slope, and it needs stable representations to exist; both are checked. + +The moduli space is smooth exactly when no proper subdimension vector has the slope of +`M.d`, and there intersection cohomology is ordinary cohomology, so this agrees with +[`poincare_polynomial`](@ref). + +# Input + +- `M::QuiverModuliSpace`: a moduli space of representations of a quiver. + +# Output + +- the Poincaré polynomial of the intersection cohomology of the moduli space. + +# Examples + +The moduli space for the 3-Kronecker quiver and dimension vector `[2, 2]` is singular, +as `[1, 1]` has the same slope, and it has the intersection cohomology of ``\\mathbb{P}^5``: +```jldoctest +julia> Q = kronecker_quiver(3); + +julia> M = QuiverModuliSpace(Q, [2, 2]); + +julia> intersection_poincare_polynomial(M) +L^5 + L^4 + L^3 + L^2 + L + 1 +``` + +Reflection functors identify moduli spaces for different dimension vectors: +```jldoctest +julia> Q = kronecker_quiver(4); + +julia> M = QuiverModuliSpace(Q, [3, 3]); N = QuiverModuliSpace(Q, [3, 9]); + +julia> intersection_poincare_polynomial(M) == intersection_poincare_polynomial(N) +true +``` + +In the smooth case this is the ordinary Poincaré polynomial: +```jldoctest +julia> M = QuiverModuliSpace(kronecker_quiver(3), [2, 3]); + +julia> intersection_poincare_polynomial(M) +L^6 + L^5 + 3*L^4 + 3*L^3 + 3*L^2 + L + 1 + +julia> intersection_poincare_polynomial(M) == poincare_polynomial(M) +true +``` + +There is nothing to compute if no representation of dimension vector `M.d` is stable: +```jldoctest +julia> M = QuiverModuliSpace(kronecker_quiver(2), [2, 2]); + +julia> intersection_poincare_polynomial(M) +ERROR: ArgumentError: there are no stable representations of dimension vector [2, 2] +``` +""" +function intersection_poincare_polynomial(M::QuiverModuliSpace) + M.condition == "semistable" || throw( + ArgumentError("intersection cohomology is computed for the semistable moduli space") + ) + Q, d, theta, denom = M.Q, M.d, M.theta, M.denom + + # the dimension vectors of the slope of `d`, i.e. the part of the monoid + # ``\Lambda_\mu`` below `d`; the zero vector is left out and treated separately + lattice = filter( + e -> slope(e, theta, denom) == slope(d, theta, denom), + all_subdimension_vectors(d; nonzero=true), + ) + all(euler_form(Q, e, f) == euler_form(Q, f, e) for e in lattice, f in lattice) || throw( + ArgumentError( + "the stability parameter is not generic for the slope of $(Vector(d)), " * + "so intersection cohomology is out of reach", + ), + ) + + R, ws = polynomial_ring(Singular.QQ, ["w"]) + w = ws[1] + F = fraction_field(R) + # the Adams operation ``\psi^n``, and the passage from the Lefschetz class to `w` + psi(x, n) = F(numerator(x)(w^n))//F(denominator(x)(w^n)) + half(m) = + F(Singular.n_transExt_to_spoly(numerator(m))(w^2)) // + F(Singular.n_transExt_to_spoly(denominator(m))(w^2)) + + # the generating series, without its constant term + E = typeof(F(w)) + series = Dict{Vector{Int},E}( + e => power(F(-w), euler_form(Q, e, e)) * half(motive(Q, e, theta, denom)) for + e in lattice + ) + + # the ordinary logarithm ``\log(1 + x) = \sum_k (-1)^{k-1}/k x^k``; the kth power is + # supported on sums of k nonzero dimension vectors, so the sum stops at ``|d|`` + logarithm = Dict{Vector{Int},E}() + term = series + for k in 1:sum(d) + isempty(term) && break + scale = F((-1)^(k - 1))//F(k) + for (e, value) in term + logarithm[e] = get(logarithm, e, zero(F)) + scale * value + end + # multiply by `series`, dropping everything that is no longer below `d` + next = Dict{Vector{Int},E}() + for (e, value) in term, (f, factor) in series + is_subdimension_vector(e + f, d) || continue + next[e + f] = get(next, e + f, zero(F)) + value * factor + end + term = next + end + + # and the plethystic one, ``Log(1 + x) = \sum_n \mu(n)/n \psi^n(\log(1 + x))``; only + # those `n` with `n * e = d` for some `e` contribute, i.e. the divisors of `gcd(d)` + total = get(logarithm, Vector{Int}(d), zero(F)) + g = gcd(d) + for k in 2:g + (g % k == 0 && _mobius(k) != 0) || continue + piece = get(logarithm, Vector{Int}(d .÷ k), zero(F)) + total += F(_mobius(k))//F(k) * psi(piece, k) + end + + # ``DT_d = (L^{1/2} - L^{-1/2}) [Log Q]_{t^d}``, then ``E(IH^*) = L^{\dim/2} DT_d`` + root = F(-w) + result = power(root, 1 - euler_form(Q, d, d)) * (root - inv(root)) * total + iszero(result) && throw( + ArgumentError("there are no stable representations of dimension vector $(Vector(d))") + ) + isone(denominator(result)) || + throw(DomainError("intersection cohomology is not polynomial")) + + # intersection cohomology of these moduli spaces is concentrated in even degree, so the + # answer has to be a polynomial in `w^2`; that is a real check on the whole computation + S, Ls = polynomial_ring(Singular.QQ, ["L"]) + L = Ls[1] + invariant = numerator(result) + P = zero(S) + for (c, e) in + zip(Singular.coefficients(invariant), Singular.exponent_vectors(invariant)) + isodd(e[1]) && throw(DomainError("intersection cohomology in odd degree")) + P += S(c) * L^(e[1] ÷ 2) + end + return P +end + +""" + intersection_betti_numbers(M::QuiverModuliSpace) + +Compute the Betti numbers of the intersection cohomology of the moduli space `M`. + +See [`intersection_poincare_polynomial`](@ref) for the algorithm and its hypotheses. + +# Input + +- `M::QuiverModuliSpace`: a moduli space of representations of a quiver. + +# Output + +- a list of intersection Betti numbers of the moduli space, indexed by cohomological + degree ``0, \\dots, 2\\dim M``. + +# Examples + +The singular moduli space for the 3-Kronecker quiver and dimension vector `[2, 2]`: +```jldoctest +julia> M = QuiverModuliSpace(kronecker_quiver(3), [2, 2]); + +julia> intersection_betti_numbers(M) +11-element Vector{Int64}: + 1 + 0 + 1 + 0 + 1 + 0 + 1 + 0 + 1 + 0 + 1 +``` + +In the smooth case these are the ordinary Betti numbers: +```jldoctest +julia> M = QuiverModuliSpace(kronecker_quiver(3), [2, 3]); + +julia> intersection_betti_numbers(M) == betti_numbers(M) +true +``` +""" +function intersection_betti_numbers(M::QuiverModuliSpace) + P = intersection_poincare_polynomial(M) + # entry 2k + 1 is the coefficient of L^k in P, i.e. the intersection Betti number + # in cohomological degree 2k; all odd ones vanish + betti = zeros(Int, 2 * dimension(M) + 1) + for (c, e) in zip(Singular.coefficients(P), Singular.exponent_vectors(P)) + betti[2 * e[1] + 1] = Int(numerator(c)) + end + return betti +end + +""" + intersection_hodge_diamond(M::QuiverModuliSpace) + +Compute the Hodge diamond of the intersection cohomology of the moduli space `M`. + +See [`intersection_poincare_polynomial`](@ref) for the algorithm and its hypotheses. +The Hodge structure is of Hodge--Tate type, so the diamond is concentrated on the +diagonal. + +# Input + +- `M::QuiverModuliSpace`: a moduli space of representations of a quiver. + +# Output + +- the Hodge diamond of the intersection cohomology of the moduli space. + +# Examples + +The singular moduli space for the 3-Kronecker quiver and dimension vector `[2, 2]` has +the intersection cohomology of ``\\mathbb{P}^5``: +```jldoctest +julia> M = QuiverModuliSpace(kronecker_quiver(3), [2, 2]); + +julia> intersection_hodge_diamond(M) +6×6 Matrix{Int64}: + 1 0 0 0 0 0 + 0 1 0 0 0 0 + 0 0 1 0 0 0 + 0 0 0 1 0 0 + 0 0 0 0 1 0 + 0 0 0 0 0 1 +``` + +In the smooth case this is the ordinary Hodge diamond: +```jldoctest +julia> M = QuiverModuliSpace(kronecker_quiver(3), [2, 3]); + +julia> intersection_hodge_diamond(M) == hodge_diamond(M) +true +``` +""" +function intersection_hodge_diamond(M::QuiverModuliSpace) + return Matrix{Int}(diagonal(intersection_betti_numbers(M)[1:2:end])) +end diff --git a/src/QuiverTools.jl b/src/QuiverTools.jl index 7186e68..51cc651 100644 --- a/src/QuiverTools.jl +++ b/src/QuiverTools.jl @@ -70,6 +70,11 @@ export is_nonempty, codimension_unstable_locus, dimension, is_smooth, # Hodge export hodge_diamond, hodge_polynomial, picard_rank, index, betti_numbers +# Intersection cohomology +export intersection_poincare_polynomial, + intersection_betti_numbers, + intersection_hodge_diamond + # Chow export chow_ring, motive, index, betti_numbers, poincare_polynomial, is_smooth, is_projective, From 779b756942790783fac5a91f9cc727b8f75f6bf1 Mon Sep 17 00:00:00 2001 From: Pieter Belmans Date: Fri, 31 Jul 2026 15:45:29 +0200 Subject: [PATCH 2/7] docs: list the intersection cohomology methods --- docs/src/methods.md | 1 + docs/src/methods/quiver-moduli.md | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/docs/src/methods.md b/docs/src/methods.md index 93464bb..1d9aa69 100644 --- a/docs/src/methods.md +++ b/docs/src/methods.md @@ -35,6 +35,7 @@ QuiverTools.weights_universal_bundle_on_stratum QuiverTools.local_quiver_setting QuiverTools.thin_dimension_vector QuiverTools.solve +QuiverTools._mobius QuiverTools.todd_Q QuiverTools.is_root QuiverTools.total_chern_class_universal diff --git a/docs/src/methods/quiver-moduli.md b/docs/src/methods/quiver-moduli.md index 4ee95d4..b20d125 100644 --- a/docs/src/methods/quiver-moduli.md +++ b/docs/src/methods/quiver-moduli.md @@ -94,3 +94,21 @@ hodge_diamond hodge_polynomial betti_numbers ``` + +## Intersection cohomology + +If some proper subdimension vector has the slope of ``\mathbf{d}`` there are properly +semistable representations and the moduli space is singular, so the methods above no +longer apply. Its intersection cohomology is still computable, by the theorem of +[Meinhardt--Reineke](https://mathscinet.ams.org/mathscinet-getitem?mr=4000572) +identifying the Donaldson--Thomas invariants of the quiver with it. This needs the +stability parameter to be generic for the slope of ``\mathbf{d}``, and stable +representations to exist. + +The two agree in the smooth case, where intersection cohomology is ordinary cohomology. + +```@docs +intersection_poincare_polynomial +intersection_betti_numbers +intersection_hodge_diamond +``` From e43ce0b51fae52e06a5b6005c5e1d803b19774b8 Mon Sep 17 00:00:00 2001 From: Pieter Belmans Date: Fri, 31 Jul 2026 15:45:29 +0200 Subject: [PATCH 3/7] test: intersection cohomology of quiver moduli --- test/runtests.jl | 61 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/test/runtests.jl b/test/runtests.jl index baacaa5..a06df07 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -180,3 +180,64 @@ end; @test string(poincare_polynomial(M)) == "L^6 + L^5 + 3*L^4 + 3*L^3 + 3*L^2 + L + 1" end; + +@testset "intersection cohomology" begin + # intersection cohomology agrees with ordinary cohomology on smooth moduli spaces, so + # route a few of those through the Meinhardt--Reineke branch on purpose + for (Q, d, theta) in [ + (kronecker_quiver(2), [1, 1], [1, -1]), + (kronecker_quiver(5), [1, 1], nothing), + (kronecker_quiver(4), [1, 2], [2, 1]), + (kronecker_quiver(5), [2, 3], nothing), + (Quiver([0 1 1; 0 0 2; 0 0 0]), [1, 1, 1], nothing), + ] + M = if isnothing(theta) + QuiverModuliSpace(Q, d) + else + QuiverModuliSpace(Q, d, theta) + end + @test intersection_betti_numbers(M) == betti_numbers(M) + end + + # reflection functors identify M(a, b) with M(b, m*b - a) for the m-Kronecker quiver, + # on dimension vectors whose moduli space is singular + for (m, d, e) in [ + (3, [2, 2], [2, 4]), + (3, [3, 3], [3, 6]), + (3, [2, 3], [3, 7]), + (4, [3, 3], [3, 9]), + (5, [2, 2], [2, 8]), + ] + Q = kronecker_quiver(m) + @test intersection_poincare_polynomial(QuiverModuliSpace(Q, d)) == + intersection_poincare_polynomial(QuiverModuliSpace(Q, e)) + end + + # intersection cohomology of a projective variety satisfies Poincaré duality + for (Q, d) in [ + (kronecker_quiver(3), [2, 2]), + (kronecker_quiver(4), [2, 4]), + (Quiver([0 1 1; 0 0 2; 0 0 0]), [2, 2, 2]), + ] + M = QuiverModuliSpace(Q, d) + betti = intersection_betti_numbers(M) + @test betti == reverse(betti) + @test all(b >= 0 for b in betti) + @test length(betti) == 2 * dimension(M) + 1 + end + + # the Donaldson--Thomas invariant vanishes when nothing of dimension vector `d` is + # stable, which is an exact cancellation in the plethystic logarithm + for n in 2:4 + M = QuiverModuliSpace(kronecker_quiver(2), [n, n]) + @test_throws ArgumentError intersection_poincare_polynomial(M) + end + + # the theorem needs the stability parameter to be generic for the slope of `d` + M = QuiverModuliSpace(Quiver([0 1 1 0; 0 0 1 0; 0 0 0 1; 0 0 0 0]), [3, 3, 4, 1]) + @test_throws ArgumentError intersection_poincare_polynomial(M) + + # and it describes the semistable moduli space + M = QuiverModuliSpace(kronecker_quiver(3), [2, 3], [3, -2], "stable") + @test_throws ArgumentError intersection_poincare_polynomial(M) +end; From 01a049a1b482e22ed7b1e5cb1d4fc429e7a045ae Mon Sep 17 00:00:00 2001 From: Pieter Belmans Date: Fri, 31 Jul 2026 15:59:44 +0200 Subject: [PATCH 4/7] refactor: tighten the intersection cohomology computation --- src/Hodge.jl | 64 +++++++++++++++++++++++++++++----------------------- 1 file changed, 36 insertions(+), 28 deletions(-) diff --git a/src/Hodge.jl b/src/Hodge.jl index 132fdc3..2139c2b 100644 --- a/src/Hodge.jl +++ b/src/Hodge.jl @@ -629,7 +629,7 @@ end """ _mobius(n::Int) -Return the Möbius function ``\\mu(n)``. +Return the Möbius function ``\\mu(n)``, by trial division. This is an internal method, only used in the plethystic logarithm computing intersection cohomology. @@ -652,15 +652,16 @@ julia> QuiverTools._mobius.(1:10) ``` """ function _mobius(n::Int) - result = 1 - for p in 2:n # the range is fixed before the loop divides `n` down - if n % p == 0 - n = n ÷ p - n % p == 0 && return 0 - result = -result + sign, remaining = 1, n + for divisor in 2:isqrt(n) + if iszero(remaining % divisor) + remaining ÷= divisor + iszero(remaining % divisor) && return 0 + sign = -sign end end - return result + # what is left is 1 or the one prime factor above the square root + return isone(remaining) ? sign : -sign end """ @@ -734,12 +735,12 @@ function intersection_poincare_polynomial(M::QuiverModuliSpace) ArgumentError("intersection cohomology is computed for the semistable moduli space") ) Q, d, theta, denom = M.Q, M.d, M.theta, M.denom + mu = slope(d, theta, denom) # the dimension vectors of the slope of `d`, i.e. the part of the monoid # ``\Lambda_\mu`` below `d`; the zero vector is left out and treated separately lattice = filter( - e -> slope(e, theta, denom) == slope(d, theta, denom), - all_subdimension_vectors(d; nonzero=true), + e -> slope(e, theta, denom) == mu, all_subdimension_vectors(d; nonzero=true) ) all(euler_form(Q, e, f) == euler_form(Q, f, e) for e in lattice, f in lattice) || throw( ArgumentError( @@ -751,17 +752,22 @@ function intersection_poincare_polynomial(M::QuiverModuliSpace) R, ws = polynomial_ring(Singular.QQ, ["w"]) w = ws[1] F = fraction_field(R) - # the Adams operation ``\psi^n``, and the passage from the Lefschetz class to `w` - psi(x, n) = F(numerator(x)(w^n))//F(denominator(x)(w^n)) - half(m) = - F(Singular.n_transExt_to_spoly(numerator(m))(w^2)) // - F(Singular.n_transExt_to_spoly(denominator(m))(w^2)) + E = Singular.elem_type(F) + root = F(-w) # ``L^{1/2}``, which sits in odd degree + # substitute `image` for the variable of a univariate rational function + substitute(num, den, image) = F(num(image))//F(den(image)) + adams(x, n) = substitute(numerator(x), denominator(x), w^n) + # the motive lives in ``\mathbb{Q}(L)``; move it to the square root, ``L = w^2`` + to_square_root(m) = substitute( + Singular.n_transExt_to_spoly(numerator(m)), + Singular.n_transExt_to_spoly(denominator(m)), + w^2, + ) # the generating series, without its constant term - E = typeof(F(w)) series = Dict{Vector{Int},E}( - e => power(F(-w), euler_form(Q, e, e)) * half(motive(Q, e, theta, denom)) for - e in lattice + e => power(root, euler_form(Q, e, e)) * to_square_root(motive(Q, e, theta, denom)) + for e in lattice ) # the ordinary logarithm ``\log(1 + x) = \sum_k (-1)^{k-1}/k x^k``; the kth power is @@ -770,31 +776,33 @@ function intersection_poincare_polynomial(M::QuiverModuliSpace) term = series for k in 1:sum(d) isempty(term) && break - scale = F((-1)^(k - 1))//F(k) + scale = F((-1)^(k - 1))//k for (e, value) in term logarithm[e] = get(logarithm, e, zero(F)) + scale * value end + k == sum(d) && break # multiply by `series`, dropping everything that is no longer below `d` next = Dict{Vector{Int},E}() - for (e, value) in term, (f, factor) in series + for (e, left) in term, (f, right) in series is_subdimension_vector(e + f, d) || continue - next[e + f] = get(next, e + f, zero(F)) + value * factor + next[e + f] = get(next, e + f, zero(F)) + left * right end term = next end # and the plethystic one, ``Log(1 + x) = \sum_n \mu(n)/n \psi^n(\log(1 + x))``; only # those `n` with `n * e = d` for some `e` contribute, i.e. the divisors of `gcd(d)` - total = get(logarithm, Vector{Int}(d), zero(F)) - g = gcd(d) - for k in 2:g - (g % k == 0 && _mobius(k) != 0) || continue - piece = get(logarithm, Vector{Int}(d .÷ k), zero(F)) - total += F(_mobius(k))//F(k) * psi(piece, k) + target = Vector{Int}(d) + total = get(logarithm, target, zero(F)) + common = gcd(d) + for k in 2:common + iszero(common % k) || continue + mobius = _mobius(k) + iszero(mobius) && continue + total += F(mobius)//k * adams(get(logarithm, target .÷ k, zero(F)), k) end # ``DT_d = (L^{1/2} - L^{-1/2}) [Log Q]_{t^d}``, then ``E(IH^*) = L^{\dim/2} DT_d`` - root = F(-w) result = power(root, 1 - euler_form(Q, d, d)) * (root - inv(root)) * total iszero(result) && throw( ArgumentError("there are no stable representations of dimension vector $(Vector(d))") From f797ce6228423b14ea05411137d1312e7b3fff74 Mon Sep 17 00:00:00 2001 From: Pieter Belmans Date: Fri, 31 Jul 2026 16:01:26 +0200 Subject: [PATCH 5/7] docs: expand the intersection cohomology examples --- docs/src/methods/quiver-moduli.md | 21 +++++++- src/Hodge.jl | 84 ++++++++++++++++++++++++++++++- 2 files changed, 102 insertions(+), 3 deletions(-) diff --git a/docs/src/methods/quiver-moduli.md b/docs/src/methods/quiver-moduli.md index b20d125..946c7c4 100644 --- a/docs/src/methods/quiver-moduli.md +++ b/docs/src/methods/quiver-moduli.md @@ -1,3 +1,9 @@ +```@meta +DocTestSetup = quote + using QuiverTools +end +``` + # Quiver moduli The main purpose of QuiverTools is to treat quiver moduli. @@ -105,7 +111,20 @@ identifying the Donaldson--Thomas invariants of the quiver with it. This needs t stability parameter to be generic for the slope of ``\mathbf{d}``, and stable representations to exist. -The two agree in the smooth case, where intersection cohomology is ordinary cohomology. +For instance, the 3-Kronecker quiver with dimension vector ``(2,2)`` has the ``(1,1)``'s +of the same slope, so its moduli space is singular. It turns out to have the intersection +cohomology of ``\mathbb{P}^5``: + +```jldoctest +julia> M = QuiverModuliSpace(kronecker_quiver(3), [2, 2]); + +julia> intersection_poincare_polynomial(M) +L^5 + L^4 + L^3 + L^2 + L + 1 +``` + +The quiver need not be acyclic. With one vertex, ``m`` loops and the trivial stability +parameter one gets the classical spaces of matrix invariants, which are affine, so that +the answer is then the Poincaré polynomial for cohomology with compact support. ```@docs intersection_poincare_polynomial diff --git a/src/Hodge.jl b/src/Hodge.jl index 2139c2b..027d6b6 100644 --- a/src/Hodge.jl +++ b/src/Hodge.jl @@ -680,6 +680,10 @@ The moduli space is smooth exactly when no proper subdimension vector has the sl `M.d`, and there intersection cohomology is ordinary cohomology, so this agrees with [`poincare_polynomial`](@ref). +The quiver need not be acyclic, but then the moduli space is affine rather than +projective and, as for [`poincare_polynomial`](@ref), the answer is the Poincaré +polynomial for cohomology with compact support. + # Input - `M::QuiverModuliSpace`: a moduli space of representations of a quiver. @@ -701,6 +705,14 @@ julia> intersection_poincare_polynomial(M) L^5 + L^4 + L^3 + L^2 + L + 1 ``` +The dimension vector `[3, 3]` for the same quiver gives a singular 10-fold: +```jldoctest +julia> M = QuiverModuliSpace(kronecker_quiver(3), [3, 3]); + +julia> intersection_poincare_polynomial(M) +L^10 + L^9 + 2*L^8 + 2*L^7 + 2*L^6 + 2*L^5 + 2*L^4 + 2*L^3 + 2*L^2 + L + 1 +``` + Reflection functors identify moduli spaces for different dimension vectors: ```jldoctest julia> Q = kronecker_quiver(4); @@ -722,6 +734,22 @@ julia> intersection_poincare_polynomial(M) == poincare_polynomial(M) true ``` +The quiver with one vertex and `m` loops and the trivial stability parameter gives the +classical space of matrix invariants, `m`-tuples of operators on a `d`-dimensional vector +space up to simultaneous conjugation. It is affine of dimension ``(m-1)d^2+1``, singular +except for ``d = 1`` or ``m = d = 2``, and its intersection cohomology is worked out in +Theorem 8.2 of [[MR4000572](https://mathscinet.ams.org/mathscinet-getitem?mr=4000572)]: +```jldoctest +julia> intersection_poincare_polynomial(QuiverModuliSpace(loop_quiver(3), [1])) +L^3 + +julia> intersection_poincare_polynomial(QuiverModuliSpace(loop_quiver(2), [3])) +L^10 + +julia> intersection_poincare_polynomial(QuiverModuliSpace(loop_quiver(4), [2])) +L^13 + L^11 +``` + There is nothing to compute if no representation of dimension vector `M.d` is stable: ```jldoctest julia> M = QuiverModuliSpace(kronecker_quiver(2), [2, 2]); @@ -729,6 +757,15 @@ julia> M = QuiverModuliSpace(kronecker_quiver(2), [2, 2]); julia> intersection_poincare_polynomial(M) ERROR: ArgumentError: there are no stable representations of dimension vector [2, 2] ``` + +Nor is there anything to compute if the stability parameter is not generic for the slope +of `M.d`, which happens on a fake wall: +```jldoctest +julia> Q = Quiver([0 1 1 0; 0 0 1 0; 0 0 0 1; 0 0 0 0]); + +julia> intersection_poincare_polynomial(QuiverModuliSpace(Q, [3, 3, 4, 1])) +ERROR: ArgumentError: the stability parameter is not generic for the slope of [3, 3, 4, 1], so intersection cohomology is out of reach +``` """ function intersection_poincare_polynomial(M::QuiverModuliSpace) M.condition == "semistable" || throw( @@ -838,11 +875,12 @@ See [`intersection_poincare_polynomial`](@ref) for the algorithm and its hypothe # Output - a list of intersection Betti numbers of the moduli space, indexed by cohomological - degree ``0, \\dots, 2\\dim M``. + degree ``0, \\dots, 2\\dim M``. The odd ones vanish. # Examples -The singular moduli space for the 3-Kronecker quiver and dimension vector `[2, 2]`: +The singular moduli space for the 3-Kronecker quiver and dimension vector `[2, 2]` has +the intersection cohomology of ``\\mathbb{P}^5``: ```jldoctest julia> M = QuiverModuliSpace(kronecker_quiver(3), [2, 2]); @@ -861,6 +899,34 @@ julia> intersection_betti_numbers(M) 1 ``` +Intersection cohomology of a projective variety still satisfies Poincaré duality, so the +Betti numbers of a singular moduli space are palindromic just as well: +```jldoctest +julia> M = QuiverModuliSpace(kronecker_quiver(4), [2, 4]); + +julia> betti = intersection_betti_numbers(M); + +julia> betti == reverse(betti) +true + +julia> betti[1:2:end] +14-element Vector{Int64}: + 1 + 1 + 3 + 4 + 6 + 6 + 7 + 7 + 6 + 6 + 4 + 3 + 1 + 1 +``` + In the smooth case these are the ordinary Betti numbers: ```jldoctest julia> M = QuiverModuliSpace(kronecker_quiver(3), [2, 3]); @@ -914,6 +980,20 @@ julia> intersection_hodge_diamond(M) 0 0 0 0 0 1 ``` +A quiver on a wall, where the moduli space is a singular 5-fold: +```jldoctest +julia> Q = Quiver("1-2,1-3,2--3"); M = QuiverModuliSpace(Q, [2, 2, 2]); + +julia> intersection_hodge_diamond(M) +6×6 Matrix{Int64}: + 1 0 0 0 0 0 + 0 2 0 0 0 0 + 0 0 3 0 0 0 + 0 0 0 3 0 0 + 0 0 0 0 2 0 + 0 0 0 0 0 1 +``` + In the smooth case this is the ordinary Hodge diamond: ```jldoctest julia> M = QuiverModuliSpace(kronecker_quiver(3), [2, 3]); From f5bae08f340da18e97b8d23562c52841423fe88b Mon Sep 17 00:00:00 2001 From: Pieter Belmans Date: Fri, 31 Jul 2026 16:01:40 +0200 Subject: [PATCH 6/7] test: run the doctests in the manual too --- test/runtests.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/runtests.jl b/test/runtests.jl index a06df07..9f1f3e3 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -12,7 +12,7 @@ using Test, QuiverTools, Documenter # activates and its doctests resolve `Oscar.*`, without pulling Oscar's exports into # scope (which would clash with QuiverTools names such as `index`, `todd_class`, ...). DocMeta.setdocmeta!(QuiverTools, :DocTestSetup, :(using QuiverTools; import Oscar)) -doctest(QuiverTools; manual=false, testset="Doctests") +doctest(QuiverTools; manual=true, testset="Doctests") @testset "strict sst" begin # proper-semistability From 9c1f8b9fe53b454f88163df7506aaa4b3b979948 Mon Sep 17 00:00:00 2001 From: Pieter Belmans Date: Fri, 31 Jul 2026 16:01:41 +0200 Subject: [PATCH 7/7] test: check the spaces of matrix invariants --- test/runtests.jl | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/runtests.jl b/test/runtests.jl index 9f1f3e3..e47b0ed 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -226,6 +226,28 @@ end; @test length(betti) == 2 * dimension(M) + 1 end + # a quiver need not be acyclic, but then the moduli space is affine rather than + # projective and the answer is compactly supported. These are the spaces of matrix + # invariants, whose intersection cohomology is Theorem 8.2 of [MR4000572]; for `d = 1` + # the moduli space is A^m, and for m = d = 2 it is A^5 because the five traces and + # determinants are independent. + for (m, d, expected) in [ + (2, 1, [2]), + (3, 1, [3]), + (2, 2, [5]), + (2, 3, [10]), + (2, 4, [17, 15]), + (3, 2, [9]), + (3, 3, [19, 17, 16]), + (4, 2, [13, 11]), + ] + M = QuiverModuliSpace(loop_quiver(m), [d]) + @test dimension(M) == (m - 1) * d^2 + 1 + betti = intersection_betti_numbers(M) + @test findall(!iszero, betti) .- 1 == sort(2 .* expected) + @test all(isone, betti[findall(!iszero, betti)]) + end + # the Donaldson--Thomas invariant vanishes when nothing of dimension vector `d` is # stable, which is an exact cancellation in the plethystic logarithm for n in 2:4