]> gitweb.michael.orlitzky.com - sage.d.git/commitdiff
mjo/clan/unital_clan.py: VinbergClan inits and passes tests
authorMichael Orlitzky <michael@orlitzky.com>
Sun, 15 Feb 2026 02:42:04 +0000 (21:42 -0500)
committerMichael Orlitzky <michael@orlitzky.com>
Sun, 15 Feb 2026 02:42:04 +0000 (21:42 -0500)
mjo/clan/unital_clan.py

index 0e497bbadfe725ce561eb319207f8e8c4d10098c..63fb737f47d99baf93e28c2a2815bdadba2faa08 100644 (file)
@@ -534,6 +534,7 @@ class HnClan(MatrixClan):
         """
         return f"Clan H^{self.rank()} over {self.base_ring()}"
 
+
 class VinbergClan(NormalDecomposition):
     r"""
     The clan corresponding to the Vinberg cone (as defined by Ishi).
@@ -556,6 +557,67 @@ class VinbergClan(NormalDecomposition):
     real numbers, and provide lifts to the matrix representation. The
     clan product is in terms of the up-hat and down-hat that is
     defined on `S^{2}.
+
+    Following Gikdikin (p. 91), the basis for this clan is
+
+      e[(0,0,1)] = [1,0], [1,0]
+                   [0,0]  [0,0]
+
+      e[(1,0,1)] = [0,1], [0,0]
+                   [1,0]  [0,0]
+
+      e[(1,1,1)] = [0,0], [0,0]
+                   [0,1]  [0,0]
+
+      e[(2,0,1)] = [0,0], [0,1]
+                   [0,0]  [1,0]
+
+      e[(2,1,1)] = nonexistent, component is trivial
+
+      e[(2,2,1)] = [0,0], [0,0]
+                   [0,0]  [0,1]
+
+    Check the unit element::
+
+        sage: C = VinbergClan()
+        sage: x = C.an_element()
+        sage: x*C.one() == x
+        True
+        sage: C.one()*x == x
+        True
+
+    Verifying the axioms::
+
+        sage: C = VinbergClan()
+        sage: e = C.basis()
+        sage: all( e[i,i,1]*e[i,i,1] == e[i,i,1] for i in range(3) )
+        True
+        sage: all( e[i,i,1]*e[j,i,1] == e[j,i,1]/2
+        ....:      for i in range(3)
+        ....:      for j in range(i+1,3)
+        ....:      if e.has_key((j,i,1)))
+        True
+        sage: all( e[i,i,1]*e[i,k,1] == e[i,k,1]/2
+        ....:      for i in range(2)
+        ....:      for k in range(i))
+        True
+        sage: all( (e[i,i,1]*e[j,k,1]).is_zero()
+        ....:      for i in range(2)
+        ....:      for j in range(2)
+        ....:      for k in range(j+1)
+        ....:      if not i in [j,k] )
+        True
+        sage: all( (e[i,k,1]*e[i,i,1]).is_zero()
+        ....:      for i in range(2)
+        ....:      for k in range(i) )
+        True
+        sage: all( (e[j,k,1]*e[i,i,1]).is_zero()
+        ....:      for i in range(2)
+        ....:      for j in range(2)
+        ....:      for k in range(j)
+        ....:      if not i in [j,k] )
+        True
+
     """
     def __init__(self, scalar_field=QQ, **kwargs):
         from sage.matrix.matrix_space import MatrixSpace
@@ -579,12 +641,15 @@ class VinbergClan(NormalDecomposition):
                                       ambient=M2)
 
         # We need an Ishi basis (i,j,k) for R^5 if we want to use
-        # NormalDecomposition.
+        # NormalDecomposition. We imagine the (2,1,1) component
+        # being trivial to keep the "triangle" intact. We are
+        # example (f) on page 91 of Gindikin where we can cheat
+        # and see the normal decomposition.
         R5 = VectorSpace(scalar_field, [
             (0,0,1),
             (1,0,1),
             (1,1,1),
-            (2,1,1),
+            (2,0,1),
             (2,2,1)
         ])
 
@@ -593,7 +658,7 @@ class VinbergClan(NormalDecomposition):
             return ( A[(0,0,1)]*R5((0,0,1)) +
                      A[(1,0,1)]*R5((1,0,1)) +
                      A[(1,1,1)]*R5((1,1,1)) +
-                     B[(1,0,1)]*R5((2,1,1)) +
+                     B[(1,0,1)]*R5((2,0,1)) +
                      B[(1,1,1)]*R5((2,2,1)) )
 
         def lift(v):
@@ -601,16 +666,23 @@ class VinbergClan(NormalDecomposition):
                    self._S2((1,0,1))*v[(1,0,1)] +
                    self._S2((1,1,1))*v[(1,1,1)] )
             M2 = ( self._S2((0,0,1))*v[(0,0,1)] +
-                   self._S2((1,0,1))*v[(2,1,1)] +
+                   self._S2((1,0,1))*v[(2,0,1)] +
                    self._S2((1,1,1))*v[(2,2,1)] )
             return (M1,M2)
 
         def cp(x,y):
+            # up_hat and down_hat need MatrixSpace elements, not
+            # submodules with custom bases, so we need to lift
+            # everything twice.
             X1,X2 = lift(x)
             Y1,Y2 = lift(y)
+            X1 = X1.lift()
+            X2 = X2.lift()
+            Y1 = Y1.lift()
+            Y2 = Y2.lift()
             Z1 = MatrixClan._down_hat(X1)*Y1 + Y1*MatrixClan._up_hat(X1)
             Z2 = MatrixClan._down_hat(X2)*Y2 + Y2*MatrixClan._up_hat(X2)
-            return unlift(Z1,Z2)
+            return unlift((self._S2(Z1), self._S2(Z2)))
 
         def ip(x,y):
             two = x.base_ring()(2)