back to table · edit · history · where entries came from · files · download
11163 bytes, as of the version from 2026-09-16 02:24 (current). Recorded here, not run.
"""Degrees of irreducible characters of sporadic simple groups -- numberdb.org/T255
This generator reads CTblLib 1.3.11 and stores the exact degrees chi_i(1) of
the nontrivial irreducible complex characters of the 26 sporadic simple
groups. The ordering is CTblLib's ordering for each ordinary character table,
and chi_1 is omitted because it is the trivial character of degree 1.
Run it with SageMath:
$ sage -pip install numberdb # once
$ sage -python generate.py # check the table against this code
$ sage -python generate.py --publish # fill the draft, with NUMBERDB_API_KEY set
Set CTBLLIB_TARBALL to a local copy of ctbllib-1.3.11.tar.gz to avoid
downloading the public archive again.
"""
import io
import os
import re
import sys
import tarfile
import urllib.request
from functools import lru_cache
import numberdb.sage as numberdb
from sage.rings.integer_ring import ZZ
TABLE = "T255"
CTBLLIB_URL = (
"https://www.math.rwth-aachen.de/~Thomas.Breuer/ctbllib/"
"ctbllib-1.3.11.tar.gz"
)
OEIS_MONSTER_URL = "https://oeis.org/A001379/b001379.txt"
GROUPS = (
("M11", "M11", "ctomathi.tbl"),
("M12", "M12", "ctomathi.tbl"),
("M22", "M22", "ctomathi.tbl"),
("M23", "M23", "ctomathi.tbl"),
("M24", "M24", "ctomathi.tbl"),
("J1", "J1", "ctoconja.tbl"),
("J2", "J2", "ctoconja.tbl"),
("J3", "J3", "ctoconja.tbl"),
("J4", "J4", "ctoconja.tbl"),
("Co3", "Co3", "ctoconja.tbl"),
("Co2", "Co2", "ctoconja.tbl"),
("Co1", "Co1", "ctoconja.tbl"),
("Fi22", "Fi22", "ctofisc1.tbl"),
("Fi23", "Fi23", "ctofisc2.tbl"),
("Fi24", "F3+", "ctofisc2.tbl"),
("HS", "HS", "ctospora.tbl"),
("McL", "McL", "ctospora.tbl"),
("He", "He", "ctospora.tbl"),
("Ru", "Ru", "ctospora.tbl"),
("Suz", "Suz", "ctospora.tbl"),
("ON", "ON", "ctospora.tbl"),
("HN", "HN", "ctospora.tbl"),
("Ly", "Ly", "ctospora.tbl"),
("Th", "Th", "ctospora.tbl"),
("B", "B", "ctomonst.tbl"),
("M", "M", "ctomonst.tbl"),
)
EXPECTED_CHARACTER_COUNTS = {
"M11": 10,
"M12": 15,
"M22": 12,
"M23": 17,
"M24": 26,
"J1": 15,
"J2": 21,
"J3": 21,
"J4": 62,
"Co3": 42,
"Co2": 60,
"Co1": 101,
"Fi22": 65,
"Fi23": 98,
"Fi24": 108,
"HS": 24,
"McL": 24,
"He": 33,
"Ru": 36,
"Suz": 43,
"ON": 30,
"HN": 54,
"Ly": 53,
"Th": 48,
"B": 184,
"M": 194,
}
def _key_from_stdin():
if os.environ.get("NUMBERDB_KEY_FROM_STDIN") != "1":
return
token = sys.stdin.read().strip()
if "=" in token and token.split("=", 1)[0].isupper():
token = token.split("=", 1)[1].strip().strip("'\"")
if token:
os.environ["NUMBERDB_API_KEY"] = token
def _download(url):
request = urllib.request.Request(
url, headers={"User-Agent": "numberdb-sporadic-character-degrees"})
with urllib.request.urlopen(request, timeout=120) as answer:
return answer.read()
def _archive_bytes():
path = os.environ.get("CTBLLIB_TARBALL")
if path:
with open(path, "rb") as handle:
return handle.read()
cache = "/tmp/ctbllib-1.3.11.tar.gz"
if os.path.exists(cache):
with open(cache, "rb") as handle:
return handle.read()
data = _download(CTBLLIB_URL)
try:
with open(cache, "wb") as handle:
handle.write(data)
except OSError:
pass
return data
def _ordinary_sources():
wanted = sorted({filename for _, _, filename in GROUPS})
source = {}
with tarfile.open(fileobj=io.BytesIO(_archive_bytes()), mode="r:gz") as archive:
for filename in wanted:
member = "ctbllib-1.3.11/data/" + filename
source[filename] = archive.extractfile(member).read().decode(
"utf8", "replace")
return source
def _split_top_level(text):
parts = []
start = 0
bracket_depth = 0
paren_depth = 0
in_string = False
escaped = False
for index, char in enumerate(text):
if in_string:
if escaped:
escaped = False
elif char == "\\":
escaped = True
elif char == '"':
in_string = False
continue
if char == '"':
in_string = True
elif char == "[":
bracket_depth += 1
elif char == "]":
bracket_depth -= 1
elif char == "(":
paren_depth += 1
elif char == ")":
paren_depth -= 1
elif char == "," and bracket_depth == 0 and paren_depth == 0:
parts.append(text[start:index].strip())
start = index + 1
parts.append(text[start:].strip())
return parts
def _list_elements(text):
text = text.strip()
if not (text.startswith("[") and text.endswith("]")):
raise ValueError("expected a GAP list, got %r" % text[:40])
return _split_top_level(text[1:-1])
def _extract_mot_call(source, name):
needle = 'MOT("%s"' % name
start = source.find(needle)
if start < 0:
raise KeyError("CTblLib table %s was not found" % name)
open_paren = source.find("(", start)
depth = 0
in_string = False
escaped = False
for index in range(open_paren, len(source)):
char = source[index]
if in_string:
if escaped:
escaped = False
elif char == "\\":
escaped = True
elif char == '"':
in_string = False
continue
if char == '"':
in_string = True
elif char == "(":
depth += 1
elif char == ")":
depth -= 1
if depth == 0:
return source[open_paren + 1:index]
raise ValueError("unterminated MOT record for %s" % name)
def _degree_from_row(row, degrees):
row = row.strip()
if row.startswith("[GALOIS"):
found = re.match(r"\[GALOIS,\s*\[(\d+)", row)
if not found:
raise ValueError("could not read GALOIS row %r" % row[:80])
return degrees[int(found.group(1)) - 1]
if not row.startswith("["):
raise ValueError("expected a character row, got %r" % row[:80])
first = _split_top_level(row[1:-1])[0]
if not re.fullmatch(r"\d+", first):
raise ValueError("character degree is not an integer: %r" % first[:80])
return int(first)
def _parse_degrees(characters):
degrees = []
for row in _list_elements(characters):
degrees.append(_degree_from_row(row, degrees))
return tuple(degrees)
@lru_cache(maxsize=1)
def character_degrees():
sources = _ordinary_sources()
out = {}
counts = {}
for key, ctbl_name, filename in GROUPS:
arguments = _split_top_level(_extract_mot_call(sources[filename], ctbl_name))
centralizers = _list_elements(arguments[2])
degrees = _parse_degrees(arguments[4])
if len(degrees) != len(centralizers):
raise ValueError("%s has %d degrees and %d classes" % (
key, len(degrees), len(centralizers)))
if len(degrees) != EXPECTED_CHARACTER_COUNTS[key]:
raise ValueError("%s has %d characters, expected %d" % (
key, len(degrees), EXPECTED_CHARACTER_COUNTS[key]))
out[key] = degrees
counts[key] = len(centralizers)
return out, counts
def stored_sporadic_orders():
table = numberdb.table("T77")
return {
group: int(entry["number"] if isinstance(entry, dict) else entry)
for group, entry in table["Numbers"].items()
}
def monster_degrees_from_oeis():
text = _download(OEIS_MONSTER_URL).decode("utf8", "replace")
values = []
for line in text.splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
_, value = line.split(None, 1)
values.append(int(value))
return tuple(values)
def run_integrity_checks():
degrees, counts = character_degrees()
if sum(counts.values()) != 1396:
raise ValueError("expected 1396 irreducible characters")
if sum(len(rows) - 1 for rows in degrees.values()) != 1370:
raise ValueError("expected 1370 nontrivial character degrees")
orders = stored_sporadic_orders()
for group, rows in degrees.items():
total = sum(degree * degree for degree in rows)
if total != orders[group]:
raise ValueError("%s: sum of squares is %s, order is %s" % (
group, total, orders[group]))
oeis_monster = monster_degrees_from_oeis()
if tuple(degrees["M"]) != oeis_monster[:len(degrees["M"])]:
raise ValueError("Monster degrees disagree with OEIS A001379")
return True
class SporadicCharacterDegrees(numberdb.Generator):
table = TABLE
parameters = ("G", "chi")
type = "Z"
rigour = "exact"
def enumerate(self):
degrees, _ = character_degrees()
for group, _, _ in GROUPS:
for index in range(2, len(degrees[group]) + 1):
yield {"G": group, "chi": str(index)}
def value(self, params, digits):
degrees, _ = character_degrees()
return ZZ(degrees[str(params["G"])][int(params["chi"]) - 1])
def fill_draft_once(generator, message):
"""Fill a fresh draft without the client's empty upsert probe."""
from numberdb._generate import (
_check_precision,
_check_rigour,
_producer,
_run_name,
_source_files,
)
from numberdb._write import Entries, attach, submit_entries, to_text
table = generator.table
run = _run_name(generator)
entries = Entries(*generator.parameters)
for params in generator.enumerate():
params = dict(params)
wanted = generator.digits_for(params)
entry = generator._entry(params, wanted)
value = entry["number"]
identity = ",".join(str(params[name]) for name in generator.parameters)
_check_rigour(generator, table, identity, value)
written = to_text(value, wanted, generator.format)
_check_precision(table, identity, written, wanted, lowering=False)
record = dict(entry)
record.pop("digits", None)
entries.add(**params, **record, digits=wanted)
answer = submit_entries(
table,
entries,
message=message,
produced_by=_producer(generator),
upsert=False,
run=run,
rigour=generator.rigour,
)
for name, body in sorted(_source_files(generator).items()):
attach(table, name, body, run=run, message=message,
rigour=generator.rigour)
return answer
if __name__ == "__main__":
_key_from_stdin()
generator = SporadicCharacterDegrees()
if "--publish" in sys.argv or os.environ.get("NUMBERDB_PUBLISH") == "1":
run_integrity_checks()
print(fill_draft_once(
generator,
message="sporadic irreducible character degrees from CTblLib"))
else:
run_integrity_checks()
report = generator.verify(sample=None)
print(report)
sys.exit(0 if report.ok else 1)