diff --git a/Project.toml b/Project.toml index eb46435051c4..ee3be536eabd 100644 --- a/Project.toml +++ b/Project.toml @@ -16,6 +16,7 @@ Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a" Nemo = "2edaba10-b0f1-5616-af89-8c11ac63239a" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" Polymake = "d720cf60-89b5-51f5-aff5-213f193123e7" +ProgressMeter = "92933f4c-e287-5a05-a399-4b506db050ca" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" RandomExtensions = "fb686558-2515-59ef-acaa-46db3789a887" Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b" @@ -37,6 +38,7 @@ Markdown = "1.6" Nemo = "0.47.1" Pkg = "1.6" Polymake = "0.11.20" +ProgressMeter = "1.10.2" Random = "1.6" RandomExtensions = "0.4.3" Serialization = "1.6" diff --git a/docs/oscar_references.bib b/docs/oscar_references.bib index c6e93053f394..c486f333ccec 100644 --- a/docs/oscar_references.bib +++ b/docs/oscar_references.bib @@ -1631,6 +1631,24 @@ @Article{Kah10 doi = {10.1007/s10463-010-0290-9} } +@InProceedings{Kal02, + author = {Kalai, Gil}, + title = {Algebraic shifting}, + editor = {Hibi, Takayuki}, + booktitle = {Computational commutative algebra and combinatorics}, + series = {Advanced Studies in Pure Mathematics}, + number = {33}, + publisher = {Mathematical Society of Japan}, + pages = {121--163}, + year = {2002}, + doi = {10.2969/aspm/03310121}, + venue = {Osaka}, + eventdate = {1999}, + shortseries = {Adv. Stud. Pure Math.}, + shortpublisher= {Math. Soc. Japan}, + location = {Tokyo} +} + @Article{Kem02, author = {Kemper, Gregor}, title = {The calculation of radical ideals in positive characteristic}, diff --git a/experimental/AlgebraicShifting/README.md b/experimental/AlgebraicShifting/README.md new file mode 100644 index 000000000000..c4d4b15e8874 --- /dev/null +++ b/experimental/AlgebraicShifting/README.md @@ -0,0 +1,12 @@ +# An example template for the experimental section + +## Aims + +This package grew out of a project of Antony della Vecchia, Michael Joswig and Fabian Lenzen on "Partial Algebraic Shifting". +Part of this project is the development of new methods for algebraic shifting; these are implemented here. + +## Status + +Full and partial shifting in the exterior algebra fully functional. + + diff --git a/experimental/AlgebraicShifting/docs/doc.main b/experimental/AlgebraicShifting/docs/doc.main new file mode 100644 index 000000000000..bd183bbd137a --- /dev/null +++ b/experimental/AlgebraicShifting/docs/doc.main @@ -0,0 +1,8 @@ +[ + "Algebraic Shifting" => [ + "introduction.md", + "exterior_shifting.md", + "partial_shift_graph.md" + ] +] + diff --git a/experimental/AlgebraicShifting/docs/src/exterior_shifting.md b/experimental/AlgebraicShifting/docs/src/exterior_shifting.md new file mode 100644 index 000000000000..f482d29c781c --- /dev/null +++ b/experimental/AlgebraicShifting/docs/src/exterior_shifting.md @@ -0,0 +1,24 @@ +```@meta +CurrentModule = Oscar +DocTestSetup = Oscar.doctestsetup() +``` + +# Exterior Shifting + +## Uniform Hypergraphs +```@docs +uniform_hypergraph +``` + +## Matrix Constructions + +```@docs +generic_unipotent_matrix +rothe_matrix +compound_matrix +``` + +## Exterior (Partial) Shifting +```@docs +exterior_shift +``` diff --git a/experimental/AlgebraicShifting/docs/src/introduction.md b/experimental/AlgebraicShifting/docs/src/introduction.md new file mode 100644 index 000000000000..517644268648 --- /dev/null +++ b/experimental/AlgebraicShifting/docs/src/introduction.md @@ -0,0 +1,29 @@ +```@meta +CurrentModule = Oscar +DocTestSetup = Oscar.doctestsetup() +``` + + +# Introduction + +Algebraic shifting is a widely applicable method for converting a uniform hypergraph $S$ into another hypergraph, $\Delta(S)$, which is somehow simpler but still retains key properties of $S$. +Often this is applied to simplicial complexes, where each layer of $(k-1)$-faces forms a $k$-uniform hypergraph. + +Here we focus on full and partial algebraic shifting in the exterior algebra. +For some background on the subject we refer to [Kal02](@cite). + + +## Status + +This part of OSCAR is in an experimental state; please see [Adding new projects to experimental](@ref) for what this means. + +## Contact + +Please direct questions about this part of OSCAR to the following people: +* [Antony Della Vecchia](https://antonydellavecchia.github.io) +* [Michael Joswig](https://page.math.tu-berlin.de/~joswig/) + + +You can ask questions in the [OSCAR Slack](https://www.oscar-system.org/community/#slack). + +Alternatively, you can [raise an issue on github](https://www.oscar-system.org/community/#how-to-report-issues). diff --git a/experimental/AlgebraicShifting/docs/src/partial_shift_graph.md b/experimental/AlgebraicShifting/docs/src/partial_shift_graph.md new file mode 100644 index 000000000000..876ea0af7d97 --- /dev/null +++ b/experimental/AlgebraicShifting/docs/src/partial_shift_graph.md @@ -0,0 +1,12 @@ +```@meta +CurrentModule = Oscar +DocTestSetup = Oscar.doctestsetup() +``` + +# Partial Shift Graph + +```@docs +partial_shift_graph_vertices +partial_shift_graph +contracted_partial_shift_graph +``` diff --git a/experimental/AlgebraicShifting/src/AlgebraicShifting.jl b/experimental/AlgebraicShifting/src/AlgebraicShifting.jl new file mode 100644 index 000000000000..4dc743c923a4 --- /dev/null +++ b/experimental/AlgebraicShifting/src/AlgebraicShifting.jl @@ -0,0 +1,119 @@ +include("UniformHypergraph.jl") +include("PartialShift.jl") +include("PartialShiftGraph.jl") + +export UniformHypergraph + +export compound_matrix +export contracted_partial_shift_graph +export exterior_shift +export face_size +export generic_unipotent_matrix +export partial_shift_graph +export partial_shift_graph_vertices +export rothe_matrix +export uniform_hypergraph + +function independent_columns(A::MatElem) + col_indices = Int[] + row_index = 1 + row_bound = nrows(A) + for col_index in 1:ncols(A) + if !is_zero(A[row_index, col_index]) + push!(col_indices, col_index) + row_index += 1 + end + row_index > row_bound && break + end + return col_indices +end + + +""" + symmetric_shift(F::Field, K::SimplicialComplex) + +Returns the symmetric shift of `K` + +see survey on algebraic shifting Gil Kalai (properly cite at some point) +""" +function symmetric_shift(F::Field, K::SimplicialComplex; + change_of_basis::T = nothing) where T <: Union{Nothing, MatElem} + + n = n_vertices(K) + is_generic = false + if isnothing(change_of_basis) + is_generic = true + # generic change of basis + Fy, y = polynomial_ring(F, :y => (1:n, 1:n)) + change_of_basis = matrix(Fy, hcat(y)) + matrix_base = Fy + else + matrix_base = base_ring(change_of_basis) + if matrix_base isa MPolyRing + is_generic = true + end + end + + Rx, x = polynomial_ring(matrix_base, n) + # the generators of the stanley reisner ideal are combinations of [x_1, ..., x_n] + R_K, _ = stanley_reisner_ring(Rx, K) + + # the computation is a over the field of fractions Fyx + # we use a different ring to generate monomial_basis, coefficients need to be a field, + # but we want to avoid using fraction field of Ry during row reduction + mb_ring, z = graded_polynomial_ring(F, n) + + + input_faces = apply_on_faces(K) do (dim_face, faces) + r = dim_face + 1 + + mb = reverse(monomial_basis(mb_ring, r)) + A = Vector{elem_type(matrix_base)}[] + mb_exponents = first.(collect.(exponents.(mb))) # gets monomial exponents + + for b in mb + # need to compare with some alternatives + transformed_monomial = evaluate(b, change_of_basis * gens(R_K)) + + # we need to iterate through exponents here since the functions terms, coefficients or exponents + # do not return 0 terms and we need to make sure the generic col aligns with the others + # this is needed for the case when the field has finite characteristic + # we use the lift because currently there is no function for get the coeff of + # a MPolyQuoRingElem, which means we also need to check if it's zero before adding the coefficient + col = [ + !is_zero(R_K(monomial(Rx, e))) ? coeff(lift(transformed_monomial), e) : matrix_base(0) + for e in mb_exponents] + + push!(A, col) + end + + C = matrix(matrix_base, reduce(hcat, A)) + + if is_generic + Oscar.ModStdQt.ref_ff_rc!(C) + else + rref!(C) + end + + smallest_basis_el = z[r]^r + smallest_index = findfirst(a -> a == smallest_basis_el, mb) + col_indices = filter(x -> x >= smallest_index, independent_columns(C)) + monomial_exponents = first.(exponents.(mb[col_indices])) + + shifted_sets = Vector{Int}[] + for me in monomial_exponents + shifted_set = Int[] + index_count = 1 + for (i, e) in enumerate(me) + for j in 1:e + push!(shifted_set, i - (r - index_count)) + index_count += 1 + end + end + push!(shifted_sets, shifted_set) + end + return shifted_sets + end + + return simplicial_complex(input_faces) +end diff --git a/experimental/AlgebraicShifting/src/PartialShift.jl b/experimental/AlgebraicShifting/src/PartialShift.jl new file mode 100644 index 000000000000..5c0916472305 --- /dev/null +++ b/experimental/AlgebraicShifting/src/PartialShift.jl @@ -0,0 +1,299 @@ +################################################################################ +# helpful functions for Partial Shifts +################################################################################ + +const ComplexOrHypergraph = Union{UniformHypergraph, SimplicialComplex} + +################################################################################ +# Matrix construction helper functions +function inversions(g::PermGroupElem) + return [(i,j) for j in 2:degree(parent(g)) for i in 1:j-1 if g(i) > g(j)] +end + +function generic_unipotent_matrix(R::MPolyRing) + x = gens(R) + n_vars = length(x) + @req is_square(n_vars) "indeterminants should come from a square matrix" + n = Int(sqrt(n_vars)) + x = matrix(R, reshape(x, n, n)) + u = identity_matrix(R, n) + # make unipotent matrix + for i in 1:n + for j in i + 1:n + u[i, j] = x[i, j] + end + end + return u +end + +@doc raw""" + generic_unipotent_matrix(R::MPolyRing) + generic_unipotent_matrix(F::Field, n::Int) + +Constructs a unipotent matrix with entries in a polynomial ring `R`. +One can also provide a field `F` and an integer `n`, +then the entries of the unipotent matrix will lie in a multivariate +polynomial ring over `F` with `n^2` variables. + +# Examples +```jldoctest +julia> R, x = polynomial_ring(QQ, :x=> (1:2, 1:2)) +(Multivariate polynomial ring in 4 variables over QQ, QQMPolyRingElem[x[1, 1] x[1, 2]; x[2, 1] x[2, 2]]) + +julia> generic_unipotent_matrix(R) +[1 x[1, 2]] +[0 1] + +julia> generic_unipotent_matrix(GF(2), 2) +[1 x[1, 2]] +[0 1] +``` +""" +function generic_unipotent_matrix(F::Field, n::Int) + Fx, x = polynomial_ring(F, :x => (1:n, 1:n)) + return generic_unipotent_matrix(Fx) +end + +@doc raw""" + rothe_matrix(F::Field, w::WeylGroupElem; K::Union{SimplicialComplex, Nothing} = nothing) + +For a base field `F` and a Weyl group element `w` return the matrix with entries in the +multivariate polynomial ring `R` with `n^2` many indeterminants where `n - 1` is the rank of the +root system of the Weyl group. +As `general_linear_group(n^2, R)` has a Bruhat decomposition, any element lies in a unique double coset $BwB$, where $B$ is the Borel group of upper triangular matrices. +The Rothe matrix is a normal form for the matrix on the left of a representative for the double coset corresponding to `w`. +This will be explained further once the corresponding preprint is on the arXiv. +We use the name Rothe matrix because of its resemblance with a Rothe diagram. (add ref? Knuth?) + +# Examples +```jldoctest +julia> W = weyl_group(:A, 4) +Weyl group for root system defined by Cartan matrix [2 -1 0 0; -1 2 -1 0; 0 -1 2 -1; 0 0 -1 2] + +julia> s = gens(W) +4-element Vector{WeylGroupElem}: + s1 + s2 + s3 + s4 + +julia> w = s[2] * s[3] * s[4] +s2 * s3 * s4 + +julia> rothe_matrix(GF(2), w) +[1 0 0 0 0] +[0 x[2, 3] x[2, 4] x[2, 5] 1] +[0 1 0 0 0] +[0 0 1 0 0] +[0 0 0 1 0] + +julia> rothe_matrix(QQ, perm([2, 3, 1])) +[x[1, 3] 1 0] +[x[2, 3] 0 1] +[ 1 0 0] +``` +""" +function rothe_matrix(F::Field, w::WeylGroupElem) + W = parent(w) + phi = isomorphism(PermGroup, W) + return rothe_matrix(F, phi(w)) +end + +function rothe_matrix(F::Field, p::PermGroupElem) + n = degree(parent(p)) + Fx, x = polynomial_ring(F, :x => (1:n, 1:n)) + u = identity_matrix(Fx, n) + for (i, j) in inversions(p) + u[i, j] = x[i, j] + end + return u * permutation_matrix(F, p) +end + +@doc raw""" + compound_matrix(m::MatElem, k::Int) + compound_matrix(p::PermGroupElem, k::Int) + compound_matrix(w::WeylGroupElem, k::Int) + compound_matrix(m::MatElem, K::Vector{Vector{Int}}) + +Given a matrix `m`, return the matrix where each entry is a `k`$\times$`k`-minor of `m`. +The entries of the compound matrix are ordered with respect to the lexicographic order on sets. +When passed a `PermGroupElem` or `WeylGroupElem`, return the compound matrix for their permutation matrix representation. + +Alternatively, passing a `UniformHypergraph` `K` will return the compound matrix with entries the `face_size(K)` minors, and restrict the rows to the rows corresponding to `K`. + +# Examples +```jldoctest +julia> M = generic_unipotent_matrix(QQ, 3) +[1 x[1, 2] x[1, 3]] +[0 1 x[2, 3]] +[0 0 1] + +julia> compound_matrix(M, 2) +[1 x[2, 3] x[1, 2]*x[2, 3] - x[1, 3]] +[0 1 x[1, 2]] +[0 0 1] + +julia> compound_matrix(perm([1, 3, 2]), 2) +[0 1 0] +[1 0 0] +[0 0 -1] + +julia> W = weyl_group(:A, 2) +Weyl group for root system defined by Cartan matrix [2 -1; -1 2] + +julia> compound_matrix(longest_element(W), 2) +[ 0 0 -1] +[ 0 -1 0] +[-1 0 0] + +julia> K = uniform_hypergraph([[1, 2], [2, 3]]) +UniformHypergraph(3, 2, [[1, 2], [2, 3]]) + +julia> compound_matrix(M, K) +[1 x[2, 3] x[1, 2]*x[2, 3] - x[1, 3]] +[0 0 1] +``` +""" +function compound_matrix(m::MatElem, K::UniformHypergraph) + @req size(m,1) == size(m,2) "Only valid for square matrices" + n = size(m, 1) + k = face_size(K) + nCk = sort(subsets(n, k)) + return matrix(base_ring(m), [det(m[row, col]) for row in faces(K), col in nCk]) +end + +compound_matrix(m::MatElem, k::Int) = compound_matrix(m, uniform_hypergraph(sort(subsets(size(m, 1), k)))) +compound_matrix(p::PermGroupElem, k::Int) = compound_matrix(permutation_matrix(ZZ, p), k) + +function compound_matrix(w::WeylGroupElem, k::Int) + iso = isomorphism(PermGroup, parent(w)) + return compound_matrix(permutation_matrix(ZZ, iso(w)), k) +end + +# this might be removed (currently is not used) +function _set_to_zero(K::SimplicialComplex, indices::Tuple{Int, Int}) + row, col = indices + row == col && return false + K_facets = facets(K) + for facet in K_facets + # if row index is not in face there is nothing to do + !(row in facet) && continue + # replace index from row with index from col in facet + S = push!(delete!(copy(facet), row), col) + !any(is_subset(S, check_facet) for check_facet in K_facets) && return false + end + return true +end + +_set_to_zero(K::UniformHypergraph, indices::Tuple{Int, Int}) = _set_to_zero(simplicial_complex(K), indices) + +############################################################################### +# Exterior shift +############################################################################### +function exterior_shift(K::UniformHypergraph, g::MatElem) + # the exterior shifting works in a different algebra that lends + # itself to an easier implementation + @req size(g, 1) == size(g, 2) "Change of basis matrix must be square." + @req size(g, 1) == n_vertices(K) "Matrix size does not match K." + matrix_base = base_ring(g) + nCk = sort!(subsets(n_vertices(K), face_size(K))) + c = compound_matrix(g, K) + if matrix_base isa MPolyRing + Oscar.ModStdQt.ref_ff_rc!(c) + elseif matrix_base isa MPolyQuoRing + lifted_c = lift.(c) + Oscar.ModStdQt.ref_ff_rc!(lifted_c) + c = simplify.(matrix_base.(lifted_c)) + else + rref!(c) + end + return uniform_hypergraph(nCk[independent_columns(c)], n_vertices(K), face_size(K)) +end + +function exterior_shift(K::SimplicialComplex, g::MatElem) + return simplicial_complex([ + (faces(exterior_shift(uniform_hypergraph(K, k), g)) for k in 1:dim(K)+1)...; + [[i] for i in 1:n_vertices(K)] # Make sure result is a complex on n vertices + ]) +end + +@doc raw""" + exterior_shift(F::Field, K::SimplicialComplex, w::WeylGroupElem) + exterior_shift(F::Field, K::UniformHypergraph, w::WeylGroupElem) + exterior_shift(K::SimplicialComplex, w::WeylGroupElem) + exterior_shift(K::UniformHypergraph, w::WeylGroupElem) + exterior_shift(K::SimplicialComplex) + exterior_shift(K::UniformHypergraph) + +Computes the (partial) exterior shift of a simplical complex or uniform hypergraph `K` with respect to the Weyl group element `w` and the field `F`. +If the field is not given then `QQ` is used during the computation. +If `w` is not given then `longest_element(weyl_group(:A, n_vertices(K) - 1))` is used + +# Examples +```jldoctest +julia> K = real_projective_plane() +Abstract simplicial complex of dimension 2 on 6 vertices + +julia> is_shifted(K) +false + +julia> L = exterior_shift(K) +Abstract simplicial complex of dimension 2 on 6 vertices + +julia> facets(L) +10-element Vector{Set{Int64}}: + Set([2, 3, 1]) + Set([4, 2, 1]) + Set([5, 2, 1]) + Set([6, 2, 1]) + Set([4, 3, 1]) + Set([5, 3, 1]) + Set([6, 3, 1]) + Set([5, 4, 1]) + Set([4, 6, 1]) + Set([5, 6, 1]) + +julia> is_shifted(L) +true + +julia> betti_numbers(L) == betti_numbers(K) +true + +julia> W = weyl_group(:A, n_vertices(K) - 1) +Weyl group for root system defined by Cartan matrix [2 -1 0 0 0; -1 2 -1 0 0; 0 -1 2 -1 0; 0 0 -1 2 -1; 0 0 0 -1 2] + +julia> s = gens(W) +5-element Vector{WeylGroupElem}: + s1 + s2 + s3 + s4 + s5 + +julia> w = s[2] * s[3] * s[4] +s2 * s3 * s4 + +julia> L = exterior_shift(GF(2), K, w) +Abstract simplicial complex of dimension 2 on 6 vertices +``` +""" +function exterior_shift(F::Field, K::ComplexOrHypergraph, p::PermGroupElem) + n = n_vertices(K) + @req n == degree(parent(p)) "number of vertices - 1 should equal the rank of the root system" + + return exterior_shift(K, rothe_matrix(F, p)) +end + +function exterior_shift(F::Field, K::ComplexOrHypergraph, w::WeylGroupElem) + n = n_vertices(K) + phi = isomorphism(PermGroup, parent(w)) + return exterior_shift(F, K, phi(w)) +end + +function exterior_shift(F::Field, K::ComplexOrHypergraph) + n = n_vertices(K) + W = weyl_group(:A, n - 1) + return exterior_shift(F, K, longest_element(W)) +end + +exterior_shift(K::ComplexOrHypergraph) = exterior_shift(QQ, K) diff --git a/experimental/AlgebraicShifting/src/PartialShiftGraph.jl b/experimental/AlgebraicShifting/src/PartialShiftGraph.jl new file mode 100644 index 000000000000..b9122155e34c --- /dev/null +++ b/experimental/AlgebraicShifting/src/PartialShiftGraph.jl @@ -0,0 +1,297 @@ +const EdgeLabels = Dict{Tuple{Int, Int}, Vector{WeylGroupElem}} + +function isless_lex(S1::Set{Set{Int}}, S2::Set{Set{Int}}) + S_diff = collect(symdiff(S1, S2)) + isempty(S_diff) && return false + set_cmp(a, b) = min(symdiff(a, b)...) in a + + return sort(S_diff;lt=set_cmp)[1] in S1 +end + +""" + Given two simplicial complexes `K1`, `K2` return true if + `K1` is lexicographically less than `K2` +""" +isless_lex(K1::ComplexOrHypergraph, K2::ComplexOrHypergraph) = isless_lex(Set(facets(K1)), Set(facets(K2))) + +@doc raw""" + partial_shift_graph_vertices(F::Field,::SimplicialComplex, W::Union{WeylGroup, Vector{WeylGroupElem}};) + +Given a field `F` discover the vertices of the partial shift graph starting from `K` +using exterior partial shifts corresponding to elements in `W`. +Returns a `Vector{SimplicialCompplex}` ordered lexicographically. + +#Example +```jldoctest +julia> K = simplicial_complex([[1, 2], [2, 3], [3, 4]]) +Abstract simplicial complex of dimension 1 on 4 vertices + +julia> shifts = partial_shift_graph_vertices(QQ, K, weyl_group(:A, 3)) +6-element Vector{SimplicialComplex}: + Abstract simplicial complex of dimension 1 on 4 vertices + Abstract simplicial complex of dimension 1 on 4 vertices + Abstract simplicial complex of dimension 1 on 4 vertices + Abstract simplicial complex of dimension 1 on 4 vertices + Abstract simplicial complex of dimension 1 on 4 vertices + Abstract simplicial complex of dimension 1 on 4 vertices + +julia> facets.(shifts) +6-element Vector{Vector{Set{Int64}}}: + [Set([3, 1]), Set([4, 1]), Set([2, 1])] + [Set([3, 1]), Set([2, 3]), Set([2, 1]), Set([4])] + [Set([3, 1]), Set([4, 2]), Set([2, 1])] + [Set([4, 3]), Set([3, 1]), Set([2, 1])] + [Set([2, 1]), Set([2, 3]), Set([4, 2])] + [Set([2, 1]), Set([2, 3]), Set([4, 3])] +``` +""" +function partial_shift_graph_vertices(F::Field, + K::SimplicialComplex, + W::Union{WeylGroup, Vector{WeylGroupElem}};) + current = K + visited = [current] + phi = isomorphism(PermGroup, parent(first(W))) + # by properties of algebraic shifting + # we know that K will be the last in this sorted list + # sorting here should also speed up unique according to julia docs + unvisited = unique( + x -> Set(facets(x)), + sort([exterior_shift(F, K, phi(w)) for w in W]; lt=isless_lex))[1:end - 1] + + while(!isempty(unvisited)) + current = pop!(unvisited) + push!(visited, current) + shifts = unique( + x -> Set(facets(x)), + sort([exterior_shift(F, current, phi(w)) for w in W]; lt=isless_lex))[1:end - 1] + + # dont visit things twice + new_facets = filter(x -> !(x in Set.(facets.(visited))), Set.(facets.(shifts))) + unvisited = simplicial_complex.( + union(Set.(facets.(unvisited)), new_facets)) + end + return sort(collect(visited); lt=isless_lex) +end + +""" Compute the multi edges, that is, for each complex `K` compute which + other complexes can be reached by applying the partial shifts `deltas` + to K, and store the shift matrices that give rise to each edge.""" +function multi_edges(F::Field, + permutations::Vector{PermGroupElem}, + complexes::Vector{Tuple{Int,T}}, + complex_labels::Dict{Set{Set{Int}}, Int} +) :: Dict{Tuple{Int, Int}, Vector{PermGroupElem}} where T <: ComplexOrHypergraph; + # For each complex K with index i, compute the shifted complex delta of K by w for each w in W. + # For nontrivial delta, place (i, delta) → w in a singleton dictionary, and eventually merge all dictionaries + # to obtain a dictionary (i, delta) → [w that yield the shift K → delta] + reduce( + (d1, d2) -> mergewith!(vcat, d1, d2), + ( + Dict((i, complex_labels[Set(facets(delta))]) => [p]) + for (i, K) in complexes + for (p, delta) in ((p, exterior_shift(F, K, p)) for p in permutations) + if Set(facets(delta)) != Set(facets(K))); + init=Dict{Tuple{Int, Int}, Vector{PermGroupElem}}() + ) +end + +@doc raw""" + partial_shift_graph(F::Field, complexes::Vector{Simplicialcomplex}; parallel=false, show_progress=true) + partial_shift_graph(F::Field, complexes::Vector{Uniformhypergraph}; parallel=false, show_progress=true) + partial_shift_graph(F::Field, complexes::Vector{Simplicialcomplex}, W::Union{WeylGroup, Vector{WeylGroupElem}}; parallel=false, show_progress=true) + partial_shift_graph(F::Field, complexes::Vector{Uniformhypergraph}, W::Union{WeylGroup, Vector{WeylGroupElem}}; parallel=false, show_progress=true) + +Constructs the partial shift graph on `complexes`. + +Returns a tuple `(G, EL, VL)`, where `G` is a `Graph{Directed}`, `EL` is a `Dict{Tuple{Int Int}, Vector{Weylgroupelem}` and +`VL` is a lexicographically sorted `complexes`, hence is either a `Vector{SimplicialComplex}` or `Vector{Uniformhypergraph}`. +`EL` are the edges labels and `VL` are the vertex labels. +There is an edge from the vertex labelled `K` to the vertex labelled `L` if `L` is the partial shift of `K` by some `w` in `W`. +If `K` and `L` are the `i`th and `j`th entry of `VL`, resp., +`EL[i,j]` contains all `w` in `W` such that `L` is the partial generic shift of `K` by `w`. + +# Arguments +- `complexes`: A vector of simplicial complexes or uniform hypergraphs (all should have the same number of vertices). +- `W`: The user may provide a list `W` of Weyl group elements to be used to construct the shifts. + All elements of `W` should have the same parent. + `W` must be a subset the (same instance of the) symmetric group of order equal to the number of vertices of a complex in complexes (they should all be equal). + If `W` is not provided, the function will use the symmetric group of the same order as vertices in each complex complexes. +- `parallel`: run the process in parrallel using the `Distributed` package; make sure to do `@everywhere using Oscar`. +- `show_progress`: Set to `true` by default, can be used to suppress progress meter. +- `task_size`: While running in parallel serialization might become a bottleneck, +setting a higher task_size should increase performance if the processes are not running at maximum capacity + + + +# Examples +```jldoctest +julia> gamma(n,k,l) = uniform_hypergraph.(subsets(subsets(n, k), l), n) +gamma (generic function with 1 method) + +julia> Ks = gamma(4,2,5) +6-element Vector{UniformHypergraph}: + UniformHypergraph(4, 2, [[1, 2], [1, 3], [1, 4], [2, 3], [3, 4]]) + UniformHypergraph(4, 2, [[1, 2], [1, 3], [2, 3], [2, 4], [3, 4]]) + UniformHypergraph(4, 2, [[1, 2], [1, 3], [1, 4], [2, 3], [2, 4]]) + UniformHypergraph(4, 2, [[1, 2], [1, 4], [2, 3], [2, 4], [3, 4]]) + UniformHypergraph(4, 2, [[1, 2], [1, 3], [1, 4], [2, 4], [3, 4]]) + UniformHypergraph(4, 2, [[1, 3], [1, 4], [2, 3], [2, 4], [3, 4]]) + +julia> G, EL, VL = partial_shift_graph(QQ, Ks; show_progress=false); + +julia> collect(edges(G)) +14-element Vector{Edge}: + Edge(2, 1) + Edge(3, 1) + Edge(3, 2) + Edge(4, 1) + Edge(4, 2) + Edge(5, 1) + Edge(5, 2) + Edge(5, 3) + Edge(5, 4) + Edge(6, 1) + Edge(6, 2) + Edge(6, 3) + Edge(6, 4) + Edge(6, 5) + +julia> EL[6, 5] +4-element Vector{WeylGroupElem}: + s1 * s2 + s2 + s3 * s1 * s2 + s3 * s2 + +julia> facets.(VL[[6, 5]]) +2-element Vector{Vector{Set{Int64}}}: + [Set([3, 1]), Set([4, 1]), Set([2, 3]), Set([4, 2]), Set([4, 3])] + [Set([2, 1]), Set([4, 1]), Set([2, 3]), Set([4, 2]), Set([4, 3])] +``` +""" +function partial_shift_graph(F::Field, complexes::Vector{T}, W::Union{WeylGroup, Vector{WeylGroupElem}}; + parallel::Bool = false, + show_progress::Bool = true, + task_size::Int=100) :: Tuple{Graph{Directed}, EdgeLabels, Vector{ComplexOrHypergraph}} where T <: ComplexOrHypergraph; + # Deal with trivial case + if length(complexes) <= 1 + return (graph_from_adjacency_matrix(Directed, zeros(length(complexes),length(complexes))), EdgeLabels()) + end + + # maybe we provide a flag to skip if the complexes are already sorted? + complexes = sort(complexes;lt=Oscar.isless_lex) + + ns_vertices = Set(n_vertices.(complexes)) + @req length(ns_vertices) == 1 "All complexes are required to have the same number of vertices." + n = collect(ns_vertices)[1] + + # inverse lookup K → index of K in complexes + complex_labels = Dict(Set(facets(K)) => index for (index, K) in enumerate(complexes)) + + W2 = only(unique(parent.(W))) + rs_type = root_system_type(root_system(W2)) + @req rs_type[1][1] == :A && rs_type[1][2] == n - 1 "Only Weyl groups type A_$(n-1) are currently support and received type $(T[1])." + + phi = isomorphism(PermGroup, parent(first(W))) + # oscar runs tests in parallel, which can make pmap run in parallel even if parallel=false + # next line fixes this issue + map_function = map + if parallel + # setup parallel parameters + channels = Oscar.params_channels(Union{PermGroup, Vector{SimplicialComplex}, Vector{UniformHypergraph}}) + # setup parents needed to be sent to each process + Oscar.put_params(channels, codomain(phi)) + map_function = pmap + end + + if show_progress + edge_labels = reduce((d1, d2) -> mergewith!(vcat, d1, d2), + @showprogress map_function( + Ks -> multi_edges(F, phi.(W), Ks, complex_labels), + Iterators.partition(enumerate(complexes), task_size))) + else + edge_labels = reduce((d1, d2) -> mergewith!(vcat, d1, d2), + map_function( + Ks -> multi_edges(F, phi.(W), Ks, complex_labels), + Iterators.partition(enumerate(complexes), task_size))) + end + graph = graph_from_edges(Directed, [[i,j] for (i,j) in keys(edge_labels)]) + return (graph, Dict(k => inv(phi).(v) for (k, v) in edge_labels), complexes) +end + +function partial_shift_graph(F::Field, complexes::Vector{T}; + kwargs...) where T <: ComplexOrHypergraph + # Deal with trivial case + if length(complexes) <= 1 + return (graph_from_adjacency_matrix(Directed, zeros(length(complexes),length(complexes))), EdgeLabels()) + end + + n = n_vertices(complexes[1]) + W = weyl_group(:A, n - 1) + return partial_shift_graph(F, complexes, W; kwargs...) +end + +@doc raw""" + contracted_partial_shift_graph(G::Graph{Directed}, edge_labels::Dict{Tuple{Int, Int}, Vector{WeylGroupElem}}) + +Returns a triple `(CG, S, P)`, where `CG` is a graph that contains a vertex `v` for every vertex `S[v]` in `G`. +`S` is a list of indices for the sinks in the original graph `G`. +A vertex `i` is in `P[s]` if there exists an edge from `i` to `s` in `G` with `w0` in its edge label, +in this way `P` is a partition of the vertices of the orignal graph `G`. +There is an edge from `s` to `t` in `CG` whenever there is an edge from `i` to `j` in `G` and `i` in `P[s]` and `j` in `P[t]`. + +# Examples +```jldoctest +julia> gamma(n,k,l) = uniform_hypergraph.(subsets(subsets(n, k), l), n) +gamma (generic function with 1 method) + +julia> Ks = gamma(4,2,5) +6-element Vector{UniformHypergraph}: + UniformHypergraph(4, 2, [[1, 2], [1, 3], [1, 4], [2, 3], [3, 4]]) + UniformHypergraph(4, 2, [[1, 2], [1, 3], [2, 3], [2, 4], [3, 4]]) + UniformHypergraph(4, 2, [[1, 2], [1, 3], [1, 4], [2, 3], [2, 4]]) + UniformHypergraph(4, 2, [[1, 2], [1, 4], [2, 3], [2, 4], [3, 4]]) + UniformHypergraph(4, 2, [[1, 2], [1, 3], [1, 4], [2, 4], [3, 4]]) + UniformHypergraph(4, 2, [[1, 3], [1, 4], [2, 3], [2, 4], [3, 4]]) + +julia> G, EL, VL = partial_shift_graph(QQ, Ks; show_progress=false); + +julia> contracted_partial_shift_graph(G, EL) +(Directed graph with 1 nodes and 0 edges, [1], [[5, 4, 6, 2, 3, 1]]) +``` +""" +function contracted_partial_shift_graph(G::Graph{Directed}, edge_labels::Dict{Tuple{Int, Int}, Vector{WeylGroupElem}}) + n = n_vertices(G) + W = parent(first(first(values(edge_labels)))) + w0 = longest_element(W) + + # all arrows corresponding to edges that contain w0 in their edge labels + w0_action = Dict(i => j for ((i,j), ws) in edge_labels if w0 in ws) + + sinks = findall(iszero, degree(G)) + sinks_indices = Dict(s => i for (i,s) in enumerate(sinks)) + for i in 1:n + if !haskey(w0_action, i) + @req i in sinks "Vertex $i is not a sink, but has no outbound edge with w0 in its edge label." + w0_action[i] = i + end + end + + p = [Int[] for _ in sinks] + for (i, s) in w0_action + push!(p[sinks_indices[s]], i) + end + + return ( + graph_from_edges(Directed, [ + [sinks_indices[s],sinks_indices[t]] + for (s,t) in ( + (w0_action[i], (haskey(w0_action, j) ? w0_action[j] : j)) + for (i, j) in keys(edge_labels) + ) + if s != t + ], length(sinks)), + sinks, + p + ) +end diff --git a/experimental/AlgebraicShifting/src/UniformHypergraph.jl b/experimental/AlgebraicShifting/src/UniformHypergraph.jl new file mode 100644 index 000000000000..52f67c541b91 --- /dev/null +++ b/experimental/AlgebraicShifting/src/UniformHypergraph.jl @@ -0,0 +1,87 @@ +# this might not be necessary if we can use polymake hasse diagram functionality? +function complex_faces(K :: SimplicialComplex, d :: Int) :: Vector{Vector{Int}} + return sort(union(subsets.(sort.(collect.(facets(K))), d+1)...)) + # return union(subsets.(facets(K), d+1)...) +end + +struct UniformHypergraph + n_vertices::Int + k::Int + faces::Vector{Vector{Int}} +end + +function uniform_hypergraph(faces::Vector{Vector{Int}}, n::Int, k::Int) + faces = sort(collect(Set(sort.(collect.(Set.(faces)))))) + @req all(length(f) == k && 1 <= minimum(f) && maximum(f) <= n for f in faces) "Parameters don't define a uniform hypergraph." + return UniformHypergraph(n, k, faces) +end + +function uniform_hypergraph(faces::Vector{Vector{Int}}, n::Int) + return uniform_hypergraph(faces, n, only(Set(length.(faces)))) +end + +function uniform_hypergraph(faces::Vector{Vector{Int}}) + return uniform_hypergraph(faces, maximum(maximum.(faces))) +end + +@doc raw""" + uniform_hypergraph(faces::Vector{Vector{Int}}, n::Int, k::Int) + uniform_hypergraph(faces::Vector{Vector{Int}}, n::Int) + uniform_hypergraph(faces::Vector{Vector{Int}}) + uniform_hypergraph(K::SimplicialComplex, k::Int) + +Create a uniform hypergraph using `faces`, the size of each face should be `k` and all faces should be subsets of $[n]$. +One can also create a `UniformHypergraph` for the `k` faces of a `SimplicialComplex` `K`. + +#Examples +```jldoctext +julia> U = uniform_hypergraph([[1, 2], [2, 3]], 4) +UniformHypergraph(4, 2, [[1, 2], [2, 3]]) + +julia> U = uniform_hypergraph([[1, 2], [2, 3]]) +UniformHypergraph(3, 2, [[1, 2], [2, 3]]) + +julia> U = uniform_hypergraph(simplicial_complex([[1 ,2, 3], [1, 3, 4]]), 2) +UniformHypergraph(4, 2, [[1, 2], [1, 3], [1, 4], [2, 3], [3, 4]]) + +julia> face_size(U) +2 + +julia> faces(U) +5-element Vector{Vector{Int64}}: + [1, 2] + [1, 3] + [1, 4] + [2, 3] + [3, 4] +``` +""" +function uniform_hypergraph(K::SimplicialComplex, k::Int) + return uniform_hypergraph(complex_faces(K, k-1), n_vertices(K), k) +end + +function simplicial_complex(K::UniformHypergraph) + return simplicial_complex([[[i] for i in 1:n_vertices(K)]; faces(K)]) +end + +n_vertices(K::UniformHypergraph) = K.n_vertices + +faces(K::UniformHypergraph) = K.faces +# added for covenience when writting functions for Simplicial complex and Uniform Hypergraph +facets(K::UniformHypergraph) = Set.(K.faces) +face_size(K::UniformHypergraph) = K.k + +function Base.hash(K :: UniformHypergraph, u :: UInt) + return hash(K.n_vertices, hash(K.k, hash(K.faces, u))) +end + +function Base.:(==)(K :: UniformHypergraph, L :: UniformHypergraph) + return K.n_vertices == L.n_vertices && K.k == L.k && K.faces == L.faces +end + +raw""" Alexander dual, seen as bijection ``\binom{[n]}{k} \to \binom{[n]}{n-k}`` """ +function alexander_dual(K::UniformHypergraph) + return uniform_hypergraph(alexander_dual(simplicial_complex([[[i] for i in 1:K.n_vertices]; K.faces])), K.n_vertices - K.k) +end + +is_shifted(K::UniformHypergraph) = is_shifted(simplicial_complex(K)) diff --git a/experimental/AlgebraicShifting/test/runtests.jl b/experimental/AlgebraicShifting/test/runtests.jl new file mode 100644 index 000000000000..25e3adca9eb1 --- /dev/null +++ b/experimental/AlgebraicShifting/test/runtests.jl @@ -0,0 +1,13 @@ +@testset "Algebraic Shifting" begin + K = simplicial_complex([[1, 3] , [2, 3]]) + + @testset "Partial Shift Graph" begin + n = n_vertices(K) + W = weyl_group(:A, n - 1) + s = gens(W) + all_shifts = partial_shift_graph_vertices(QQ, K, W) + directed_graph, edge_labels = partial_shift_graph(QQ, all_shifts) + @test collect(edges(directed_graph)) == [Edge(t...) for t in [[2, 1], [3, 1], [3, 2]]] + @test word.(edge_labels[2, 1]) == word.([s[1], s[1] * s[2], s[1] * s[2] * s[1], s[2] * s[1]]) + end +end diff --git a/experimental/Experimental.jl b/experimental/Experimental.jl index 878a6e356922..27a67cfd1895 100644 --- a/experimental/Experimental.jl +++ b/experimental/Experimental.jl @@ -8,6 +8,7 @@ const expdir = joinpath(@__DIR__, "../experimental") const orderedpkgs = [ "LieAlgebras", "BasisLieHighestWeight", # needs code from LieAlgebras + "AlgebraicShifting", # Needs code form Lie Algebras (Weyl Groups specifically) "SetPartitions", "PartitionedPermutations", # needs code from SetPartitions "Schemes", diff --git a/experimental/LieAlgebras/src/LieAlgebras.jl b/experimental/LieAlgebras/src/LieAlgebras.jl index e866dfbd8e64..c102519fbca6 100644 --- a/experimental/LieAlgebras/src/LieAlgebras.jl +++ b/experimental/LieAlgebras/src/LieAlgebras.jl @@ -74,6 +74,7 @@ import ..Oscar: ngens, order, parent_type, + permutation_matrix, permutation_group, rank, root, diff --git a/src/Combinatorics/Graphs/functions.jl b/src/Combinatorics/Graphs/functions.jl index 747ddf20595f..af7296239dac 100644 --- a/src/Combinatorics/Graphs/functions.jl +++ b/src/Combinatorics/Graphs/functions.jl @@ -1172,8 +1172,7 @@ end function graph_from_edges(::Type{T}, edges::Vector{Edge}, n_vertices::Int=-1) where {T <: Union{Directed, Undirected}} - - n_needed = maximum(reduce(append!,[[src(e),dst(e)] for e in edges])) + n_needed = maximum(reduce(append!,[[src(e),dst(e)] for e in edges]; init=[0]);) @req (n_vertices >= n_needed || n_vertices < 0) "n_vertices must be at least the maximum vertex in the edges" g = Graph{T}(max(n_needed, n_vertices)) diff --git a/src/Combinatorics/SimplicialComplexes.jl b/src/Combinatorics/SimplicialComplexes.jl index c8c6c04a58a0..55164f34b70b 100644 --- a/src/Combinatorics/SimplicialComplexes.jl +++ b/src/Combinatorics/SimplicialComplexes.jl @@ -59,6 +59,10 @@ function simplicial_complex(generators::Union{AbstractVector{<:AbstractVector{<: SimplicialComplex(K) end +function simplicial_complex(generators::Union{Set{<:AbstractVector{<:Base.Integer}}, Set{<:AbstractSet{<:Base.Integer}}}) + return simplicial_complex(collect(generators)) +end + function simplicial_complex(generators::IncidenceMatrix) K = Polymake.@convert_to Array{Set} Polymake.common.rows(generators) simplicial_complex(K) @@ -693,3 +697,82 @@ function on_simplicial_complex(K::SimplicialComplex, g::PermGroupElem) new_facets = on_sets_sets(Set(facets(K)), g) simplicial_complex(collect(new_facets)) end + +@doc raw""" + simplicial_product(K1::SimplicialComplex, K2::SimplicialComplex) + +Given simplicial complexes `K1` and `K2` return their simplicial product. + +# Examples +```jldoctest +julia> K1 = simplicial_complex([[1, 2], [2, 3]]) +Abstract simplicial complex of dimension 1 on 3 vertices + +julia> K2 = simplicial_complex([[1, 2], [1, 3]]) +Abstract simplicial complex of dimension 1 on 3 vertices + +julia> facets(simplicial_product(K1, K2)) +8-element Vector{Set{Int64}}: + Set([5, 2, 1]) + Set([5, 4, 1]) + Set([2, 8, 1]) + Set([7, 8, 1]) + Set([6, 2, 3]) + Set([5, 6, 2]) + Set([2, 9, 3]) + Set([2, 9, 8]) +``` +""" +function simplicial_product(K1::SimplicialComplex, K2::SimplicialComplex) + return SimplicialComplex(Polymake.topaz.simplicial_product(pm_object(K1), pm_object(K2))) +end + +@doc raw""" + link_subcomplex(K::SimplicialComplex, f::Union{<:AbstractSet{Int},<:AbstractVector{Int}})) + +Given simplicial complex `K` and a face of `face` of `K` return the link of `face`. +simplicial complex at the . + +# Examples +```jldoctest +julia> K = simplicial_complex([[1, 2], [2, 3], [3, 4]]) +Abstract simplicial complex of dimension 1 on 4 vertices + +julia> facets(link_subcomplex(K, [2])) +2-element Vector{Set{Int64}}: + Set([1]) + Set([2]) +``` +""" +function link_subcomplex(K::SimplicialComplex, face::Union{<:AbstractSet{Int},<:AbstractVector{Int}}) + zero_based_face = Polymake.to_zero_based_indexing(face) + return SimplicialComplex(Polymake.link_subcomplex(pm_object(K), zero_based_face)) +end + +@doc raw""" + barycentric_subdivision(K::SimplicialComplex) + +Given simplicial complex `K` returns its barycentric subdivision. + +# Examples +```jldoctest +julia> K = simplicial_complex([[1, 2, 3]]) +Abstract simplicial complex of dimension 2 on 3 vertices + +julia> facets(barycentric_subdivision(K)) +6-element Vector{Set{Int64}}: + Set([5, 2, 1]) + Set([5, 3, 1]) + Set([6, 2, 1]) + Set([4, 6, 1]) + Set([7, 3, 1]) + Set([4, 7, 1]) +``` +""" +function barycentric_subdivision(K::SimplicialComplex) + return SimplicialComplex(Polymake.topaz.barycentric_subdivision(pm_object(K))) +end + +function is_shifted(K::SimplicialComplex) + return pm_object(K).SHIFTED +end diff --git a/src/exports.jl b/src/exports.jl index 2eec9e786eb1..aefd0cc33da8 100644 --- a/src/exports.jl +++ b/src/exports.jl @@ -287,6 +287,7 @@ export atlas_subgroup export augmented_chow_ring export aut export automorphism_group +export barycentric_subdivision export base_ring export base_scheme export bases @@ -899,6 +900,7 @@ export is_right export is_semiregular export is_semisimple export is_semistandard +export is_shifted export is_simple, has_is_simple, set_is_simple export is_simplicial export is_singular @@ -1429,6 +1431,7 @@ export signed_incidence_matrix export signed_permutahedron export simplex export simplicial_complex +export simplicial_product export simplified_fp_group export simplify export simplify! diff --git a/src/imports.jl b/src/imports.jl index 41769e0ab4bf..ed98de755884 100644 --- a/src/imports.jl +++ b/src/imports.jl @@ -1,5 +1,6 @@ # standard packages using Pkg +using ProgressMeter using Random using RandomExtensions using UUIDs