diff --git a/.JuliaFormatter.jl b/.JuliaFormatter.jl index 648968a..fb88954 100644 --- a/.JuliaFormatter.jl +++ b/.JuliaFormatter.jl @@ -4,6 +4,7 @@ files = [ "src/Bundles.jl", "src/Chow.jl", "src/Constructors.jl", + "src/CoveringQuiver.jl", "src/Hodge.jl", "src/Misc.jl", "src/Moduli.jl", @@ -17,6 +18,7 @@ files = [ "ext/QuiverToolsOscarExt.jl", "docs/docs.jl", "benchmark/benchmarks.jl", + "test/covering_quiver.jl", "test/runtests.jl", ".JuliaFormatter.jl", ] diff --git a/docs/docs.jl b/docs/docs.jl index 8140432..3f1b03c 100644 --- a/docs/docs.jl +++ b/docs/docs.jl @@ -29,6 +29,7 @@ makedocs(; "Constructors" => "methods/constructors.md", "Quiver moduli" => "methods/quiver-moduli.md", "Representation theory" => "methods/representation-theory.md", + "Covering quiver" => "methods/covering-quiver.md", "Teleman quantization" => "methods/teleman-quantization.md", "Chow rings" => "methods/chow-rings.md", "Walls and chamber decompositions" => "methods/walls-and-chambers.md", diff --git a/docs/src/methods/covering-quiver.md b/docs/src/methods/covering-quiver.md new file mode 100644 index 0000000..7155dcc --- /dev/null +++ b/docs/src/methods/covering-quiver.md @@ -0,0 +1,116 @@ +# Covering quiver + +For a finite quiver ``Q`` with arrow set ``Q_1 = \{a_1, \ldots, a_m\}``, +the *covering quiver* ``Q(w)`` is the (infinite) quiver + +```math +\begin{aligned} +Q(w)_0 &= Q_0 \times \mathbb{Z}^m,\\ +Q(w)_1 &= \bigl\{ (s(a_k), \xi) \to (t(a_k), \xi + e_k) + \;\bigm|\; a_k \in Q_1,\ \xi \in \mathbb{Z}^m \bigr\}, +\end{aligned} +``` + +where ``(e_k)_{k=1}^m`` is the standard basis of ``\mathbb{Z}^m``. The projection +``(i, \xi) \mapsto i`` is the universal abelian cover of ``Q`` with respect to the +free abelian group on ``Q_1``. Coordinates use the order returned by +[`arrows`](@ref), so a character vector and an arrow index always refer to +the same ordering. + +A *compatible dimension vector* for ``d \in \mathbb{N}^{Q_0}`` is a function +``\beta\colon Q_0 \times \mathbb{Z}^m \to \mathbb{N}`` with finite support such that +``\sum_\xi \beta(i, \xi) = d_i`` for every ``i \in Q_0``. The group ``\mathbb{Z}^m`` +acts on compatible dimension vectors by +``s_\chi(\beta)(i, \xi) = \beta(i, \xi + \chi)``. + +Over an algebraically closed field, every connected component of the +natural-torus fixed locus of the stable moduli space ``M^\theta(Q, d)`` is of +the form +``F_\beta \cong M^{\hat\theta}(Q(w), \beta)``, where +``\hat\theta_{i, \xi} = \theta_i``, for a shift-equivalence class of compatible +``\beta``. A class contributes only when this lifted stable moduli space is +nonempty. This is the distinction between the candidates returned by +[`compatible_dimension_vectors`](@ref) and the actual components returned by +[`torus_fixed_components`](@ref); see +[[Theorem 3.1, Boos--Franzen](https://doi.org/10.1112/blms.12649)] and +[[Theorem 3.8, Weist](https://doi.org/10.1090/S1088-4165-2013-00436-3)]. + +For a semistable moduli space, `torus_fixed_components` requires the stable and +semistable loci to agree. Constructing `M` with `condition="stable"` explicitly +requests the fixed locus of the stable locus. Candidate enumeration is +combinatorial and can grow quickly with ``d`` and ``|Q_1|``; it is intended for +small and medium dimension vectors. + +At a point of a stable fixed component, the dimension of the ``\chi``-weight +space of the tangent space is given by +[[Theorem 6.1, Boos--Franzen](https://doi.org/10.1112/blms.12649)]: + +```math +\dim (T_{[M]} \mathcal M)_\chi + = \delta_{\chi, 0} - \langle \beta, s_{-\chi}\beta\rangle_{Q(w)}. +``` + +Accordingly, [`weight_space_dimension`](@ref) and +[`tangent_weight_multiplicities`](@ref) accept the ambient moduli space and +verify that ``\beta`` has a nonempty stable lift. The latter includes the zero +character when the fixed component itself has positive dimension. + +## Typical workflow + +First enumerate candidates, then filter them using stability: + +```julia +Q = kronecker_quiver(3) +M = QuiverModuliSpace(Q, [2, 3]) + +candidates = compatible_dimension_vectors(Q, M.d) +components = torus_fixed_components(M) + +length(candidates) # 55 connected-support shift classes +length(components) # 13 nonempty stable fixed components + +component = first(components) +tangent_weight_multiplicities(M, component.beta) +``` + +## Data structure + +```@docs +CoveringDimVector +``` + +## Operations on covering dimension vectors + +```@docs +shift_beta +covering_euler_form +extract_finite_subquiver +compatible_dimension_vectors +``` + +## Fixed components + +```@docs +torus_fixed_components +``` + +## Tangent weight spaces at fixed points + +```@docs +weight_space_dimension +tangent_weight_multiplicities +``` + +## References + +- M. Boos and H. Franzen, *Weight spaces and attracting sets for torus actions + on quiver moduli*, Bulletin of the London Mathematical Society **54** (2022), + 1658--1682. [doi:10.1112/blms.12649](https://doi.org/10.1112/blms.12649), + [arXiv:2002.12049](https://arxiv.org/abs/2002.12049). +- A. D. King, *Moduli of representations of finite-dimensional algebras*, + Quarterly Journal of Mathematics **45** (1994), 515--530. + [doi:10.1093/qmath/45.4.515](https://doi.org/10.1093/qmath/45.4.515). +- T. Weist, *Localization in quiver moduli spaces*, Representation Theory + **17** (2013), 382--425. + [doi:10.1090/S1088-4165-2013-00436-3](https://doi.org/10.1090/S1088-4165-2013-00436-3), + [arXiv:0903.5442](https://arxiv.org/abs/0903.5442). diff --git a/docs/src/tutorial.md b/docs/src/tutorial.md index f42e4c9..894ab02 100644 --- a/docs/src/tutorial.md +++ b/docs/src/tutorial.md @@ -61,8 +61,10 @@ julia> is_coprime(d, θ) true ``` -Here, `is_coprime()` checks if ``d`` is θ-coprime, i.e., if none of the -proper subdimension vectors ``0 \neq d' \nleq d`` satisfies ``\theta \cdot d' = 0``. +Here, `is_coprime()` checks if ``d`` is θ-coprime, i.e., if no proper nonzero +subdimension vector has the same slope as ``d``. When ``\theta \cdot d=0``, +this is equivalent to requiring ``\theta \cdot d' \neq 0`` for every proper +nonzero subdimension vector ``d'``. The bilinear Euler form relative to a quiver Q of any two vectors in ``\mathbb{Z}^{Q_0}`` can be computed: diff --git a/src/CoveringQuiver.jl b/src/CoveringQuiver.jl new file mode 100644 index 0000000..ecb60c4 --- /dev/null +++ b/src/CoveringQuiver.jl @@ -0,0 +1,942 @@ +######################################################################################## +# Covering quivers of a finite quiver Q +# +# Given a finite quiver Q with arrow set Q_1 = {a_1, ..., a_m}, the *covering quiver* +# Q(w) is the (infinite) quiver +# +# vertices: Q_0 x Z^m, +# arrows: (s(a_k), xi) -> (t(a_k), xi + e_k) for each a_k in Q_1, each xi in Z^m, +# +# where (e_k)_{k=1}^m is the standard basis of Z^m. The projection (i, xi) -> i is the +# universal abelian cover of Q with respect to the free abelian group on Q_1. +# +# These covers describe the fixed loci of the natural action of the full-rank torus +# T = G_m^{Q_1} on M^{theta}(Q, d), see +# +# * Boos--Franzen, *Weight spaces and attracting sets for torus actions on +# quiver moduli*, Bull. Lond. Math. Soc. 54 (2022), 1658--1682, +# [doi:10.1112/blms.12649](https://doi.org/10.1112/blms.12649), +# +# which builds on Weist's localisation +# +# * Weist, *Localization in quiver moduli spaces*, +# Represent. Theory 17 (2013), 382--425, +# [doi:10.1090/S1088-4165-2013-00436-3](https://doi.org/10.1090/S1088-4165-2013-00436-3). +# +# A *compatible dimension vector* for d in N^{Q_0} is a function +# beta: Q_0 x Z^m -> N with finite support such that +# sum_{xi} beta(i, xi) = d_i for each i in Q_0. The group Z^m acts on compatible +# dimension vectors by s_chi(beta)(i, xi) = beta(i, xi + chi). A connected-support +# shift class is a candidate for a component of the T-fixed locus. It contributes +# precisely when the lifted stable moduli space +# F_beta = M^{theta_hat}(Q(w), beta), where theta_hat_{i, xi} = theta_i, is nonempty. +######################################################################################## + +""" +# Summary + +`const CoveringDimVector = Dict{Tuple{Int, Vector{Int}}, Int}` + +A finitely-supported dimension vector on a covering quiver `Q(w)`. + +Keys are pairs `(i, xi)` where `i` is a vertex of `Q` and `xi` is a lattice point +in `Z^{n_arrows(Q)}`; values are positive multiplicities. Only nonzero entries +are stored. + +The lattice coordinates are mutable `Vector`s for compatibility with the rest of +the package. Do not mutate a coordinate while it is used as a dictionary key; +doing so invalidates the dictionary's hash table. Public covering-quiver +operations validate vertex indices, coordinate lengths, and multiplicities. + +For the role of `CoveringDimVector` in the description of the natural torus fixed +locus of `M^{theta}(Q, d)`, see +[[Theorem 3.1, Boos--Franzen](https://doi.org/10.1112/blms.12649)]. +The shift action of `Z^{n_arrows(Q)}` is realised by [`shift_beta`](@ref); +the enumeration of equivalence classes by [`compatible_dimension_vectors`](@ref). + +# Examples + +```jldoctest +julia> beta = CoveringDimVector((1, [0, 0]) => 1, (2, [1, 0]) => 1); + +julia> beta[(1, [0, 0])] +1 + +julia> length(beta) +2 +``` +""" +const CoveringDimVector = Dict{Tuple{Int,Vector{Int}},Int} + +_covering_arrows(Q::Quiver) = n_arrows(Q) == 0 ? Tuple{Int,Int}[] : arrows(Q) + +function _validate_covering_dimension_vector( + beta::CoveringDimVector, + lattice_rank::Int; + vertex_count::Union{Nothing,Int}=nothing, + name::String="beta", +) + for ((v, xi), multiplicity) in beta + v > 0 || throw(ArgumentError("$name contains the nonpositive vertex index $v")) + if vertex_count !== nothing && v > vertex_count + throw( + ArgumentError( + "$name contains vertex $v, but the quiver has $vertex_count vertices" + ), + ) + end + length(xi) == lattice_rank || + throw( + DimensionMismatch( + "$name contains a lattice point of length $(length(xi)); " * + "expected $lattice_rank", + ), + ) + multiplicity > 0 || + throw(ArgumentError("$name contains the nonpositive multiplicity $multiplicity")) + end + return nothing +end + +function _validate_covering_dimension_vector( + Q::Quiver, + beta::CoveringDimVector; + name::String="beta", +) + return _validate_covering_dimension_vector( + beta, + n_arrows(Q); + vertex_count=n_vertices(Q), + name=name, + ) +end + +""" + shift_beta(beta::CoveringDimVector, chi::AbstractVector{Int}) + +Compute the shift `s_chi(beta)` of `beta` on the covering quiver `Q(w)`. + +For `chi` in `Z^{n_arrows(Q)}`, the shift action is +```math +s_\\chi(\\beta)_{i, \\xi} = \\beta_{i, \\xi + \\chi}. +``` +At the level of the underlying `Dict`, this maps each key `(i, xi)` to +`(i, xi - chi)`, so that reading the shifted vector at `(i, eta)` returns the +original multiplicity at `(i, eta + chi)`. + +See [[Section 3, Boos--Franzen](https://doi.org/10.1112/blms.12649)]. + +# Input + +- `beta::CoveringDimVector` a finitely-supported dimension vector on `Q(w)`. +- `chi::AbstractVector{Int}` a lattice shift in `Z^{n_arrows(Q)}`. + +# Output + +- the shifted dimension vector `s_chi(beta)`. + +# Examples + +```jldoctest +julia> beta = CoveringDimVector((1, [0, 0]) => 1, (2, [1, 0]) => 1); + +julia> shift_beta(beta, [1, 0]) == CoveringDimVector((1, [-1, 0]) => 1, (2, [0, 0]) => 1) +true + +julia> shift_beta(beta, [0, 0]) == beta +true + +julia> shift_beta(shift_beta(beta, [1, 0]), [-1, 0]) == beta +true +``` +""" +function shift_beta(beta::CoveringDimVector, chi::AbstractVector{Int}) + _validate_covering_dimension_vector(beta, length(chi)) + shifted = CoveringDimVector() + sizehint!(shifted, length(beta)) + for ((v, xi), count) in beta + shifted[(v, xi .- chi)] = count + end + return shifted +end + +""" + covering_euler_form(Q::Quiver, beta::CoveringDimVector, gamma::CoveringDimVector) + +Compute the Euler form `\\langle \\beta, \\gamma\\rangle_{Q(w)}` on the covering quiver. + +For finitely-supported `beta`, `gamma`, this is +```math +\\langle \\beta, \\gamma\\rangle_{Q(w)} + = \\sum_{i \\in Q_0} \\sum_{\\xi} \\beta_{i, \\xi}\\, \\gamma_{i, \\xi} + - \\sum_{a\\colon i \\to j} \\sum_{\\xi} + \\beta_{i, \\xi}\\, \\gamma_{j, \\xi + e_a}, +``` +where `e_a` is the standard basis vector of `Z^{n_arrows(Q)}` indexing the arrow `a`. + +This is the natural lift of [`euler_form`](@ref) along the covering map +`Q(w) -> Q`. If `beta` and `gamma` push forward to dimension vectors `d` +and `e` on `Q`, respectively, then +```math +\\langle d, e\\rangle_Q + = \\sum_{\\chi \\in \\mathbb Z^{Q_1}} + \\langle \\beta, s_{-\\chi}\\gamma\\rangle_{Q(w)}. +``` +Only finitely many summands on the right are nonzero. + +See [[Section 6, Boos--Franzen](https://doi.org/10.1112/blms.12649)]. + +# Input + +- `Q::Quiver` a quiver. +- `beta::CoveringDimVector` a finitely-supported dimension vector on `Q(w)`. +- `gamma::CoveringDimVector` a finitely-supported dimension vector on `Q(w)`. + +# Output + +- the Euler form `\\langle \\beta, \\gamma\\rangle_{Q(w)}` as an `Int`. + +# Examples + +For a real-root fixed point of the moduli space `M^{theta}(K_2, (1, 1))` +the covering Euler form is `1`: + +```jldoctest +julia> Q = kronecker_quiver(2); + +julia> beta = CoveringDimVector((1, [0, 0]) => 1, (2, [1, 0]) => 1); + +julia> covering_euler_form(Q, beta, beta) +1 +``` + +For finitely-supported `beta`, the covering Euler form equals the underlying +Euler form on the finite subquiver induced on `supp(beta)`: + +```jldoctest +julia> Q = kronecker_quiver(2); + +julia> beta = CoveringDimVector((1, [0, 0]) => 1, (2, [1, 0]) => 1); + +julia> (sub_Q, sub_d, _) = extract_finite_subquiver(Q, beta); + +julia> covering_euler_form(Q, beta, beta) == euler_form(sub_Q, sub_d, sub_d) +true +``` +""" +function covering_euler_form( + Q::Quiver, beta::CoveringDimVector, gamma::CoveringDimVector +) + _validate_covering_dimension_vector(Q, beta) + _validate_covering_dimension_vector(Q, gamma; name="gamma") + arrow_list = _covering_arrows(Q) + m = n_arrows(Q) + + vertex_sum = 0 + for ((v, xi), b_val) in beta + g_val = get(gamma, (v, xi), 0) + vertex_sum += b_val * g_val + end + + arrow_sum = 0 + for (a_idx, (s, t)) in enumerate(arrow_list) + e_a = zeros(Int, m) + e_a[a_idx] = 1 + for ((v, xi), b_val) in beta + v == s || continue + g_val = get(gamma, (t, xi .+ e_a), 0) + arrow_sum += b_val * g_val + end + end + + return vertex_sum - arrow_sum +end + +""" + extract_finite_subquiver(Q::Quiver, beta::CoveringDimVector) + +Build the finite subquiver of `Q(w)` induced on the support of `beta`. + +The returned `sub_Q` is a finite `Quiver` whose vertices are the elements of +`supp(beta)`, ordered lexicographically, and whose arrows are the arrows of `Q(w)` +whose endpoints both lie in `supp(beta)`. The companion `sub_d` records the +multiplicities `beta(i, xi)`. + +A stability parameter `theta` on `Q` lifts to `Q(w)` via the projection +`(i, xi) -> i`, i.e. `theta_hat_{i, xi} = theta_i`; this lift is +```julia +sub_theta = [theta[k[1]] for k in sort(collect(keys(beta)))] +``` +and is the construction used to identify `F_beta` with `M^{theta_hat}(Q(w), beta)` +in [[Theorem 3.1, Boos--Franzen](https://doi.org/10.1112/blms.12649)]. + +# Input + +- `Q::Quiver` a quiver. +- `beta::CoveringDimVector` a finitely-supported dimension vector on `Q(w)`. + +# Output + +A 3-tuple `(sub_Q, sub_d, vertex_map)`: + +- `sub_Q::Quiver` the induced finite subquiver. +- `sub_d::Vector{Int}` the dimension vector on `sub_Q` matching `beta`. +- `vertex_map::Dict{Tuple{Int, Vector{Int}}, Int}` mapping each support point + `(i, xi)` to its vertex index in `sub_Q`. + +# Examples + +A real-root fixed point of `M^{theta}(K_2, (1, 1))` has induced subquiver `A_2`: + +```jldoctest +julia> Q = kronecker_quiver(2); + +julia> beta = CoveringDimVector((1, [0, 0]) => 1, (2, [1, 0]) => 1); + +julia> (sub_Q, sub_d, _) = extract_finite_subquiver(Q, beta); + +julia> (n_vertices(sub_Q), n_arrows(sub_Q), sub_d) +(2, 1, [1, 1]) +``` + +For a fixed point of `M^{theta}(K_3, (1, 1))` corresponding to one of the three +arrows, the subquiver is the linear `A_2`: + +```jldoctest +julia> Q = kronecker_quiver(3); + +julia> beta = CoveringDimVector( + (1, [0, 0, 0]) => 1, + (2, [1, 0, 0]) => 1, + ); + +julia> (sub_Q, sub_d, _) = extract_finite_subquiver(Q, beta); + +julia> (n_vertices(sub_Q), n_arrows(sub_Q), sub_d) +(2, 1, [1, 1]) +``` +""" +function extract_finite_subquiver(Q::Quiver, beta::CoveringDimVector) + _validate_covering_dimension_vector(Q, beta) + arrow_list = _covering_arrows(Q) + m = n_arrows(Q) + + support_verts = sort(collect(keys(beta))) + vertex_map = Dict{Tuple{Int,Vector{Int}},Int}() + for (idx, v) in enumerate(support_verts) + vertex_map[v] = idx + end + n_sub = length(support_verts) + + adj = zeros(Int, n_sub, n_sub) + for (a_idx, (s, t)) in enumerate(arrow_list) + e_a = zeros(Int, m) + e_a[a_idx] = 1 + for (v, xi) in support_verts + v == s || continue + target = (t, xi .+ e_a) + if haskey(vertex_map, target) + adj[vertex_map[(v, xi)], vertex_map[target]] += 1 + end + end + end + + sub_Q = Quiver(adj) + sub_d = [beta[v] for v in support_verts] + return (sub_Q, sub_d, vertex_map) +end + +""" + extract_finite_subquiver( + Q::Quiver, + beta::CoveringDimVector, + theta::AbstractVector{Int}, + ) + +Build the finite subquiver induced on the support of `beta`, together with the +lift of the stability parameter `theta`. + +The lifted parameter is defined by `theta_hat[(i, xi)] = theta[i]`. Its entries +are ordered compatibly with the dimension vector returned by +[`extract_finite_subquiver(Q, beta)`](@ref). + +# Input + +- `Q::Quiver` a quiver. +- `beta::CoveringDimVector` a finitely-supported dimension vector on `Q(w)`. +- `theta::AbstractVector{Int}` a stability parameter on `Q`. + +# Output + +A 4-tuple `(sub_Q, sub_d, sub_theta, vertex_map)`, where the first, second, and +fourth entries are as in [`extract_finite_subquiver(Q, beta)`](@ref), and +`sub_theta` is the lifted stability parameter. + +# Examples + +```jldoctest +julia> Q = kronecker_quiver(2); + +julia> beta = CoveringDimVector((1, [0, 0]) => 1, (2, [1, 0]) => 1); + +julia> (_, sub_d, sub_theta, _) = extract_finite_subquiver(Q, beta, [1, -1]); + +julia> (sub_d, sub_theta) +([1, 1], [1, -1]) +``` +""" +function extract_finite_subquiver( + Q::Quiver, + beta::CoveringDimVector, + theta::AbstractVector{Int}, +) + length(theta) == n_vertices(Q) || + throw(ArgumentError("length(theta) must equal n_vertices(Q)")) + + sub_Q, sub_d, vertex_map = extract_finite_subquiver(Q, beta) + sub_theta = Vector{Int}(undef, length(vertex_map)) + for ((v, _), i) in vertex_map + sub_theta[i] = theta[v] + end + return (sub_Q, sub_d, sub_theta, vertex_map) +end + +""" + compatible_dimension_vectors(Q::Quiver, d::AbstractVector{Int}) + +Enumerate the connected-support dimension vectors on `Q(w)` compatible with `d`, +up to the `Z^{n_arrows(Q)}`-shift action. + +A `beta::CoveringDimVector` is *compatible* with `d` if +`sum_{xi} beta(i, xi) = d_i` for each vertex `i`. By +[[Theorem 3.1, Boos--Franzen](https://doi.org/10.1112/blms.12649)] and +[[Theorem 3.8, Weist](https://doi.org/10.1090/S1088-4165-2013-00436-3)], each +nonempty fixed component of the natural torus action on `M^{theta}(Q, d)` arises +from a shift-equivalence class of such `beta`. + +This function returns one representative per shift-equivalence class +*with connected support*. A dimension vector with disconnected support in +(the underlying graph of) `Q(w)` decomposes any representation as a direct sum, +so its `theta_hat`-stable moduli is empty and it never contributes a fixed point. +The connected-support classes returned here are candidates: use +[`torus_fixed_components`](@ref) to retain exactly those with nonempty lifted +stable moduli. Compatible `beta` with disconnected support are intentionally +omitted from the output. + +# Input + +- `Q::Quiver` a quiver. +- `d::AbstractVector{Int}` a dimension vector on `Q`. + +# Output + +- `Vector{CoveringDimVector}` of canonical representatives, one per + shift-equivalence class of connected-support compatible dimension vectors. + +# Examples + +For the `r`-Kronecker quiver with `d = (1, 1)` the moduli space is +`P^{r-1}`, so the enumeration recovers the `r` torus-fixed points: + +```jldoctest +julia> length(compatible_dimension_vectors(kronecker_quiver(2), [1, 1])) +2 + +julia> length(compatible_dimension_vectors(kronecker_quiver(3), [1, 1])) +3 + +julia> length(compatible_dimension_vectors(kronecker_quiver(5), [1, 1])) +5 +``` + +For the 3-Kronecker quiver with `d = (2, 3)` there are 55 shift classes: + +```jldoctest +julia> length(compatible_dimension_vectors(kronecker_quiver(3), [2, 3])) +55 +``` + +The zero dimension vector has a single (empty) class: + +```jldoctest +julia> compatible_dimension_vectors(kronecker_quiver(2), [0, 0]) == [CoveringDimVector()] +true +``` +""" +function compatible_dimension_vectors(Q::Quiver, d::AbstractVector{Int}) + length(d) == n_vertices(Q) || + throw(ArgumentError("length(d) must equal n_vertices(Q)")) + any(<(0), d) && throw(ArgumentError("d must be nonnegative")) + + total_d = sum(d; init=0) + total_d == 0 && return [CoveringDimVector()] + + m = n_arrows(Q) + arrow_list = _covering_arrows(Q) + e_vecs = [_standard_basis_vector(m, k) for k in 1:m] + + # Anchor: place one unit of i_0 at the origin and recurse. After + # `_normalize`, every shift class has its lex-min i_0 position at the + # origin, so each class is reached. + i_0 = findfirst(>(0), d) + origin = zeros(Int, m) + + initial_beta = CoveringDimVector((i_0, origin) => 1) + remaining = collect(d) + remaining[i_0] -= 1 + + memo = Set{Tuple{Vector{Tuple{Tuple{Int,Vector{Int}},Int}},Vector{Int}}}() + results = Set{Vector{Tuple{Tuple{Int,Vector{Int}},Int}}}() + + _build_betas!(results, remaining, initial_beta, memo, i_0, arrow_list, e_vecs) + + canonical_results = sort!(collect(results)) + return [CoveringDimVector(pairs) for pairs in canonical_results] +end + +function _standard_basis_vector(m::Int, k::Int) + v = zeros(Int, m) + v[k] = 1 + return v +end + +""" + _canonicalize(beta::CoveringDimVector) + +Sorted list of `((vertex, lattice_point), multiplicity)` triples used as a +hashable canonical form of `beta`. +""" +_canonicalize(beta::CoveringDimVector) = sort!([(k, v) for (k, v) in beta]) + +""" + _memo_key(beta::CoveringDimVector, remaining::Vector{Int}) + +Hashable key for memoising partial states in `_build_betas!`. The `remaining` +vector is copied defensively because the caller mutates a private copy +between recursive invocations. +""" +function _memo_key(beta::CoveringDimVector, remaining::Vector{Int}) + return (_canonicalize(beta), copy(remaining)) +end + +""" + _normalize(beta::CoveringDimVector, i_0::Int) + +Shift `beta` so that the lexicographically smallest `xi` with `beta(i_0, xi) > 0` +lands at the origin. Produces the unique canonical representative of `beta`'s +`Z^{n_arrows(Q)}`-shift equivalence class. + +The function is a no-op when `beta` has no `i_0` support (e.g. when `beta` is +empty). All callers in this module guarantee `beta(i_0, .) != 0`. +""" +function _normalize(beta::CoveringDimVector, i_0::Int) + xi_min = nothing + for ((v, xi), _) in beta + v == i_0 || continue + if xi_min === nothing || xi < xi_min + xi_min = xi + end + end + xi_min === nothing && return beta + all(==(0), xi_min) && return beta + return shift_beta(beta, xi_min) +end + +""" + _neighbor_points(beta::CoveringDimVector, arrow_list, e_vecs) + +Return the set of `(v, xi)` adjacent in `Q(w)` to `supp(beta)` but not in +`supp(beta)` itself. Used to expand the frontier during enumeration. +""" +function _neighbor_points( + beta::CoveringDimVector, + arrow_list::Vector{Tuple{Int,Int}}, + e_vecs::Vector{Vector{Int}}, +) + neighbors = Set{Tuple{Int,Vector{Int}}}() + for (a_idx, (s, t)) in enumerate(arrow_list) + e_a = e_vecs[a_idx] + for (v, xi) in keys(beta) + if v == s + candidate = (t, xi .+ e_a) + haskey(beta, candidate) || push!(neighbors, candidate) + end + if v == t + candidate = (s, xi .- e_a) + haskey(beta, candidate) || push!(neighbors, candidate) + end + end + end + return neighbors +end + +""" + _build_betas!(results, remaining, current_beta, memo, i_0, arrow_list, e_vecs) + +Recursive worker of `compatible_dimension_vectors`. Places one unit per step, +branching on every `(v, xi)` in `supp(beta) ∪ frontier(beta)` with `remaining[v] > 0`, +and uses memoisation to prune branches that revisit a partial state. + +Connected-support completeness: every compatible `beta` with connected support +can be assembled one unit at a time along the connectivity graph of `Q(w)` +starting from `(i_0, 0)`, so every connected-support shift class is reached by +some branch. +""" +function _build_betas!( + results::Set{Vector{Tuple{Tuple{Int,Vector{Int}},Int}}}, + remaining::Vector{Int}, + current_beta::CoveringDimVector, + memo::Set{Tuple{Vector{Tuple{Tuple{Int,Vector{Int}},Int}},Vector{Int}}}, + i_0::Int, + arrow_list::Vector{Tuple{Int,Int}}, + e_vecs::Vector{Vector{Int}}, +) + if all(==(0), remaining) + push!(results, _canonicalize(_normalize(current_beta, i_0))) + return nothing + end + + key = _memo_key(current_beta, remaining) + key in memo && return nothing + push!(memo, key) + + candidates = Tuple{Int,Vector{Int}}[] + for nbr in _neighbor_points(current_beta, arrow_list, e_vecs) + remaining[nbr[1]] > 0 && push!(candidates, nbr) + end + for (v, xi) in keys(current_beta) + remaining[v] > 0 && push!(candidates, (v, xi)) + end + isempty(candidates) && return nothing + + for (v, xi) in candidates + new_beta = copy(current_beta) + new_beta[(v, xi)] = get(new_beta, (v, xi), 0) + 1 + new_remaining = copy(remaining) + new_remaining[v] -= 1 + _build_betas!(results, new_remaining, new_beta, memo, i_0, arrow_list, e_vecs) + end +end + +""" + weight_space_dimension( + M::QuiverModuliSpace, + beta::CoveringDimVector, + chi::AbstractVector{Int}, + ) + +Return the dimension of the `chi`-weight space of the tangent space along the +stable fixed component indexed by `beta`. + +Here `beta` must be compatible with the dimension vector of `M`, and its lifted +stability condition must admit stable representations. For a stable +representation `N` of `Q(w)` with dimension vector `beta`, the weight space is +`Ext^1_{Q(w)}(N, s_{-chi} N)`. By +[[Theorem 6.1, Boos--Franzen](https://doi.org/10.1112/blms.12649)], +```math +\\dim (T_{[M]} \\mathcal M)_\\chi + = \\delta_{\\chi, 0} - \\langle \\beta, s_{-\\chi}\\beta\\rangle_{Q(w)}. +``` + +# Input + +- `M::QuiverModuliSpace` the ambient stable moduli space, or a semistable moduli + space whose stable and semistable loci agree. +- `beta::CoveringDimVector` a finitely-supported dimension vector on `Q(w)`. +- `chi::AbstractVector{Int}` a character in `Z^{n_arrows(Q)}`. + +# Output + +- the dimension as an `Int`. + +# Examples + +At a real-root fixed point of `M^{theta}(K_2, (1, 1)) \\cong \\mathbb P^1`, the +`chi = 0` weight space is `0`-dimensional (the fixed point is isolated): + +```jldoctest +julia> Q = kronecker_quiver(2); + +julia> M = QuiverModuliSpace(Q, [1, 1], [1, -1]); + +julia> beta = CoveringDimVector((1, [0, 0]) => 1, (2, [1, 0]) => 1); + +julia> weight_space_dimension(M, beta, [0, 0]) +0 +``` + +The tangent space at the same fixed point has its single nonzero weight +at `chi = [-1, 1]`: + +```jldoctest +julia> Q = kronecker_quiver(2); + +julia> M = QuiverModuliSpace(Q, [1, 1], [1, -1]); + +julia> beta = CoveringDimVector((1, [0, 0]) => 1, (2, [1, 0]) => 1); + +julia> weight_space_dimension(M, beta, [-1, 1]) +1 + +julia> weight_space_dimension(M, beta, [1, -1]) +0 +``` +""" +function weight_space_dimension( + M::QuiverModuliSpace, + beta::CoveringDimVector, + chi::AbstractVector{Int}, +) + _require_stable_fixed_locus(M) + _fixed_component_moduli(M, beta) === nothing && + throw(ArgumentError("beta does not admit a stable lift for M")) + return _stable_weight_space_dimension(M.Q, beta, chi) +end + +function _stable_weight_space_dimension( + Q::Quiver, + beta::CoveringDimVector, + chi::AbstractVector{Int}, +) + m = n_arrows(Q) + length(chi) == m || throw(ArgumentError("length(chi) must equal n_arrows(Q)")) + + vertex_sum = 0 + for ((v, xi), beta_value) in beta + vertex_sum += beta_value * get(beta, (v, xi .- chi), 0) + end + + arrow_sum = 0 + for (a_idx, (s, t)) in enumerate(_covering_arrows(Q)) + e_a = _standard_basis_vector(m, a_idx) + for ((v, xi), beta_value) in beta + v == s || continue + arrow_sum += beta_value * get(beta, (t, xi .+ e_a .- chi), 0) + end + end + + delta = all(iszero, chi) ? 1 : 0 + return delta - vertex_sum + arrow_sum +end + +""" + tangent_weight_multiplicities( + M::QuiverModuliSpace, + beta::CoveringDimVector, + ) + +Return the characters `chi in Z^{n_arrows(Q)}` with +`weight_space_dimension(M, beta, chi) > 0`, together with their multiplicities. +The zero character is included when the fixed component has positive dimension. + +By [[Theorem 6.1, Boos--Franzen](https://doi.org/10.1112/blms.12649)], +the support of the character of `T_{[M]} \\mathcal M` for the full-rank torus +is finite, with the only possible nonzero weights of the form +`chi = xi + e_a - xi'` for an arrow `a: s -> t` and points +`(s, xi), (t, xi')` in `supp(beta)`, or `chi = xi - xi'` for points +`(v, xi), (v, xi')` of the same vertex in `supp(beta)` (including `chi = 0`). + +# Input + +- `M::QuiverModuliSpace` the ambient stable moduli space, or a semistable moduli + space whose stable and semistable loci agree. +- `beta::CoveringDimVector` a finitely-supported dimension vector on `Q(w)`. + +# Output + +- `Vector{Tuple{Vector{Int}, Int}}` of `(chi, dim)` pairs with `dim > 0`. + +# Examples + +At each of the two fixed points of `\\mathbb P^1 \\cong M^{theta}(K_2, (1, 1))`, +there is exactly one nonzero weight, of dimension `1`: + +```jldoctest +julia> Q = kronecker_quiver(2); + +julia> M = QuiverModuliSpace(Q, [1, 1], [1, -1]); + +julia> beta = CoveringDimVector((1, [0, 0]) => 1, (2, [1, 0]) => 1); + +julia> tangent_weight_multiplicities(M, beta) +1-element Vector{Tuple{Vector{Int64}, Int64}}: + ([-1, 1], 1) +``` + +For every stable fixed component, all weight multiplicities are nonnegative and +sum to `dim M = 1 - \\langle d, d\\rangle_Q`: + +```jldoctest +julia> Q = kronecker_quiver(3); d = [1, 1]; + +julia> M = QuiverModuliSpace(Q, d, [1, -1]); + +julia> all(torus_fixed_components(M)) do component + sum(last, tangent_weight_multiplicities(M, component.beta); init=0) == + 1 - euler_form(Q, d, d) + end +true +``` +""" +function tangent_weight_multiplicities( + M::QuiverModuliSpace, + beta::CoveringDimVector, +) + _require_stable_fixed_locus(M) + _fixed_component_moduli(M, beta) === nothing && + throw(ArgumentError("beta does not admit a stable lift for M")) + + out = Tuple{Vector{Int},Int}[] + for chi in _weight_candidates(M.Q, beta) + dim = _stable_weight_space_dimension(M.Q, beta, chi) + dim > 0 && push!(out, (chi, dim)) + end + return sort!(out; by=first) +end + +""" + _weight_candidates(Q::Quiver, beta::CoveringDimVector) + +Return the (finite) set of characters `chi in Z^{n_arrows(Q)}` outside of which +the tangent-weight expression vanishes. Used internally by +[`tangent_weight_multiplicities`](@ref). +""" +function _weight_candidates(Q::Quiver, beta::CoveringDimVector) + arrow_list = _covering_arrows(Q) + m = n_arrows(Q) + + support_by_vertex = Dict{Int,Vector{Vector{Int}}}() + for ((v, xi), count) in beta + count > 0 || continue + push!(get!(support_by_vertex, v, Vector{Int}[]), xi) + end + + candidates = Set{Vector{Int}}() + for (a_idx, (s, t)) in enumerate(arrow_list) + e_a = zeros(Int, m) + e_a[a_idx] = 1 + s_points = get(support_by_vertex, s, Vector{Int}[]) + t_points = get(support_by_vertex, t, Vector{Int}[]) + for xi in s_points, xi_prime in t_points + push!(candidates, xi .+ e_a .- xi_prime) + end + end + for (_, points) in support_by_vertex + for xi in points, xi_prime in points + push!(candidates, xi .- xi_prime) + end + end + return candidates +end + +function _lift_denominator(M::QuiverModuliSpace, vertex_map::Dict) + function lifted_denominator(sub_d::AbstractVector{Int}) + length(sub_d) == length(vertex_map) || + throw(ArgumentError("length(sub_d) must equal length(vertex_map)")) + projected_d = zeros(Int, n_vertices(M.Q)) + for ((v, _), i) in vertex_map + projected_d[v] += sub_d[i] + end + return M.denom(projected_d) + end + return lifted_denominator +end + +function _require_stable_fixed_locus(M::QuiverModuliSpace) + if M.condition == "semistable" && !semistable_equals_stable(M) + throw( + ArgumentError( + "semistable and stable loci must agree; use condition=\"stable\" " * + "to compute the fixed locus of the stable moduli space", + ), + ) + end + return nothing +end + +function _fixed_component_moduli( + M::QuiverModuliSpace, + beta::CoveringDimVector, +) + _validate_covering_dimension_vector(M.Q, beta) + + projected_d = zeros(Int, n_vertices(M.Q)) + for ((v, _), multiplicity) in beta + projected_d[v] += multiplicity + end + projected_d == M.d || + throw(ArgumentError("beta is not compatible with the dimension vector of M")) + + sub_Q, sub_d, sub_theta, vertex_map = extract_finite_subquiver(M.Q, beta, M.theta) + sub_denom = _lift_denominator(M, vertex_map) + has_stables(sub_Q, sub_d, sub_theta, sub_denom) || return nothing + return QuiverModuliSpace(sub_Q, sub_d, sub_theta, "stable", sub_denom) +end + +""" + torus_fixed_components(M::QuiverModuliSpace) + +Return the connected components of the fixed locus of the natural arrow-scaling +torus on `M`. + +The natural torus is `T = G_m^{Q_1}`. Each returned entry is a named tuple +`(beta=beta, moduli=F_beta)`, where `beta` is the canonical representative of a +shift class of compatible covering dimension vectors and `F_beta` is the stable +moduli space on the finite subquiver induced by `supp(beta)`. +Both the stability parameter and a custom denominator are lifted along the +covering projection. + +If `M` parametrizes semistable representations, its semistable and stable loci +must agree. For a moduli space constructed with `condition="stable"`, the +function computes the fixed components of the stable locus directly. + +# Input + +- `M::QuiverModuliSpace` a quiver moduli space. + +# Output + +- a vector of named tuples `(beta, moduli)` describing the nonempty fixed + components. + +# Examples + +The natural torus action on `M^theta(K_2, (1, 1))` has two isolated fixed +points: + +```jldoctest +julia> M = QuiverModuliSpace(kronecker_quiver(2), [1, 1], [1, -1]); + +julia> components = torus_fixed_components(M); + +julia> length(components) +2 + +julia> all(component -> dimension(component.moduli) == 0, components) +true +``` + +Compatible covering dimension vectors whose lifted stable moduli are empty do +not occur in the output: + +```jldoctest +julia> Q = Quiver([0 1; 0 0]); + +julia> M = QuiverModuliSpace(Q, [1, 2], [2, -1]); + +julia> isempty(torus_fixed_components(M)) +true +``` +""" +function torus_fixed_components(M::QuiverModuliSpace) + _require_stable_fixed_locus(M) + + component_type = NamedTuple{(:beta, :moduli),Tuple{CoveringDimVector,QuiverModuliSpace}} + components = component_type[] + for beta in compatible_dimension_vectors(M.Q, M.d) + moduli = _fixed_component_moduli(M, beta) + moduli === nothing && continue + push!(components, (beta=beta, moduli=moduli)) + end + return components +end diff --git a/src/Hodge.jl b/src/Hodge.jl index 8a903ee..1f01062 100644 --- a/src/Hodge.jl +++ b/src/Hodge.jl @@ -454,7 +454,7 @@ julia> betti_numbers(M) ``` """ function betti_numbers(M::QuiverModuliSpace) - !is_coprime(M.d, M.theta) && throw(ArgumentError("d and theta are not coprime")) + !is_coprime(M) && throw(ArgumentError("d and theta are not coprime")) N = dimension(M) P = poincare_polynomial(M) @@ -503,7 +503,7 @@ L^6 + L^5 + 3*L^4 + 3*L^3 + 3*L^2 + L + 1 ``` """ function poincare_polynomial(M::QuiverModuliSpace) - !is_coprime(M.d, M.theta) && throw(ArgumentError("d and theta are not coprime")) + !is_coprime(M) && throw(ArgumentError("d and theta are not coprime")) m = motive(M.Q, M.d, M.theta, M.denom) v = Singular.transcendence_basis(Singular.parent(m))[1] diff --git a/src/Moduli.jl b/src/Moduli.jl index 4dc9f19..8702782 100644 --- a/src/Moduli.jl +++ b/src/Moduli.jl @@ -42,8 +42,9 @@ end """ is_coprime(M::QuiverModuli) -Checks if the stability parameter is coprime with the dimension vector, -i.e., if for all subdimension vectors ``e`` of ``d``, ``\\theta\\cdot e \\neq 0``. +Checks if the slope stability is coprime with the dimension vector, i.e., if +every proper nonzero subdimension vector has slope different from that of +`M.d`. # Input @@ -65,7 +66,7 @@ true ``` """ function is_coprime(M::QuiverModuli) - return is_coprime(M.d, M.theta) + return is_coprime(M.d, M.theta, M.denom) end """ @@ -587,7 +588,7 @@ false ``` """ function semistable_equals_stable(M::QuiverModuli) - if is_coprime(M.d, M.theta) || !has_semistables(M.Q, M.d, M.theta, M.denom) + if is_coprime(M) || !has_semistables(M.Q, M.d, M.theta, M.denom) return true end return length(all_luna_types(M; stable=false)) == 0 diff --git a/src/QuiverTools.jl b/src/QuiverTools.jl index 7186e68..2a705ac 100644 --- a/src/QuiverTools.jl +++ b/src/QuiverTools.jl @@ -62,6 +62,11 @@ export euler_form, euler_matrix, is_root, is_schur_root, is_real_root, is_imagin is_isotropic_root, general_ext, general_hom, canonical_decomposition, in_fundamental_domain +# Covering quiver +export CoveringDimVector, shift_beta, covering_euler_form, extract_finite_subquiver, + compatible_dimension_vectors, torus_fixed_components, + weight_space_dimension, tangent_weight_multiplicities + # Moduli export all_luna_types, is_luna_type, dimension_of_luna_stratum export is_nonempty, codimension_unstable_locus, dimension, is_smooth, @@ -137,6 +142,7 @@ include("Stability.jl") include("RepresentationTheory.jl") include("Misc.jl") include("Constructors.jl") +include("CoveringQuiver.jl") include("Moduli.jl") include("Hodge.jl") include("Chow.jl") diff --git a/src/Stability.jl b/src/Stability.jl index c9dbaba..4df7193 100644 --- a/src/Stability.jl +++ b/src/Stability.jl @@ -32,18 +32,25 @@ function canonical_stability(Q::Quiver, d::AbstractVector{Int}) end """ - is_coprime(d::AbstractVector{Int}, theta::AbstractVector{Int}) + is_coprime( + d::AbstractVector{Int}, + theta::AbstractVector{Int}, + denom::Function=sum, + ) -Check if `d` is `theta`-coprime. +Check if `d` is coprime for the slope determined by `theta` and `denom`. -A dimension vector ``d`` is said to be ``\\theta``-coprime for the -stability parameter ``\\theta`` if all subdimension vectors ``0 \\neq e < d`` -satisfy ``\\theta * e \\neq 0``. +A dimension vector ``d`` is coprime for a slope ``\\mu`` if every +subdimension vector ``0 \\neq e < d`` satisfies +``\\mu(e) \\neq \\mu(d)``. If ``\\theta \\cdot d = 0``, this is +equivalent to ``\\theta \\cdot e \\neq 0``. # Input - `d::AbstractVector{Int}` a dimension vector. - `theta::AbstractVector{Int}` a stability parameter. +- `denom::Function` a function computing the slope denominator. Default is + `sum`. # Output @@ -59,11 +66,19 @@ true julia> is_coprime([3, 3], theta) false + +julia> is_coprime([2], [1]) +false ``` """ -function is_coprime(d::AbstractVector{Int}, theta::AbstractVector{Int}) +function is_coprime( + d::AbstractVector{Int}, + theta::AbstractVector{Int}, + denom::Function=sum, +) + slope_d = slope(d, theta, denom) return all( - e -> theta' * e != 0, + e -> slope(e, theta, denom) != slope_d, all_subdimension_vectors(d; nonzero=true, strict=true), ) end @@ -751,6 +766,6 @@ end function has_properly_semistables( Q::Quiver, d::AbstractVector{Int}, theta::AbstractVector{Int}, denom::Function=sum ) - is_coprime(d, theta) && return false + is_coprime(d, theta, denom) && return false return !isempty(all_luna_types(Q, d, theta, denom; stable=false)) end diff --git a/test/covering_quiver.jl b/test/covering_quiver.jl new file mode 100644 index 0000000..4bac965 --- /dev/null +++ b/test/covering_quiver.jl @@ -0,0 +1,251 @@ +@testset "covering quiver" begin + @testset "input validation" begin + Q = kronecker_quiver(2) + beta = CoveringDimVector((1, [0, 0]) => 1) + M = QuiverModuliSpace(Q, [1, 1], [1, -1]) + stable_beta = CoveringDimVector( + (1, [0, 0]) => 1, + (2, [1, 0]) => 1, + ) + + @test_throws ArgumentError compatible_dimension_vectors(Q, [1, 1, 1]) + @test_throws ArgumentError compatible_dimension_vectors(Q, [-1, 1]) + @test_throws ArgumentError extract_finite_subquiver(Q, beta, [1]) + @test_throws ArgumentError weight_space_dimension(M, stable_beta, [0]) + @test_throws ArgumentError weight_space_dimension(M, beta, [0, 0]) + + invalid_vertex = CoveringDimVector((3, [0, 0]) => 1) + @test_throws ArgumentError extract_finite_subquiver(Q, invalid_vertex) + @test_throws ArgumentError covering_euler_form(Q, invalid_vertex, beta) + + nonpositive_vertex = CoveringDimVector((0, [0, 0]) => 1) + @test_throws ArgumentError extract_finite_subquiver(Q, nonpositive_vertex) + + wrong_coordinate = CoveringDimVector((1, [0]) => 1) + @test_throws DimensionMismatch extract_finite_subquiver(Q, wrong_coordinate) + @test_throws DimensionMismatch shift_beta(beta, [0]) + + zero_multiplicity = CoveringDimVector((1, [0, 0]) => 0) + negative_multiplicity = CoveringDimVector((1, [0, 0]) => -1) + @test_throws ArgumentError extract_finite_subquiver(Q, zero_multiplicity) + @test_throws ArgumentError shift_beta(negative_multiplicity, [0, 0]) + + gamma = CoveringDimVector((2, [0]) => 1) + @test_throws DimensionMismatch covering_euler_form(Q, beta, gamma) + + empty_lift = QuiverModuliSpace(Quiver([0 1; 0 0]), [1, 2], [2, -1], "stable") + empty_beta = CoveringDimVector((1, [0]) => 1, (2, [1]) => 2) + @test_throws ArgumentError weight_space_dimension(empty_lift, empty_beta, [0]) + end + + @test compatible_dimension_vectors(kronecker_quiver(2), [0, 0]) == + [CoveringDimVector()] + + @testset "Kronecker counts" begin + for m in 1:5 + @test length(compatible_dimension_vectors(kronecker_quiver(m), [1, 1])) == m + end + + @test length(compatible_dimension_vectors(kronecker_quiver(3), [1, 2])) == 6 + @test length(compatible_dimension_vectors(kronecker_quiver(3), [2, 3])) == 55 + end + + @testset "deterministic representatives" begin + betas = compatible_dimension_vectors(kronecker_quiver(3), [2, 3]) + canonical_betas = QuiverTools._canonicalize.(betas) + @test issorted(canonical_betas) + + beta = CoveringDimVector( + (1, [0, 0, 0]) => 1, + (2, [1, 0, 0]) => 1, + ) + M = QuiverModuliSpace(kronecker_quiver(3), [1, 1], [1, -1]) + @test issorted(first.(tangent_weight_multiplicities(M, beta))) + end + + @testset "quivers without arrows" begin + Q = Quiver(zeros(Int, 1, 1)) + beta = only(compatible_dimension_vectors(Q, [2])) + + @test beta == CoveringDimVector((1, Int[]) => 2) + @test covering_euler_form(Q, beta, beta) == 4 + @test shift_beta(beta, Int[]) == beta + end + + @testset "finite support calculations" begin + examples = [ + (kronecker_quiver(2), [1, 1]), + (kronecker_quiver(3), [1, 1]), + (kronecker_quiver(3), [2, 3]), + (subspace_quiver(3), [1, 1, 1, 2]), + ] + + for (Q, d) in examples, beta in compatible_dimension_vectors(Q, d) + sub_Q, sub_d, _ = extract_finite_subquiver(Q, beta) + @test covering_euler_form(Q, beta, beta) == euler_form(sub_Q, sub_d, sub_d) + end + + Q = kronecker_quiver(3) + beta = CoveringDimVector( + (1, [0, 0, 0]) => 1, + (2, [1, 0, 0]) => 1, + ) + gamma = CoveringDimVector( + (1, [0, 0, 0]) => 1, + (2, [0, 1, 0]) => 1, + ) + for chi in ([0, 0, 0], [1, 0, 0], [2, -1, 3], [-5, 4, 7]) + @test covering_euler_form(Q, beta, gamma) == covering_euler_form( + Q, + shift_beta(beta, chi), + shift_beta(gamma, chi), + ) + end + @test shift_beta(shift_beta(beta, [3, -2, 1]), [-3, 2, -1]) == beta + + _, sub_d, sub_theta, vertex_map = extract_finite_subquiver(Q, beta, [1, -1]) + vertices = sort(collect(keys(vertex_map))) + @test sub_d == [beta[vertex] for vertex in vertices] + @test sub_theta == [vertex[1] == 1 ? 1 : -1 for vertex in vertices] + + pushed_beta = [1, 1] + pushed_gamma = [1, 1] + shifted_sum = sum( + covering_euler_form(Q, beta, shift_beta(gamma, [-i, -j, -k])) for + i in -2:2 for j in -2:2 for k in -2:2 + ) + @test shifted_sum == euler_form(Q, pushed_beta, pushed_gamma) + end + + @testset "tangent weights" begin + examples = QuiverModuliSpace[ + QuiverModuliSpace(kronecker_quiver(2), [1, 1], [1, -1]), + QuiverModuliSpace(kronecker_quiver(3), [1, 1], [1, -1]), + QuiverModuliSpace(kronecker_quiver(3), [2, 3]), + QuiverModuliSpace(subspace_quiver(3), [1, 1, 1, 2]), + ] + + for M in examples + components = torus_fixed_components(M) + for component in components + weights = tangent_weight_multiplicities(M, component.beta) + @test all(last(weight) > 0 for weight in weights) + @test sum(last, weights; init=0) == 1 - euler_form(M.Q, M.d, M.d) + end + end + + for m in 2:4 + Q = kronecker_quiver(m) + M = QuiverModuliSpace(Q, [1, 1], [1, -1]) + expected = 1 - euler_form(Q, [1, 1], [1, 1]) + for component in torus_fixed_components(M) + weights = tangent_weight_multiplicities(M, component.beta) + @test all(last(weight) > 0 for weight in weights) + @test sum(last, weights; init=0) == expected + end + end + + Q = kronecker_quiver(2) + M = QuiverModuliSpace(Q, [1, 1], [1, -1]) + weights = [ + tangent_weight_multiplicities(M, component.beta) for + component in torus_fixed_components(M) + ] + @test length(weights) == 2 + @test all(length(weight) == 1 for weight in weights) + @test only(first.(weights[1])) == .-only(first.(weights[2])) + + Q = kronecker_quiver(3) + M = QuiverModuliSpace(Q, [3, 4], [4, -3], "stable") + positive_dimensional_beta = CoveringDimVector( + (1, [0, 0, 0]) => 1, + (1, [0, 1, -1]) => 1, + (1, [1, 0, -1]) => 1, + (2, [0, 0, 1]) => 1, + (2, [0, 1, 0]) => 1, + (2, [1, 0, 0]) => 1, + (2, [1, 1, -1]) => 1, + ) + @test weight_space_dimension(M, positive_dimensional_beta, zeros(Int, 3)) == 1 + weights = tangent_weight_multiplicities(M, positive_dimensional_beta) + @test ([0, 0, 0], 1) in weights + @test sum(last, weights; init=0) == 1 - euler_form(Q, M.d, M.d) + + Q = jordan_quiver() + M = QuiverModuliSpace(Q, [1], [0]) + component = only(torus_fixed_components(M)) + @test tangent_weight_multiplicities(M, component.beta) == [([1], 1)] + end + + @testset "fixed components" begin + M = QuiverModuliSpace(kronecker_quiver(2), [1, 1], [1, -1]) + components = torus_fixed_components(M) + @test length(components) == 2 + @test all(component.moduli.condition == "stable" for component in components) + @test all(dimension(component.moduli) == 0 for component in components) + @test all( + [ + sum( + multiplicity for ((v, _), multiplicity) in component.beta if v == i; + init=0, + ) for i in eachindex(M.d) + ] == M.d + for component in components + ) + + Q = Quiver([0 1; 0 0]) + M = QuiverModuliSpace(Q, [1, 2], [2, -1]) + @test isempty(torus_fixed_components(M)) + + M = QuiverModuliSpace(kronecker_quiver(3), [2, 3]) + @test length(torus_fixed_components(M)) == 13 + + M = QuiverModuliSpace(kronecker_quiver(3), [2, 2], [1, -1]) + @test_throws ArgumentError torus_fixed_components(M) + @test_throws ArgumentError tangent_weight_multiplicities( + M, + first(compatible_dimension_vectors(M.Q, M.d)), + ) + + nonnormalized = QuiverModuliSpace( + Quiver(zeros(Int, 1, 1)), + [2], + [1], + "semistable", + ) + @test_throws ArgumentError torus_fixed_components(nonnormalized) + + stable_locus = QuiverModuliSpace( + kronecker_quiver(3), + [2, 2], + [1, -1], + "stable", + ) + @test torus_fixed_components(stable_locus) isa Vector + + denom = d -> 2 * d[1] + d[2] + custom_denom_locus = QuiverModuliSpace( + kronecker_quiver(3), + [1, 2], + [2, -1], + "stable", + denom, + ) + components = torus_fixed_components(custom_denom_locus) + @test !isempty(components) + @test all( + component.moduli.denom(component.moduli.d) == denom(custom_denom_locus.d) + for component in components + ) + for component in components + _, _, vertex_map = extract_finite_subquiver(custom_denom_locus.Q, component.beta) + for ((v, _), sub_v) in vertex_map + sub_e = zeros(Int, length(vertex_map)) + sub_e[sub_v] = 1 + projected_e = zeros(Int, n_vertices(custom_denom_locus.Q)) + projected_e[v] = 1 + @test component.moduli.denom(sub_e) == denom(projected_e) + end + end + end +end diff --git a/test/runtests.jl b/test/runtests.jl index baacaa5..ba28ec6 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -29,6 +29,12 @@ doctest(QuiverTools; manual=false, testset="Doctests") @test has_stables(Q, [0, 3], [1, 0]) == false @test has_semistables(Q, [3, 0], [0, -1]) == true @test has_semistables(Q, [0, 3], [1, 0]) == true + + # Coprimality compares slopes, also when theta is not normalized by theta*d = 0. + Q = Quiver(zeros(Int, 1, 1)) + M = QuiverModuliSpace(Q, [2], [1], "semistable") + @test !is_coprime(M) + @test !semistable_equals_stable(M) end; @testset "HN types" begin @@ -47,6 +53,8 @@ end; @test string(all_hn_types(Q, d, theta; ordered=true)) == expected end; +include("covering_quiver.jl") + @testset "Constructors" begin # equivalences from the Sage docstrings; == compares adjacency matrices only @test thickened_subspace_quiver(2, 6) == three_vertex_quiver(0, 6, 6)