diff --git a/docs/src/methods/walls-and-chambers.md b/docs/src/methods/walls-and-chambers.md index 6d4a396..a26be3f 100644 --- a/docs/src/methods/walls-and-chambers.md +++ b/docs/src/methods/walls-and-chambers.md @@ -1,10 +1,12 @@ # Walls and Chambers decomposition QuiverTools implements the walls-and-chambers decomposition -of the GIT problem of quiver moduli using the polyhedral geometry -interface of `Oscar.jl`. +of the GIT problem of quiver moduli. +The boolean equivalence test [`git_equivalent`](@ref) is implemented combinatorially +and has no optional dependencies. +Constructing the polyhedral cones and fans uses the geometry interface of `Oscar.jl`. -This functionality lives in a *package extension* +The polyhedral functionality lives in a *package extension* that depends on the `Oscar` algebra system. To use it, one must diff --git a/docs/src/tutorial.md b/docs/src/tutorial.md index f42e4c9..24b1b95 100644 --- a/docs/src/tutorial.md +++ b/docs/src/tutorial.md @@ -587,7 +587,8 @@ The provided methods are described in [Walls and Chambers decomposition](@ref). ### Technical note -The walls and chambers functionality is contained in a *package extension* +The boolean function [`git_equivalent`](@ref) does not require an optional dependency. +Constructing the walls, chambers, and VGIT fan uses a *package extension* that depends on the `Oscar` algebra system. To use it, one must diff --git a/ext/QuiverToolsOscarExt.jl b/ext/QuiverToolsOscarExt.jl index fde35f8..960deb5 100644 --- a/ext/QuiverToolsOscarExt.jl +++ b/ext/QuiverToolsOscarExt.jl @@ -16,7 +16,7 @@ using IterTools: IterTools import Memoization: @memoize import QuiverTools: is_special_subdimension_vector, all_special_subdimension_vectors, sst, - vgit_walls, wall_system, vgit_chambers, vgit_fan, git_equivalent, all_stability_parameters + vgit_walls, wall_system, vgit_chambers, vgit_fan, all_stability_parameters # Disambiguate between Singular's and Oscar's overloads of `(::PolyRing)(::spoly)`, # which collide once both packages are loaded. @@ -86,13 +86,14 @@ function __helper_accelerate(P::Oscar.Polyhedron) end @memoize Dict function sst(Q, e) - e_perp = Oscar.polyhedron([e, -e], [0, 0]) #e^{\perp} - all_gen = filter( - eprime -> !all(ei == 0 for ei in eprime) && eprime != e, - all_general_subdimension_vectors(Q, e), + data = QuiverTools.__semistable_cone_data(Q, e) + equations = vcat(data.equations, .-data.equations) + e_perp = Oscar.polyhedron(equations, zeros(Int, length(equations))) + isempty(data.inequalities) && return e_perp + return intersect( + e_perp, + Oscar.polyhedron(data.inequalities, zeros(Int, length(data.inequalities))), ) - isempty(all_gen) && return e_perp - return intersect(e_perp, Oscar.polyhedron(all_gen, zeros(Int, length(all_gen)))) end @memoize Dict function vgit_walls(Q, d; inner=false, top_dimension=true) @@ -280,19 +281,6 @@ function vgit_fan(Q, d; verbose=false) ) end -function git_equivalent(Q, d, theta1, theta2) - theta1 == theta2 && return true - line = Oscar.convex_hull([theta1, theta2]) # 1-dimensional iif theta1 != theta2 - - # either the line lies in a wall or it intersects none of them - for w in vgit_walls(Q, d; top_dimension=false) - if !Oscar.issubset(line, w) && Oscar.is_feasible(Oscar.intersect(line, w)) - return false - end - end - return true -end - """ __sst_cone(Q::Quiver, e::AbstractVector{Int}) @@ -302,12 +290,10 @@ but seen as an Oscar `Cone` object. For internal use only for now. """ function __sst_cone(Q::Quiver, e::AbstractVector{Int}) - all_gen = filter( - eprime -> !all(ei == 0 for ei in eprime) && eprime != e, - all_general_subdimension_vectors(Q, e), - ) - isempty(all_gen) && return Oscar.cone_from_inequalities([e, -e]) - return Oscar.cone_from_inequalities(all_gen, [e]) + data = QuiverTools.__semistable_cone_data(Q, e) + isempty(data.inequalities) && + return Oscar.cone_from_inequalities(vcat(data.equations, .-data.equations)) + return Oscar.cone_from_inequalities(data.inequalities, data.equations) end """ diff --git a/src/QuiverTools.jl b/src/QuiverTools.jl index 21944b9..c19b489 100644 --- a/src/QuiverTools.jl +++ b/src/QuiverTools.jl @@ -139,12 +139,12 @@ include("Misc.jl") include("Stability.jl") include("RepresentationTheory.jl") include("Constructors.jl") +include("WallsAndChambers.jl") include("Moduli.jl") include("Hodge.jl") include("Chow.jl") include("Teleman.jl") include("Bundles.jl") -include("WallsAndChambers.jl") # Warm the JIT for the shared Chow/Hodge computation path so the user's first # invariant computation is near-instant. Compilation is input-independent, so a diff --git a/src/WallsAndChambers.jl b/src/WallsAndChambers.jl index 812af5d..e480fde 100644 --- a/src/WallsAndChambers.jl +++ b/src/WallsAndChambers.jl @@ -1,7 +1,145 @@ -# Walls-and-chambers and VGIT functionality relies on Oscar's polyhedral geometry -# and therefore lives in the Oscar package extension (ext/QuiverToolsOscarExt.jl). -# The functions below are only method stubs: the extension adds the real methods -# once Oscar is loaded. Until then, calling any of them raises a clear error. +# The combinatorial VGIT layer is independent of Oscar. It records the equations and +# inequalities defining semistable cones and walls, and provides exact point and line +# segment predicates. The Oscar extension consumes the same data to construct the +# corresponding polyhedra. + +# Equations and inequalities defining sst(d). The inequalities are Schofield's general +# subdimension vectors; zero and d impose no inequalities and are omitted. +function __semistable_cone_data(Q::Quiver, d::AbstractVector{Int}) + return __semistable_cone_data(Q, Vector{Int}(d)) +end + +@memoize Dict function __semistable_cone_data(Q::Quiver, d::Vector{Int}) + inequalities = filter( + e -> any(!=(0), e) && e != d, + all_general_subdimension_vectors(Q, d), + ) + return (equations=[d], inequalities=inequalities) +end + +# Equations and inequalities defining W_e = sst(e) ∩ sst(d-e) ∩ sst(d). +function __vgit_wall_data( + Q::Quiver, + d::AbstractVector{Int}, + e::AbstractVector{Int}, +) + return __vgit_wall_data(Q, Vector{Int}(d), Vector{Int}(e)) +end + +@memoize Dict function __vgit_wall_data(Q::Quiver, d::Vector{Int}, e::Vector{Int}) + data = __semistable_cone_data.(Ref(Q), (e, d - e, d)) + equations = unique!(vcat((datum.equations for datum in data)...)) + inequalities = unique!(vcat((datum.inequalities for datum in data)...)) + return (equations=equations, inequalities=inequalities) +end + +# Evaluate a scalar product without losing exactness when the parameter is rational. +function __exact_dot(x::AbstractVector{Int}, y::AbstractVector) + length(x) == length(y) || throw(DimensionMismatch("vectors must have equal lengths")) + value = big(0) // big(1) + for i in eachindex(x, y) + value += big(x[i]) * y[i] + end + return value +end + +function __in_rational_cone(data, theta::AbstractVector) + return all(normal -> iszero(__exact_dot(normal, theta)), data.equations) && + all(normal -> __exact_dot(normal, theta) <= 0, data.inequalities) +end + +"""Return whether `theta` belongs to the semistable cone `sst(d)`.""" +function __in_semistable_cone( + Q::Quiver, + d::AbstractVector{Int}, + theta::AbstractVector, +) + return __in_rational_cone(__semistable_cone_data(Q, d), theta) +end + +"""Return whether `theta` belongs to the VGIT wall `W_e`.""" +function __in_vgit_wall( + Q::Quiver, + d::AbstractVector{Int}, + e::AbstractVector{Int}, + theta::AbstractVector, +) + return __in_rational_cone(__vgit_wall_data(Q, d, e), theta) +end + +# Intersect the segment theta1--theta2 with a rational polyhedral cone. The result is +# the exact closed interval of parameters t in [0,1] for which +# (1-t)theta1 + t theta2 belongs to the cone, or nothing when it is empty. +function __segment_cone_intersection(data, theta1::AbstractVector, theta2::AbstractVector) + length(theta1) == length(theta2) || + throw(DimensionMismatch("stability parameters must have equal lengths")) + lower = big(0) // big(1) + upper = big(1) // big(1) + + for normal in data.equations + initial = __exact_dot(normal, theta1) + delta = __exact_dot(normal, theta2) - initial + if iszero(delta) + iszero(initial) || return nothing + else + crossing = -initial / delta + lower = max(lower, crossing) + upper = min(upper, crossing) + lower <= upper || return nothing + end + end + + for normal in data.inequalities + initial = __exact_dot(normal, theta1) + delta = __exact_dot(normal, theta2) - initial + if iszero(delta) + initial <= 0 || return nothing + elseif delta > 0 + upper = min(upper, -initial / delta) + else + lower = max(lower, -initial / delta) + end + lower <= upper || return nothing + end + + return (lower, upper) +end + +"""Return whether `theta` lies in a VGIT chamber rather than on a wall.""" +function __is_vgit_chamber_parameter( + Q::Quiver, + d::AbstractVector{Int}, + theta::AbstractVector, +) + __in_semistable_cone(Q, d, theta) || return false + return all(all_subdimension_vectors(d; nonzero=true, strict=true)) do e + !__in_vgit_wall(Q, d, e, theta) + end +end + +# The target may meet a wall at the endpoint of the segment, but the segment must not +# meet any wall earlier. Using complete cone data also handles smaller walls when the +# whole segment lies in their defining hyperplane. +function __in_closure_of_vgit_chamber( + Q::Quiver, + d::AbstractVector{Int}, + theta::AbstractVector, + thetabar::AbstractVector, +) + __is_vgit_chamber_parameter(Q, d, theta) || return false + __in_semistable_cone(Q, d, thetabar) || return false + endpoint = big(1) // big(1) + for e in all_subdimension_vectors(d; nonzero=true, strict=true) + intersection = __segment_cone_intersection(__vgit_wall_data(Q, d, e), theta, thetabar) + intersection === nothing && continue + intersection == (endpoint, endpoint) || return false + end + return true +end + +# Polyhedral wall and chamber objects rely on Oscar and therefore live in the Oscar +# package extension (ext/QuiverToolsOscarExt.jl). The declarations below are method +# stubs which load that extension on demand. # # To enable them, load Oscar alongside QuiverTools. Use `import Oscar` rather than # `using Oscar`: it activates the extension without bringing Oscar's exports into @@ -201,7 +339,8 @@ By [[Corollary 4.4, MR5007902](https://mathscinet.ams.org/mathscinet-getitem?mr= this is equivalent to their convex hull either lying in a wall or not intersecting any of them. -Requires Oscar: run `import Oscar` to enable this function. +This computation uses the combinatorial equations and inequalities defining the VGIT +walls and does not require Oscar. # Example @@ -251,7 +390,21 @@ julia> any(y in w for w in W) true ``` """ -function git_equivalent end +function git_equivalent( + Q::Quiver, + d::AbstractVector{Int}, + theta1::AbstractVector, + theta2::AbstractVector, +) + theta1 == theta2 && return true + for e in all_subdimension_vectors(d; nonzero=true, strict=true) + data = __vgit_wall_data(Q, d, e) + __segment_cone_intersection(data, theta1, theta2) === nothing && continue + __in_rational_cone(data, theta1) && __in_rational_cone(data, theta2) && continue + return false + end + return true +end """ all_stability_parameters(Q::Quiver, d::AbstractVector{Int}; generic::Bool=false) @@ -356,8 +509,7 @@ end # take precedence once Oscar has been loaded. for f in ( :is_special_subdimension_vector, :all_special_subdimension_vectors, :sst, - :vgit_walls, :wall_system, :vgit_chambers, :vgit_fan, :git_equivalent, - :all_stability_parameters, + :vgit_walls, :wall_system, :vgit_chambers, :vgit_fan, :all_stability_parameters, ) @eval @oscar_stub $f end diff --git a/test/runtests.jl b/test/runtests.jl index fc79524..0b8cacc 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -8,12 +8,74 @@ using Test, QuiverTools, Documenter @info "Almost all the tests are in the documentation." +@testset "Oscar-independent VGIT predicates" begin + # VGIT boolean queries must not load the optional Oscar extension. + @test Base.get_extension(QuiverTools, :QuiverToolsOscarExt) === nothing + + Q = three_vertex_quiver(2, 3, 4) + d = [1, 2, 2] + theta1 = [2, -1 // 2, -1 // 2] + theta2 = [2, 1 // 2, -3 // 2] + @test !git_equivalent(Q, d, theta1, theta2) + @test git_equivalent(Q, d, theta1, 2 .* theta1) + + S = subspace_quiver(6) + d = [1, 1, 1, 1, 1, 2, 3] + theta = [3, 3, 3, 3, 3, 3, -7] + thetabar = [1, 1, 1, 1, 1, 2, -3] + @test QuiverTools.__is_vgit_chamber_parameter(S, d, theta) + @test QuiverTools.__in_closure_of_vgit_chamber(S, d, theta, thetabar) + @test !QuiverTools.__is_vgit_chamber_parameter(S, d, thetabar) + + @test Base.get_extension(QuiverTools, :QuiverToolsOscarExt) === nothing +end; + # `import Oscar` (not `using`) loads Oscar so the walls-and-chambers / VGIT extension # 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") +import Oscar + +@testset "VGIT predicates agree with Oscar" begin + Q = three_vertex_quiver(2, 3, 4) + d = [1, 2, 2] + parameters = [ + [2, -1 // 2, -1 // 2], + [2, 1 // 2, -3 // 2], + [1, 3 // 2, -2], + [0, 1, -1], + [1, 0, -1 // 2], + [0, 0, 0], + ] + + cone = sst(Q, d) + for theta in parameters + @test QuiverTools.__in_semistable_cone(Q, d, theta) == (theta in cone) + end + + for e in QuiverTools.all_subdimension_vectors(d; nonzero=true, strict=true) + wall = reduce(Oscar.intersect, (sst(Q, e), sst(Q, d - e), cone)) + for theta in parameters + @test QuiverTools.__in_vgit_wall(Q, d, e, theta) == (theta in wall) + end + end + + walls = vgit_walls(Q, d; top_dimension=false) + function oscar_git_equivalent(theta1, theta2) + theta1 == theta2 && return true + line = Oscar.convex_hull([theta1, theta2]) + return all(walls) do wall + Oscar.issubset(line, wall) || !Oscar.is_feasible(Oscar.intersect(line, wall)) + end + end + for theta1 in parameters, theta2 in parameters + @test git_equivalent(Q, d, theta1, theta2) == + oscar_git_equivalent(theta1, theta2) + end +end; + @testset "strict sst" begin # proper-semistability Q = kronecker_quiver(2)