From 81edf42f5bc74e1dc3ae52e09e6bbc97ea3d639f Mon Sep 17 00:00:00 2001 From: Pieter Belmans Date: Tue, 1 Sep 2026 13:03:30 +0200 Subject: [PATCH] feat: add Bourbaki tables --- Project.toml | 2 + docs/src/roots.md | 33 ++++ src/BourbakiTable.jl | 391 +++++++++++++++++++++++++++++++++++++++++++ src/Semisimple.jl | 4 + test/runtests.jl | 56 +++++++ 5 files changed, 486 insertions(+) create mode 100644 src/BourbakiTable.jl diff --git a/Project.toml b/Project.toml index d3caf14..911d322 100644 --- a/Project.toml +++ b/Project.toml @@ -9,6 +9,7 @@ LRUCache = "8ac3fa9e-de4c-5943-b1dc-09c6b5f20637" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a" Preferences = "21216c6a-2e73-6563-6e65-726566657250" +PrettyTables = "08abe8d2-0d0c-5749-adfa-8a2ac140af0d" StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" [compat] @@ -19,6 +20,7 @@ LRUCache = "1.6" LinearAlgebra = "1" PrecompileTools = "1" Preferences = "1" +PrettyTables = "2.3.2" StaticArrays = "1" Test = "1" julia = "1.9" diff --git a/docs/src/roots.md b/docs/src/roots.md index 3269725..2af96f6 100644 --- a/docs/src/roots.md +++ b/docs/src/roots.md @@ -222,6 +222,39 @@ dual_coxeter_number degrees_fundamental_invariants ``` +## Bourbaki tables + +`bourbaki_table` collects the root-system data appearing in Bourbaki's finite-type +plates into one programmatic object. It uses intrinsic simple-root coordinates rather +than a separate Euclidean realization for each Dynkin family. + +```jldoctest roots +julia> table = bourbaki_table(TypeA{2}); + +julia> table.exponents +2-element Vector{Int64}: + 1 + 2 + +julia> table.root_lattice_quotient +1-element Vector{Int64}: + 3 + +julia> table.opposition_involution +2-element StaticArraysCore.SVector{2, Int64} with indices SOneTo(2): + 2 + 1 +``` + +Display the table itself to see a Unicode plate rendered with PrettyTables.jl: the +Dynkin diagram, all positive roots and coroots, fundamental weights, Coxeter and +lattice invariants, Weyl-group data, and finite and affine Cartan matrices. + +```@docs +BourbakiTable +bourbaki_table +``` + ## Examples ### A2 diff --git a/src/BourbakiTable.jl b/src/BourbakiTable.jl new file mode 100644 index 0000000..32828ff --- /dev/null +++ b/src/BourbakiTable.jl @@ -0,0 +1,391 @@ +# ═══════════════════════════════════════════════════════════════════════════════ +# Bourbaki tables — intrinsic versions of the finite root-system plates +# +# Bourbaki's plates use family-specific Euclidean realizations. Semisimple.jl +# instead has one uniform realization: roots and coroots are expressed in the +# simple-root basis. This file assembles the corresponding intrinsic data from +# the package's existing root-system, Cartan, weight-lattice, and Weyl-group API. +# ═══════════════════════════════════════════════════════════════════════════════ + +export BourbakiTable, bourbaki_table + +""" + BourbakiTable{DT,R} + +An intrinsic version of the Bourbaki plate for the irreducible Dynkin type `DT`. + +Roots and coroots are represented in the simple-root basis. The columns of +`fundamental_weight_coefficients` express the fundamental weights in that same +basis. The remaining fields collect the affine Cartan matrix, Coxeter data, +the root-lattice quotient, Weyl-group data, and the opposition involution. + +Construct a table with [`bourbaki_table`](@ref). +""" +struct BourbakiTable{ + DT<:SimpleDynkinType, + R, + CM<:AbstractMatrix{Int}, + FWM<:AbstractMatrix{Rational{Int}}, +} + dynkin_type::Type{DT} + dynkin_diagram::DynkinDiagram + positive_roots::Vector{RootSpaceElem{DT,R}} + positive_coroots::Vector{RootSpaceElem{DT,R}} + highest_root::RootSpaceElem{DT,R} + highest_short_root::RootSpaceElem{DT,R} + highest_coroot::RootSpaceElem{DT,R} + fundamental_weights::Vector{WeightLatticeElem{DT,R}} + fundamental_weight_coefficients::FWM + weyl_vector::WeightLatticeElem{DT,R} + sum_positive_roots::RootSpaceElem{DT,R} + cartan_matrix::CM + affine_cartan_matrix::Matrix{Int} + coxeter_coefficients::SVector{R,Int} + dual_coxeter_coefficients::SVector{R,Int} + coxeter_number::Int + dual_coxeter_number::Int + connection_index::Int + root_lattice_quotient::Vector{Int} + exponents::Vector{Int} + degrees_fundamental_invariants::Vector{Int} + weyl_order::BigInt + longest_element::WeylGroupElem{DT,R} + opposition_involution::SVector{R,Int} +end + +# The affine simple root is α₀ = -θ. Its coroot is -θ∨, where θ∨ is the +# coroot paired with the highest root (not the highest coroot in general). +function _affine_cartan_matrix( + C::AbstractMatrix{Int}, + highest_root_coefficients::AbstractVector{Int}, + highest_root_coroot_coefficients::AbstractVector{Int}, +) + R = size(C, 1) + affine = zeros(Int, R + 1, R + 1) + affine[1, 1] = 2 + affine[2:end, 2:end] .= C + affine[2:end, 1] = collect(-(C * highest_root_coefficients)) + affine[1, 2:end] = collect(-(transpose(highest_root_coroot_coefficients) * C)) + return affine +end + +# P(R)/Q(R) has order det(C). For an irreducible finite root system it is +# cyclic unless its order is four and the exponent, read from C⁻¹, is two +# (the even D case). Empty invariant factors denote the trivial group. +function _root_lattice_quotient_invariants( + Cinv::AbstractMatrix{Rational{Int}}, connection_index::Int +) + connection_index == 1 && return Int[] + quotient_exponent = foldl(lcm, denominator.(Cinv); init=1) + if quotient_exponent == connection_index + return [connection_index] + elseif connection_index == 4 && quotient_exponent == 2 + return [2, 2] + end + error( + "unexpected root-lattice quotient of order $connection_index and exponent $quotient_exponent" + ) +end + +function _opposition_involution( + fundamental_weights::Vector{WeightLatticeElem{DT,R}}, + w0::WeylGroupElem{DT,R}, +) where {DT,R} + return SVector{R,Int}( + ntuple(Val(R)) do i + image = -(fundamental_weights[i] * w0) + j = findfirst(==(1), image.vec) + j === nothing && error("the longest element did not permute the fundamental weights") + image.vec == fundamental_weights[j].vec || + error("the longest element did not permute the fundamental weights") + return j + end, + ) +end + +""" + bourbaki_table(::Type{DT}) -> BourbakiTable{DT} + bourbaki_table(dt::SimpleDynkinType) -> BourbakiTable + +Assemble an intrinsic Bourbaki table for an irreducible finite Dynkin type. + +Unlike the family-specific ambient-coordinate realizations in Bourbaki's +plates, this table expresses every root and coroot uniformly in the simple-root +basis. All entries are computed from Semisimple.jl's existing root data. +Product types are rejected because the Bourbaki plates are indexed by +irreducible root systems. + +# Examples +```jldoctest +julia> using Semisimple + +julia> table = bourbaki_table(TypeA{2}); + +julia> table.exponents +2-element Vector{Int64}: + 1 + 2 + +julia> table.root_lattice_quotient +1-element Vector{Int64}: + 3 + +julia> table.opposition_involution +2-element StaticArraysCore.SVector{2, Int64} with indices SOneTo(2): + 2 + 1 +``` +""" +function bourbaki_table(::Type{DT}) where {DT<:SimpleDynkinType} + check_dynkin_type(DT) + R = rank(DT) + RS = RootSystem(DT) + positive_roots_data = positive_roots(RS) + positive_coroots_data = positive_coroots(RS) + highest_root_data = highest_root(RS) + highest_short_root_data = highest_short_root(RS) + highest_coroot_data = highest_coroot(RS) + fundamental_weights_data = fundamental_weights(DT) + C = cartan_matrix(DT) + Cinv = cartan_matrix_inverse(DT) + connection_index = cartan_determinant(DT) + degrees = sort!(collect(degrees_fundamental_invariants(DT))) + exponents = degrees .- 1 + w0 = longest_element(weyl_group(DT)) + + root_sum = zeros(Int, R) + for root in positive_roots_data + root_sum .+= root.vec + end + + affine_C = _affine_cartan_matrix( + C, + highest_root_data.vec, + RS.positive_coroots_list[end], + ) + + return BourbakiTable{ + DT, + R, + typeof(C), + typeof(Cinv), + }( + DT, + dynkin_diagram(DT), + positive_roots_data, + positive_coroots_data, + highest_root_data, + highest_short_root_data, + highest_coroot_data, + fundamental_weights_data, + Cinv, + weyl_vector(DT), + RootSpaceElem(DT, root_sum), + C, + affine_C, + coxeter_coefficients(DT), + dual_coxeter_coefficients(DT), + coxeter_number(DT), + dual_coxeter_number(DT), + connection_index, + _root_lattice_quotient_invariants(Cinv, connection_index), + exponents, + degrees, + weyl_order(DT), + w0, + _opposition_involution(fundamental_weights_data, w0), + ) +end + +bourbaki_table(dt::SimpleDynkinType) = bourbaki_table(typeof(dt)) + +function bourbaki_table(::Type{DT}) where {DT<:ProductDynkinType} + throw(ArgumentError("bourbaki_table requires an irreducible Dynkin type, got $DT")) +end + +bourbaki_table(dt::ProductDynkinType) = bourbaki_table(typeof(dt)) + +const _SUBSCRIPT_DIGITS = ('₀', '₁', '₂', '₃', '₄', '₅', '₆', '₇', '₈', '₉') + +function _subscript(n::Integer) + n >= 0 || throw(ArgumentError("subscripts must be non-negative, got $n")) + return join(_SUBSCRIPT_DIGITS[Int(digit) + 1] for digit in digits(n; base=10)[end:-1:1]) +end + +function _unicode_type_name(::Type{DT}) where {DT<:SimpleDynkinType} + name = _type_name(DT) + return string(first(name), _subscript(parse(Int, name[2:end]))) +end + +_alpha_label(i::Integer) = "α" * _subscript(i) +_coroot_label(i::Integer) = _alpha_label(i) * "∨" +_omega_label(i::Integer) = "ω" * _subscript(i) + +_number_string(n::Integer) = replace(string(n), '-' => '−') + +function _number_string(q::Rational) + denominator(q) == 1 && return _number_string(numerator(q)) + return _number_string(numerator(q)) * "⁄" * _number_string(denominator(q)) +end + +_coordinate_string(v) = "[" * join(_number_string.(v), ", ") * "]" + +function _quotient_string(invariants::Vector{Int}) + isempty(invariants) && return "trivial" + return join(("ℤ/$(n)ℤ" for n in invariants), " × ") +end + +function _show_text_box(io::IO, title::AbstractString, contents::AbstractString) + lines = split(contents, '\n') + title_width = textwidth(title) + inner_width = max(title_width + 2, maximum(textwidth, lines)) + println(io, "╭─ ", title, " ", repeat("─", inner_width - title_width - 1), "╮") + for line in lines + println(io, "│ ", line, repeat(" ", inner_width - textwidth(line)), " │") + end + println(io, "╰", repeat("─", inner_width + 2), "╯") + return nothing +end + +function _summary_rows(table::BourbakiTable{DT,R}) where {DT,R} + opposition = join( + ( + _alpha_label(i) * " ↦ " * _alpha_label(table.opposition_involution[i]) for i in 1:R + ), + ", ", + ) + return Any[ + "rank" R + "|Φ|" 2 * length(table.positive_roots) + "|Φ⁺|" length(table.positive_roots) + "θ" _coordinate_string(table.highest_root.vec) + "θₛ" _coordinate_string(table.highest_short_root.vec) + "θ∨" _coordinate_string(table.highest_coroot.vec) + "2ρ" _coordinate_string(table.sum_positive_roots.vec) + "marks mᵢ" _coordinate_string(table.coxeter_coefficients) + "dual marks mᵢ∨" _coordinate_string(table.dual_coxeter_coefficients) + "h" table.coxeter_number + "h∨" table.dual_coxeter_number + "P/Q" _quotient_string(table.root_lattice_quotient) + "f = |P/Q|" table.connection_index + "exponents" _coordinate_string(table.exponents) + "degrees" _coordinate_string(table.degrees_fundamental_invariants) + "|W|" table.weyl_order + "ℓ(w₀)" length(table.longest_element) + "−w₀" opposition + ] +end + +function _show_summary_table(io::IO, table::BourbakiTable) + pretty_table( + io, + _summary_rows(table); + header=["invariant", "value"], + title="ROOT-SYSTEM INVARIANTS", + title_alignment=:l, + tf=tf_unicode_rounded, + alignment=[:l, :l], + crop=:none, + ) + return nothing +end + +function _show_coordinate_table(io::IO, entries; coroot::Bool=false) + n = length(entries) + R = length(first(entries).vec) + data = Matrix{Any}(undef, n, R + 2) + for (i, entry) in enumerate(entries) + data[i, 1] = i + data[i, 2] = height(entry) + for j in 1:R + data[i, j + 2] = _number_string(entry.vec[j]) + end + end + + labels = coroot ? [_coroot_label(i) for i in 1:R] : [_alpha_label(i) for i in 1:R] + pretty_table( + io, + data; + header=["#", "ht", labels...], + title=coroot ? "(Φ∨)⁺ · POSITIVE COROOTS" : "Φ⁺ · POSITIVE ROOTS", + title_alignment=:l, + tf=tf_unicode_rounded, + alignment=fill(:c, R + 2), + crop=:none, + ) + return nothing +end + +function _show_fundamental_weight_table(io::IO, Cinv::AbstractMatrix) + R = size(Cinv, 1) + data = Matrix{String}(undef, R, R) + for i in 1:R, j in 1:R + data[i, j] = _number_string(Cinv[j, i]) + end + + pretty_table( + io, + data; + header=[_alpha_label(i) for i in 1:R], + row_labels=[_omega_label(i) for i in 1:R], + row_label_column_title="", + title="FUNDAMENTAL WEIGHTS · SIMPLE-ROOT COORDINATES", + title_alignment=:l, + tf=tf_unicode_rounded, + alignment=:c, + crop=:none, + ) + return nothing +end + +function _show_cartan_table(io::IO, matrix::AbstractMatrix{Int}; affine::Bool=false) + first_node = affine ? 0 : 1 + last_node = first_node + size(matrix, 1) - 1 + nodes = first_node:last_node + data = _number_string.(matrix) + + pretty_table( + io, + data; + header=[_alpha_label(i) for i in nodes], + row_labels=[_coroot_label(i) for i in nodes], + row_label_column_title="", + title=if affine + "AFFINE CARTAN MATRIX · NODES ₀…$(_subscript(last_node))" + else + "CARTAN MATRIX" + end, + title_alignment=:l, + tf=tf_unicode_rounded, + alignment=:c, + crop=:none, + ) + return nothing +end + +function Base.show(io::IO, table::BourbakiTable{DT}) where {DT} + print(io, "BourbakiTable(", _unicode_type_name(DT), ")") +end + +function Base.show( + io::IO, ::MIME"text/plain", table::BourbakiTable{DT,R} +) where {DT,R} + _show_text_box( + io, + "BOURBAKI PLATE · " * _unicode_type_name(DT), + string(table.dynkin_diagram), + ) + println(io) + _show_summary_table(io, table) + println(io) + _show_coordinate_table(io, table.positive_roots) + println(io) + _show_coordinate_table(io, table.positive_coroots; coroot=true) + println(io) + _show_fundamental_weight_table(io, table.fundamental_weight_coefficients) + println(io) + _show_cartan_table(io, table.cartan_matrix) + println(io) + _show_cartan_table(io, table.affine_cartan_matrix; affine=true) + return nothing +end diff --git a/src/Semisimple.jl b/src/Semisimple.jl index 32ea295..11f8cb7 100644 --- a/src/Semisimple.jl +++ b/src/Semisimple.jl @@ -19,6 +19,7 @@ module Semisimple using LRUCache using PrecompileTools using Preferences +using PrettyTables: pretty_table, tf_unicode_rounded using StaticArrays using LinearAlgebra: I as _I @@ -77,6 +78,9 @@ include("Weylloop.jl") # ─── Characters and representation ring ───────────────────────────────────── include("Characters.jl") +# ─── Bourbaki tables ───────────────────────────────────────────────────────── +include("BourbakiTable.jl") + # ─── Cache configuration (Preferences + runtime API) ──────────────────────── include("CacheConfig.jl") diff --git a/test/runtests.jl b/test/runtests.jl index 812a788..4a0c453 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -248,6 +248,62 @@ end end end +# ═══════════════════════════════════════════════════════════════════════ +# Bourbaki tables +# ═══════════════════════════════════════════════════════════════════════ +@testset "Bourbaki tables" begin + table_A2 = bourbaki_table(TypeA{2}) + @test table_A2 isa BourbakiTable{TypeA{2},2} + @test bourbaki_table(TypeA{2}()).cartan_matrix == table_A2.cartan_matrix + @test length(table_A2.positive_roots) == 3 + @test length(table_A2.positive_coroots) == 3 + @test coefficients(table_A2.highest_root) == [1, 1] + @test coefficients(table_A2.sum_positive_roots) == [2, 2] + @test table_A2.fundamental_weight_coefficients == [2//3 1//3; 1//3 2//3] + @test table_A2.coxeter_number == 3 + @test table_A2.dual_coxeter_number == 3 + @test table_A2.connection_index == 3 + @test table_A2.root_lattice_quotient == [3] + @test table_A2.exponents == [1, 2] + @test table_A2.degrees_fundamental_invariants == [2, 3] + @test table_A2.weyl_order == 6 + @test length(table_A2.longest_element) == 3 + @test table_A2.opposition_involution == [2, 1] + @test table_A2.affine_cartan_matrix == [2 -1 -1; -1 2 -1; -1 -1 2] + + table_B2 = bourbaki_table(TypeB{2}) + @test coefficients(table_B2.highest_root) == [1, 2] + @test coefficients(table_B2.highest_short_root) == [1, 1] + @test table_B2.coxeter_coefficients == [1, 2] + @test table_B2.dual_coxeter_coefficients == [1, 1] + @test table_B2.root_lattice_quotient == [2] + + table_D4 = bourbaki_table(TypeD{4}) + @test table_D4.root_lattice_quotient == [2, 2] + @test table_D4.exponents == [1, 3, 3, 5] + + table_E6 = bourbaki_table(TypeE{6}) + @test table_E6.opposition_involution == [6, 2, 5, 4, 3, 1] + + table_E8 = bourbaki_table(TypeE{8}) + @test isempty(table_E8.root_lattice_quotient) + @test table_E8.opposition_involution == 1:8 + + rendered = sprint(show, MIME"text/plain"(), table_A2) + @test startswith(rendered, "╭─ BOURBAKI PLATE · A₂") + @test occursin("Φ⁺ · POSITIVE ROOTS", rendered) + @test occursin("FUNDAMENTAL WEIGHTS · SIMPLE-ROOT COORDINATES", rendered) + @test occursin("α₁", rendered) + @test occursin("ω₁", rendered) + @test occursin("ℤ/3ℤ", rendered) + @test occursin("AFFINE CARTAN MATRIX · NODES ₀…₂", rendered) + @test sprint(show, table_A2) == "BourbakiTable(A₂)" + + PT = ProductDynkinType{Tuple{TypeA{1},TypeA{1}}} + @test_throws ArgumentError bourbaki_table(PT) + @test_throws ArgumentError bourbaki_table(PT()) +end + # ═══════════════════════════════════════════════════════════════════════ # Dynkin diagram layouts # ═══════════════════════════════════════════════════════════════════════