pyw-algebra 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyw/__init__.py +30 -0
- pyw/algorithms/__init__.py +11 -0
- pyw/algorithms/nilpotent_orbits.py +667 -0
- pyw/core/__init__.py +88 -0
- pyw/core/affine_lie_algebra.py +1908 -0
- pyw/core/affine_weight.py +1358 -0
- pyw/core/bruhat.py +690 -0
- pyw/core/character.py +1496 -0
- pyw/core/hybrid_affine_character.py +820 -0
- pyw/core/kazhdan_lusztig.py +1358 -0
- pyw/core/root_system.py +292 -0
- pyw/core/weight_space.py +193 -0
- pyw/core/weyl_group.py +1607 -0
- pyw/embedding/__init__.py +19 -0
- pyw/embedding/sl2_triple.py +812 -0
- pyw/fractional/__init__.py +17 -0
- pyw/fractional/admissible.py +188 -0
- pyw/fractional/boundary_admissible.py +302 -0
- pyw/fractional/level.py +255 -0
- pyw/fractional/nondegenerate.py +279 -0
- pyw/fractional/principal.py +492 -0
- pyw/fractional/translation_conventions.py +307 -0
- pyw/fractional/verify_sl2_boundary.py +217 -0
- pyw/utils/__init__.py +32 -0
- pyw/utils/predicates.py +400 -0
- pyw/utils/theta_functions.py +286 -0
- pyw_algebra-0.1.0.dist-info/METADATA +220 -0
- pyw_algebra-0.1.0.dist-info/RECORD +31 -0
- pyw_algebra-0.1.0.dist-info/WHEEL +5 -0
- pyw_algebra-0.1.0.dist-info/licenses/LICENSE +21 -0
- pyw_algebra-0.1.0.dist-info/top_level.txt +1 -0
pyw/__init__.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pyw - Root System & Affine Lie Algebra Computation Library
|
|
3
|
+
|
|
4
|
+
A Python library for computing with root systems and affine Lie algebras,
|
|
5
|
+
with special support for fractional levels/weights. W-algebra constructions
|
|
6
|
+
are a future goal, not a currently available module.
|
|
7
|
+
|
|
8
|
+
Based on SageMath's root system and Lie algebra implementations.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
__version__ = "0.1.0"
|
|
12
|
+
__author__ = "pyw contributors"
|
|
13
|
+
|
|
14
|
+
from pyw.core.root_system import AffineRootSystem
|
|
15
|
+
from pyw.core.weyl_group import AffineWeylGroup
|
|
16
|
+
from pyw.core.weight_space import FractionalWeightSpace
|
|
17
|
+
from pyw.fractional.level import FractionalLevel
|
|
18
|
+
from pyw.fractional.admissible import AdmissibleWeight
|
|
19
|
+
from pyw.fractional.principal import PrincipalAdmissibleWeight
|
|
20
|
+
from pyw.fractional.nondegenerate import NondegenerateChecker
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"AffineRootSystem",
|
|
24
|
+
"AffineWeylGroup",
|
|
25
|
+
"FractionalWeightSpace",
|
|
26
|
+
"FractionalLevel",
|
|
27
|
+
"AdmissibleWeight",
|
|
28
|
+
"PrincipalAdmissibleWeight",
|
|
29
|
+
"NondegenerateChecker",
|
|
30
|
+
]
|
|
@@ -0,0 +1,667 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Nilpotent Orbits of Classical Lie Algebras
|
|
3
|
+
|
|
4
|
+
This module enumerates nilpotent orbits of finite-dimensional simple Lie algebras
|
|
5
|
+
of classical type (A, B, C, D) via the partition correspondence.
|
|
6
|
+
|
|
7
|
+
Classification (Collingwood-McGovern, Chapter 5):
|
|
8
|
+
|
|
9
|
+
Type A_n (sl(n+1)): Partitions of n+1
|
|
10
|
+
Type B_n (so(2n+1)): Partitions of 2n+1 where even parts have even multiplicity
|
|
11
|
+
Type C_n (sp(2n)): Partitions of 2n where odd parts have even multiplicity
|
|
12
|
+
Type D_n (so(2n)): Partitions of 2n where even parts have even multiplicity
|
|
13
|
+
(very even partitions correspond to TWO orbits)
|
|
14
|
+
|
|
15
|
+
For each orbit the module computes:
|
|
16
|
+
- Partition label λ
|
|
17
|
+
- Orbit dimension = dim G - dim Z_G(e)
|
|
18
|
+
- Dual partition λ^t (transpose / Barbasch-Vogan dual for type A)
|
|
19
|
+
- Whether the orbit is Richardson, even, or distinguished
|
|
20
|
+
|
|
21
|
+
References:
|
|
22
|
+
- Collingwood, D. H., McGovern, W. M.
|
|
23
|
+
"Nilpotent Orbits in Semisimple Lie Algebras" (1993)
|
|
24
|
+
- Carter, R. W. "Finite Groups of Lie Type" (1985)
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
from collections import Counter
|
|
30
|
+
from dataclasses import dataclass, field
|
|
31
|
+
from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
|
|
32
|
+
|
|
33
|
+
from sage.all import Partitions
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
# Partition helpers
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _transpose_partition(p: List[int]) -> List[int]:
|
|
42
|
+
"""Return the conjugate (transpose) partition of p.
|
|
43
|
+
|
|
44
|
+
>>> _transpose_partition([4, 2, 1])
|
|
45
|
+
[3, 2, 1, 1]
|
|
46
|
+
"""
|
|
47
|
+
if not p:
|
|
48
|
+
return []
|
|
49
|
+
result: List[int] = []
|
|
50
|
+
for i in range(1, p[0] + 1):
|
|
51
|
+
result.append(sum(1 for part in p if part >= i))
|
|
52
|
+
return result
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _multiplicity_map(p: List[int]) -> Dict[int, int]:
|
|
56
|
+
"""Return {part: multiplicity} for the partition *p*."""
|
|
57
|
+
return dict(Counter(p))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
# Partition filters for classical types
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _is_type_B_partition(p: List[int]) -> bool:
|
|
66
|
+
"""Partition of 2n+1 for so(2n+1): even parts must occur with even multiplicity."""
|
|
67
|
+
mult = _multiplicity_map(p)
|
|
68
|
+
return all(mult[k] % 2 == 0 for k in mult if k % 2 == 0)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _is_type_C_partition(p: List[int]) -> bool:
|
|
72
|
+
"""Partition of 2n for sp(2n): odd parts must occur with even multiplicity."""
|
|
73
|
+
mult = _multiplicity_map(p)
|
|
74
|
+
return all(mult[k] % 2 == 0 for k in mult if k % 2 == 1)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _is_type_D_partition(p: List[int]) -> bool:
|
|
78
|
+
"""Partition of 2n for so(2n): even parts must occur with even multiplicity."""
|
|
79
|
+
mult = _multiplicity_map(p)
|
|
80
|
+
return all(mult[k] % 2 == 0 for k in mult if k % 2 == 0)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _is_very_even(p: List[int]) -> bool:
|
|
84
|
+
"""A partition is *very even* if every part is even (only relevant for type D)."""
|
|
85
|
+
return all(part % 2 == 0 for part in p)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# ---------------------------------------------------------------------------
|
|
89
|
+
# Orbit dimension formulas
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _orbit_dim_A(p: List[int]) -> int:
|
|
94
|
+
r"""Dimension of the nilpotent orbit labelled by *p* in sl(N).
|
|
95
|
+
|
|
96
|
+
$$
|
|
97
|
+
\dim \mathcal{O}_\lambda = N^2 - \sum_i (\lambda_i^t)^2
|
|
98
|
+
$$
|
|
99
|
+
|
|
100
|
+
where $\lambda^t$ is the transpose partition.
|
|
101
|
+
"""
|
|
102
|
+
n = sum(p)
|
|
103
|
+
pt = _transpose_partition(p)
|
|
104
|
+
return n * n - sum(x * x for x in pt)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _orbit_dim_B(p: List[int]) -> int:
|
|
108
|
+
r"""Dimension of the nilpotent orbit labelled by *p* in so(2n+1).
|
|
109
|
+
|
|
110
|
+
$$
|
|
111
|
+
\dim \mathcal{O}_\lambda
|
|
112
|
+
= \frac{N(N-1)}{2} - \frac{1}{2}\Bigl(\sum_i (\lambda_i^t)^2 - |\{j : \lambda_j \text{ odd}\}|\Bigr)
|
|
113
|
+
$$
|
|
114
|
+
|
|
115
|
+
with $N = 2n+1$.
|
|
116
|
+
"""
|
|
117
|
+
N = sum(p)
|
|
118
|
+
pt = _transpose_partition(p)
|
|
119
|
+
num_odd_parts = sum(1 for x in p if x % 2 == 1)
|
|
120
|
+
# dim so(N) = N(N-1)/2
|
|
121
|
+
dim_g = N * (N - 1) // 2
|
|
122
|
+
centralizer_dim = (sum(x * x for x in pt) - num_odd_parts) // 2
|
|
123
|
+
return dim_g - centralizer_dim
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _orbit_dim_C(p: List[int]) -> int:
|
|
127
|
+
r"""Dimension of the nilpotent orbit labelled by *p* in sp(2n).
|
|
128
|
+
|
|
129
|
+
$$
|
|
130
|
+
\dim \mathcal{O}_\lambda
|
|
131
|
+
= \frac{N(N+1)}{2} - \frac{1}{2}\Bigl(\sum_i (\lambda_i^t)^2 + |\{j : \lambda_j \text{ odd}\}|\Bigr)
|
|
132
|
+
$$
|
|
133
|
+
|
|
134
|
+
with $N = 2n$.
|
|
135
|
+
"""
|
|
136
|
+
N = sum(p)
|
|
137
|
+
pt = _transpose_partition(p)
|
|
138
|
+
num_odd_parts = sum(1 for x in p if x % 2 == 1)
|
|
139
|
+
# dim sp(N) = N(N+1)/2
|
|
140
|
+
dim_g = N * (N + 1) // 2
|
|
141
|
+
centralizer_dim = (sum(x * x for x in pt) + num_odd_parts) // 2
|
|
142
|
+
return dim_g - centralizer_dim
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _orbit_dim_D(p: List[int]) -> int:
|
|
146
|
+
r"""Dimension of the nilpotent orbit labelled by *p* in so(2n).
|
|
147
|
+
|
|
148
|
+
Same formula as type B.
|
|
149
|
+
"""
|
|
150
|
+
N = sum(p)
|
|
151
|
+
pt = _transpose_partition(p)
|
|
152
|
+
num_odd_parts = sum(1 for x in p if x % 2 == 1)
|
|
153
|
+
dim_g = N * (N - 1) // 2
|
|
154
|
+
centralizer_dim = (sum(x * x for x in pt) - num_odd_parts) // 2
|
|
155
|
+
return dim_g - centralizer_dim
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
# ---------------------------------------------------------------------------
|
|
159
|
+
# Data class for a single orbit
|
|
160
|
+
# ---------------------------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@dataclass
|
|
164
|
+
class NilpotentOrbit:
|
|
165
|
+
"""A nilpotent orbit in a classical Lie algebra.
|
|
166
|
+
|
|
167
|
+
Attributes:
|
|
168
|
+
lie_type: Cartan type, e.g. ('A', 4) or ('D', 6).
|
|
169
|
+
partition: The partition labelling this orbit.
|
|
170
|
+
dimension: Complex dimension of the orbit.
|
|
171
|
+
transpose: The transpose (conjugate) partition.
|
|
172
|
+
is_even: True if all parts of the transpose partition are even.
|
|
173
|
+
is_distinguished: True if no part of the partition repeats
|
|
174
|
+
more than once (all multiplicities ≤ 1) for type A;
|
|
175
|
+
for BCD see Collingwood-McGovern §6.
|
|
176
|
+
is_very_even: (Type D only) True if every part is even.
|
|
177
|
+
roman_numeral: For type D very-even partitions: 'I' or 'II'.
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
lie_type: Tuple[str, int]
|
|
181
|
+
partition: List[int]
|
|
182
|
+
dimension: int
|
|
183
|
+
transpose: List[int] = field(default_factory=list)
|
|
184
|
+
is_even: bool = False
|
|
185
|
+
is_distinguished: bool = False
|
|
186
|
+
is_very_even: bool = False
|
|
187
|
+
roman_numeral: Optional[str] = None
|
|
188
|
+
|
|
189
|
+
# Convenient label
|
|
190
|
+
@property
|
|
191
|
+
def label(self) -> str:
|
|
192
|
+
base = str(self.partition)
|
|
193
|
+
if self.roman_numeral is not None:
|
|
194
|
+
base += f" ({self.roman_numeral})"
|
|
195
|
+
return base
|
|
196
|
+
|
|
197
|
+
def weighted_dynkin_diagram(self) -> List[int]:
|
|
198
|
+
r"""Compute the weighted Dynkin diagram for this orbit.
|
|
199
|
+
|
|
200
|
+
Returns the labels `(d_1, \dots, d_n)` such that
|
|
201
|
+
`\alpha_i(h) = d_i` for the semisimple element `h`
|
|
202
|
+
of the corresponding `\mathfrak{sl}_2`-triple.
|
|
203
|
+
|
|
204
|
+
Returns
|
|
205
|
+
-------
|
|
206
|
+
list of int
|
|
207
|
+
Non-negative integer labels, each in `\{0, 1, 2\}`.
|
|
208
|
+
|
|
209
|
+
Examples
|
|
210
|
+
--------
|
|
211
|
+
>>> orb = nilpotent_orbits('A', 3)[-2] # partition [3,1]
|
|
212
|
+
>>> orb.weighted_dynkin_diagram()
|
|
213
|
+
[2, 0, 2]
|
|
214
|
+
"""
|
|
215
|
+
from pyw.embedding.sl2_triple import weighted_dynkin_diagram as _wdd
|
|
216
|
+
|
|
217
|
+
return _wdd(self.lie_type, self.partition)
|
|
218
|
+
|
|
219
|
+
def sl2_triple(self):
|
|
220
|
+
r"""Construct the `\mathfrak{sl}_2`-triple `(h, e, f)` for this orbit.
|
|
221
|
+
|
|
222
|
+
Returns an :class:`~pyw.embedding.sl2_triple.SL2Triple` object
|
|
223
|
+
containing the Chevalley-basis elements `h, e, f` satisfying
|
|
224
|
+
|
|
225
|
+
$$[h, e] = 2e, \quad [h, f] = -2f, \quad [e, f] = h.$$
|
|
226
|
+
|
|
227
|
+
Returns
|
|
228
|
+
-------
|
|
229
|
+
SL2Triple
|
|
230
|
+
The `\mathfrak{sl}_2`-triple for this nilpotent orbit.
|
|
231
|
+
|
|
232
|
+
Examples
|
|
233
|
+
--------
|
|
234
|
+
>>> orb = nilpotent_orbits('A', 3)[-1] # principal orbit [4]
|
|
235
|
+
>>> triple = orb.sl2_triple()
|
|
236
|
+
>>> triple.verify()
|
|
237
|
+
True
|
|
238
|
+
"""
|
|
239
|
+
from pyw.embedding.sl2_triple import compute_sl2_triple
|
|
240
|
+
|
|
241
|
+
return compute_sl2_triple(self.lie_type, self.partition)
|
|
242
|
+
|
|
243
|
+
def sl2_triplet_regular_in_standard_levi(self):
|
|
244
|
+
r"""Construct a triple with `f'` regular in a standard Levi.
|
|
245
|
+
|
|
246
|
+
Returns an object carrying the conjugate-style representatives
|
|
247
|
+
``hprime, eprime, fprime`` and ``xprime = hprime/2``. The standard Levi
|
|
248
|
+
is determined from the Bala-Carter Levi attached to this orbit and then
|
|
249
|
+
moved to the ambient dominant chamber.
|
|
250
|
+
"""
|
|
251
|
+
from pyw.embedding.sl2_triple import compute_standard_levi_regular_sl2_triple
|
|
252
|
+
|
|
253
|
+
return compute_standard_levi_regular_sl2_triple(self.lie_type, self.partition)
|
|
254
|
+
|
|
255
|
+
def root_grading(self):
|
|
256
|
+
r"""Compute the root space grading `\Delta = \bigcup_j \Delta_j`.
|
|
257
|
+
|
|
258
|
+
For the semisimple element `h` of the `\mathfrak{sl}_2`-triple,
|
|
259
|
+
the roots are decomposed by their eigenvalue under `\mathrm{ad}(h)`:
|
|
260
|
+
|
|
261
|
+
$$\Delta_j = \{\alpha \in \Delta \mid [h, E^\alpha] = j\, E^\alpha\}.$$
|
|
262
|
+
|
|
263
|
+
Returns
|
|
264
|
+
-------
|
|
265
|
+
RootSpaceGrading
|
|
266
|
+
Object with ``grading[j]`` giving the roots in `\Delta_j`.
|
|
267
|
+
|
|
268
|
+
Examples
|
|
269
|
+
--------
|
|
270
|
+
>>> orb = nilpotent_orbits('A', 3)[-1] # principal orbit
|
|
271
|
+
>>> rg = orb.root_grading()
|
|
272
|
+
>>> rg.grades
|
|
273
|
+
[-2, 2]
|
|
274
|
+
"""
|
|
275
|
+
from pyw.embedding.sl2_triple import root_space_grading
|
|
276
|
+
|
|
277
|
+
wdd = self.weighted_dynkin_diagram()
|
|
278
|
+
return root_space_grading(self.lie_type, wdd)
|
|
279
|
+
|
|
280
|
+
def levi_subalgebra(self) -> LeviSubalgebra:
|
|
281
|
+
r"""Return the Levi subalgebra corresponding to this orbit.
|
|
282
|
+
|
|
283
|
+
The Levi is computed from an ``sl_2``-triple by
|
|
284
|
+
|
|
285
|
+
$$
|
|
286
|
+
\mathfrak{h}^f = \{h \in \mathfrak{h} \mid [h,f]=0\},
|
|
287
|
+
\qquad
|
|
288
|
+
\Delta_\mathfrak{l} = \{\alpha \in \Delta \mid \alpha|_{\mathfrak{h}^f}=0\}.
|
|
289
|
+
$$
|
|
290
|
+
|
|
291
|
+
Returns
|
|
292
|
+
-------
|
|
293
|
+
LeviSubalgebra
|
|
294
|
+
Levi descriptor with root subsystem ``Delta_l``.
|
|
295
|
+
"""
|
|
296
|
+
triple = self.sl2_triple()
|
|
297
|
+
return LeviSubalgebra(lie_type=self.lie_type, roots=triple.delta_l())
|
|
298
|
+
|
|
299
|
+
def regular_levi(self) -> LeviSubalgebra:
|
|
300
|
+
"""Return the canonical Levi attached to this orbit."""
|
|
301
|
+
return self.levi_subalgebra()
|
|
302
|
+
|
|
303
|
+
def is_regular_in(self, levi: Optional[LeviSubalgebra] = None) -> bool:
|
|
304
|
+
r"""Check whether this orbit is regular in a chosen Levi subalgebra.
|
|
305
|
+
|
|
306
|
+
Parameters
|
|
307
|
+
----------
|
|
308
|
+
levi : LeviSubalgebra, optional
|
|
309
|
+
Levi to test against. If omitted, use orbit's corresponding Levi.
|
|
310
|
+
|
|
311
|
+
Returns
|
|
312
|
+
-------
|
|
313
|
+
bool
|
|
314
|
+
``True`` iff a representative ``f`` of this orbit is regular in
|
|
315
|
+
the Levi, i.e. ``dim Z_l(f) = rank(l)``.
|
|
316
|
+
"""
|
|
317
|
+
triple = self.sl2_triple()
|
|
318
|
+
if levi is None:
|
|
319
|
+
levi = self.regular_levi()
|
|
320
|
+
if tuple(levi.lie_type) != tuple(self.lie_type):
|
|
321
|
+
raise ValueError(f"Levi type {levi.lie_type} does not match orbit type {self.lie_type}")
|
|
322
|
+
return levi.is_regular_element(triple.f)
|
|
323
|
+
|
|
324
|
+
def is_regular(
|
|
325
|
+
self, levi_subalgebra: Optional[Union[LeviSubalgebra, Sequence[Any]]] = None
|
|
326
|
+
) -> bool:
|
|
327
|
+
"""Alias of :meth:`is_regular_in` for concise API usage."""
|
|
328
|
+
|
|
329
|
+
if levi_subalgebra is None:
|
|
330
|
+
return self.is_regular_in()
|
|
331
|
+
if isinstance(levi_subalgebra, LeviSubalgebra):
|
|
332
|
+
return self.is_regular_in(levi=levi_subalgebra)
|
|
333
|
+
return self.is_regular_in(
|
|
334
|
+
levi=LeviSubalgebra(lie_type=self.lie_type, roots=list(levi_subalgebra))
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
def __repr__(self) -> str:
|
|
338
|
+
return (
|
|
339
|
+
f"NilpotentOrbit({self.lie_type[0]}{self.lie_type[1]}, "
|
|
340
|
+
f"{self.label}, dim={self.dimension})"
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
# ---------------------------------------------------------------------------
|
|
345
|
+
# Orbit classification helpers
|
|
346
|
+
# ---------------------------------------------------------------------------
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _is_even_orbit_A(p: List[int]) -> bool:
|
|
350
|
+
"""In type A every orbit is even iff all parts of λ^t are even,
|
|
351
|
+
equivalently iff all parts of λ differ by at least 2."""
|
|
352
|
+
pt = _transpose_partition(p)
|
|
353
|
+
return all(x % 2 == 0 for x in pt)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _is_distinguished_A(p: List[int]) -> bool:
|
|
357
|
+
"""Distinguished orbits in type A: all parts are distinct."""
|
|
358
|
+
return len(p) == len(set(p))
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _is_even_orbit_BCD(p: List[int], lie_type: str) -> bool:
|
|
362
|
+
"""An orbit is even when the weighted Dynkin diagram has only even entries.
|
|
363
|
+
|
|
364
|
+
For types B, C, D the criterion on partitions:
|
|
365
|
+
- Type B: the partition has only odd parts
|
|
366
|
+
- Type C: the partition has only even parts
|
|
367
|
+
- Type D: the partition has only odd parts
|
|
368
|
+
(These ensure the orbit is even in the sense of Dynkin diagrams.)
|
|
369
|
+
"""
|
|
370
|
+
if lie_type == "C":
|
|
371
|
+
return all(x % 2 == 0 for x in p)
|
|
372
|
+
else: # B or D
|
|
373
|
+
return all(x % 2 == 1 for x in p)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _is_distinguished_BCD(p: List[int]) -> bool:
|
|
377
|
+
"""Distinguished in type B/C/D: no part repeats more than twice,
|
|
378
|
+
equivalently the parts of λ^t decrease by at most 1 each step.
|
|
379
|
+
A simpler criterion: all parts are distinct for B,D;
|
|
380
|
+
for C all even parts are distinct.
|
|
381
|
+
|
|
382
|
+
Actually, Collingwood-McGovern:
|
|
383
|
+
Distinguished ↔ Z_G(e) is unipotent ↔ each part has multiplicity 1.
|
|
384
|
+
This is the same as type A for BCD.
|
|
385
|
+
"""
|
|
386
|
+
return len(p) == len(set(p))
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
# ---------------------------------------------------------------------------
|
|
390
|
+
# Main enumeration
|
|
391
|
+
# ---------------------------------------------------------------------------
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def nilpotent_orbits(
|
|
395
|
+
lie_type: Union[str, Tuple[str, int]],
|
|
396
|
+
rank: Optional[int] = None,
|
|
397
|
+
) -> List[NilpotentOrbit]:
|
|
398
|
+
r"""Enumerate all nilpotent orbits for a classical Lie algebra.
|
|
399
|
+
|
|
400
|
+
Parameters
|
|
401
|
+
----------
|
|
402
|
+
lie_type : str or tuple
|
|
403
|
+
Either a single letter ``'A'``, ``'B'``, ``'C'``, ``'D'``
|
|
404
|
+
(in which case *rank* must be given), or a tuple ``('A', n)``.
|
|
405
|
+
rank : int, optional
|
|
406
|
+
Rank of the algebra. Ignored when *lie_type* is a tuple.
|
|
407
|
+
|
|
408
|
+
Returns
|
|
409
|
+
-------
|
|
410
|
+
list of NilpotentOrbit
|
|
411
|
+
Orbits sorted by dimension (ascending). For type D, very-even
|
|
412
|
+
partitions produce **two** orbit objects (labelled I and II).
|
|
413
|
+
|
|
414
|
+
Examples
|
|
415
|
+
--------
|
|
416
|
+
>>> orbits = nilpotent_orbits('A', 3) # sl(4)
|
|
417
|
+
>>> len(orbits) # Partitions(4) has 5 elements
|
|
418
|
+
5
|
|
419
|
+
>>> orbits[-1].partition # regular orbit = single part
|
|
420
|
+
[4]
|
|
421
|
+
|
|
422
|
+
>>> orbits_D4 = nilpotent_orbits('D', 4) # so(8)
|
|
423
|
+
>>> vev = [o for o in orbits_D4 if o.is_very_even]
|
|
424
|
+
>>> len(vev) # [4,4] and [2,2,2,2] each give 2 orbits → 4
|
|
425
|
+
4
|
|
426
|
+
|
|
427
|
+
References
|
|
428
|
+
----------
|
|
429
|
+
Collingwood & McGovern, *Nilpotent Orbits in Semisimple Lie Algebras*,
|
|
430
|
+
Van Nostrand Reinhold, 1993. Chapters 5–6.
|
|
431
|
+
"""
|
|
432
|
+
# --- Parse input ---
|
|
433
|
+
if isinstance(lie_type, (list, tuple)):
|
|
434
|
+
letter, n = lie_type[0], int(lie_type[1])
|
|
435
|
+
else:
|
|
436
|
+
letter = str(lie_type).upper()
|
|
437
|
+
if rank is None:
|
|
438
|
+
raise ValueError("rank must be specified when lie_type is a string")
|
|
439
|
+
n = int(rank)
|
|
440
|
+
|
|
441
|
+
letter = letter.upper()
|
|
442
|
+
if letter not in ("A", "B", "C", "D"):
|
|
443
|
+
raise ValueError(f"Only classical types A, B, C, D are supported; got '{letter}'")
|
|
444
|
+
if n < 1:
|
|
445
|
+
raise ValueError(f"Rank must be ≥ 1; got {n}")
|
|
446
|
+
if letter == "D" and n < 2:
|
|
447
|
+
raise ValueError(f"Type D requires rank ≥ 2; got {n}")
|
|
448
|
+
|
|
449
|
+
# --- Determine the integer N to partition ---
|
|
450
|
+
if letter == "A":
|
|
451
|
+
N = n + 1 # sl(n+1)
|
|
452
|
+
elif letter == "B":
|
|
453
|
+
N = 2 * n + 1 # so(2n+1)
|
|
454
|
+
elif letter == "C":
|
|
455
|
+
N = 2 * n # sp(2n)
|
|
456
|
+
else: # D
|
|
457
|
+
N = 2 * n # so(2n)
|
|
458
|
+
|
|
459
|
+
# --- Filter partitions ---
|
|
460
|
+
filter_fn = {
|
|
461
|
+
"A": lambda p: True,
|
|
462
|
+
"B": _is_type_B_partition,
|
|
463
|
+
"C": _is_type_C_partition,
|
|
464
|
+
"D": _is_type_D_partition,
|
|
465
|
+
}[letter]
|
|
466
|
+
|
|
467
|
+
dim_fn = {
|
|
468
|
+
"A": _orbit_dim_A,
|
|
469
|
+
"B": _orbit_dim_B,
|
|
470
|
+
"C": _orbit_dim_C,
|
|
471
|
+
"D": _orbit_dim_D,
|
|
472
|
+
}[letter]
|
|
473
|
+
|
|
474
|
+
orbits: List[NilpotentOrbit] = []
|
|
475
|
+
|
|
476
|
+
for sage_partition in Partitions(N):
|
|
477
|
+
p = list(sage_partition)
|
|
478
|
+
if not filter_fn(p):
|
|
479
|
+
continue
|
|
480
|
+
|
|
481
|
+
dim = dim_fn(p)
|
|
482
|
+
pt = _transpose_partition(p)
|
|
483
|
+
|
|
484
|
+
if letter == "A":
|
|
485
|
+
is_even = _is_even_orbit_A(p)
|
|
486
|
+
is_dist = _is_distinguished_A(p)
|
|
487
|
+
else:
|
|
488
|
+
is_even = _is_even_orbit_BCD(p, letter)
|
|
489
|
+
is_dist = _is_distinguished_BCD(p)
|
|
490
|
+
|
|
491
|
+
very_even = (letter == "D") and _is_very_even(p)
|
|
492
|
+
|
|
493
|
+
if letter == "D" and very_even:
|
|
494
|
+
# Very even partition → two distinct orbits (I and II)
|
|
495
|
+
for numeral in ("I", "II"):
|
|
496
|
+
orbits.append(
|
|
497
|
+
NilpotentOrbit(
|
|
498
|
+
lie_type=(letter, n),
|
|
499
|
+
partition=p,
|
|
500
|
+
dimension=dim,
|
|
501
|
+
transpose=pt,
|
|
502
|
+
is_even=is_even,
|
|
503
|
+
is_distinguished=is_dist,
|
|
504
|
+
is_very_even=True,
|
|
505
|
+
roman_numeral=numeral,
|
|
506
|
+
)
|
|
507
|
+
)
|
|
508
|
+
else:
|
|
509
|
+
orbits.append(
|
|
510
|
+
NilpotentOrbit(
|
|
511
|
+
lie_type=(letter, n),
|
|
512
|
+
partition=p,
|
|
513
|
+
dimension=dim,
|
|
514
|
+
transpose=pt,
|
|
515
|
+
is_even=is_even,
|
|
516
|
+
is_distinguished=is_dist,
|
|
517
|
+
is_very_even=very_even,
|
|
518
|
+
)
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
# Sort by dimension (ascending)
|
|
522
|
+
orbits.sort(key=lambda o: o.dimension)
|
|
523
|
+
return orbits
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
# ---------------------------------------------------------------------------
|
|
527
|
+
# Convenience: summary table
|
|
528
|
+
# ---------------------------------------------------------------------------
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def nilpotent_orbit_table(
|
|
532
|
+
lie_type: Union[str, Tuple[str, int]],
|
|
533
|
+
rank: Optional[int] = None,
|
|
534
|
+
) -> str:
|
|
535
|
+
r"""Return a human-readable table of nilpotent orbits.
|
|
536
|
+
|
|
537
|
+
Examples
|
|
538
|
+
--------
|
|
539
|
+
>>> print(nilpotent_orbit_table('A', 3))
|
|
540
|
+
Nilpotent orbits of A3 (sl(4)): 5 orbits
|
|
541
|
+
─────────────────────────────────────────────
|
|
542
|
+
# Partition Dim Transpose Flags
|
|
543
|
+
1 [1, 1, 1, 1] 0 [4]
|
|
544
|
+
2 [2, 1, 1] 4 [3, 1]
|
|
545
|
+
3 [2, 2] 6 [2, 2] even
|
|
546
|
+
4 [3, 1] 8 [2, 1, 1] dist
|
|
547
|
+
5 [4] 12 [1, 1, 1, 1] dist even
|
|
548
|
+
"""
|
|
549
|
+
orbits = nilpotent_orbits(lie_type, rank)
|
|
550
|
+
if isinstance(lie_type, (list, tuple)):
|
|
551
|
+
letter, n = lie_type[0], int(lie_type[1])
|
|
552
|
+
else:
|
|
553
|
+
letter, n = str(lie_type).upper(), int(rank)
|
|
554
|
+
|
|
555
|
+
algebra_name = {
|
|
556
|
+
"A": f"sl({n + 1})",
|
|
557
|
+
"B": f"so({2 * n + 1})",
|
|
558
|
+
"C": f"sp({2 * n})",
|
|
559
|
+
"D": f"so({2 * n})",
|
|
560
|
+
}[letter]
|
|
561
|
+
|
|
562
|
+
header = f"Nilpotent orbits of {letter}{n} ({algebra_name}): {len(orbits)} orbits"
|
|
563
|
+
sep = "\u2500" * max(len(header), 50)
|
|
564
|
+
|
|
565
|
+
lines = [header, sep]
|
|
566
|
+
lines.append(f" {'#':>3} {'Partition':<20} {'Dim':>5} {'Transpose':<20} Flags")
|
|
567
|
+
|
|
568
|
+
for i, orb in enumerate(orbits, 1):
|
|
569
|
+
flags: List[str] = []
|
|
570
|
+
if orb.is_distinguished:
|
|
571
|
+
flags.append("dist")
|
|
572
|
+
if orb.is_even:
|
|
573
|
+
flags.append("even")
|
|
574
|
+
if orb.is_very_even:
|
|
575
|
+
flags.append("v-even")
|
|
576
|
+
flag_str = " ".join(flags)
|
|
577
|
+
lines.append(
|
|
578
|
+
f" {i:>3} {orb.label:<20} {orb.dimension:>5} {str(orb.transpose):<20} {flag_str}"
|
|
579
|
+
)
|
|
580
|
+
return "\n".join(lines)
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
@dataclass
|
|
584
|
+
class LeviSubalgebra:
|
|
585
|
+
"""Levi subalgebra data attached to a nilpotent orbit.
|
|
586
|
+
|
|
587
|
+
The Levi is encoded by its root subsystem
|
|
588
|
+
``Delta_l = {alpha in Delta | alpha|_{h^f} = 0}`` and always contains
|
|
589
|
+
the full Cartan subalgebra of the ambient Lie algebra.
|
|
590
|
+
"""
|
|
591
|
+
|
|
592
|
+
lie_type: Tuple[str, int]
|
|
593
|
+
roots: List[Any] = field(default_factory=list)
|
|
594
|
+
|
|
595
|
+
@property
|
|
596
|
+
def rank(self) -> int:
|
|
597
|
+
"""Rank of the Levi subalgebra (same as ambient rank)."""
|
|
598
|
+
return int(self.lie_type[1])
|
|
599
|
+
|
|
600
|
+
@property
|
|
601
|
+
def dimension(self) -> int:
|
|
602
|
+
"""Dimension of Levi: ``rank + |Delta_l|``."""
|
|
603
|
+
return self.rank + len(self.roots)
|
|
604
|
+
|
|
605
|
+
def contains_element(self, element: Any) -> bool:
|
|
606
|
+
"""Return whether an ambient Lie algebra element lies in this Levi."""
|
|
607
|
+
from sage.all import LieAlgebra, QQ, RootSystem
|
|
608
|
+
|
|
609
|
+
letter, n = self.lie_type
|
|
610
|
+
L = LieAlgebra(QQ, cartan_type=[letter, n])
|
|
611
|
+
B = L.basis()
|
|
612
|
+
rl = RootSystem([letter, n]).root_lattice()
|
|
613
|
+
alpha_check = rl.simple_coroots()
|
|
614
|
+
|
|
615
|
+
allowed = set(self.roots)
|
|
616
|
+
for i in range(1, n + 1):
|
|
617
|
+
allowed.add(alpha_check[i])
|
|
618
|
+
|
|
619
|
+
coeffs = element.monomial_coefficients()
|
|
620
|
+
return all((c == 0) or (k in allowed) for k, c in coeffs.items())
|
|
621
|
+
|
|
622
|
+
def centralizer_dimension(self, element: Any) -> int:
|
|
623
|
+
"""Return ``dim Z_l(element)`` where ``l`` is this Levi."""
|
|
624
|
+
from sage.all import LieAlgebra, Matrix, QQ, RootSystem
|
|
625
|
+
|
|
626
|
+
letter, n = self.lie_type
|
|
627
|
+
L = LieAlgebra(QQ, cartan_type=[letter, n])
|
|
628
|
+
B = L.basis()
|
|
629
|
+
rl = RootSystem([letter, n]).root_lattice()
|
|
630
|
+
alpha_check = rl.simple_coroots()
|
|
631
|
+
|
|
632
|
+
levi_basis = [B[alpha_check[i]] for i in range(1, n + 1)]
|
|
633
|
+
for root in self.roots:
|
|
634
|
+
levi_basis.append(B[root])
|
|
635
|
+
|
|
636
|
+
ambient_keys = sorted(B.keys(), key=str)
|
|
637
|
+
rows = []
|
|
638
|
+
for key in ambient_keys:
|
|
639
|
+
rows.append(
|
|
640
|
+
[L.bracket(b, element).monomial_coefficients().get(key, 0) for b in levi_basis]
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
M = Matrix(QQ, rows)
|
|
644
|
+
return int(M.right_kernel().dimension())
|
|
645
|
+
|
|
646
|
+
def is_regular_element(self, element: Any, *, require_nilpotent: bool = True) -> bool:
|
|
647
|
+
r"""Check if ``element`` is regular in this Levi.
|
|
648
|
+
|
|
649
|
+
Regular means ``dim Z_l(element) = rank(l)``.
|
|
650
|
+
"""
|
|
651
|
+
if not self.contains_element(element):
|
|
652
|
+
return False
|
|
653
|
+
|
|
654
|
+
if require_nilpotent:
|
|
655
|
+
is_nilpotent_fn = getattr(element, "is_nilpotent", None)
|
|
656
|
+
if callable(is_nilpotent_fn):
|
|
657
|
+
try:
|
|
658
|
+
if not bool(is_nilpotent_fn()):
|
|
659
|
+
return False
|
|
660
|
+
except Exception:
|
|
661
|
+
pass
|
|
662
|
+
|
|
663
|
+
return self.centralizer_dimension(element) == self.rank
|
|
664
|
+
|
|
665
|
+
def is_regular(self, element: Any, *, require_nilpotent: bool = True) -> bool:
|
|
666
|
+
"""Alias of :meth:`is_regular_element` for concise API usage."""
|
|
667
|
+
return self.is_regular_element(element, require_nilpotent=require_nilpotent)
|