"""HTML output for the web page: port of spherical.cgi and rootSystem.cgi. Every function returns an HTML fragment (a string) with the same text as the corresponding page of the Perl CGI programs; the markup uses classes styled by /css/explorer.css instead of tags and popup windows. """ import html import re from . import spherical from .spherical import process, flatten, partitions, sort_desc from .perlval import pstr, pnum, frac, pcmp, fix_str, str_to_num from .examples import getExamples, getFileExamples from .examples import AVAILABLE as AVAILABLE_FILE_EXAMPLES from . import rootsystem from .rootsystem import (fundamentalWeights, transpose, simpleCoRoots, showAll) class InputError(Exception): """A problem with what the user typed (shown as a message).""" def esc(x): return html.escape(pstr(x), quote=False) def _cells(row, tag="td"): return "".join("<%s>%s" % (tag, c, tag) for c in row) def cleanUp(values): return [fix_str(x) for x in values] def g_name(type, rank): """showG: the group G for a type and rank.""" rank = int(pnum(rank)) if type == "A": g = "GL(%d)" % (rank + 1) elif type == "B": g = "SO(%d)" % (2 * rank + 1) elif type == "C": g = "Sp(%d)" % (2 * rank) elif type == "D": g = "SO(%d)" % (2 * rank) else: g = "" return "%s (type %s%d)" % (g, type, rank) def showG(type, rank): return '

G: %s

' % g_name(type, rank) # ---------------------------------------------------------------------- # Parsing input # ---------------------------------------------------------------------- _TOKEN = re.compile(r"^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?" r"(/[+-]?(\d+\.?\d*|\.\d+))?$") def parse_lambda(text): """Split a typed lambda into coordinates (as spherical.cgi testOne). Coordinates are separated by commas and/or spaces; brackets are ignored. """ text = re.sub(r"[()\[\]{}]", " ", text or "") tokens = [t for t in re.split(r"[,\s]+", text.strip()) if t] if not tokens: raise InputError("Please enter lambda: a list of numbers such as " ".75 1 1/2 0.") for t in tokens: if not _TOKEN.match(t): raise InputError("Could not read the coordinate " "“%s”: use numbers such as 2, " "-1.34 or 7/8." % html.escape(t)) if "/" in t: d = t.split("/")[1] if str_to_num(d) == 0: raise InputError("Can not have 0 as the denominator " "(“%s”)." % html.escape(t)) return tokens def processLine(type, line, coordinates): """One line of a file of lambdas (spherical.cgi processLine). As in the original, everything after # is ignored, as are all characters except digits, '.', '/', commas and spaces (so minus signs are dropped). Returns None for a line with no parameter. """ line = line.rstrip("\n") line = re.sub(r"#.*", "", line) line = re.sub(r"[,\s]+", ",", line) line = re.sub(r"[^0-9,\./]", "", line) if not line or line == "0": return None items = [t for t in line.split(",") if t != ""] if not items: return None for t in items: if "/" in t: num, d = (t.split("/") + [""])[:2] if str_to_num(d) == 0 or not re.match(r"^[0-9.]+$", num or "x"): raise InputError("Could not read the coordinate " "“%s”." % html.escape(t)) elif not re.match(r"^(\d+\.?\d*|\.\d+)$", t): raise InputError("Could not read the coordinate " "“%s”." % html.escape(t)) if not re.search("bourbaki|gap", coordinates or ""): # The Perl sorted the coordinates numerically ("3/2" counts as # 3) before testing. process() sorts them again, so this only # matters for ties; it is kept for fidelity. (For weight # coordinates the Perl sort permuted the coordinates, which # was a bug, so it is not done.) items = sorted(items, key=lambda t: -_perl_num(t)) return items def _perl_num(t): v = str_to_num(t) return float(v) # ---------------------------------------------------------------------- # One result (spherical.cgi outputOne) # ---------------------------------------------------------------------- def verdict(result): if result == 1: return 'UNITARY' return 'NOT UNITARY' def _msg(message): return esc(message).replace("\n", "
\n") def outputOne(r, detail=True, unitaryOnly=False, comment=None): """HTML for one tested parameter. With detail, the full report (two tables); otherwise one table row (the caller supplies the table). Returns '' for a parameter that is skipped (unitaryOnly) or not Hermitian (low detail). """ out = [] p = out.append message = r.message if detail and (comment or message): text = "" if comment is not None: text = comment + "
\n" if message: text += _msg(message) p('

%s

' % text) if unitaryOnly and not r.result: return "".join(out) if r.result == "stop": return "".join(out) type = r.type M_0 = r.M_0 m0 = pstr(M_0[0]) if M_0 else "" if type == "A": M0 = "GL(%s)" % m0 elif type == "B": M0 = "SO(%s)" % pstr(2 * pnum(M_0[0]) + 1) elif type == "C": M0 = "Sp(%s)" % pstr(2 * pnum(M_0[0])) else: M0 = "SO(%s)" % pstr(2 * pnum(M_0[0])) M_GL = r.M_GL indices = sorted(range(len(M_GL)), key=spherical._key(lambda a, b: pcmp(M_GL[b], M_GL[a]))) rows = [_cells(["M", "OM", "h", "nu"], "th")] rows.append(_cells([M0, "(" + ", ".join(esc(x) for x in r.O_0) + ")", "(" + ", ".join(esc(x) for x in flatten(r.h_0)) + ")", " "])) levi = [] for i in indices: factor = "GL(%s)" % pstr(M_GL[i]) levi.append(factor) rows.append(_cells([ factor, "(" + ", ".join(esc(x) for x in r.O_GL[i]) + ")", "(" + ", ".join(esc(x) for x in r.h_GL[i]) + ")", esc(r.nu_GL[i])])) table1 = "".join("%s" % x for x in rows) fullLevi = list(levi) if M_0 and pnum(M_0[0]) > 0: fullLevi.insert(0, M0) psd = r.psd zkeys = sorted(r.Z.keys(), key=lambda k: -pnum(k)) centralizer = "x".join("%s(%s)" % (r.Z[j][1], pstr(r.Z[j][0])) for j in zkeys) lam = ",".join(esc(x) for x in cleanUp(r.lam)) if detail: p('

lambda: (%s) %s
\n' % (lam, verdict(r.result))) p("Weight Coordinates (Bourbaki): [%s]
\n" % ",".join(esc(x) for x in r.lambdaWeight)) p("Weight Coordinates (Gap): [[%s]]
\n" % ",".join(esc(x) for x in r.lambdaWeightGap)) p("Orbit O: (%s)
\n" % ", ".join(esc(x) for x in r.O)) p("Centralizer Z:%s
\n" % centralizer) p("Levi Factor M: %s

\n" % "x".join(fullLevi)) p('
%s
\n' % table1) rows = [_cells(["row length", "multiplicity", "Z", "unitary", "nu"], "th")] # (The Perl built a list of psd keys plus rows of O_0 here, but # a later "my @keys" shadowed it: the rows are the keys of Z.) for i in zkeys: if psd.get(i) is not None: mult = r.Z[i][0] factor = r.Z[i][1] rows.append(_cells([ esc(i), pstr(mult), "%s(%s)" % (factor, pstr(mult)), "+" if psd[i] == 1 else "-", "(" + ", ".join(esc(x) for x in r.nu[i]) + ")"])) else: rows.append(_cells([esc(i), "1", "O(1)", "+", "*"])) p('
%s
\n' % "".join("%s" % x for x in rows)) p("
\n") else: p("(%s)[%s](%s)%s%s" "%s\n" % ( ", ".join(esc(x) for x in cleanUp(r.lam)), ",".join(esc(x) for x in r.lambdaWeight), ",".join(esc(x) for x in r.O), centralizer, "x".join(fullLevi), verdict(r.result))) return "".join(out) LOW_HEAD = ('
' '' '' '\n') LOW_FOOT = "
lambda
standard
lambda
weight
Orbit OCentralizer ZLevi Factor MUnitary?
\n" def _check_rank(type, n, coordinates=""): """Reject parameters the original program crashed on.""" if type == "D" and n < 2: raise InputError("For type D lambda must have at least 2 " "coordinates.") def _run(type, lam, coordinates=""): _check_rank(type, len(lam), coordinates) if type == "A" and len(lam) == 1 and \ not re.search("bourbaki|gap", coordinates or "") and \ pcmp(str_to_num(pstr(lam[0])) if not isinstance(lam[0], str) or "/" not in lam[0] else frac(*lam[0].split("/")[:2]), 0) == 0: raise InputError("For type A lambda must have at least 2 " "coordinates.") r = process(type, lam, coordinates) if r is None: raise InputError("Please enter lambda.") return r # ---------------------------------------------------------------------- # Pages # ---------------------------------------------------------------------- def testOne(type, text, coordinates="standard"): """Test a single point (Submit=testOne).""" lam = parse_lambda(text) r = _run(type, lam, coordinates) rank = len(r.lam) - 1 if type == "A" else len(r.lam) out = ['

Unitary test for %s%d

' % (type, rank)] out.append(showG(type, rank)) gap = "gap" if coordinates == "gap" else "" out.append(diagram_details(type, rank, gap)) out.append(outputOne(r, True)) return "".join(out) def example(n): """Run one of the built-in examples (Submit=example).""" types, lambdas, titles, comments = getExamples() n = int(n) type = types[n] lam = list(lambdas[n]) rank = len(lam) - 1 if type == "A" else len(lam) r = _run(type, lam, "standard") out = ['

Unitary test for %s%d

' % (type, rank)] out.append(diagram_details(type, rank, "")) out.append(outputOne(r, True, comment=comments[n])) return "".join(out) def testList(type, text, detail=True, coordinates="standard", unitaryOnly=False, title=None): """Test every line of a list of lambdas (Submit=file).""" out = ['

Unitary test for type %s

' % type] if title: out.append('

%s

' % title) if not detail: out.append(LOW_HEAD) count = 0 for n, line in enumerate((text or "").splitlines(), 1): try: items = processLine(type, line, coordinates) except InputError as err: out.append(_line_error(n, str(err), detail)) continue if items is None: continue count += 1 try: r = _run(type, items, coordinates) except Exception as err: # show and go on out.append(_line_error(n, html.escape(str(err)), detail)) continue if detail and not (unitaryOnly and not r.result): out.append(showG(type, len(r.lambdaWeight))) out.append(outputOne(r, detail, unitaryOnly)) if not detail: out.append(LOW_FOOT) if count == 0: out.append('

No parameters found. Put one ' 'lambda on each line.

') return "".join(out) def _line_error(n, msg, detail): text = "Line %d: %s" % (n, msg) if detail: return '

%s

' % text return '%s' % text def fileExample(key, text, detail=True, unitaryOnly=False): """Test the points in one of the example files (Submit=fileExample).""" typeAndRanks, files, fileTitles = getFileExamples() key = int(key) typeAndRank = typeAndRanks[key] type = re.sub("[^A-D]", "", typeAndRank) rank = int(re.sub("[^0-9]", "", typeAndRank)) out = ['

Unitary test for %s%d

' % (type, rank)] out.append(showG(type, rank)) out.append(diagram_details(type, rank, "")) out.append('

Testing points from file ' '%s

' % (files[key], files[key])) if not detail: out.append(LOW_HEAD) for line in (text or "").splitlines(): items = processLine(type, line, "") if items is None: continue r = _run(type, items, "") out.append(outputOne(r, detail, unitaryOnly)) if not detail: out.append(LOW_FOOT) return "".join(out) MAX_UNIPOTENT_RANK = 16 def unipotentLambdas(type, rank): """The unipotent parameters for a type and rank, in the Perl order. One for each partition of n (A: n = rank+1; B: n = 2 rank, odd rows of even multiplicity; C: n = 2 rank + 1 and D: n = 2 rank, even rows of even multiplicity), coordinates 1,2,..,(r-1)/2 for an odd row r and 1/2,3/2,..,(r-1)/2 for an even row. """ parity = -1 if type == "A": n = rank + 1 elif type == "B": n = 2 * rank parity = 1 elif type == "C": n = 2 * rank + 1 parity = 0 elif type == "D": # The Perl tested "B" twice here, so type D never worked; the # table in its comments gives n = 2r and parity 0. n = 2 * rank parity = 0 parts = [] for x in partitions(n): d = {} for j in x: d[j] = d.get(j, 0) + 1 parts.append(d) if parity >= 0: good = [] for d in parts: ok = True for rowLength in d: if (rowLength & 1) != (parity & 1): continue if d[rowLength] & 1: ok = False break if ok: good.append(d) parts = good result = [] for d in reversed(parts): lam = [] for r in d: base = frac(1) if (r & 1) else frac(1 / 2) for i in range(0, r // 2): v = spherical.padd(base, i) lam.extend([v] * d[r]) if type == "A": lam.extend([spherical.psub(spherical.pneg(base), i)] * d[r]) if type == "A": lam.extend([frac(0)] * (rank + 1 - len(lam))) else: lam.extend([0] * (rank - len(lam))) result.append(lam) return result def unipotent(type, rank, detail=True, unitaryOnly=False): """All unipotent representations (Submit=unipotent).""" try: rank = int(str(rank).strip()) except ValueError: raise InputError("The rank must be a positive integer.") if rank < 1: raise InputError("The rank must be a positive integer.") if rank > MAX_UNIPOTENT_RANK: raise InputError("Please choose a rank of at most %d (the number " "of unipotent parameters grows quickly)." % MAX_UNIPOTENT_RANK) if type == "D" and rank < 2: raise InputError("For type D the rank must be at least 2.") out = ['

All unipotent Representations for ' 'type %s%d

' % (type, rank)] out.append(showG(type, rank)) if unitaryOnly: out.append('

Showing unitary points only

') out.append(diagram_details(type, rank, "")) if not detail: out.append(LOW_HEAD) for lam in unipotentLambdas(type, rank): r = _run(type, lam, "") out.append(outputOne(r, detail, unitaryOnly)) if not detail: out.append(LOW_FOOT) return "".join(out) # ---------------------------------------------------------------------- # Dynkin diagram and coordinates (Submit=diagram) # ---------------------------------------------------------------------- def _matrix_text(m): lines = [] rootsystem.showMatrix(m, lines) return "".join(lines) def diagram_html(type, rank, gap=""): """Dynkin diagram and the coordinate change matrices.""" out = ['

Dynkin Diagram and coordinates for type %s%d

' % (type, rank)] out.append("

Dynkin Diagram:

") if gap: out.append("

Alternate Labelling (see " 'Root Systems for more ' "information)

") out.append("
%s
" % esc("".join( rootsystem.diagram(type, rank, gap)))) out.append("

This labelling is used in the fundamental weight " "coordinates.

") fw = fundamentalWeights(type, rank, gap) tsc = transpose(simpleCoRoots(type, rank, gap)) out.append("
Fundamental Weights in standard coordinates:\nM=\n%s\n"
               "M-1 (=transpose of simple coroots in standard "
               "coordinates):\n%s(standard coordinates)*M-1="
               "(weight coordinates)
" % (esc(_matrix_text(fw)), esc(_matrix_text(tsc)))) return "".join(out) def diagram_details(type, rank, gap=""): try: body = diagram_html(type, rank, gap) except Exception: return "" return ('
Dynkin Diagram and ' 'coordinates%s

For more detail see the ' 'Root System section.

' '
' % (body, type, rank)) # ---------------------------------------------------------------------- # Root systems (rootSystem.cgi) # ---------------------------------------------------------------------- ROOT_TYPES = ["A", "B", "C", "D", "E", "F", "F*", "G", "G*"] def rootLabels(type, rank, labelling, other=""): """Validate the input of the root system form; returns labels.""" rank = (rank or "").strip() if not re.match(r"^[0-9]*$", rank): raise InputError("The rank must be a positive integer") if not rank or int(rank) == 0: raise InputError("You must enter a rank") rank = int(rank) if type == "E" and (rank < 6 or rank > 8): raise InputError("Very funny. For type E the rank must be 6,7, " "or 8.") if type in ("F", "F*") and rank != 4: raise InputError("Very funny. For type F the rank must be 4.") if type in ("G", "G*") and rank != 2: raise InputError("Very funny. For type G the rank must be 2.") if type == "D" and rank < 2: raise InputError("For type D the rank must be at least 2.") if rank > 40: raise InputError("Please choose a rank of at most 40.") if labelling == "Other": c = re.sub(r"[\s,]+", ",", other or "") c = re.sub(r"[^0-9,]*", "", c) labels = [x for x in c.split(",") if x != ""] if len(labels) != rank: raise InputError("The number of coordinates does not match the " "rank") if sorted(int(x) for x in labels) != list(range(1, rank + 1)): raise InputError("Enter a permutation of 1,2,...,%d" % rank) return rank, [int(x) for x in labels] if labelling == "Gap": return rank, "Gap" return rank, "" def rootSystemText(type, rank, labelling="Bourbaki", other=""): """Plain text of the root data (the Download button).""" rank, labels = rootLabels(type, rank, labelling, other) return "".join(showAll(type, rank, labels)) def rootSystem(type, rank, labelling="Bourbaki", other=""): """Root data for a type and rank (Submit=View).""" rank, labels = rootLabels(type, rank, labelling, other) text = "".join(showAll(type, rank, labels)) return ('

Root Data for type %s%d' '

%s
' % (esc(type), rank, esc(text))) def run(action, **kw): """Entry point for the page: returns {'html': ...} or {'error': ...}.""" fn = {"testOne": testOne, "example": example, "file": testList, "fileExample": fileExample, "unipotent": unipotent, "roots": rootSystem, "rootsText": rootSystemText, "diagram": diagram_html}[action] try: return {"html": fn(**kw)} except InputError as err: return {"error": str(err)} except RecursionError: return {"error": "This computation is too large."} except (ValueError, ZeroDivisionError) as err: return {"error": html.escape(str(err))}