From c7f88c6baa767294f95e9a98f37d4244e8fcd493 Mon Sep 17 00:00:00 2001 From: Pieter Belmans Date: Wed, 12 Aug 2026 17:51:29 +0200 Subject: [PATCH 1/6] feat: fold weights into the dominant chamber of a root subsystem --- src/WeightLattice.jl | 82 +++++++++++++++++++++++++++++++------------- test/runtests.jl | 58 +++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 23 deletions(-) diff --git a/src/WeightLattice.jl b/src/WeightLattice.jl index 1d339e9..c76f442 100644 --- a/src/WeightLattice.jl +++ b/src/WeightLattice.jl @@ -351,35 +351,42 @@ end # ─── Conjugation to dominant chamber ──────────────────────────────────────── -# Core dominant-chamber fold: fold `v` into the dominant chamber in place and -# return the number of simple reflections applied. `@inline` so each caller -# const-folds its compile-time-constant (sparse) Cartan matrix into the loop — -# this is a hot path and the method is small enough to re-specialize cheaply. +# Core dominant-chamber fold: fold `v` into the dominant chamber of the root +# subsystem spanned by the simple roots `nodes`, in place, and return the number +# of simple reflections applied. `@inline` so each caller const-folds its +# compile-time-constant (sparse) Cartan matrix into the loop — this is a hot path +# and the method is small enough to re-specialize cheaply. @inline function _fold_dominant!( - v::MVector{R,Int}, C::SMatrix{R,R,Int} + v::MVector{R,Int}, C::SMatrix{R,R,Int}, nodes=Base.OneTo(R) ) where {R} len = 0 - s = 1 - @inbounds while s <= R + k = 1 + @inbounds while k <= length(nodes) + s = nodes[k] if v[s] < 0 pairing = v[s] for j in 1:R v[j] -= pairing * C[j, s] end len += 1 - s = 1 + k = 1 else - s += 1 + k += 1 end end return len end """ - conjugate_dominant_weight(w::WeightLatticeElem{DT,R}) -> WeightLatticeElem{DT,R} + conjugate_dominant_weight(w::WeightLatticeElem{DT,R}, nodes=1:R) -> WeightLatticeElem{DT,R} Return the unique dominant weight in the Weyl orbit of `w`. +Passing `nodes` reflects only in those simple roots, giving the unique +``\\mathrm{W}_S``-orbit representative that is dominant for the root subsystem +spanned by ``S`` = `nodes`, i.e. dominant for the corresponding Levi subgroup. +The coordinates outside `nodes` are still moved along. + # Examples ```jldoctest julia> using Semisimple @@ -390,55 +397,77 @@ julia> conjugate_dominant_weight(WeightLatticeElem(TypeA{2}, [-1, 1])) julia> conjugate_dominant_weight(fundamental_weight(TypeA{3}, 1)) ω1 ``` + +Reflecting only in the second node leaves the first coordinate negative, since +``s_2`` is the only reflection available: + +```jldoctest +julia> using Semisimple + +julia> conjugate_dominant_weight(WeightLatticeElem(TypeA{2}, [-1, -1]), (2,)) +-2ω1 + ω2 +``` """ -@inline function conjugate_dominant_weight(w::WeightLatticeElem{DT,R}) where {DT,R} +@inline function conjugate_dominant_weight( + w::WeightLatticeElem{DT,R}, nodes=Base.OneTo(R) +) where {DT,R} v = MVector{R,Int}(w.vec) - _fold_dominant!(v, cartan_matrix(DT)) + _fold_dominant!(v, cartan_matrix(DT), nodes) return WeightLatticeElem{DT,R}(SVector{R,Int}(v)) end # Like `_fold_dominant!` but records the word of simple reflections applied. function _fold_dominant_with_word!( - v::MVector{R,Int}, C::SMatrix{R,R,Int} + v::MVector{R,Int}, C::SMatrix{R,R,Int}, nodes=Base.OneTo(R) ) where {R} word = Int[] - s = 1 - while s <= R + k = 1 + while k <= length(nodes) + s = nodes[k] if v[s] < 0 pairing = v[s] for j in 1:R v[j] -= pairing * C[j, s] end push!(word, s) - s = 1 + k = 1 else - s += 1 + k += 1 end end return word end """ - conjugate_dominant_weight_with_elem(w::WeightLatticeElem{DT,R}) -> (WeightLatticeElem, Vector{Int}) + conjugate_dominant_weight_with_elem(w::WeightLatticeElem{DT,R}, nodes=1:R) -> (WeightLatticeElem, Vector{Int}) Return the dominant weight and the sequence of simple reflections applied. +Passing `nodes` reflects only in those simple roots, as in +[`conjugate_dominant_weight`](@ref); the word is then a reduced word of an +element of ``\\mathrm{W}_S``. + # Examples ```jldoctest julia> using Semisimple julia> conjugate_dominant_weight_with_elem(WeightLatticeElem(TypeA{2}, [-1, 1])) (ω1, [1]) + +julia> conjugate_dominant_weight_with_elem(WeightLatticeElem(TypeA{2}, [-1, 1]), (2,)) +(-ω1 + ω2, Int64[]) ``` """ -function conjugate_dominant_weight_with_elem(w::WeightLatticeElem{DT,R}) where {DT,R} +function conjugate_dominant_weight_with_elem( + w::WeightLatticeElem{DT,R}, nodes=Base.OneTo(R) +) where {DT,R} v = MVector{R,Int}(w.vec) - word = _fold_dominant_with_word!(v, cartan_matrix(DT)) + word = _fold_dominant_with_word!(v, cartan_matrix(DT), nodes) return WeightLatticeElem{DT,R}(SVector{R,Int}(v)), word end """ - conjugate_dominant_weight_with_length(w::WeightLatticeElem{DT,R}) -> (WeightLatticeElem, Int) + conjugate_dominant_weight_with_length(w::WeightLatticeElem{DT,R}, nodes=1:R) -> (WeightLatticeElem, Int) Return the dominant weight in the Weyl orbit of `w` together with the number of simple reflections applied (i.e. the length of the Weyl group element @@ -447,6 +476,10 @@ mapping `w` into the dominant chamber). This is faster than [`conjugate_dominant_weight_with_elem`](@ref) because it only tracks a counter instead of building the full word. +Passing `nodes` reflects only in those simple roots, as in +[`conjugate_dominant_weight`](@ref); the length is then that of an element of +``\\mathrm{W}_S``. + # Examples ```jldoctest julia> using Semisimple @@ -456,13 +489,16 @@ julia> conjugate_dominant_weight_with_length(WeightLatticeElem(TypeA{2}, [-1, 1] julia> conjugate_dominant_weight_with_length(fundamental_weight(TypeA{3}, 1)) (ω1, 0) + +julia> conjugate_dominant_weight_with_length(WeightLatticeElem(TypeA{3}, [-1, 2, -1]), (2, 3)) +(-ω1 + ω2 + ω3, 1) ``` """ @inline function conjugate_dominant_weight_with_length( - w::WeightLatticeElem{DT,R} + w::WeightLatticeElem{DT,R}, nodes=Base.OneTo(R) ) where {DT,R} v = MVector{R,Int}(w.vec) - len = _fold_dominant!(v, cartan_matrix(DT)) + len = _fold_dominant!(v, cartan_matrix(DT), nodes) return WeightLatticeElem{DT,R}(SVector{R,Int}(v)), len end diff --git a/test/runtests.jl b/test/runtests.jl index bb9696a..a924607 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1110,6 +1110,64 @@ end end end +# ═══════════════════════════════════════════════════════════════════════ +# Folding and Borel–Weil–Bott inside a root subsystem +# +# Reflecting only in a subset S of the simple roots folds a weight into the +# dominant chamber of the Levi subgroup L_S. The ground truth is the same +# computation performed inside the sub-root-system itself. +# ═══════════════════════════════════════════════════════════════════════ +@testset "Levi-restricted fold" begin + CASES = [ + (TypeA{3}, (1, 2, 3)), (TypeA{3}, (2, 3)), (TypeA{3}, (1, 3)), (TypeA{3}, (2,)), + (TypeA{4}, (1, 2, 4)), (TypeB{3}, (2, 3)), (TypeB{4}, (1, 3, 4)), + (TypeC{3}, (1, 2)), (TypeD{4}, (2, 3, 4)), (TypeD{5}, (1, 2, 4)), + (TypeG2, (2,)), (TypeF4, (2, 3, 4)), (TypeE{6}, (2, 4, 5)), + ] + COORDS = [-3, -1, 0, 1, 4] + + @testset "$DT restricted to $S" for (DT, S) in CASES + R = rank(DT) + LT, ord = sub_dynkin_type_with_ordering(DT, S) + + for seed in 1:6 + λ = WeightLatticeElem(DT, Int[COORDS[1 + (seed * i) % length(COORDS)] for i in 1:R]) + dom, len = conjugate_dominant_weight_with_length(λ, S) + + # Dominant for the subsystem, and only for it. + @test all(coefficients(dom)[s] >= 0 for s in S) + + # Ground truth: the same fold carried out inside the sub-root-system. + sub = WeightLatticeElem(LT, Int[coefficients(λ)[ord[k]] for k in 1:rank(LT)]) + sub_dom, sub_len = conjugate_dominant_weight_with_length(sub) + @test Int[coefficients(dom)[ord[k]] for k in 1:rank(LT)] == coefficients(sub_dom) + @test len == sub_len + + # The word from _with_elem has the same length and reproduces `dom`. + dom_e, word = conjugate_dominant_weight_with_elem(λ, S) + @test dom_e == dom + @test length(word) == len + @test issubset(word, S) + @test foldl(reflect, word; init=λ) == dom + + # Folding is idempotent, and the coordinates outside S move only by roots + # of the subsystem, so a node not touched by S keeps its coordinate. + @test conjugate_dominant_weight(dom, S) == dom + for i in 1:R + if all(cartan_matrix(DT)[i, s] == 0 for s in S) + @test coefficients(dom)[i] == coefficients(λ)[i] + end + end + end + + # Passing every node is the absolute statement. + λ = WeightLatticeElem(DT, Int[i % 3 == 0 ? -2 : 1 for i in 1:R]) + @test conjugate_dominant_weight(λ, 1:R) == conjugate_dominant_weight(λ) + @test conjugate_dominant_weight_with_length(λ, Tuple(1:R)) == + conjugate_dominant_weight_with_length(λ) + end +end + # ═══════════════════════════════════════════════════════════════════════ # StaticArrays: verify types are compile-time static # ═══════════════════════════════════════════════════════════════════════ From 14144e533f2b83d2dc86794d25d83aab84cf6e50 Mon Sep 17 00:00:00 2001 From: Pieter Belmans Date: Wed, 12 Aug 2026 17:52:58 +0200 Subject: [PATCH 2/6] =?UTF-8?q?feat:=20relative=20Borel=E2=80=93Weil?= =?UTF-8?q?=E2=80=93Bott=20via=20an=20optional=20node=20set?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/WeylGroup.jl | 33 +++++++++++++++++++++++++++----- test/runtests.jl | 49 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 5 deletions(-) diff --git a/src/WeylGroup.jl b/src/WeylGroup.jl index b9fcec1..0495ffd 100644 --- a/src/WeylGroup.jl +++ b/src/WeylGroup.jl @@ -959,7 +959,7 @@ end # ─── Borel–Weil–Bott ──────────────────────────────────────────────────────── """ - borel_weil_bott(λ::WeightLatticeElem{DT,R}) -> Union{Nothing, Tuple{Int, WeightLatticeElem{DT,R}}} + borel_weil_bott(λ::WeightLatticeElem{DT,R}, nodes=1:R) -> Union{Nothing, Tuple{Int, WeightLatticeElem{DT,R}}} Apply the Borel–Weil–Bott theorem to the weight `λ`. @@ -978,6 +978,16 @@ all cohomology vanishes and we return `nothing`. Otherwise, return and all other cohomology groups vanish. +Passing `nodes` restricts both the reflections and the singularity test to the +root subsystem spanned by ``S`` = `nodes`, so `w` is sought in +``\\mathrm{W}_S``. This is the relative statement along a projection of flag +varieties, the higher direct images of ``\\mathcal{L}_λ`` along +``\\mathrm{G}/\\mathrm{B} \\to \\mathrm{G}/\\mathrm{P}_I`` with `nodes` the nodes +unmarked in ``I``: taking `nodes` to be all of them recovers the absolute +statement. Note that ``ρ = ρ_{\\mathrm{G}}`` remains the right shift, because +``ρ_{\\mathrm{G}} - ρ_S`` pairs to zero with every coroot in ``S`` and is +therefore ``\\mathrm{W}_S``-invariant. + # Examples ```jldoctest julia> using Semisimple; import Semisimple: borel_weil_bott @@ -991,17 +1001,30 @@ julia> borel_weil_bott(WeightLatticeElem(TypeA{2}, [-2, 1])) julia> borel_weil_bott(-weyl_vector(TypeA{2})) === nothing true ``` + +The same weight, but reflecting only in the second node: it is already dominant +there, so it stays put in degree zero. + +```jldoctest +julia> using Semisimple; import Semisimple: borel_weil_bott + +julia> borel_weil_bott(WeightLatticeElem(TypeA{2}, [-2, 1]), (2,)) +(0, -2ω1 + ω2) + +julia> borel_weil_bott(-weyl_vector(TypeA{2}), (2,)) === nothing # singular for s2 too +true +``` """ -function borel_weil_bott(λ::WeightLatticeElem{DT,R}) where {DT,R} +function borel_weil_bott(λ::WeightLatticeElem{DT,R}, nodes=Base.OneTo(R)) where {DT,R} ρ = weyl_vector(DT) μ = λ + ρ # Move μ to the dominant chamber; the number of reflections is the degree - μ_dom, d = conjugate_dominant_weight_with_length(μ) + μ_dom, d = conjugate_dominant_weight_with_length(μ, nodes) - # If any coordinate of μ_dom is zero, λ + ρ lies on a Weyl chamber wall + # If any coordinate of μ_dom is zero, λ + ρ lies on a wall of the chamber # (including the case μ = 0 when λ = -ρ), so all cohomology vanishes. - any(==(0), μ_dom.vec) && return nothing + any(s -> μ_dom.vec[s] == 0, nodes) && return nothing return (d, μ_dom - ρ) end diff --git a/test/runtests.jl b/test/runtests.jl index a924607..df28181 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1165,9 +1165,58 @@ end @test conjugate_dominant_weight(λ, 1:R) == conjugate_dominant_weight(λ) @test conjugate_dominant_weight_with_length(λ, Tuple(1:R)) == conjugate_dominant_weight_with_length(λ) + @test borel_weil_bott(λ, 1:R) == borel_weil_bott(λ) end end +@testset "Levi-restricted Borel–Weil–Bott" begin + # Agreement with the absolute statement inside the sub-root-system: the degree + # is the same, and the output weight restricts to the sub-diagram output. Note + # ρ_G is the correct shift on both sides, since ρ_G - ρ_S is W_S-invariant. + @testset "$DT restricted to $S" for (DT, S) in + [(TypeA{3}, (1, 3)), (TypeA{4}, (2, 3, 4)), + (TypeB{3}, (1, 2)), (TypeC{3}, (2, 3)), + (TypeD{4}, (1, 3, 4)), (TypeF4, (1, 2))] + R = rank(DT) + LT, ord = sub_dynkin_type_with_ordering(DT, S) + ρ, ρ_S = weyl_vector(DT), weyl_vector(LT) + + for seed in 1:8 + λ = WeightLatticeElem(DT, Int[((seed * i) % 7) - 3 for i in 1:R]) + result = borel_weil_bott(λ, S) + + sub_λ = + WeightLatticeElem( + LT, Int[coefficients(λ)[ord[k]] + coefficients(ρ)[ord[k]] for k in 1:rank(LT)] + ) - ρ_S + sub_result = borel_weil_bott(sub_λ) + + @test (result === nothing) == (sub_result === nothing) + result === nothing && continue + d, μ = result + sub_d, sub_μ = sub_result + @test d == sub_d + @test Int[coefficients(μ)[ord[k]] for k in 1:rank(LT)] == coefficients(sub_μ) + # The output is dominant for the subsystem, as the theorem promises. + @test all(coefficients(μ)[s] >= 0 for s in S) + end + end + + # A weight that is regular for the whole group but singular for a subsystem, + # and one that is singular for the whole group but regular for a subsystem. + @test borel_weil_bott(WeightLatticeElem(TypeA{2}, [-2, 1])) == + (1, WeightLatticeElem(TypeA{2})) + @test borel_weil_bott(WeightLatticeElem(TypeA{2}, [-2, 1]), (2,)) == + (0, WeightLatticeElem(TypeA{2}, [-2, 1])) + @test borel_weil_bott(WeightLatticeElem(TypeA{2}, [0, -1])) === nothing + @test borel_weil_bott(WeightLatticeElem(TypeA{2}, [0, -1]), (1,)) == + (0, WeightLatticeElem(TypeA{2}, [0, -1])) + + # No nodes to reflect in: nothing can be singular and nothing moves. + @test borel_weil_bott(WeightLatticeElem(TypeA{2}, [-5, -5]), ()) == + (0, WeightLatticeElem(TypeA{2}, [-5, -5])) +end + # ═══════════════════════════════════════════════════════════════════════ # StaticArrays: verify types are compile-time static # ═══════════════════════════════════════════════════════════════════════ From f3e97da07be3e3483b9e2e6210462faf0520efd3 Mon Sep 17 00:00:00 2001 From: Pieter Belmans Date: Wed, 12 Aug 2026 18:02:22 +0200 Subject: [PATCH 3/6] feat: restrict the singularity test to a root subsystem --- src/WeylGroup.jl | 27 +++++++++++++++++++++++---- test/runtests.jl | 31 +++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/src/WeylGroup.jl b/src/WeylGroup.jl index 0495ffd..920b245 100644 --- a/src/WeylGroup.jl +++ b/src/WeylGroup.jl @@ -930,7 +930,7 @@ weyl_dimension(dt::DynkinType, v) = degree(typeof(dt), v) # ─── Singularity ───────────────────────────────────────────────────────────── """ - is_singular(w::WeightLatticeElem{DT,R}) -> Bool + is_singular(w::WeightLatticeElem{DT,R}, nodes=1:R) -> Bool Check whether the weight `w` is singular, i.e. lies on some wall of a Weyl chamber. Equivalently, `w` is singular iff `⟨α∨, w⟩ = 0` for some positive @@ -940,6 +940,12 @@ For a dominant weight this simplifies to checking whether any fundamental weight coordinate is zero. For a general weight, we first conjugate to the dominant chamber. +Passing `nodes` restricts the question to the root subsystem spanned by +``S`` = `nodes`, as in [`conjugate_dominant_weight`](@ref): the result is then +whether ``⟨α^\\vee, w⟩ = 0`` for some positive root ``α`` of that subsystem. +This is the vanishing criterion of the relative +[`borel_weil_bott`](@ref). + # Examples ```jldoctest julia> using Semisimple @@ -950,10 +956,23 @@ true julia> is_singular(weyl_vector(TypeA{2})) false ``` + +For the subsystem spanned by the second node the only positive root is +``α_2``, so singularity is decided by the pairing with ``α_2^\\vee`` alone: + +```jldoctest +julia> using Semisimple + +julia> is_singular(fundamental_weight(TypeA{2}, 1), (2,)) +true + +julia> is_singular(fundamental_weight(TypeA{2}, 2), (2,)) +false +``` """ -function is_singular(w::WeightLatticeElem{DT,R}) where {DT,R} - dom = conjugate_dominant_weight(w) - return any(i -> dom.vec[i] == 0, 1:R) +function is_singular(w::WeightLatticeElem{DT,R}, nodes=Base.OneTo(R)) where {DT,R} + dom = conjugate_dominant_weight(w, nodes) + return any(s -> dom.vec[s] == 0, nodes) end # ─── Borel–Weil–Bott ──────────────────────────────────────────────────────── diff --git a/test/runtests.jl b/test/runtests.jl index df28181..134cad4 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1202,6 +1202,37 @@ end end end + # is_singular is the vanishing criterion, so it must agree with borel_weil_bott + # on exactly when nothing survives. + @testset "is_singular restricted: $DT / $S" for (DT, S) in + [(TypeA{3}, (1, 3)), (TypeB{3}, (2, 3)), + (TypeC{3}, (1, 2)), (TypeD{4}, (2, 3, 4)), + (TypeG2, (1,))] + R = rank(DT) + ρ = weyl_vector(DT) + RS = RootSystem(DT) + sub_positive = [ + α for α in positive_roots(RS) if + all(coefficients(α)[i] == 0 for i in 1:R if !(i in S)) + ] + + for seed in 1:10 + λ = WeightLatticeElem(DT, Int[((seed * i) % 7) - 3 for i in 1:R]) + # Ground truth: pair λ + ρ against every positive root of the subsystem. + expected = any(iszero(dot(α, λ + ρ)) for α in sub_positive) + @test is_singular(λ + ρ, S) == expected + @test (borel_weil_bott(λ, S) === nothing) == expected + + # Singular for the subsystem implies singular for the whole system, since + # the offending root is a root of both. + @test !is_singular(λ + ρ, S) || is_singular(λ + ρ) + end + + # Passing every node is the absolute statement. + λ = WeightLatticeElem(DT, Int[i % 2 == 0 ? 0 : 2 for i in 1:R]) + @test is_singular(λ, 1:R) == is_singular(λ) + end + # A weight that is regular for the whole group but singular for a subsystem, # and one that is singular for the whole group but regular for a subsystem. @test borel_weil_bott(WeightLatticeElem(TypeA{2}, [-2, 1])) == From a6ddc6b0c6e858f545c90d7802f0e52eaefb2c16 Mon Sep 17 00:00:00 2001 From: Pieter Belmans Date: Wed, 12 Aug 2026 19:04:04 +0200 Subject: [PATCH 4/6] refactor: test node membership instead of indexing a node list Scanning 1:R and testing membership keeps every index into the coordinate vector and the Cartan matrix provably in range, so an out-of-range node can no longer read past the end under @inbounds, and any container supporting `in` is accepted. `nothing` marks the full fold, whose membership test const-folds away. --- src/WeightLattice.jl | 72 ++++++++++++++++++++++++++++---------------- src/WeylGroup.jl | 12 ++++---- test/runtests.jl | 29 ++++++++++++++++++ 3 files changed, 81 insertions(+), 32 deletions(-) diff --git a/src/WeightLattice.jl b/src/WeightLattice.jl index c76f442..efa28ff 100644 --- a/src/WeightLattice.jl +++ b/src/WeightLattice.jl @@ -351,34 +351,51 @@ end # ─── Conjugation to dominant chamber ──────────────────────────────────────── +# Reject reflection nodes outside the diagram, so that a mistyped node is an +# error rather than a silently skipped reflection. `nothing` means every node. +_check_nodes(::Nothing, ::Integer) = nothing + +function _check_nodes(nodes, R::Integer) + for s in nodes + 1 <= s <= R || + throw(ArgumentError("Reflection node $s is out of range for rank $R")) + end + return nodes +end + # Core dominant-chamber fold: fold `v` into the dominant chamber of the root # subsystem spanned by the simple roots `nodes`, in place, and return the number -# of simple reflections applied. `@inline` so each caller const-folds its -# compile-time-constant (sparse) Cartan matrix into the loop — this is a hot path -# and the method is small enough to re-specialize cheaply. +# of simple reflections applied. `nodes === nothing` means every node, and the +# membership test then const-folds away, so the full fold stays exactly as fast +# as it was before the subsystem case existed. `@inline` so each caller +# const-folds its compile-time-constant (sparse) Cartan matrix into the loop — +# this is a hot path and the method is small enough to re-specialize cheaply. +# +# The scan runs over `1:R` and *tests* membership rather than iterating over +# `nodes`: that keeps every index into `v` and `C` provably in range under +# `@inbounds`, and accepts any container supporting `in`. @inline function _fold_dominant!( - v::MVector{R,Int}, C::SMatrix{R,R,Int}, nodes=Base.OneTo(R) + v::MVector{R,Int}, C::SMatrix{R,R,Int}, nodes=nothing ) where {R} len = 0 - k = 1 - @inbounds while k <= length(nodes) - s = nodes[k] - if v[s] < 0 + s = 1 + @inbounds while s <= R + if v[s] < 0 && (nodes === nothing || s in nodes) pairing = v[s] for j in 1:R v[j] -= pairing * C[j, s] end len += 1 - k = 1 + s = 1 else - k += 1 + s += 1 end end return len end """ - conjugate_dominant_weight(w::WeightLatticeElem{DT,R}, nodes=1:R) -> WeightLatticeElem{DT,R} + conjugate_dominant_weight(w::WeightLatticeElem{DT,R}, nodes=nothing) -> WeightLatticeElem{DT,R} Return the unique dominant weight in the Weyl orbit of `w`. @@ -387,6 +404,10 @@ Passing `nodes` reflects only in those simple roots, giving the unique spanned by ``S`` = `nodes`, i.e. dominant for the corresponding Levi subgroup. The coordinates outside `nodes` are still moved along. +`nodes` may be any container of node indices supporting `in`, such as a tuple, +vector, range, or `Set`; `nothing` means every simple root. A node outside +`1:R` throws an `ArgumentError`. + # Examples ```jldoctest julia> using Semisimple @@ -409,37 +430,36 @@ julia> conjugate_dominant_weight(WeightLatticeElem(TypeA{2}, [-1, -1]), (2,)) ``` """ @inline function conjugate_dominant_weight( - w::WeightLatticeElem{DT,R}, nodes=Base.OneTo(R) + w::WeightLatticeElem{DT,R}, nodes=nothing ) where {DT,R} v = MVector{R,Int}(w.vec) - _fold_dominant!(v, cartan_matrix(DT), nodes) + _fold_dominant!(v, cartan_matrix(DT), _check_nodes(nodes, R)) return WeightLatticeElem{DT,R}(SVector{R,Int}(v)) end # Like `_fold_dominant!` but records the word of simple reflections applied. function _fold_dominant_with_word!( - v::MVector{R,Int}, C::SMatrix{R,R,Int}, nodes=Base.OneTo(R) + v::MVector{R,Int}, C::SMatrix{R,R,Int}, nodes=nothing ) where {R} word = Int[] - k = 1 - while k <= length(nodes) - s = nodes[k] - if v[s] < 0 + s = 1 + while s <= R + if v[s] < 0 && (nodes === nothing || s in nodes) pairing = v[s] for j in 1:R v[j] -= pairing * C[j, s] end push!(word, s) - k = 1 + s = 1 else - k += 1 + s += 1 end end return word end """ - conjugate_dominant_weight_with_elem(w::WeightLatticeElem{DT,R}, nodes=1:R) -> (WeightLatticeElem, Vector{Int}) + conjugate_dominant_weight_with_elem(w::WeightLatticeElem{DT,R}, nodes=nothing) -> (WeightLatticeElem, Vector{Int}) Return the dominant weight and the sequence of simple reflections applied. @@ -459,15 +479,15 @@ julia> conjugate_dominant_weight_with_elem(WeightLatticeElem(TypeA{2}, [-1, 1]), ``` """ function conjugate_dominant_weight_with_elem( - w::WeightLatticeElem{DT,R}, nodes=Base.OneTo(R) + w::WeightLatticeElem{DT,R}, nodes=nothing ) where {DT,R} v = MVector{R,Int}(w.vec) - word = _fold_dominant_with_word!(v, cartan_matrix(DT), nodes) + word = _fold_dominant_with_word!(v, cartan_matrix(DT), _check_nodes(nodes, R)) return WeightLatticeElem{DT,R}(SVector{R,Int}(v)), word end """ - conjugate_dominant_weight_with_length(w::WeightLatticeElem{DT,R}, nodes=1:R) -> (WeightLatticeElem, Int) + conjugate_dominant_weight_with_length(w::WeightLatticeElem{DT,R}, nodes=nothing) -> (WeightLatticeElem, Int) Return the dominant weight in the Weyl orbit of `w` together with the number of simple reflections applied (i.e. the length of the Weyl group element @@ -495,10 +515,10 @@ julia> conjugate_dominant_weight_with_length(WeightLatticeElem(TypeA{3}, [-1, 2, ``` """ @inline function conjugate_dominant_weight_with_length( - w::WeightLatticeElem{DT,R}, nodes=Base.OneTo(R) + w::WeightLatticeElem{DT,R}, nodes=nothing ) where {DT,R} v = MVector{R,Int}(w.vec) - len = _fold_dominant!(v, cartan_matrix(DT), nodes) + len = _fold_dominant!(v, cartan_matrix(DT), _check_nodes(nodes, R)) return WeightLatticeElem{DT,R}(SVector{R,Int}(v)), len end diff --git a/src/WeylGroup.jl b/src/WeylGroup.jl index 920b245..243c876 100644 --- a/src/WeylGroup.jl +++ b/src/WeylGroup.jl @@ -930,7 +930,7 @@ weyl_dimension(dt::DynkinType, v) = degree(typeof(dt), v) # ─── Singularity ───────────────────────────────────────────────────────────── """ - is_singular(w::WeightLatticeElem{DT,R}, nodes=1:R) -> Bool + is_singular(w::WeightLatticeElem{DT,R}, nodes=nothing) -> Bool Check whether the weight `w` is singular, i.e. lies on some wall of a Weyl chamber. Equivalently, `w` is singular iff `⟨α∨, w⟩ = 0` for some positive @@ -970,15 +970,15 @@ julia> is_singular(fundamental_weight(TypeA{2}, 2), (2,)) false ``` """ -function is_singular(w::WeightLatticeElem{DT,R}, nodes=Base.OneTo(R)) where {DT,R} +function is_singular(w::WeightLatticeElem{DT,R}, nodes=nothing) where {DT,R} dom = conjugate_dominant_weight(w, nodes) - return any(s -> dom.vec[s] == 0, nodes) + return any(s -> dom.vec[s] == 0, something(nodes, 1:R)) end # ─── Borel–Weil–Bott ──────────────────────────────────────────────────────── """ - borel_weil_bott(λ::WeightLatticeElem{DT,R}, nodes=1:R) -> Union{Nothing, Tuple{Int, WeightLatticeElem{DT,R}}} + borel_weil_bott(λ::WeightLatticeElem{DT,R}, nodes=nothing) -> Union{Nothing, Tuple{Int, WeightLatticeElem{DT,R}}} Apply the Borel–Weil–Bott theorem to the weight `λ`. @@ -1034,7 +1034,7 @@ julia> borel_weil_bott(-weyl_vector(TypeA{2}), (2,)) === nothing # singular fo true ``` """ -function borel_weil_bott(λ::WeightLatticeElem{DT,R}, nodes=Base.OneTo(R)) where {DT,R} +function borel_weil_bott(λ::WeightLatticeElem{DT,R}, nodes=nothing) where {DT,R} ρ = weyl_vector(DT) μ = λ + ρ @@ -1043,7 +1043,7 @@ function borel_weil_bott(λ::WeightLatticeElem{DT,R}, nodes=Base.OneTo(R)) where # If any coordinate of μ_dom is zero, λ + ρ lies on a wall of the chamber # (including the case μ = 0 when λ = -ρ), so all cohomology vanishes. - any(s -> μ_dom.vec[s] == 0, nodes) && return nothing + any(s -> μ_dom.vec[s] == 0, something(nodes, 1:R)) && return nothing return (d, μ_dom - ρ) end diff --git a/test/runtests.jl b/test/runtests.jl index 134cad4..608e1ed 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1169,6 +1169,35 @@ end end end +@testset "Levi-restricted fold: node argument" begin + λ = WeightLatticeElem(TypeA{3}, [-1, 2, -1]) + + # Any container of node indices works, since membership is all that is asked + # of it. + expected = conjugate_dominant_weight(λ, (2, 3)) + @test conjugate_dominant_weight(λ, [2, 3]) == expected + @test conjugate_dominant_weight(λ, 2:3) == expected + @test conjugate_dominant_weight(λ, Set([2, 3])) == expected + @test conjugate_dominant_weight(λ, (3, 2)) == expected + @test conjugate_dominant_weight(λ, (2, 3, 2)) == expected # duplicates are harmless + + # A node outside the diagram is a mistake, not a reflection to skip. Note the + # test suite runs under --check-bounds=yes; this guard is what keeps an ordinary + # build from indexing past the end of the coordinate vector. + for nodes in [(0,), (4,), (17,), (1, 5), [-1]] + @test_throws ArgumentError conjugate_dominant_weight(λ, nodes) + @test_throws ArgumentError conjugate_dominant_weight_with_length(λ, nodes) + @test_throws ArgumentError conjugate_dominant_weight_with_elem(λ, nodes) + @test_throws ArgumentError borel_weil_bott(λ, nodes) + @test_throws ArgumentError is_singular(λ, nodes) + end + + # No nodes at all: nothing moves. + @test conjugate_dominant_weight(λ, ()) == λ + @test conjugate_dominant_weight_with_length(λ, ()) == (λ, 0) + @test !is_singular(λ, ()) +end + @testset "Levi-restricted Borel–Weil–Bott" begin # Agreement with the absolute statement inside the sub-root-system: the degree # is the same, and the output weight restricts to the sub-diagram output. Note From 1794b169889040ac7bef146a1ed696d53531fe22 Mon Sep 17 00:00:00 2001 From: Pieter Belmans Date: Thu, 13 Aug 2026 07:16:25 +0200 Subject: [PATCH 5/6] refactor: check nodes in one method rather than dispatching on nothing --- src/WeightLattice.jl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/WeightLattice.jl b/src/WeightLattice.jl index efa28ff..8e5c736 100644 --- a/src/WeightLattice.jl +++ b/src/WeightLattice.jl @@ -353,9 +353,8 @@ end # Reject reflection nodes outside the diagram, so that a mistyped node is an # error rather than a silently skipped reflection. `nothing` means every node. -_check_nodes(::Nothing, ::Integer) = nothing - function _check_nodes(nodes, R::Integer) + nodes === nothing && return nothing for s in nodes 1 <= s <= R || throw(ArgumentError("Reflection node $s is out of range for rank $R")) From afc212beea0958b9051feb29e19aaf26ab4dda32 Mon Sep 17 00:00:00 2001 From: Pieter Belmans Date: Thu, 13 Aug 2026 07:22:31 +0200 Subject: [PATCH 6/6] refactor: validate the reflection nodes inside the fold kernels Every fold goes through _fold_dominant! or _fold_dominant_with_word!, so checking there rather than at each caller means a future entry point taking a node set cannot reach a fold without the check. Free on the default path, where the check specializes to nothing. --- src/WeightLattice.jl | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/WeightLattice.jl b/src/WeightLattice.jl index 8e5c736..11369e8 100644 --- a/src/WeightLattice.jl +++ b/src/WeightLattice.jl @@ -353,6 +353,8 @@ end # Reject reflection nodes outside the diagram, so that a mistyped node is an # error rather than a silently skipped reflection. `nothing` means every node. +# Called from the fold kernels themselves rather than from their callers, so no +# future entry point can reach a fold without passing through it. function _check_nodes(nodes, R::Integer) nodes === nothing && return nothing for s in nodes @@ -376,6 +378,7 @@ end @inline function _fold_dominant!( v::MVector{R,Int}, C::SMatrix{R,R,Int}, nodes=nothing ) where {R} + _check_nodes(nodes, R) len = 0 s = 1 @inbounds while s <= R @@ -403,9 +406,9 @@ Passing `nodes` reflects only in those simple roots, giving the unique spanned by ``S`` = `nodes`, i.e. dominant for the corresponding Levi subgroup. The coordinates outside `nodes` are still moved along. -`nodes` may be any container of node indices supporting `in`, such as a tuple, -vector, range, or `Set`; `nothing` means every simple root. A node outside -`1:R` throws an `ArgumentError`. +`nodes` may be any iterable of node indices — a tuple, vector, range or `Set` — +and `nothing` means every simple root. A node outside `1:R` throws an +`ArgumentError`. # Examples ```jldoctest @@ -432,7 +435,7 @@ julia> conjugate_dominant_weight(WeightLatticeElem(TypeA{2}, [-1, -1]), (2,)) w::WeightLatticeElem{DT,R}, nodes=nothing ) where {DT,R} v = MVector{R,Int}(w.vec) - _fold_dominant!(v, cartan_matrix(DT), _check_nodes(nodes, R)) + _fold_dominant!(v, cartan_matrix(DT), nodes) return WeightLatticeElem{DT,R}(SVector{R,Int}(v)) end @@ -440,6 +443,7 @@ end function _fold_dominant_with_word!( v::MVector{R,Int}, C::SMatrix{R,R,Int}, nodes=nothing ) where {R} + _check_nodes(nodes, R) word = Int[] s = 1 while s <= R @@ -481,7 +485,7 @@ function conjugate_dominant_weight_with_elem( w::WeightLatticeElem{DT,R}, nodes=nothing ) where {DT,R} v = MVector{R,Int}(w.vec) - word = _fold_dominant_with_word!(v, cartan_matrix(DT), _check_nodes(nodes, R)) + word = _fold_dominant_with_word!(v, cartan_matrix(DT), nodes) return WeightLatticeElem{DT,R}(SVector{R,Int}(v)), word end @@ -517,7 +521,7 @@ julia> conjugate_dominant_weight_with_length(WeightLatticeElem(TypeA{3}, [-1, 2, w::WeightLatticeElem{DT,R}, nodes=nothing ) where {DT,R} v = MVector{R,Int}(w.vec) - len = _fold_dominant!(v, cartan_matrix(DT), _check_nodes(nodes, R)) + len = _fold_dominant!(v, cartan_matrix(DT), nodes) return WeightLatticeElem{DT,R}(SVector{R,Int}(v)), len end