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 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,11 @@
1
+ from .nilpotent_orbits import (
2
+ NilpotentOrbit,
3
+ nilpotent_orbits,
4
+ nilpotent_orbit_table,
5
+ )
6
+
7
+ __all__ = [
8
+ "NilpotentOrbit",
9
+ "nilpotent_orbits",
10
+ "nilpotent_orbit_table",
11
+ ]
@@ -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)