Repository: oscar-system/Oscar.jl
Pull request #4695: Fix Chevalley basis; add more functionality
Author: login: lgoettgens
Labels: name: topic: lie theory
name: release notes: to be added
description: PRs introducing changes that should be (but have not yet been) mentioned in the release notes
name: backport 1.3.x
description: backport for release branch 1.3
name: bug: wrong result
Created: 2025-03-04T17:57:08Z | Merged: 2025-03-09T08:53:47Z

Reported by Willem de Graaf on slack. The example was added to the conformance tests, the conformance tests were improved to fail on that example, and then the algorithm was fixed (or rather: implemented properly instead of hoping for the best).

# Release notes
- Fix `chevalley_basis` (sometimes returning wrong results)
- Add `change_base_ring(::Field, ::LieAlgebra)`
- Add `structure_constant_table(::LieAlgebra, basis)`


Diff:
diff --git a/experimental/LieAlgebras/src/AbstractLieAlgebra.jl b/experimental/LieAlgebras/src/AbstractLieAlgebra.jl
index ff5478b7f9ca..7f876abbf50e 100644
--- a/experimental/LieAlgebras/src/AbstractLieAlgebra.jl
+++ b/experimental/LieAlgebras/src/AbstractLieAlgebra.jl
@@ -134,6 +134,21 @@ function set_root_system_and_chevalley_basis!(
   L.chevalley_basis = chev
 end
 
+###############################################################################
+#
+#   Structure constant table and change_base_ring
+#
+###############################################################################
+
+function change_base_ring(R::Field, L::AbstractLieAlgebra)
+  struct_consts = map(e -> change_base_ring(R, e), structure_constant_table(L; copy=false))
+  return lie_algebra(R, struct_consts, symbols(L); check=false)
+end
+
+function structure_constant_table(L::AbstractLieAlgebra; copy::Bool=true)
+  return copy ? deepcopy(_struct_consts(L)) : _struct_consts(L)
+end
+
 ###############################################################################
 #
 #   Constructor
diff --git a/experimental/LieAlgebras/src/LieAlgebra.jl b/experimental/LieAlgebras/src/LieAlgebra.jl
index c300478d8579..561fb1332cf6 100644
--- a/experimental/LieAlgebras/src/LieAlgebra.jl
+++ b/experimental/LieAlgebras/src/LieAlgebra.jl
@@ -77,7 +77,7 @@ end
 Check whether the Lie algebra element `x` is zero.
 """
 function iszero(x::LieAlgebraElem)
-  return iszero(coefficients(x))
+  return iszero(_matrix(x))
 end
 
 @inline function _matrix(x::LieAlgebraElem{C}) where {C<:FieldElem}
@@ -259,13 +259,13 @@ end
 
 function Base.:(==)(x1::LieAlgebraElem{C}, x2::LieAlgebraElem{C}) where {C<:FieldElem}
   check_parent(x1, x2)
-  return coefficients(x1) == coefficients(x2)
+  return _matrix(x1) == _matrix(x2)
 end
 
 function Base.hash(x::LieAlgebraElem, h::UInt)
   b = 0x6724cbedbd860982 % UInt
   h = hash(parent(x), h)
-  h = hash(coefficients(x), h)
+  h = hash(_matrix(x), h)
   return xor(h, b)
 end
 
@@ -471,7 +471,7 @@ function adjoint_matrix(x::LieAlgebraElem{C}) where {C<:FieldElem}
   L = parent(x)
   mat = zero_matrix(coefficient_ring(L), dim(L), dim(L))
   tmp = zero(mat)
-  for (c, g) in zip(coefficients(x), adjoint_matrices(L))
+  for (c, g) in zip(_matrix(x), adjoint_matrices(L))
     mat = addmul!(mat, g, c, tmp)
   end
   return mat
@@ -604,10 +604,9 @@ function _root_system_and_chevalley_basis(
 ) where {C<:FieldElem}
   @req base_lie_algebra(H) === L "Incompatible Lie algebras."
   # we just assume that H is indeed a Cartan subalgebra
-
   @req is_invertible(killing_matrix(L)) "The Killing form is degenerate"
 
-  F = coefficient_ring(L)
+  R = coefficient_ring(L)
 
   # compute the common eigenspaces of the adjoint action of H.
   # B is a list of subspaces of L that gets refined in each iteration.
@@ -645,7 +644,7 @@ function _root_system_and_chevalley_basis(
 
   # compute an R-basis of the root space, s.t. the corresponding co-roots are a basis of H
   roots_basis = empty(roots)
-  basis_mat_H = zero_matrix(F, 0, dim(L))
+  basis_mat_H = zero_matrix(R, 0, dim(L))
   for root in roots
     nrows(basis_mat_H) == dim(H) && break
     x_j = only(basis(root_spaces[root]))
@@ -719,31 +718,177 @@ function _root_system_and_chevalley_basis(
   )
   type, ordering = cartan_type_with_ordering(cm; check=false)
   permute!(roots_simple, ordering)
-  R = root_system(type)
+  rs = root_system(type)
 
-  # compute a Chevalley basis of L
+  # compute conrete root vectors of L
   root_vectors = [
     begin
       concrete_root = sum(
-        c .* root_simple for
+        Int(c) .* root_simple for
         (c, root_simple) in zip(coefficients(abstract_root), roots_simple)
       )
       @assert concrete_root in roots
       root_vector = only(basis(root_spaces[concrete_root]))
-    end for abstract_root in Oscar.roots(R)
+    end for abstract_root in Oscar.roots(rs)
   ]
-  xs = root_vectors[1:n_positive_roots(R)]
-  ys = Vector{elem_type(L)}([
+
+  # scale the root vectors to get canonical generators, i.e.
+  # 1. [h_i,h_j] = 0            2. [x_i,y_j] = delta_ij * h_i
+  # 3. [h_i,x_j] = a_ij * x_j   4. [h_i,y_j] = -a_ij * y_j
+  canonical_gens_xs = root_vectors[1:n_simple_roots(rs)]
+  canonical_gens_ys = Vector{elem_type(L)}([
     begin
-      x = xs[i]
-      y = root_vectors[n_positive_roots(R) + i]
+      x = canonical_gens_xs[i]
+      y = root_vectors[n_positive_roots(rs) + i]
       y *= 2//only(solve(_matrix(x), _matrix(bracket(bracket(x, y), x)); side=:left))
       y
-    end for i in 1:n_positive_roots(R)
+    end for i in 1:n_simple_roots(rs)
   ])
-  hs = elem_type(L)[xs[i] * ys[i] for i in 1:n_simple_roots(R)]
+  canonical_gens_hs = elem_type(L)[
+    canonical_gens_xs[i] * canonical_gens_ys[i] for i in 1:n_simple_roots(rs)
+  ]
+
+  # Construct an automorphism `f` of `L` with `f(L_alpha) = L_-alpha`, and `f(h) = -h` for `h` in `H`.
+  # This is already defined by mapping `canonical_gens_xs` to `canonical_gens_ys` and vice versa.
+  # To save computation time, we only compute f on L_alpha for alpha a positive root
+  domain_basis = copy(canonical_gens_xs)
+  codomain_basis = copy(canonical_gens_ys)
+  for (i, alpha) in enumerate(positive_roots(rs))
+    i <= n_simple_roots(rs) && continue # already set above
+    j, k = 0, 0
+    found = false
+    for outer j in 1:rank(rs)
+      (fl, k) = is_positive_root_with_index(alpha - simple_root(rs, j))
+      if fl
+        found = true
+        break
+      end
+    end
+    @assert found
+    push!(domain_basis, canonical_gens_xs[j] * domain_basis[k])
+    push!(codomain_basis, canonical_gens_ys[j] * codomain_basis[k])
+  end
+
+  # For each positive root vector `x` we set `y = -f(x)`.
+  # We compute a scalar `coeff` such that `h = 2/coeff * [x,y]`.
+  # We need to multiply `x` and `y` by `sqrt(2/coeff)` to get elements of a Chevally basis.
+  pos_root_vectors = elem_type(L)[]
+  neg_root_vectors = elem_type(L)[]
+  scaled_cartan_elems = elem_type(L)[]
+  coeffs = C[]
+  for i in 1:n_positive_roots(rs)
+    x = root_vectors[i]
+    y = -only(solve(_matrix(domain_basis[i]), _matrix(x); side=:left)) * codomain_basis[i] # y = -f(x)
+    h = bracket(x, y)
+    coeff = only(solve(_matrix(x), _matrix(bracket(h, x)); side=:left))
+    if i <= n_simple_roots(rs)
+      push!(scaled_cartan_elems, (2//coeff) * h)
+    end
+    push!(coeffs, 2//coeff)
+    push!(pos_root_vectors, x)
+    push!(neg_root_vectors, y)
+  end
+
+  # In general, `coeffs` may not be squares in the coefficient field.
+  # Construct a field extension `F` where they are.
+  if all(is_square, coeffs)
+    F = R
+    coeffs_F_sqrt = sqrt.(coeffs)
+  else
+    F, coeffs_F_sqrt = _field_ext_with_sqrts(R, coeffs)
+  end
+
+  # Construct a Lie algebra `L_F` over `F` with the same structure constants as `L`,
+  # and construct the Chevalley basis of `L_F`. 
+  L_F = change_base_ring(F, L)
+  L_F_chev_basis_mat = reduce(vcat,
+    [
+      (coeff .* _matrix(rv) for (coeff, rv) in zip(coeffs_F_sqrt, pos_root_vectors))...,
+      (coeff .* _matrix(rv) for (coeff, rv) in zip(coeffs_F_sqrt, neg_root_vectors))...,
+      (F.(_matrix(h)) for h in scaled_cartan_elems)...,
+    ]; init=zero_matrix(F, 0, dim(L_F)))
+  # The structure constants of `L_F` w.r.t. the Chevalley basis lie in `R`,
+  # so we can construct the Lie algebra `K` over `R` with the these structure constants.
+  # The basis elements of `K` form a Chevalley basis.
+  K = lie_algebra(
+    R, map(e -> change_base_ring(R, e), _structure_constant_table(L_F, L_F_chev_basis_mat))
+  )
+
+  # Construct an isomorphism f from K to L.
+  # To save computation time, we only compute f on L_alpha for alpha a root.
+  domain_basis_xs = [basis(K, i) for i in 1:n_simple_roots(rs)]
+  domain_basis_ys = [basis(K, n_positive_roots(rs) + i) for i in 1:n_simple_roots(rs)]
+  codomain_basis_xs = copy(canonical_gens_xs)
+  codomain_basis_ys = copy(canonical_gens_ys)
+  for (i, alpha) in enumerate(positive_roots(rs))
+    i <= n_simple_roots(rs) && continue # already set above
+    j, k = 0, 0
+    found = false
+    for outer j in 1:rank(rs)
+      (fl, k) = is_positive_root_with_index(alpha - simple_root(rs, j))
+      if fl
+        found = true
+        break
+      end
+    end
+    @assert found
+    push!(domain_basis_xs, domain_basis_xs[j] * domain_basis_xs[k])
+    push!(domain_basis_ys, domain_basis_ys[j] * domain_basis_ys[k])
+    push!(codomain_basis_xs, codomain_basis_xs[j] * codomain_basis_xs[k])
+    push!(codomain_basis_ys, codomain_basis_ys[j] * codomain_basis_ys[k])
+  end
+
+  # The image of the Chevalley basis of K under f is a Chevalley basis of L.
+  xs = [
+    only(solve(_matrix(domain_basis_xs[i]), _matrix(basis(K, i)); side=:left)) *
+    codomain_basis_xs[i] for i in 1:n_positive_roots(rs)
+  ]
+  ys = [
+    only(
+      solve(
+        _matrix(domain_basis_ys[i]), _matrix(basis(K, n_positive_roots(rs) + i)); side=:left
+      ),
+    ) * codomain_basis_ys[i] for i in 1:n_positive_roots(rs)
+  ]
+
+  return rs, (xs, ys, scaled_cartan_elems)
+end
+
+# TODO: clean the following up, once we have a unified interface for field extensions
+function _field_ext_with_sqrts(F::QQField, elems::Vector{QQFieldElem})
+  i = findfirst(!is_square, elems)
+  if isnothing(i)
+    return F, sqrt.(elems)
+  end
+  Fx, x = polynomial_ring(F; cached=false)
+  F, _ = number_field(x^2 - elems[i]; cached=false)
+  return _field_ext_with_sqrts(F, [F(c) for c in elems])
+end
+
+function _field_ext_with_sqrts(F::NumField, elems::Vector{<:NumFieldElem})
+  @req all(e -> parent(e) === F, elems) "Incompatible parent fields"
+  elems_F = elems
+  i = findfirst(!is_square, elems_F)
+  while !isnothing(i)
+    Fx, x = polynomial_ring(F; cached=false)
+    F, _ = number_field(x^2 - elems_F[i]; cached=false)
+    elems_F = [F(c) for c in elems_F]
+    i = findfirst(!is_square, elems_F)
+  end
+  return F, sqrt.(elems_F)
+end
 
-  return R, (xs, ys, hs)
+function _field_ext_with_sqrts(F::FqField, elems::Vector{<:FqFieldElem})
+  @req all(e -> parent(e) === F, elems) "Incompatible parent fields"
+  elems_F = elems
+  i = findfirst(!is_square, elems_F)
+  while !isnothing(i)
+    Fx, x = polynomial_ring(F; cached=false)
+    F, _ = finite_field(x^2 - elems_F[i]; cached=false)
+    elems_F = [F(c) for c in elems_F]
+    i = findfirst(!is_square, elems_F)
+  end
+  return F, sqrt.(elems_F)
 end
 
 function assure_root_system(L::LieAlgebra{C}) where {C<:FieldElem}
@@ -821,6 +966,68 @@ function cartan_subalgebra(L::LieAlgebra{C}) where {C<:FieldElem}
   end
 end
 
+###############################################################################
+#
+#   Structure constant table
+#
+###############################################################################
+
+function structure_constant_table(L::LieAlgebra; copy::Bool=true)
+  # copy gets ignored as the generic implementation creates a new table anyway
+
+  R = coefficient_ring(L)
+  struct_consts = Matrix{sparse_row_type(R)}(undef, dim(L), dim(L))
+
+  basis = Oscar.basis(L)
+
+  for i in 1:dim(L)
+    struct_consts[i, i] = sparse_row(R)
+  end
+  for i in 1:dim(L), j in (i + 1):dim(L)
+    struct_consts[i, j] = sparse_row(_matrix(bracket(basis[i], basis[j])))
+    struct_consts[j, i] = -struct_consts[i, j]
+  end
+
+  return struct_consts
+end
+
+function structure_constant_table(
+  L::LieAlgebra{C}, basis::Vector{<:LieAlgebraElem{C}}
+) where {C}
+  @req all(b -> parent(b) === L, basis) "Incompatible Lie algebras"
+  basis_mat = reduce(
+    vcat, _matrix.(basis); init=zero_matrix(coefficient_ring(L), 0, dim(L))
+  )
+  return _structure_constant_table(L, basis_mat)
+end
+
+function _structure_constant_table(L::LieAlgebra{C}, basis_mat::MatElem{C}) where {C}
+  @req is_invertible(basis_mat) "input is not a basis"
+  basis_mat_ctx = solve_init(basis_mat)
+
+  R = coefficient_ring(L)
+  struct_consts = Matrix{sparse_row_type(R)}(undef, dim(L), dim(L))
+  sc_standard_basis = structure_constant_table(L; copy=false)
+
+  for i in 1:dim(L)
+    struct_consts[i, i] = sparse_row(R)
+  end
+  for i in 1:dim(L), j in (i + 1):dim(L)
+    # lookup entry in `basis_mat * sc_standard_basis * transpose(basis_mat)`
+    entry = zero_matrix(R, 1, dim(L))
+    for k in 1:dim(L), l in 1:dim(L)
+      coeff = basis_mat[i, k] * basis_mat[j, l]
+      if !is_zero(coeff)
+        entry = addmul!(entry, coeff, dense_row(sc_standard_basis[k, l], dim(L)))
+      end
+    end
+    struct_consts[i, j] = sparse_row(solve(basis_mat_ctx, entry; side=:left))
+    struct_consts[j, i] = -struct_consts[i, j]
+  end
+
+  return struct_consts
+end
+
 ###############################################################################
 #
 #   Universal enveloping algebra
@@ -839,7 +1046,7 @@ function universal_enveloping_algebra(L::LieAlgebra; ordering::Symbol=:lex)
   b = basis(L)
 
   to_R(x::LieAlgebraElem) =
-    sum(c * g for (c, g) in zip(coefficients(x), gensR); init=zero(R))
+    sum(c * g for (c, g) in zip(_matrix(x), gensR); init=zero(R))
 
   rel = strictly_upper_triangular_matrix([
     to_R(b[i]) * to_R(b[j]) - to_R(b[i] * b[j]) for i in 1:(n - 1) for j in (i + 1):n
@@ -848,7 +1055,7 @@ function universal_enveloping_algebra(L::LieAlgebra; ordering::Symbol=:lex)
 
   L_to_U = MapFromFunc(
     L, U, function (x::LieAlgebraElem)
-      sum(c * g for (c, g) in zip(coefficients(x), gensU); init=zero(U))
+      sum(c * g for (c, g) in zip(_matrix(x), gensU); init=zero(U))
     end
   )
   return U, L_to_U
diff --git a/experimental/LieAlgebras/src/LieAlgebras.jl b/experimental/LieAlgebras/src/LieAlgebras.jl
index a17235532214..47b7afb3d402 100644
--- a/experimental/LieAlgebras/src/LieAlgebras.jl
+++ b/experimental/LieAlgebras/src/LieAlgebras.jl
@@ -35,6 +35,7 @@ import ..Oscar:
   cartan_matrix,
   center,
   centralizer,
+  change_base_ring,
   character,
   characteristic,
   check_parent,
@@ -80,6 +81,7 @@ import ..Oscar:
   permutation_group,
   rank,
   root_system,
+  structure_constant_table,
   sub,
   symbols,
   symmetric_power,
diff --git a/experimental/LieAlgebras/src/LinearLieAlgebra.jl b/experimental/LieAlgebras/src/LinearLieAlgebra.jl
index e6cbae94b3ad..1fbfc6e4b126 100644
--- a/experimental/LieAlgebras/src/LinearLieAlgebra.jl
+++ b/experimental/LieAlgebras/src/LinearLieAlgebra.jl
@@ -203,6 +203,17 @@ function set_root_system_and_chevalley_basis!(
   L.chevalley_basis = chev
 end
 
+###############################################################################
+#
+#   change_base_ring
+#
+###############################################################################
+
+function change_base_ring(R::Field, L::LinearLieAlgebra)
+  basis = map(b -> change_base_ring(R, b), matrix_repr_basis(L))
+  return lie_algebra(R, L.n, basis, symbols(L); check=false)
+end
+
 ###############################################################################
 #
 #   Constructor
diff --git a/experimental/LieAlgebras/src/iso_oscar_gap.jl b/experimental/LieAlgebras/src/iso_oscar_gap.jl
index 106241ba0204..398b2fbcdc4f 100644
--- a/experimental/LieAlgebras/src/iso_oscar_gap.jl
+++ b/experimental/LieAlgebras/src/iso_oscar_gap.jl
@@ -90,7 +90,7 @@ function _iso_oscar_gap_set_root_system(LO::LieAlgebra, LG::GapObj, LO_to_LG::Fu
   GAP.Globals.SetPositiveRootVectors(RG, pos_root_vectorsG)
   GAP.Globals.SetNegativeRootVectors(RG, neg_root_vectorsG)
   GAP.Globals.SetCanonicalGenerators(
-    RG, GAP.Obj([pos_root_vectorsG, neg_root_vectorsG, csa_basisG])
+    RG, GAP.Obj([pos_root_vectorsG[1:rank(RO)], neg_root_vectorsG[1:rank(RO)], csa_basisG])
   )
   GAP.Globals.SetChevalleyBasis(
     LG, GAP.Obj([pos_root_vectorsG, neg_root_vectorsG, csa_basisG])
diff --git a/experimental/LieAlgebras/test/AbstractLieAlgebra-test.jl b/experimental/LieAlgebras/test/AbstractLieAlgebra-test.jl
index 8542f4349600..b4878b32267b 100644
--- a/experimental/LieAlgebras/test/AbstractLieAlgebra-test.jl
+++ b/experimental/LieAlgebras/test/AbstractLieAlgebra-test.jl
@@ -10,6 +10,105 @@
     return sc
   end
 
+  function G2_non_canonical_basis_struct_consts(R::Field)
+    # shared by Willem de Graaf on slack
+    tab = fill(sparse_row(R), 14, 14)
+    #! format: off
+    tab[1,3] = sparse_row(R, [3,8,10,11,13], [3,2,-4,-2,2])
+    tab[1,4] = sparse_row(R, [4,8,9,10,11,13], [5,4//3,-3,-8//3,8//3,-8//3])
+    tab[1,5] = sparse_row(R, [4,5,8,9], [-1,-2,1,1])
+    tab[1,6] = sparse_row(R, [4,5,6,8,9,10,11,13], [-1,-5,3,3,1,-1,1,-1])
+    tab[1,7] = sparse_row(R, [7,8,10,11,13], [-1,-2//3,4//3,-4//3,4//3])
+    tab[1,8] = sparse_row(R, [8,10,11,13], [-1//3,2//3,-2//3,2//3])
+    tab[1,9] = sparse_row(R, [4,8,9,10,11,13], [6,2,-4,-4,4,-4])
+    tab[1,10] = sparse_row(R, [8,10,11,13], [2//3,-4//3,-5//3,5//3])
+    tab[1,11] = sparse_row(R, [4,8,9,10,11,13], [1,4//3,-1,-8//3,-1//3,1//3])
+    tab[1,12] = sparse_row(R, [8,10,11,12,13], [2,-1,1,-3,-1])
+    tab[1,13] = sparse_row(R, [4,9], [1,-1])
+    tab[1,14] = sparse_row(R, [8,10,11,12,13,14], [2//3,5//3,7//3,-4,-7//3,1])
+    tab[2,3] = sparse_row(R, [3,8,10,11,13], [-2,-4//3,8//3,4//3,-4//3])
+    tab[2,4] = sparse_row(R, [4,8,9,10,11,13], [-2,-1//3,1,2//3,-2//3,2//3])
+    tab[2,5] = sparse_row(R, [4,5,8,9,10,11,13], [1,1,1//3,-1,-2//3,2//3,-2//3])
+    tab[2,6] = sparse_row(R, [4,5,6,8,9], [1,2,-1,-1,-1])
+    tab[2,7] = sparse_row(R, [7,8,10,11,13], [1,2//3,-4//3,4//3,-4//3])
+    tab[2,8] = sparse_row(R, [8,10,11,13], [5//3,-4//3,4//3,-4//3])
+    tab[2,9] = sparse_row(R, [4,8,9,10,11,13], [-2,-2//3,1,4//3,-4//3,4//3])
+    tab[2,10] = sparse_row(R, [8,10,11,13], [1//3,1//3,5//3,-5//3])
+    tab[2,11] = sparse_row(R, [4,8,9,10,11,13], [-1,-4//3,1,8//3,-2//3,-1//3])
+    tab[2,12] = sparse_row(R, [8,10,11,12,13], [-1//3,2//3,1//3,1,-1//3])
+    tab[2,13] = sparse_row(R, [4,8,9,10,11,13], [-1,-1//3,1,2//3,-2//3,-1//3])
+    tab[2,14] = sparse_row(R, [8,10,11,12,13], [1//3,-2//3,-1//3,1,1//3])
+    tab[3,4] = sparse_row(R, [4,5,7,8,9], [-1,-1,1//2,1//2,1])
+    tab[3,5] = sparse_row(R, [5,6,8,10,11,13], [2,-2,-2//3,-2//3,2//3,-2//3])
+    tab[3,6] = sparse_row(R, [10,11,13], [-1,1,-1])
+    tab[3,7] = sparse_row(R, [4,5,8,9,10,11,13], [7,-1,3,-3,-5,5,-5])
+    tab[3,8] = sparse_row(R, [4,5,6,8,9,10,11,13], [-1,3,-4,-7//3,1,5//3,-5//3,5//3])
+    tab[3,9] = sparse_row(R, [7,8,10,11,13], [1//2,1//6,-1//3,1//3,-1//3])
+    tab[3,10] = sparse_row(R, [2,4,5,6,8,9,10,11,13], [-2,-1,1,-2,-1,1,1,-1,1])
+    tab[3,11] = sparse_row(R, [2,8,10,11,12,13], [-2,-1//3,-1//3,-2//3,1,2//3])
+    tab[3,12] = sparse_row(R, [2,4,5,6,9,10,11], [-2,2,2,-2,-2,-2,2])
+    tab[3,14] = sparse_row(R, [4,5,6,9,10,11], [2,2,-2,-2,-2,2])
+    tab[4,5] = sparse_row(R, [1,4,7,8,9,10,11,12,13,14], [1,-1,-1//2,-3//2,1,6,-2,-4,-1,1])
+    tab[3,13] = sparse_row(R, [4,5,9,11,12,13], [-1,-1,1,-1,1,1])
+    tab[4,6] = sparse_row(R, [1,4,8,10,11,12,13], [1,1,-1//3,11//3,-2//3,-3,-7//3])
+    tab[4,7] = sparse_row(R, [1,2,3,4,5,8,9,10,11,12,13,14], [-2,-6,3//2,7,4,5//2,-7,-9,7//2,4,-1//2,-4])
+    tab[4,8] = sparse_row(R, [3,4,7,8,9,10,11,12,13,14], [3//2,3,-1,3//2,-3,-3,7//2,-2,-1//2,2])
+    tab[4,9] = sparse_row(R, [3,4,8,9,10,11,13], [-3//2,-6,-31//6,6,31//3,-53//6,17//6])
+    tab[4,10] = sparse_row(R, [3,4,5,7,8,9,10,11,12,13,14], [3//2,4,1,-1,3//2,-4,-4,7//2,-1,-1//2,1])
+    tab[4,11] = sparse_row(R, [4,5,7,8,9,10,11,13], [-2,1,-1//2,-19//6,2,16//3,-16//3,7//3])
+    tab[4,12] = sparse_row(R, [5,7,8,10,11,12,13,14], [1,-1,-4//3,5//3,-2//3,-1,2//3,1])
+    tab[4,13] = sparse_row(R, [3,8,10,11,13], [3//2,-1//6,1//3,-11//6,11//6])
+    tab[4,14] = sparse_row(R, [1,2,4,5,6,7,8,9,11,12,13,14], [-2,-3,3,3,-3,-1//2,-3//2,-1,1,-1,-1,1])
+    tab[5,6] = sparse_row(R, [4,7,8,9,10,11,12,13,14], [2,1//2,7//6,-1,-7//3,4//3,1,-4//3,-1])
+    tab[5,7] = sparse_row(R, [4,5,8,9,10,11,12,13,14], [5,4,7//3,-5,-26//3,5//3,1,4//3,-1])
+    tab[5,8] = sparse_row(R, [4,7,8,9,10,11,12,13,14], [1,-1,4//3,-1,-8//3,5//3,1,4//3,-1])
+    tab[5,9] = sparse_row(R, [1,7,8,10,11,12,13], [-1,1,-1,-4,-2,6,2])
+    tab[5,10] = sparse_row(R, [4,7,8,9,10,11,12,13,14], [1,-1//2,3//2,-1,-3,2,1,1,-1])
+    tab[5,11] = sparse_row(R, [1,2,7,8,10,11,12,13], [1,2,1//2,-5//6,-4//3,-5//3,3,5//3])
+    tab[5,12] = sparse_row(R, [7,8,10,11,13], [-1//2,1//6,-1//3,-2//3,2//3])
+    tab[5,13] = sparse_row(R, [1,2,4,7,8,9,10,11,12,13,14], [1,2,1,1//2,5//6,-1,-14//3,2//3,4,7//3,-1])
+    tab[5,14] = sparse_row(R, [1,2,7,8,10,11,13], [-2,-3,-3//2,-1//6,1//3,-4//3,4//3])
+    tab[6,7] = sparse_row(R, [4,5,8,9,10,11,13], [5,4,2,-5,-8,2,1])
+    tab[6,8] = sparse_row(R, [4,7,8,9,10,11,13], [1,-1,1,-1,-2,2,1])
+    tab[6,9] = sparse_row(R, [1,4,8,9,10,11,12,13], [-1,-4,-8//3,2,-2//3,-16//3,6,16//3])
+    tab[6,10] = sparse_row(R, [4,7,8,9,10,11,13], [1,-1//2,11//6,-1,-8//3,8//3,1//3])
+    tab[6,11] = sparse_row(R, [3,4,8,9,10,11,12,13], [-1//2,-2,-7//6,1,1//3,-17//6,3,17//6])
+    tab[6,12] = sparse_row(R, [1,2,7,8], [1,1,-1//2,1//2])
+    tab[6,13] = sparse_row(R, [3,4,8,10,11,12,13], [-1//2,-1,-1//2,-2,-1//2,3,7//2])
+    tab[6,14] = sparse_row(R, [1,2,7,8,10,11,13], [-1,-2,-3//2,-1//2,1,-1,1])
+    tab[7,8] = sparse_row(R, [1,2,8,10,11,12,13,14], [2,6,-2//3,4//3,2//3,-2,-2//3,2])
+    tab[7,9] = sparse_row(R, [3,4,5,8,9,10,11,12,13,14], [-3//2,-14,-8,-19//6,14,43//3,-53//6,-4,17//6,4])
+    tab[7,10] = sparse_row(R, [1,2,4,5,8,9,10,11,12,13,14], [2,6,1,1,-2//3,-1,1//3,2//3,-1,-2//3,1])
+    tab[7,11] = sparse_row(R, [4,5,8,9,10,11,13], [-4,-3,-1//3,4,11//3,-11//3,2//3])
+    tab[7,12] = sparse_row(R, [4,5,8,9,10,11,12,13,14], [1,1,-2//3,-1,1//3,2//3,-1,-2//3,1])
+    tab[7,13] = sparse_row(R, [1,2,4,5,9,10,11,13], [2,6,-5,-4,5,4,-4,1])
+    tab[7,14] = sparse_row(R, [4,8,9,10,11,12,13,14], [4,5,-2,-4,5,-1,-5,1])
+    tab[8,9] = sparse_row(R, [3,4,7,8,9,10,11,13], [-3//2,-6,2,-23//6,6,23//3,-37//6,1//6])
+    tab[8,10] = sparse_row(R, [4,5,9,10,12,14], [1,1,-1,-1,1,-1])
+    tab[8,11] = sparse_row(R, [1,2,4,5,7,8,9,10,11], [2,4,-2,1,1,-2,2,3,-3])
+    tab[8,12] = sparse_row(R, [4,5,9,10,12,14], [1,1,-1,-1,-1,1])
+    tab[8,13] = sparse_row(R, [1,2,4,7,8,9,10,11,12,13,14], [2,4,-3,1,-1,3,2,-4,2,1,-2])
+    tab[8,14] = sparse_row(R, [4,8,9,10,11,12,13,14], [4,5//3,-2,-10//3,7//3,-1,-7//3,1])
+    tab[9,10] = sparse_row(R, [3,4,7,8,9,10,11,13], [3//2,6,-3//2,4,-6,-8,13//2,-1//2])
+    tab[9,11] = sparse_row(R, [7,8,10,11,13], [-1//2,-5//6,5//3,-5//3,5//3])
+    tab[9,12] = sparse_row(R, [4,7,8,9,10,11,13], [-1,-3//2,-5//6,1,5//3,-5//3,5//3])
+    tab[9,13] = sparse_row(R, [3,4,8,9,10,11,13], [3//2,6,23//6,-6,-23//3,37//6,-1//6])
+    tab[9,14] = sparse_row(R, [1,2,4,5,6,7,8,9,10,11,13], [-4,-6,-1,3,-3,-1,-8//3,1,7//3,-7//3,7//3])
+    tab[10,11] = sparse_row(R, [1,2,4,5,7,8,9,10,11,12,13], [1,2,-2,1,1//2,-5//2,2,3,-4,1,1])
+    tab[10,12] = sparse_row(R, [4,5,8,9,10,11,12,13,14], [1,1,-1//3,-1,-1//3,1//3,-1,-1//3,1])
+    tab[10,13] = sparse_row(R, [1,2,4,5,7,8,9,10,11,12,13,14], [1,2,-4,-1,1//2,-3//2,4,3,-5,2,2,-1])
+    tab[10,14] = sparse_row(R, [4,8,9,10,11,12,13,14], [4,4//3,-2,-8//3,8//3,-1,-8//3,1])
+    tab[11,12] = sparse_row(R, [1,2,7,8,10,11,12,13], [-1,-2,-1//2,1//6,2//3,1//3,-1,-1//3])
+    tab[11,13] = sparse_row(R, [4,5,8,9,10,11,12], [2,-1,2,-2,-4,3,1])
+    tab[11,14] = sparse_row(R, [1,2,7,8,10,11,13], [-3,-5,-1//2,1//6,-1//3,1//3,-1//3])
+    tab[12,13] = sparse_row(R, [1,2,4,5,7,8,9,10,11,12,13,14], [1,2,-1,-1,1//2,1//2,1,-1,-1,2,1,-1])
+    tab[12,14] = sparse_row(R, [4,5,8,9,10,11,13], [1,1,-1//3,-1,-1//3,1//3,-1//3])
+    tab[13,14] = sparse_row(R, [1,2,4,7,8,9,10,11,12,13,14], [-3,-5,4,-1//2,7//6,-2,-7//3,10//3,-1,-10//3,1])
+    #! format: on
+    tab -= permutedims(tab)
+    return tab
+  end
+
   @testset "conformance tests" begin
     @testset "0-dim Lie algebra /QQ" begin
       L = lie_algebra(QQ, Matrix{sparse_row_type(QQFieldElem)}(undef, 0, 0), Symbol[])
@@ -75,6 +174,21 @@
         AbstractLieAlgebraElem{AbsSimpleNumFieldElem},
       )
     end
+
+    @testset "G_2(QQ) with non-canonical basis" begin
+      L = lie_algebra(QQ, G2_non_canonical_basis_struct_consts(QQ))
+      lie_algebra_conformance_test(
+        L, AbstractLieAlgebra{QQFieldElem}, AbstractLieAlgebraElem{QQFieldElem}
+      )
+    end
+
+    @testset "G_2(GF(137)) with non-canonical basis" begin
+      F = GF(137)
+      L = lie_algebra(F, G2_non_canonical_basis_struct_consts(F))
+      lie_algebra_conformance_test(
+        L, AbstractLieAlgebra{FqFieldElem}, AbstractLieAlgebraElem{FqFieldElem}
+      )
+    end
   end
 
   @testset "Lie algebra from root system" begin
diff --git a/experimental/LieAlgebras/test/setup_tests.jl b/experimental/LieAlgebras/test/setup_tests.jl
index 80dc1590266c..b89e699e0b37 100644
--- a/experimental/LieAlgebras/test/setup_tests.jl
+++ b/experimental/LieAlgebras/test/setup_tests.jl
@@ -113,8 +113,10 @@ if !isdefined(Main, :lie_algebra_conformance_test) || isinteractive()
 
     @testset "Root systems" begin
       if !(L isa DirectSumLieAlgebra) # TODO: make root_system work for DirectSumLieAlgebra
+        root_system_succeeded = false
         try
           root_system(L)
+          root_system_succeeded = true
         catch e
           e isa ArgumentError || rethrow()
           @test any(
@@ -122,17 +124,82 @@ if !isdefined(Main, :lie_algebra_conformance_test) || isinteractive()
             ["Killing form is degenerate", "Cartan subalgebra is not split"],
           )
         end
-        if has_root_system(L)
+        if root_system_succeeded
+          @test has_root_system(L)
           rs = root_system(L)
           @test rs isa RootSystem
-          @test dim(L) == n_roots(rs) + n_simple_roots(rs)
           chev = @inferred chevalley_basis(L)
           @test length(chev) == 3
-          @test length(chev[1]) == length(chev[2])
-          @test dim(L) == sum(length, chev; init=0)
+          es, fs, hs = chev
+          @test length(es) == length(fs) == n_positive_roots(rs)
+          @test length(hs) == rank(rs)
+
+          # Cartan subalgebra matches third Chevalles basis part
           H = cartan_subalgebra(L)
-          @test all(in(H), chev[3])
-          @test all(xs -> bracket(xs...) in H, zip(chev[1], chev[2]))
+          @test all(in(H), hs)
+
+          # Chevalley basis is a basis
+          @test length(es) + length(fs) + length(hs) == dim(L)
+          base_change_mat = matrix(
+            coefficient_ring(L),
+            permutedims(
+              reduce(
+                hcat,
+                [coefficients.(es); coefficients.(fs); coefficients.(hs)];
+                init=zeros(coefficient_ring(L), dim(L), 0),
+              ),
+            ),
+          )
+          @test is_invertible(base_change_mat)
+
+          # Chevalley basis properties (see [Hum72; Thm. 25.2])
+          # a) [h_i, h_j] = 0, for 1 <= i,j <= rank(L)
+          for hi in hs, hj in hs
+            @test is_zero(hi * hj)
+          end
+          # b) [h_i, x_a] = <a,a_i> x_a, for 1 <= i <= rank(L), a some root
+          for (alpha_i, hi) in zip(simple_roots(rs), hs),
+            (alpha, x_alpha) in zip(roots(rs), [es; fs])
+
+            @test hi * x_alpha ==
+              Int(2 * dot(alpha, alpha_i)//dot(alpha_i, alpha_i)) * x_alpha
+          end
+          # c) [x_a, x_-a] = h_a is a ZZ-linear combination of hs
+          # We test something stronger here, namely that the ZZ-coefficients of h_a in the hs
+          # are given by the coefficients of a^v in the simple coroots (and thus in particular integral)
+          for (alpha_v, e_alpha, f_alpha) in zip(positive_coroots(rs), es, fs)
+            @test e_alpha * f_alpha ==
+              sum(Int(c) * h for (c, h) in zip(coefficients(alpha_v), hs); init=zero(L))
+          end
+          # d) If a,b are independent roots, b-ra,...,b+qa the a-string through b,
+          #    then [x_a, x_b] = 0 if q = 0, while [x_a, x_b] = +- (r+1)x_{a+b} if a+b is a root
+          for (alpha, x_alpha) in zip(roots(rs), [es; fs]),
+            (beta, x_beta) in zip(roots(rs), [es; fs])
+
+            alpha == beta && continue
+            alpha == -beta && continue
+            r = 0
+            while is_root(beta - (r + 1) * alpha)
+              r += 1
+            end
+            q = 0
+            while is_root(beta + (q + 1) * alpha)
+              q += 1
+            end
+            if q == 0
+              @test is_zero(x_alpha * x_beta)
+            else # q >= 1
+              alpha_plus_beta = alpha + beta
+              if ((fl, k) = is_positive_root_with_index(alpha_plus_beta); fl)
+                rhs = (r + 1) * es[k]
+              else
+                fl, k = is_negative_root_with_index(alpha_plus_beta)
+                @assert fl # alpha_plus_beta is some root as q >= 1
+                rhs = (r + 1) * fs[k]
+              end
+              @test x_alpha * x_beta in (rhs, -rhs)
+            end
+          end
         end
       end
     end


Reviews:
user: login: fingolfin
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-09T00:59:27Z
user: login: fingolfin
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-09T01:01:52Z
user: login: lgoettgens
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-09T08:00:40Z
user: login: lgoettgens
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-09T08:01:17Z

Review comments:
diff_hunk: @@ -719,31 +718,177 @@ function _root_system_and_chevalley_basis(
   )
   type, ordering = cartan_type_with_ordering(cm; check=false)
   permute!(roots_simple, ordering)
-  R = root_system(type)
+  rs = root_system(type)
 
-  # compute a Chevalley basis of L
+  # compute conrete root vectors of L
   root_vectors = [
     begin
       concrete_root = sum(
-        c .* root_simple for
+        Int(c) .* root_simple for
         (c, root_simple) in zip(coefficients(abstract_root), roots_simple)
       )
       @assert concrete_root in roots
       root_vector = only(basis(root_spaces[concrete_root]))
-    end for abstract_root in Oscar.roots(R)
+    end for abstract_root in Oscar.roots(rs)
   ]
-  xs = root_vectors[1:n_positive_roots(R)]
-  ys = Vector{elem_type(L)}([
+
+  # scale the root vectors to get canonical generators, i.e.
+  # 1. [h_i,h_j] = 0            2. [x_i,y_j] = delta_ij * h_i
+  # 3. [h_i,x_j] = a_ij * x_j   4. [h_i,y_j] = -a_ij * y_j
+  canonical_gens_xs = root_vectors[1:n_simple_roots(rs)]
+  canonical_gens_ys = Vector{elem_type(L)}([
     begin
-      x = xs[i]
-      y = root_vectors[n_positive_roots(R) + i]
+      x = canonical_gens_xs[i]
+      y = root_vectors[n_positive_roots(rs) + i]
       y *= 2//only(solve(_matrix(x), _matrix(bracket(bracket(x, y), x)); side=:left))
       y
-    end for i in 1:n_positive_roots(R)
+    end for i in 1:n_simple_roots(rs)
   ])
-  hs = elem_type(L)[xs[i] * ys[i] for i in 1:n_simple_roots(R)]
+  canonical_gens_hs = elem_type(L)[
+    canonical_gens_xs[i] * canonical_gens_ys[i] for i in 1:n_simple_roots(rs)
+  ]
+
+  # Construct an automorphism `f` of `L` with `f(L_alpha) = L_-alpha`, and `f(h) = -h` for `h` in `H`.
+  # This is already defined by mapping `canonical_gens_xs` to `canonical_gens_ys` and vice versa.
+  # To save computation time, we only compute f on L_alpha for alpha a positive root
+  domain_basis = copy(canonical_gens_xs)
+  codomain_basis = copy(canonical_gens_ys)
+  for (i, alpha) in enumerate(positive_roots(rs))
+    i <= n_simple_roots(rs) && continue # already set above
+    j, k = 0, 0
+    found = false
+    for outer j in 1:rank(rs)
+      (fl, k) = is_positive_root_with_index(alpha - simple_root(rs, j))
+      if fl
+        found = true
+        break
+      end
+    end
+    @assert found
+    push!(domain_basis, canonical_gens_xs[j] * domain_basis[k])
+    push!(codomain_basis, canonical_gens_ys[j] * codomain_basis[k])
+  end
+
+  # For each positive root vector `x` we set `y = -f(x)`.
+  # We compute a scalar `coeff` such that `h = 2/coeff * [x,y]`.
+  # We need to multiply `x` and `y` by `sqrt(2/coeff)` to get elements of a Chevally basis.
+  pos_root_vectors = elem_type(L)[]
+  neg_root_vectors = elem_type(L)[]
+  scaled_cartan_elems = elem_type(L)[]
+  coeffs = C[]
+  for i in 1:n_positive_roots(rs)
+    x = root_vectors[i]
+    y = -only(solve(_matrix(domain_basis[i]), _matrix(x); side=:left)) * codomain_basis[i] # y = -f(x)
+    h = bracket(x, y)
+    coeff = only(solve(_matrix(x), _matrix(bracket(h, x)); side=:left))
+    if i <= n_simple_roots(rs)
+      push!(scaled_cartan_elems, (2//coeff) * h)
+    end
+    push!(coeffs, 2//coeff)
+    push!(pos_root_vectors, x)
+    push!(neg_root_vectors, y)
+  end
+
+  # In general, `coeffs` may not be squares in the coefficient field.
+  # Construct a field extension `F` where they are.
+  if all(is_square, coeffs)
+    F = R
+    coeffs_F_sqrt = sqrt.(coeffs)
+  else
+    F, coeffs_F_sqrt = _field_ext_with_sqrts(R, coeffs)
+  end
+
+  # Construct a Lie algebra `L_F` over `F` with the same structure constants as `L`,
+  # and construct the Chevalley basis of `L_F`. 
+  L_F = change_base_ring(F, L)
+  L_F_chev_basis_mat = reduce(vcat,
+    [
+      (coeff .* _matrix(rv) for (coeff, rv) in zip(coeffs_F_sqrt, pos_root_vectors))...,
+      (coeff .* _matrix(rv) for (coeff, rv) in zip(coeffs_F_sqrt, neg_root_vectors))...,
+      (F.(_matrix(h)) for h in scaled_cartan_elems)...,
+    ]; init=zero_matrix(F, 0, dim(L_F)))
+  # The structure constants of `L_F` w.r.t. the Chevalley basis lie in `R`,
+  # so we can construct the Lie algebra `K` over `R` with the these structure constants.
+  # The basis elements of `K` form a Chevalley basis.
+  K = lie_algebra(
+    R, map(e -> change_base_ring(R, e), _structure_constant_table(L_F, L_F_chev_basis_mat))
+  )
+
+  # Construct an isomorphism f from K to L.
+  # To save computation time, we only compute f on L_alpha for alpha a root.
+  domain_basis_xs = [basis(K, i) for i in 1:n_simple_roots(rs)]
+  domain_basis_ys = [basis(K, n_positive_roots(rs) + i) for i in 1:n_simple_roots(rs)]
+  codomain_basis_xs = copy(canonical_gens_xs)
+  codomain_basis_ys = copy(canonical_gens_ys)
+  for (i, alpha) in enumerate(positive_roots(rs))
+    i <= n_simple_roots(rs) && continue # already set above
+    j, k = 0, 0
+    found = false
+    for outer j in 1:rank(rs)
+      (fl, k) = is_positive_root_with_index(alpha - simple_root(rs, j))
+      if fl
+        found = true
+        break
+      end
+    end
+    @assert found
+    push!(domain_basis_xs, domain_basis_xs[j] * domain_basis_xs[k])
+    push!(domain_basis_ys, domain_basis_ys[j] * domain_basis_ys[k])
+    push!(codomain_basis_xs, codomain_basis_xs[j] * codomain_basis_xs[k])
+    push!(codomain_basis_ys, codomain_basis_ys[j] * codomain_basis_ys[k])
+  end
+
+  # The image of the Chevalley basis of K under f is a Chevalley basis of L.
+  xs = [
+    only(solve(_matrix(domain_basis_xs[i]), _matrix(basis(K, i)); side=:left)) *
+    codomain_basis_xs[i] for i in 1:n_positive_roots(rs)
+  ]
+  ys = [
+    only(
+      solve(
+        _matrix(domain_basis_ys[i]), _matrix(basis(K, n_positive_roots(rs) + i)); side=:left
+      ),
+    ) * codomain_basis_ys[i] for i in 1:n_positive_roots(rs)
+  ]
+
+  return rs, (xs, ys, scaled_cartan_elems)
+end
+
+# TODO: clean the following up, once we have a unified interface for field extensions
+function _field_ext_with_sqrts(F::QQField, elems::Vector{QQFieldElem})
+  i = findfirst(!is_square, elems)
+  if isnothing(i)
+    return F, sqrt.(elems)
+  end
+  Fx, x = polynomial_ring(F; cached=false)
+  F, _ = number_field(x^2 - elems[i]; cached=false)
+  return _field_ext_with_sqrts(F, [F(c) for c in elems])
+end
+
+function _field_ext_with_sqrts(F::NumField, elems::Vector{<:NumFieldElem})
+  @req all(e -> parent(e) === F, elems) "Incompatible number fields"
+  elems_F = elems
+  i = findfirst(!is_square, elems_F)
+  while !isnothing(i)
+    Fx, x = polynomial_ring(F; cached=false)
+    F, _ = number_field(x^2 - elems_F[i]; cached=false)
+    elems_F = [F(c) for c in elems_F]
+    i = findfirst(!is_square, elems_F)
+  end
+  return F, sqrt.(elems_F)
+end
 
-  return R, (xs, ys, hs)
+function _field_ext_with_sqrts(F::FqField, elems::Vector{<:FqFieldElem})
+  @req all(e -> parent(e) === F, elems) "Incompatible number fields"
path: experimental/LieAlgebras/src/LieAlgebra.jl
user: login: fingolfin
body: ```suggestion
  @req all(e -> parent(e) === F, elems) "Incompatible parent fields"
```
This is for finite fields after all.

But isn't the code identical to that for `NumFieldElem`? Perhaps merge the two methods into one then? (for `F::Field, elems::Vector{<:FieldElem}`)
created_at: 2025-03-09T00:59:27Z
author_association: MEMBER
diff_hunk: @@ -719,31 +718,177 @@ function _root_system_and_chevalley_basis(
   )
   type, ordering = cartan_type_with_ordering(cm; check=false)
   permute!(roots_simple, ordering)
-  R = root_system(type)
+  rs = root_system(type)
 
-  # compute a Chevalley basis of L
+  # compute conrete root vectors of L
   root_vectors = [
     begin
       concrete_root = sum(
-        c .* root_simple for
+        Int(c) .* root_simple for
         (c, root_simple) in zip(coefficients(abstract_root), roots_simple)
       )
       @assert concrete_root in roots
       root_vector = only(basis(root_spaces[concrete_root]))
-    end for abstract_root in Oscar.roots(R)
+    end for abstract_root in Oscar.roots(rs)
   ]
-  xs = root_vectors[1:n_positive_roots(R)]
-  ys = Vector{elem_type(L)}([
+
+  # scale the root vectors to get canonical generators, i.e.
+  # 1. [h_i,h_j] = 0            2. [x_i,y_j] = delta_ij * h_i
+  # 3. [h_i,x_j] = a_ij * x_j   4. [h_i,y_j] = -a_ij * y_j
+  canonical_gens_xs = root_vectors[1:n_simple_roots(rs)]
+  canonical_gens_ys = Vector{elem_type(L)}([
     begin
-      x = xs[i]
-      y = root_vectors[n_positive_roots(R) + i]
+      x = canonical_gens_xs[i]
+      y = root_vectors[n_positive_roots(rs) + i]
       y *= 2//only(solve(_matrix(x), _matrix(bracket(bracket(x, y), x)); side=:left))
       y
-    end for i in 1:n_positive_roots(R)
+    end for i in 1:n_simple_roots(rs)
   ])
-  hs = elem_type(L)[xs[i] * ys[i] for i in 1:n_simple_roots(R)]
+  canonical_gens_hs = elem_type(L)[
+    canonical_gens_xs[i] * canonical_gens_ys[i] for i in 1:n_simple_roots(rs)
+  ]
+
+  # Construct an automorphism `f` of `L` with `f(L_alpha) = L_-alpha`, and `f(h) = -h` for `h` in `H`.
+  # This is already defined by mapping `canonical_gens_xs` to `canonical_gens_ys` and vice versa.
+  # To save computation time, we only compute f on L_alpha for alpha a positive root
+  domain_basis = copy(canonical_gens_xs)
+  codomain_basis = copy(canonical_gens_ys)
+  for (i, alpha) in enumerate(positive_roots(rs))
+    i <= n_simple_roots(rs) && continue # already set above
+    j, k = 0, 0
+    found = false
+    for outer j in 1:rank(rs)
+      (fl, k) = is_positive_root_with_index(alpha - simple_root(rs, j))
+      if fl
+        found = true
+        break
+      end
+    end
+    @assert found
+    push!(domain_basis, canonical_gens_xs[j] * domain_basis[k])
+    push!(codomain_basis, canonical_gens_ys[j] * codomain_basis[k])
+  end
+
+  # For each positive root vector `x` we set `y = -f(x)`.
+  # We compute a scalar `coeff` such that `h = 2/coeff * [x,y]`.
+  # We need to multiply `x` and `y` by `sqrt(2/coeff)` to get elements of a Chevally basis.
+  pos_root_vectors = elem_type(L)[]
+  neg_root_vectors = elem_type(L)[]
+  scaled_cartan_elems = elem_type(L)[]
+  coeffs = C[]
+  for i in 1:n_positive_roots(rs)
+    x = root_vectors[i]
+    y = -only(solve(_matrix(domain_basis[i]), _matrix(x); side=:left)) * codomain_basis[i] # y = -f(x)
+    h = bracket(x, y)
+    coeff = only(solve(_matrix(x), _matrix(bracket(h, x)); side=:left))
+    if i <= n_simple_roots(rs)
+      push!(scaled_cartan_elems, (2//coeff) * h)
+    end
+    push!(coeffs, 2//coeff)
+    push!(pos_root_vectors, x)
+    push!(neg_root_vectors, y)
+  end
+
+  # In general, `coeffs` may not be squares in the coefficient field.
+  # Construct a field extension `F` where they are.
+  if all(is_square, coeffs)
+    F = R
+    coeffs_F_sqrt = sqrt.(coeffs)
+  else
+    F, coeffs_F_sqrt = _field_ext_with_sqrts(R, coeffs)
+  end
+
+  # Construct a Lie algebra `L_F` over `F` with the same structure constants as `L`,
+  # and construct the Chevalley basis of `L_F`. 
+  L_F = change_base_ring(F, L)
+  L_F_chev_basis_mat = reduce(vcat,
+    [
+      (coeff .* _matrix(rv) for (coeff, rv) in zip(coeffs_F_sqrt, pos_root_vectors))...,
+      (coeff .* _matrix(rv) for (coeff, rv) in zip(coeffs_F_sqrt, neg_root_vectors))...,
+      (F.(_matrix(h)) for h in scaled_cartan_elems)...,
+    ]; init=zero_matrix(F, 0, dim(L_F)))
+  # The structure constants of `L_F` w.r.t. the Chevalley basis lie in `R`,
+  # so we can construct the Lie algebra `K` over `R` with the these structure constants.
+  # The basis elements of `K` form a Chevalley basis.
+  K = lie_algebra(
+    R, map(e -> change_base_ring(R, e), _structure_constant_table(L_F, L_F_chev_basis_mat))
+  )
+
+  # Construct an isomorphism f from K to L.
+  # To save computation time, we only compute f on L_alpha for alpha a root.
+  domain_basis_xs = [basis(K, i) for i in 1:n_simple_roots(rs)]
+  domain_basis_ys = [basis(K, n_positive_roots(rs) + i) for i in 1:n_simple_roots(rs)]
+  codomain_basis_xs = copy(canonical_gens_xs)
+  codomain_basis_ys = copy(canonical_gens_ys)
+  for (i, alpha) in enumerate(positive_roots(rs))
+    i <= n_simple_roots(rs) && continue # already set above
+    j, k = 0, 0
+    found = false
+    for outer j in 1:rank(rs)
+      (fl, k) = is_positive_root_with_index(alpha - simple_root(rs, j))
+      if fl
+        found = true
+        break
+      end
+    end
+    @assert found
+    push!(domain_basis_xs, domain_basis_xs[j] * domain_basis_xs[k])
+    push!(domain_basis_ys, domain_basis_ys[j] * domain_basis_ys[k])
+    push!(codomain_basis_xs, codomain_basis_xs[j] * codomain_basis_xs[k])
+    push!(codomain_basis_ys, codomain_basis_ys[j] * codomain_basis_ys[k])
+  end
+
+  # The image of the Chevalley basis of K under f is a Chevalley basis of L.
+  xs = [
+    only(solve(_matrix(domain_basis_xs[i]), _matrix(basis(K, i)); side=:left)) *
+    codomain_basis_xs[i] for i in 1:n_positive_roots(rs)
+  ]
+  ys = [
+    only(
+      solve(
+        _matrix(domain_basis_ys[i]), _matrix(basis(K, n_positive_roots(rs) + i)); side=:left
+      ),
+    ) * codomain_basis_ys[i] for i in 1:n_positive_roots(rs)
+  ]
+
+  return rs, (xs, ys, scaled_cartan_elems)
+end
+
+# TODO: clean the following up, once we have a unified interface for field extensions
+function _field_ext_with_sqrts(F::QQField, elems::Vector{QQFieldElem})
+  i = findfirst(!is_square, elems)
+  if isnothing(i)
+    return F, sqrt.(elems)
+  end
+  Fx, x = polynomial_ring(F; cached=false)
+  F, _ = number_field(x^2 - elems[i]; cached=false)
+  return _field_ext_with_sqrts(F, [F(c) for c in elems])
+end
+
+function _field_ext_with_sqrts(F::NumField, elems::Vector{<:NumFieldElem})
+  @req all(e -> parent(e) === F, elems) "Incompatible number fields"
+  elems_F = elems
+  i = findfirst(!is_square, elems_F)
+  while !isnothing(i)
+    Fx, x = polynomial_ring(F; cached=false)
+    F, _ = number_field(x^2 - elems_F[i]; cached=false)
+    elems_F = [F(c) for c in elems_F]
+    i = findfirst(!is_square, elems_F)
+  end
+  return F, sqrt.(elems_F)
+end
 
-  return R, (xs, ys, hs)
+function _field_ext_with_sqrts(F::FqField, elems::Vector{<:FqFieldElem})
+  @req all(e -> parent(e) === F, elems) "Incompatible number fields"
path: experimental/LieAlgebras/src/LieAlgebra.jl
user: login: lgoettgens
body: Unfortunately not. The function doing the field extension itself is `number_field` or `finite_field`.
@thofma agreed with me on slack that these should eventually get a unified interface, but not right now and by me
created_at: 2025-03-09T08:00:39Z
author_association: MEMBER
diff_hunk: @@ -719,31 +718,177 @@ function _root_system_and_chevalley_basis(
   )
   type, ordering = cartan_type_with_ordering(cm; check=false)
   permute!(roots_simple, ordering)
-  R = root_system(type)
+  rs = root_system(type)
 
-  # compute a Chevalley basis of L
+  # compute conrete root vectors of L
   root_vectors = [
     begin
       concrete_root = sum(
-        c .* root_simple for
+        Int(c) .* root_simple for
         (c, root_simple) in zip(coefficients(abstract_root), roots_simple)
       )
       @assert concrete_root in roots
       root_vector = only(basis(root_spaces[concrete_root]))
-    end for abstract_root in Oscar.roots(R)
+    end for abstract_root in Oscar.roots(rs)
   ]
-  xs = root_vectors[1:n_positive_roots(R)]
-  ys = Vector{elem_type(L)}([
+
+  # scale the root vectors to get canonical generators, i.e.
+  # 1. [h_i,h_j] = 0            2. [x_i,y_j] = delta_ij * h_i
+  # 3. [h_i,x_j] = a_ij * x_j   4. [h_i,y_j] = -a_ij * y_j
+  canonical_gens_xs = root_vectors[1:n_simple_roots(rs)]
+  canonical_gens_ys = Vector{elem_type(L)}([
     begin
-      x = xs[i]
-      y = root_vectors[n_positive_roots(R) + i]
+      x = canonical_gens_xs[i]
+      y = root_vectors[n_positive_roots(rs) + i]
       y *= 2//only(solve(_matrix(x), _matrix(bracket(bracket(x, y), x)); side=:left))
       y
-    end for i in 1:n_positive_roots(R)
+    end for i in 1:n_simple_roots(rs)
   ])
-  hs = elem_type(L)[xs[i] * ys[i] for i in 1:n_simple_roots(R)]
+  canonical_gens_hs = elem_type(L)[
+    canonical_gens_xs[i] * canonical_gens_ys[i] for i in 1:n_simple_roots(rs)
+  ]
+
+  # Construct an automorphism `f` of `L` with `f(L_alpha) = L_-alpha`, and `f(h) = -h` for `h` in `H`.
+  # This is already defined by mapping `canonical_gens_xs` to `canonical_gens_ys` and vice versa.
+  # To save computation time, we only compute f on L_alpha for alpha a positive root
+  domain_basis = copy(canonical_gens_xs)
+  codomain_basis = copy(canonical_gens_ys)
+  for (i, alpha) in enumerate(positive_roots(rs))
+    i <= n_simple_roots(rs) && continue # already set above
+    j, k = 0, 0
+    found = false
+    for outer j in 1:rank(rs)
+      (fl, k) = is_positive_root_with_index(alpha - simple_root(rs, j))
+      if fl
+        found = true
+        break
+      end
+    end
+    @assert found
+    push!(domain_basis, canonical_gens_xs[j] * domain_basis[k])
+    push!(codomain_basis, canonical_gens_ys[j] * codomain_basis[k])
+  end
+
+  # For each positive root vector `x` we set `y = -f(x)`.
+  # We compute a scalar `coeff` such that `h = 2/coeff * [x,y]`.
+  # We need to multiply `x` and `y` by `sqrt(2/coeff)` to get elements of a Chevally basis.
+  pos_root_vectors = elem_type(L)[]
+  neg_root_vectors = elem_type(L)[]
+  scaled_cartan_elems = elem_type(L)[]
+  coeffs = C[]
+  for i in 1:n_positive_roots(rs)
+    x = root_vectors[i]
+    y = -only(solve(_matrix(domain_basis[i]), _matrix(x); side=:left)) * codomain_basis[i] # y = -f(x)
+    h = bracket(x, y)
+    coeff = only(solve(_matrix(x), _matrix(bracket(h, x)); side=:left))
+    if i <= n_simple_roots(rs)
+      push!(scaled_cartan_elems, (2//coeff) * h)
+    end
+    push!(coeffs, 2//coeff)
+    push!(pos_root_vectors, x)
+    push!(neg_root_vectors, y)
+  end
+
+  # In general, `coeffs` may not be squares in the coefficient field.
+  # Construct a field extension `F` where they are.
+  if all(is_square, coeffs)
+    F = R
+    coeffs_F_sqrt = sqrt.(coeffs)
+  else
+    F, coeffs_F_sqrt = _field_ext_with_sqrts(R, coeffs)
+  end
+
+  # Construct a Lie algebra `L_F` over `F` with the same structure constants as `L`,
+  # and construct the Chevalley basis of `L_F`. 
+  L_F = change_base_ring(F, L)
+  L_F_chev_basis_mat = reduce(vcat,
+    [
+      (coeff .* _matrix(rv) for (coeff, rv) in zip(coeffs_F_sqrt, pos_root_vectors))...,
+      (coeff .* _matrix(rv) for (coeff, rv) in zip(coeffs_F_sqrt, neg_root_vectors))...,
+      (F.(_matrix(h)) for h in scaled_cartan_elems)...,
+    ]; init=zero_matrix(F, 0, dim(L_F)))
+  # The structure constants of `L_F` w.r.t. the Chevalley basis lie in `R`,
+  # so we can construct the Lie algebra `K` over `R` with the these structure constants.
+  # The basis elements of `K` form a Chevalley basis.
+  K = lie_algebra(
+    R, map(e -> change_base_ring(R, e), _structure_constant_table(L_F, L_F_chev_basis_mat))
+  )
+
+  # Construct an isomorphism f from K to L.
+  # To save computation time, we only compute f on L_alpha for alpha a root.
+  domain_basis_xs = [basis(K, i) for i in 1:n_simple_roots(rs)]
+  domain_basis_ys = [basis(K, n_positive_roots(rs) + i) for i in 1:n_simple_roots(rs)]
+  codomain_basis_xs = copy(canonical_gens_xs)
+  codomain_basis_ys = copy(canonical_gens_ys)
+  for (i, alpha) in enumerate(positive_roots(rs))
+    i <= n_simple_roots(rs) && continue # already set above
+    j, k = 0, 0
+    found = false
+    for outer j in 1:rank(rs)
+      (fl, k) = is_positive_root_with_index(alpha - simple_root(rs, j))
+      if fl
+        found = true
+        break
+      end
+    end
+    @assert found
+    push!(domain_basis_xs, domain_basis_xs[j] * domain_basis_xs[k])
+    push!(domain_basis_ys, domain_basis_ys[j] * domain_basis_ys[k])
+    push!(codomain_basis_xs, codomain_basis_xs[j] * codomain_basis_xs[k])
+    push!(codomain_basis_ys, codomain_basis_ys[j] * codomain_basis_ys[k])
+  end
+
+  # The image of the Chevalley basis of K under f is a Chevalley basis of L.
+  xs = [
+    only(solve(_matrix(domain_basis_xs[i]), _matrix(basis(K, i)); side=:left)) *
+    codomain_basis_xs[i] for i in 1:n_positive_roots(rs)
+  ]
+  ys = [
+    only(
+      solve(
+        _matrix(domain_basis_ys[i]), _matrix(basis(K, n_positive_roots(rs) + i)); side=:left
+      ),
+    ) * codomain_basis_ys[i] for i in 1:n_positive_roots(rs)
+  ]
+
+  return rs, (xs, ys, scaled_cartan_elems)
+end
+
+# TODO: clean the following up, once we have a unified interface for field extensions
+function _field_ext_with_sqrts(F::QQField, elems::Vector{QQFieldElem})
+  i = findfirst(!is_square, elems)
+  if isnothing(i)
+    return F, sqrt.(elems)
+  end
+  Fx, x = polynomial_ring(F; cached=false)
+  F, _ = number_field(x^2 - elems[i]; cached=false)
+  return _field_ext_with_sqrts(F, [F(c) for c in elems])
+end
+
+function _field_ext_with_sqrts(F::NumField, elems::Vector{<:NumFieldElem})
+  @req all(e -> parent(e) === F, elems) "Incompatible number fields"
path: experimental/LieAlgebras/src/LieAlgebra.jl
user: login: lgoettgens
body: ```suggestion
  @req all(e -> parent(e) === F, elems) "Incompatible parent fields"
```
created_at: 2025-03-09T08:01:17Z
author_association: MEMBER

PR comments:
user: login: thofma
created_at: 2025-03-04T17:59:51Z
body: Should we combine/merge this with `structure_constant_table`?
author_association: COLLABORATOR
user: login: lgoettgens
created_at: 2025-03-04T18:01:46Z
body: > Should we combine/merge this with `structure_constant_table`?

It already does (https://github.com/oscar-system/Oscar.jl/blob/256b0b359be621b871a5bb3bacf9d90718ad84d2/experimental/LieAlgebras/src/LieAlgebras.jl#L84), I might have just added a typo in the function name
author_association: MEMBER
user: login: thofma
created_at: 2025-03-04T18:17:38Z
body: (I just looked at the body of the PR, but not at the code, sorry.)
author_association: COLLABORATOR
user: login: lgoettgens
created_at: 2025-03-07T13:19:39Z
body: @aaruni96 would this also be able to get backported? If you decide not to due to the amount of new functionality that I needed to add to allow for a proper implementation to fix the bug, that is fine for me as well.

And of course this first needs a review (bump @fingolfin), so please don't wait unnecessarily long with releasing 1.3.1 just because of this. If we do a 1.3.2 sometime, we can then consider this, or it would need to wait for 1.4.0
author_association: MEMBER
user: login: aaruni96
created_at: 2025-03-07T13:24:24Z
body: I am vary of adding new functionality to a patch release (if I understand correctly, major version bump = change whatever you want, point version bump = maintain backwards compatibility, patch version bump = bugfixes and very important changes only). 

Maybe if we break this PR out into two, one fixing the chevalley_basis, and another for the rest, we could add in the bugfix to 1.3.1 ?

I am of course happy to defer to @benlorenz's judgement.
author_association: MEMBER
user: login: lgoettgens
created_at: 2025-03-07T13:30:24Z
body: > I am vary of adding new functionality to a patch release (if I understand correctly, major version bump = change whatever you want, point version bump = maintain backwards compatibility, patch version bump = bugfixes and very important changes only).
> 
> Maybe if we break this PR out into two, one fixing the chevalley_basis, and another for the rest, we could add in the bugfix to 1.3.1 ?

Unfortunately, there is no rest. The new functions are an essential part of the fix, but since they also seem to be useful on their own, I put them into their own functions.

The only split up I could think of is the following:
1. The fix, the corresponding tests, including all the helper functions (but rename the helper functions by prepending a `_`)
2. Remove the added `_` from the names, thus making these helper functions "public"

But I am not really sure how useful this even is.

author_association: MEMBER
user: login: aaruni96
created_at: 2025-03-07T13:40:16Z
body: If adding functionality is a side effect of a bug fix, then, sure I guess (modulo approval from @fingolfin or so).
author_association: MEMBER
user: login: benlorenz
created_at: 2025-03-07T13:47:53Z
body: I think it is fine to add to 1.3.1.

This is also part of experimental so there are less strict rules anyway.
author_association: MEMBER