diff --git a/lmfdb/abvar/fq/isog_class.py b/lmfdb/abvar/fq/isog_class.py index 96512db0f2..e0bde4c2b9 100644 --- a/lmfdb/abvar/fq/isog_class.py +++ b/lmfdb/abvar/fq/isog_class.py @@ -171,9 +171,7 @@ def newton_plot(self): L = Graphics() xmax = len(S) ymax = ZZ(len(S)/2) - pts.append((xmax,0)) - L += polygon(pts,alpha=0.1) - pts.remove((xmax,0)) + L += polygon(pts+[(0,ymax)],alpha=0.1) for i in range(xmax+1): L += line([(i, 0), (i, ymax)], color="grey", thickness=0.5) for j in range(ymax+1): diff --git a/lmfdb/abvar/fq/main.py b/lmfdb/abvar/fq/main.py index 2c18cf9ff1..d165a0f2a5 100644 --- a/lmfdb/abvar/fq/main.py +++ b/lmfdb/abvar/fq/main.py @@ -538,7 +538,7 @@ def short_label(d): def search_types(self, info): return self._search_again(info, [ - ('', 'List of isogeny classes'), + ('List', 'List of isogeny classes'), ('Counts', 'Counts table'), ('Random', 'Random isogeny class')]) diff --git a/lmfdb/api2/searchers.py b/lmfdb/api2/searchers.py index 1959fa79aa..1c5f19d608 100644 --- a/lmfdb/api2/searchers.py +++ b/lmfdb/api2/searchers.py @@ -88,6 +88,5 @@ def register_singleton(url, table, key=None, simple_search=None, full_search=Non simple_search -- A function that modifies a query object to make it search for the requested object full_search -- A function that performs a search itself and returns the results """ - singletons[url] = {'table':table, 'key':key, 'simple_search':simple_search, 'full_search':full_search} diff --git a/lmfdb/elliptic_curves/elliptic_curve.py b/lmfdb/elliptic_curves/elliptic_curve.py index 8469c8a23b..a173291db4 100644 --- a/lmfdb/elliptic_curves/elliptic_curve.py +++ b/lmfdb/elliptic_curves/elliptic_curve.py @@ -20,7 +20,7 @@ SearchArray, TextBox, SelectBox, SubsetBox, TextBoxWithSelect, CountBox, Downloader, StatsDisplay, parse_element_of, parse_signed_ints, search_wrap, redirect_no_cache, web_latex_factored_integer) from lmfdb.utils.interesting import interesting_knowls -from lmfdb.utils.search_columns import SearchColumns, MathCol, LinkCol, ProcessedCol, MultiProcessedCol, CheckCol, FloatCol +from lmfdb.utils.search_columns import SearchColumns, MathCol, LinkCol, ProcessedCol, MultiProcessedCol, CheckCol, FloatCol, ListCol from lmfdb.utils.common_regex import ZLLIST_RE from lmfdb.utils.web_display import dispZmat_from_list from lmfdb.api import datapage @@ -470,7 +470,7 @@ def make_modcurve_link(label): ProcessedCol("equation", "ec.q.minimal_weierstrass_equation", "Weierstrass equation", latex_equation, short_title="Weierstrass equation", align="left", orig="ainvs", download_col="ainvs"), ProcessedCol("modm_images", "ec.galois_rep", r"mod-$m$ images", lambda v: "" + ", ".join([make_modcurve_link(s) for s in v[:5]] + ([r"$\ldots$"] if len(v) > 5 else [])) + "", short_title="mod-m images", default=lambda info: info.get("galois_image")), - MathCol("mwgens", "ec.mordell_weil_group", "MW-generators", default=False), + ListCol("mwgens", "ec.mordell_weil_group", "MW-generators", mathmode=True, default=False), ]) class ECDownloader(Downloader): diff --git a/lmfdb/genus2_curves/main.py b/lmfdb/genus2_curves/main.py index 9e010a2a26..2df1991957 100644 --- a/lmfdb/genus2_curves/main.py +++ b/lmfdb/genus2_curves/main.py @@ -38,7 +38,7 @@ web_latex_factored_integer, ) from lmfdb.utils.interesting import interesting_knowls -from lmfdb.utils.search_columns import SearchColumns, MathCol, CheckCol, LinkCol, ProcessedCol, MultiProcessedCol, ProcessedLinkCol, ListCol +from lmfdb.utils.search_columns import SearchColumns, MathCol, CheckCol, LinkCol, ProcessedCol, MultiProcessedCol, ProcessedLinkCol, ListCol, RationalListCol from lmfdb.utils.common_regex import ZLIST_RE, ZLLIST_RE, G2_LOOKUP_RE from lmfdb.api import datapage from lmfdb.sato_tate_groups.main import st_link_by_name, st_display_knowl @@ -608,10 +608,10 @@ class G2C_download(Downloader): ProcessedCol("regulator", "g2c.regulator", "Regulator", lambda v: r"\(%.6f\)" % v, align="right", default=False), ProcessedCol("real_period", "g2c.real_period", "Real period", lambda v: r"\(%.6f\)" % v, align="right", default=False), ProcessedCol("leading_coeff", "g2c.bsd_invariants", "Leading coefficient", lambda v: r"\(%.6f\)" % v, align="right", default=False), - ListCol("igusa_clebsch_inv", "g2c.igusa_clebsch_invariants", "Igusa-Clebsch invariants", lambda v: v.replace("'",""), short_title="Igusa-Clebsch invariants", mathmode=True, default=False), - ListCol("igusa_inv", "g2c.igusa_invariants", "Igusa invariants", lambda v: v.replace("'",""), short_title="Igusa invariants", mathmode=True, default=False), - ListCol("g2_inv", "g2c.g2_invariants", "G2-invariants", lambda v: v.replace("'",""), short_title="G2-invariants", mathmode=True, default=False), - ListCol("eqn", "g2c.minimal_equation", "Equation", lambda v: min_eqn_pretty(literal_eval(v)), mathmode=True) + RationalListCol("igusa_clebsch_inv", "g2c.igusa_clebsch_invariants", "Igusa-Clebsch invariants", lambda v: v.replace("'",""), short_title="Igusa-Clebsch invariants", mathmode=True, default=False), + RationalListCol("igusa_inv", "g2c.igusa_invariants", "Igusa invariants", lambda v: v.replace("'",""), short_title="Igusa invariants", mathmode=True, default=False), + RationalListCol("g2_inv", "g2c.g2_invariants", "G2-invariants", lambda v: v.replace("'",""), short_title="G2-invariants", mathmode=True, default=False), + RationalListCol("eqn", "g2c.minimal_equation", "Equation", lambda v: min_eqn_pretty(literal_eval(v)), mathmode=True) ]) @search_wrap( diff --git a/lmfdb/hilbert_modular_forms/test_hmf.py b/lmfdb/hilbert_modular_forms/test_hmf.py index 966de61013..8165ae748e 100644 --- a/lmfdb/hilbert_modular_forms/test_hmf.py +++ b/lmfdb/hilbert_modular_forms/test_hmf.py @@ -54,17 +54,19 @@ def test_search_base_change(self): def test_hmf_page(self): L = self.tc.get('/ModularForm/GL2/TotallyReal/2.2.73.1/holomorphic/2.2.73.1-48.4-b') - assert 'no' in L.get_data(as_text=True) - assert '-6' in L.get_data(as_text=True) - assert '2w + 10' in L.get_data(as_text=True) - assert r'\Q' in L.get_data(as_text=True) - assert '[2, 2]' in L.get_data(as_text=True) + s = L.get_data(as_text=True).replace(" ", "") + assert 'no' in s + assert '-6' in s + assert '2w+10' in s + assert r'\Q' in s + assert '[2,2]' in s def test_hmf_page_higherdim(self): L = self.tc.get('/ModularForm/GL2/TotallyReal/2.2.60.1/holomorphic/2.2.60.1-44.1-c') - assert '-2w - 4' in L.get_data(as_text=True) - assert '2e' in L.get_data(as_text=True) - assert 'defining polynomial' in L.get_data(as_text=True) + s = L.get_data(as_text=True).replace(" ", "") + assert '-2w-4' in s + assert '2e' in s + assert 'definingpolynomial' in s def test_by_field(self): L = self.tc.get('/ModularForm/GL2/TotallyReal/?field_label=4.4.725.1') diff --git a/lmfdb/local_fields/family.py b/lmfdb/local_fields/family.py new file mode 100644 index 0000000000..70b3775f99 --- /dev/null +++ b/lmfdb/local_fields/family.py @@ -0,0 +1,498 @@ +#-*- coding: utf-8 -*- + +from sage.all import lazy_attribute, point, line, polygon, cartesian_product, ZZ, QQ, PolynomialRing, srange, gcd, text, Graphics, latex +from lmfdb import db +from lmfdb.utils import encode_plot, totaler +from lmfdb.galois_groups.transitive_group import knowl_cache, transitive_group_display_knowl +from flask import url_for + +from collections import defaultdict, Counter +import re +FAMILY_RE = re.compile(r'\d+\.\d+\.\d+\.\d+[a-z]+(\d+\.\d+-\d+\.\d+\.\d+[a-z]+)?') + +def str_to_QQlist(s): + if s == "[]": + return [] + return [QQ(x) for x in s[1:-1].split(", ")] + +def latex_content(s): + # Input should be a content string, [s1, s2, ..., sm]^t_u. This converts the s_i (which might be rational numbers) to their latex form + if s is None or s == "": + return "not computed" + elif isinstance(s, list): + return '$[' + ','.join(latex(x) for x in s) + ']$' + else: + return '$' + re.sub(r"(\d+)/(\d+)", r"\\frac{\1}{\2}", s).replace("[]", r"[\ ]") + '$' + +def content_unformatter(s): + # Convert latex back to plain string + s = s.replace('$','') + return re.sub(r"\\frac\{(\d+)\}\{(\d+)\}", r"\1/\2", s) + +class pAdicSlopeFamily: + def __init__(self, label=None, base=None, slopes=[], means=[], rams=[], field_cache=None): + if label is not None: + assert not base and not slopes and not means and not rams + data = db.lf_families.lookup(label) + if data: + self.__dict__.update(data) + for col in ["visible", "slopes", "rams", "means", "tiny_rams", "small_rams"]: + setattr(self, col, str_to_QQlist(getattr(self, col))) + self.p, self.e = ZZ(self.p), ZZ(self.e) + self.artin_slopes = self.visible + base, rams, p, w = self.base, self.rams, self.p, self.w + else: + raise NotImplementedError + self.label = label + else: + raise NotImplementedError + #self.base = base + # For now, these slopes are Serre-Swan slopes, not Artin-Fontaine slopes + assert p.is_prime() + self.pw = p**w + _, self.etame = self.e.val_unit(p) + @lazy_attribute + def scaled_rams(self): + return [r / (self.etame * self.p**i) for (i, r) in enumerate(self.rams, 1)] + + @lazy_attribute + def dots(self): + p, e, f, w = self.p, self.e, self.f, self.w + # We include a large dot at the origin exactly when there is a tame part + dots = [("d", 0, 0, gcd(p**f - 1, self.etame) > 1)] + sigma = ZZ(1) + # It's convenient to add 0 at the beginning; this transforms our indexing into 1-based and helps with cases below the first band + means = [0] + self.means + slopes = [0] + self.slopes + small_rams = [0] + self.small_rams + maxslope = slopes[-1] + slopes.append(maxslope+1) # convenient since the last band is the final one in its sequence + while True: + j, i = sigma.quo_rem(self.e) + height = j + i / e + if height > maxslope: + break + sigma += 1 + if i == 0 or i.valuation(p) > w: + index = 0 + else: + index = w - i.valuation(p) + if height < means[index]: + # invisible Z-point + continue + if height in means: + band = means.index(height) + if small_rams[band] != self.e0 and slopes[band] < slopes[band+1]: + # A-point: green square + dots.append(("a", i, j, True)) + continue + if height >= slopes[index]: + # C-point: red diamond + dots.append(("c", i, j, height in self.slopes)) + else: + # B-point: blue circle + dots.append(("b", i, j, True)) + return dots + + @lazy_attribute + def link(self): + return f'{self.label}' + + def _spread_ticks(self, ticks): + ticklook0 = {a: a for a in ticks} # adjust values below when too close + if len(ticks) <= 1: + return ticklook0 + scale = ticks[-1] / 20 + cscale = 2*scale + mindiff = min((b-a) for (a,b) in zip(ticks[:-1], ticks[1:])) + if mindiff < scale: + by_mindiff = {} + for cscale in [2.0*scale, 1.5*scale, 1.0*scale, 0.8*scale, 0.6*scale, 0.4*scale, 0.2*scale]: + # Try different scales for building clusters + ticklook = dict(ticklook0) + # Group into clusters + clusters = [[ticks[0]]] + for tick in ticks[1:]: + if tick - clusters[-1][-1] < cscale: + clusters[-1].append(tick) + else: + clusters.append([tick]) + # Spread from the center in each cluster, staying away from midpoint between cluster centers + centers = [sum(C) / len(C) for C in clusters] + if len(clusters) > 1: + maxspread = [(centers[1] - centers[0]) / 2] + for i in range(1, len(clusters) - 1): + maxspread.append(min(centers[i] - centers[i-1], centers[i+1] - centers[i]) / 2) + maxspread.append((centers[-1] - centers[-2]) / 2) + # Can't actually use the full maxspread, since that would lead to different clusters combining + maxspread = [m - cscale/4 for m in maxspread] + spread = [0 if len(C) == 1 else min(cscale/2, 2 * m / (len(C) - 1)) for (m,C) in zip(maxspread, clusters)] + for s, c, C in zip(spread, centers, clusters): + n = float(len(C)) + for i, a in enumerate(C): + delta = (i - (n - 1)/2) * s + if delta > 0: + ticklook[a] = max(c + delta, a) + else: + ticklook[a] = min(c + delta, a) + adjusted = sorted(ticklook.values()) + mindiff = min((b-a) for (a,b) in zip(adjusted[:-1], adjusted[1:])) + by_mindiff[mindiff] = ticklook + ticklook0 = by_mindiff[max(by_mindiff)] + return ticklook0 + + @lazy_attribute + def spread_ticks(self): + slopeset = set(self.slopes) + meanset = set(self.means) + ticks = sorted(slopeset.union(meanset)) + ticklook = self._spread_ticks(ticks) + # We determine the overlaps of the bands + rectangles = {(a,b): 0 for (a,b) in zip(ticks[:-1], ticks[1:])} + rkeys = sorted(rectangles) + for m, s in zip(self.means, self.slopes): + # Don't worry about doing this in any fancy way, since there won't be many rectangles in practice + for (a,b) in rkeys: + if a >= m and b <= s: + rectangles[a,b] += 1 + elif a >= s: + break + return ticklook, ticks, slopeset, meanset, rectangles + + @lazy_attribute + def spread_rams(self): + # Analogue of spread_ticks for x-axis of Herbrand plot + return self._spread_ticks(sorted(set(self.rams))) + + @lazy_attribute + def picture(self): + P = Graphics() + # We want to draw a green horizontal line at each mean, a black at each slope (except when they overlap, in which case it should be dashed), and a grey rectangle between, with shading increased based on overlaps. + maxslope = self.slopes[-1] + # Draw boundaries + P += line([(0, maxslope), (0,0), (self.e, 0), (self.e, maxslope)], rgbcolor=(0.2, 0.2, 0.2), zorder=-1, thickness=1) + ticklook, ticks, slopeset, meanset, rectangles = self.spread_ticks + hscale = self.e / 18 + mindiff = min((ticklook[b]-ticklook[a]) for (a,b) in rectangles) + aspect = min(8, max(0.6 * (self.e + 3*hscale) / (1 + maxslope), self.e/(32*mindiff))) + + # Print the bands + for (a,b), cnt in rectangles.items(): + col = 1 - 0.1*cnt + P += polygon([(0, a), (self.e, a), (self.e, b), (0, b)], fill=True, rgbcolor=(col,col,col), zorder=-4) + # Horizontal green and black lines + for y in ticks: + if y in slopeset and y in meanset: + # green and black dashed line + ndashes = 11 + scale = self.e / ndashes + for x in range(1, ndashes, 2): + P += line([(x*scale, y), ((x+1)*scale, y)], color="green", zorder=-2, thickness=2) + color = "black" # Behind the green dashes + elif y in slopeset: + color = "black" + else: + color = "green" + # Mean and slope labels + P += line([(0, y), (self.e, y)], color=color, zorder=-3, thickness=2) + P += text(f"${float(y):.3f}$", (-hscale, ticklook[y]), color=color) + P += text(f"${self.e*y}$", (-2*hscale, ticklook[y]), color=color, horizontal_alignment="right") + # The spiral + for y in srange(maxslope): + y1 = min(y+1, maxslope) + x1 = (y1 - y) * self.e + P += line([(0, y), (x1, y1)], color="black", zorder=-1, thickness=1) + # x-axis Labels + vscale = maxslope / 100 + tickskip = (self.e+1)//24 + 1 + for u in range(0,self.e+1,tickskip): + P += text(f"${u}$", (u, -vscale), vertical_alignment="top", color="black", zorder=-2) + P += text(f"${u}$", (u, maxslope+vscale), vertical_alignment="bottom", color="black", zorder=-2) + # Right hand lines for arithmetic bands + for (m, s, t) in zip(self.means, self.slopes, self.small_rams): + if t == self.e0: + P += line([(self.e, m), (self.e, s)], color="black", zorder=-2, thickness=2) + # Ram labels + seen = set() + for i, (s, t) in enumerate(zip(self.slopes, self.rams)): + if s not in seen: + P += text(f"${t}$", (self.e + hscale/2, s), color="blue", horizontal_alignment="left") + seen.add(s) + P.set_aspect_ratio(aspect) + colmark = {"a": ("green", "s"), "b": ("blue", "o"), "c": ("red", "D"), "d": ("olive", "p")} + for code, i, j, big in self.dots: + # (i,j) gives the term for pi^j * x^i. We transition to the coordinates for the picture + v = j + i / self.e + size = 20 + color, marker = colmark[code] + mcolor = color + if not big: + color = "white" + P += point((i, v), markeredgecolor=mcolor, color=color, size=size, marker=marker, zorder=1) + P.axes(False) + return encode_plot(P, pad=0, pad_inches=0, bbox_inches="tight", dpi=300) + + @lazy_attribute + def ramification_polygon_plot(self): + from .main import plot_ramification_polygon + p = self.p + #L = [(self.n, self.n - self.e)] + #if self.f != 1: + # L.append((self.e, 0)) + L = [(self.e, 0)] + if self.e != self.pw: + L.append((self.pw, 0)) + cur = (self.pw, 0) + for r, nextr in zip(self.rams, self.rams[1:] + [None]): + x = cur[0] // p + y = cur[1] + x * (p - 1) * r + cur = (x, y) + if r != nextr: + L.append(cur) + L.reverse() + return plot_ramification_polygon(L, p) + + @lazy_attribute + def herbrand_function_plot(self): + # Fix duplicates + ticklook, ticks, slopeset, meanset, rectangles = self.spread_ticks + ramlook = self.spread_rams + mindiff = min((ticklook[b]-ticklook[a]) for (a,b) in rectangles) + maxram, maxslope, maxmean = self.rams[-1], self.slopes[-1], self.means[-1] + if maxram < 2: + maxx = maxram * 1.5 + else: + maxx = maxram + 1 + maxy = (maxslope - maxmean)/maxram * maxx + maxmean + tickx = float(maxx/160) + axistop = max(maxy, maxslope + 2*tickx) + ticky = maxy / 100 + hscale = maxx / 18 + aspect = min(8, max(0.6 * (maxx + 3*hscale) / axistop, maxx/(32*mindiff))) + #w = self.w + #inds = [i for i in range(w) if i==w-1 or self.slopes[i] != self.slopes[i+1]] + pts = list(zip(self.rams, self.slopes)) #[(self.rams[i],self.slopes[i]) for i in inds] + P = line([(-2*tickx,0), (maxx,0)], color="black", thickness=1) + line([(0,0),(0,axistop)], color="black", thickness=1) + P += line([(0,0)] + pts + [(maxx, maxy)], color="black", thickness=2) + P += point(pts, color="black", size=20) + # Mean and slope labels + for y in ticks: + if y in slopeset: + color = "black" + else: + color = "green" + P += text(f"${float(y):.3f}$", (-hscale, ticklook[y]), color=color) + P += text(f"${self.e*y}$", (-2*hscale, ticklook[y]), color=color, horizontal_alignment="right") + for m, r, s in zip(self.means, self.rams, self.slopes): #i in inds: + #m, r, s = self.means[i], self.rams[i], self.slopes[i] + P += line([(0, m), (r, s)], color="green", linestyle="--", thickness=1) + P += text(f"${str(r)}$", (ramlook[r], -ticky), color="blue", vertical_alignment="top") + P += line([(0, s), (tickx, s)], color="black") + P += line([(r, 0), (r, ticky)], color="black") + P.set_aspect_ratio(aspect) + P.axes(False) + return encode_plot(P, pad=0, pad_inches=0, bbox_inches="tight", dpi=300) + + @lazy_attribute + def polynomial(self): + pts = [(c, i, j) for (c, i, j, big) in self.dots if big] + names = [f"{c}{self.e*j+i}" for (c, i, j) in pts] + if self.e0 > 1: + names.append("pi") + R = PolynomialRing(ZZ, names) + S = PolynomialRing(R, "x") + if self.e == 1: + return S.gen() + x = S.gen() + if self.e0 > 1: + pi = R.gens()[-1] + else: + pi = self.p + poly = x**self.e + for i, (c, u, v) in enumerate(pts): + poly += R.gen(i) * pi**(v+1) * x**u + if not self.dots[0][3]: + # No pi*d_0 term, so need to add just pi + poly += pi + return poly + + @lazy_attribute + def gamma(self): + if self.f_absolute == 1: + return len(self.red) + e = self.e + gamma = 0 + for (u, v, _) in self.red: + s = v + u/e - 1 + cnt = self.slopes.count(s) + gamma += gcd(cnt, self.f_absolute) + return gamma + + @lazy_attribute + def base_link(self): + from .main import pretty_link + return pretty_link(self.base, self.p, self.n0, self.rf0) + + def __iter__(self): + # TODO: This needs to be fixed when base != Qp + generic = self.polynomial + R = generic.base_ring() + Zx = PolynomialRing(ZZ, "x") + names = R.variable_names() + p = self.p + opts = {"a": [ZZ(a) for a in range(1, p)], + "b": [ZZ(b) for b in range(p)], + "c": [ZZ(c) for c in range(p)]} + for vec in cartesian_product([opts[name[0]] for name in names]): + yield Zx(generic.subs(**dict(zip(names, vec)))) + + @lazy_attribute + def oldbase(self): + # Temporary until we update subfield to use new labels + return db.lf_fields.lucky({"new_label":self.base}, "old_label") + + @lazy_attribute + def fields(self): + fields = list(db.lf_fields.search( + {"family": self.label_absolute}, + ["label", "coeffs", "galT", "galois_label", "galois_degree", "slopes", "ind_of_insep", "associated_inertia", "t", "u", "aut", "hidden", "subfield", "jump_set"])) + if self.n0 > 1: + fields = [rec for rec in fields if self.base in rec["subfield"] or self.oldbase in rec["subfield"]] + glabels = list(set(rec["galois_label"] for rec in fields if rec.get("galois_label"))) + if glabels: + cache = knowl_cache(glabels) + else: + cache = {} + return fields, cache + + @lazy_attribute + def all_hidden_data_available(self): + if self.mass_found < 1: + return False + fields, cache = self.fields + for rec in fields: + if not all(rec.get(col) for col in ["galT", "galois_label", "hidden"]): + return False + return True + + @lazy_attribute + def some_hidden_data_available(self): + """ + Whether there is some field in this family where the Galois group and hidden slopes are both known. + """ + if self.mass_found == 0: + return False + fields, cache = self.fields + for rec in fields: + if all(rec.get(col) for col in ["galT", "galois_label", "hidden"]): + return True + return False + + @lazy_attribute + def galois_groups(self): + fields, cache = self.fields + opts = sorted(Counter((rec["galT"], rec["galois_label"]) for rec in fields if "galT" in rec and "galois_label" in rec).items()) + if not opts: + return "No Galois groups in this family have been computed" + def show_gal(label, cnt): + kwl = transitive_group_display_knowl(label, cache=cache) + if len(opts) == 1: + return kwl + url = url_for(".family_page", label=self.label, gal=label) + return f'{kwl} (show {cnt})' + s = ", ".join(show_gal(label, cnt) for ((t, label), cnt) in opts) + if not self.all_hidden_data_available: + s += " (incomplete)" + return s + + @lazy_attribute + def means_display(self): + return latex_content(self.means).replace("[", r"\langle").replace("]", r"\rangle") + + @lazy_attribute + def rams_display(self): + return latex_content(self.rams).replace("[", "(").replace("]", ")") + + @lazy_attribute + def hidden_slopes(self): + fields, cache = self.fields + hidden = Counter(rec["hidden"] for rec in fields if "hidden" in rec) + if not hidden: + return "No hidden slopes in this family have been computed" + def show_hidden(x, cnt): + disp = latex_content(x) + if len(hidden) == 1: + return disp + url = url_for(".family_page", label=self.label, hidden=x) + return f'{disp} (show {cnt})' + s = ", ".join(show_hidden(x, cnt) for (x, cnt) in hidden.items()) + if not self.all_hidden_data_available: + s += " (incomplete)" + return s + + @lazy_attribute + def indices_of_insep(self): + fields, cache = self.fields + ii = sorted(Counter(tuple(rec["ind_of_insep"]) for rec in fields).items()) + def show_ii(x, cnt): + disp = str(x).replace(" ","").replace("[]", r"[\ ]") + if len(ii) == 1: + return f"${disp}$" + url = url_for(".family_page", label=self.label, ind_of_insep=disp, insep_quantifier="exactly") + return f'${disp}$ (show {cnt})' + return ", ".join(show_ii(list(x), cnt) for (x,cnt) in ii) + + @lazy_attribute + def associated_inertia(self): + fields, cache = self.fields + ai = sorted(Counter(tuple(rec["associated_inertia"]) for rec in fields).items()) + def show_ai(x, cnt): + disp = str(x).replace(" ","").replace("[]", r"[\ ]") + if len(ai) == 1: + return f"${disp}$" + url = url_for(".family_page", label=self.label, associated_inertia=disp) + return f'${disp}$ (show {cnt})' + return ", ".join(show_ai(list(x), cnt) for (x,cnt) in ai) + + @lazy_attribute + def jump_set(self): + fields, cache = self.fields + js = sorted(Counter(tuple(rec["jump_set"]) for rec in fields if "jump_set" in rec).items()) + def show_js(x, cnt): + if not x: + srch = "[]" + disp = "undefined" + else: + srch = str(x).replace(" ","") + disp = f"${srch}$" + if len(js) == 1: + return disp + url = url_for(".family_page", label=self.label, jump_set=srch) + return f'{disp} (show {cnt})' + s = ", ".join(show_js(list(x), cnt) for (x,cnt) in js) + if any("jump_set" not in rec for rec in fields): + s += " (incomplete)" + return s + + @lazy_attribute + def gal_slope_tables(self): + from .main import LFStats + stats = LFStats() + fields, cache = self.fields + gps = defaultdict(set) + slopes = defaultdict(set) + for rec in fields: + if rec.get("galois_degree") is not None and rec.get("galois_label") is not None and rec.get("slopes") is not None: + gps[rec["galois_degree"]].add(rec["galois_label"]) + slopes[rec["galois_degree"]].add(rec["slopes"]) + Ns = sorted(gps) + dyns = [] + for N in Ns: + attr = { + 'cols': ['hidden', 'galois_label'], + 'constraint': {'family': self.label, 'galois_degree': N}, + 'totaler': totaler(row_counts=(len(gps[N]) > 1), col_counts=(len(slopes[N]) > 1), row_proportions=False, col_proportions=False), + 'col_title': f'Galois groups of order {N}' if len(Ns) > 1 else 'Galois groups', + } + dyns.append((N, stats.prep(attr))) + return dyns diff --git a/lmfdb/local_fields/main.py b/lmfdb/local_fields/main.py index 5b942b3c52..4555c665ff 100644 --- a/lmfdb/local_fields/main.py +++ b/lmfdb/local_fields/main.py @@ -3,22 +3,26 @@ from flask import abort, render_template, request, url_for, redirect from sage.all import ( - PolynomialRing, ZZ, QQ, RR, latex, cached_function, Integers, is_prime) -from sage.plot.all import line, points, text, Graphics + PolynomialRing, ZZ, QQ, RR, latex, cached_function, Integers, euler_phi, is_prime) +from sage.plot.all import line, points, text, Graphics, polygon from lmfdb import db from lmfdb.app import app from lmfdb.utils import ( web_latex, coeff_to_poly, teXify_pol, display_multiset, display_knowl, - parse_inertia, parse_newton_polygon, parse_bracketed_posints, - parse_galgrp, parse_ints, clean_input, parse_rats, flash_error, - SearchArray, TextBox, TextBoxWithSelect, SubsetBox, TextBoxNoEg, CountBox, to_dict, comma, - search_wrap, Downloader, StatsDisplay, totaler, proportioners, encode_plot, + parse_inertia, parse_newton_polygon, parse_bracketed_posints, parse_floats, parse_regex_restricted, + parse_galgrp, parse_ints, clean_input, parse_rats, parse_noop, flash_error, + SearchArray, TextBox, TextBoxWithSelect, SubsetBox, SelectBox, SneakyTextBox, + HiddenBox, TextBoxNoEg, CountBox, to_dict, comma, + search_wrap, count_wrap, embed_wrap, Downloader, StatsDisplay, totaler, proportioners, encode_plot, + EmbeddedSearchArray, integer_options, redirect_no_cache, raw_typeset) from lmfdb.utils.interesting import interesting_knowls -from lmfdb.utils.search_columns import SearchColumns, LinkCol, MathCol, ProcessedCol, MultiProcessedCol, ListCol, PolynomialCol, eval_rational_list +from lmfdb.utils.search_columns import SearchColumns, LinkCol, MathCol, ProcessedCol, MultiProcessedCol, RationalListCol, PolynomialCol, eval_rational_list +from lmfdb.utils.search_parsing import search_parser from lmfdb.api import datapage from lmfdb.local_fields import local_fields_page, logger +from lmfdb.local_fields.family import pAdicSlopeFamily, FAMILY_RE, latex_content, content_unformatter from lmfdb.groups.abstract.main import abstract_group_display_knowl from lmfdb.galois_groups.transitive_group import ( transitive_group_display_knowl, group_display_inertia, @@ -28,7 +32,8 @@ WebNumberField, string2list, nf_display_knowl) import re -LF_RE = re.compile(r'^\d+\.\d+\.\d+\.\d+$') +OLD_LF_RE = re.compile(r'^\d+\.\d+\.\d+\.\d+$') +NEW_LF_RE = re.compile(r'^\d+\.\d+\.\d+\.\d+[a-z]+\d+\.\d+$') def get_bread(breads=[]): bc = [("$p$-adic fields", url_for(".index"))] @@ -62,6 +67,25 @@ def lf_formatfield(coef): return raw_typeset(thepoly, thepolylatex) return nf_display_knowl(thefield.get_label(),thepolylatex) +# Takes a string '[2,5/2]' +def artin2swan(li): + if li is not None: + l1 = li.replace('[', '') + l1 = l1.replace(']', '') + l1 = l1.replace(' ', '') + if l1 == '': + return [] + return '[' + ','.join([str(QQ(z)-1) for z in l1.split(',')]) + ']' + +def hidden2swan(hid): + if hid is not None: + parts = hid.split(']') + a = parts[0].replace('[', '') + a = a.replace(' ', '') + if a == '': + return hid + return '[' + ','.join([str(QQ(z)-1) for z in a.split(',')]) + ']' + parts[1] + def local_algebra_data(labels): labs = labels.split(',') f1 = labs[0].split('.') @@ -70,22 +94,47 @@ def local_algebra_data(labels): ans += '$%s$-adic algebra' % str(f1[0]) ans += '' ans += '

' - ans += "
LabelPolynomial$e$$f$$c$$G$Slopes" - fall = [db.lf_fields.lookup(label) for label in labs] - for f in fall: - l = str(f['label']) - ans += '
%s' % (url_for_label(l),l) + ans += "' % lab + continue + if f.get('new_label'): + l = str(f['new_label']) + else: + l = str(f['old_label']) + ans += '
LabelPolynomial$e$$f$$c$$G$Artin slopes" + if all(OLD_LF_RE.fullmatch(lab) for lab in labs): + fall = {rec["old_label"]: rec for rec in db.lf_fields.search({"old_label":{"$in": labs}})} + elif all(NEW_LF_RE.fullmatch(lab) for lab in labs): + fall = {rec["new_label"]: rec for rec in db.lf_fields.search({"new_label":{"$in": labs}})} + else: + fall = {} + for lab in labs: + if OLD_LF_RE.fullmatch(lab): + fall[lab] = db.lf_fields.lucky({"old_label":lab}) + elif NEW_LF_RE.fullmatch(lab): + fall[lab] = db.lf_fields.lucky({"new_label":lab}) + else: + fall[lab] = None + for lab in labs: + f = fall[lab] + if f is None: + ans += '
Invalid label %s
%s'%(url_for_label(l),l) ans += format_coeffs(f['coeffs']) ans += '%d%d%d' % (f['e'],f['f'],f['c']) ans += transitive_group_display_knowl(f['galois_label']) - ans += '$' + show_slope_content(f['slopes'],f['t'],f['u'])+'$' + if f.get('slopes') and f.get('t') and f.get('u'): + ans += '$' + show_slope_content(f['slopes'],f['t'],f['u'])+'$' ans += '
' if len(labs) != len(set(labs)): ans += '

Fields which appear more than once occur according to their given multiplicities in the algebra' return ans def local_field_data(label): - f = db.lf_fields.lookup(label) + if OLD_LF_RE.fullmatch(label): + f = db.lf_fields.lucky({"old_label": label}) + elif NEW_LF_RE.fullmatch(label): + f = db.lf_fields.lucky({"new_label": label}) + else: + return "Invalid label %s" % label nicename = '' if f['n'] < 3: nicename = ' = ' + prettyname(f) @@ -96,7 +145,7 @@ def local_field_data(label): ans += 'Ramification index $e$: %s
' % str(f['e']) ans += 'Residue field degree $f$: %s
' % str(f['f']) ans += 'Discriminant ideal: $(p^{%s})$
' % str(f['c']) - if f.get('galois_label'): + if f.get('galois_label') is not None: gt = int(f['galois_label'].split('T')[1]) ans += 'Galois group $G$: %s
' % group_pretty_and_nTj(gn, gt, True) else: @@ -121,8 +170,10 @@ def eisensteinformlatex(pol, unram): R = PolynomialRing(QQ, 'y') Rx = PolynomialRing(R, 'x') unram2 = R(unram.replace('t', 'y')) - unram = latex(Rx(unram.replace('t', 'x'))) pol = R(pol) + if unram2.degree() == 1 or unram2.degree() == pol.degree(): + return latex(pol).replace('y', 'x') + unram = latex(Rx(unram.replace('t', 'x'))) l = [] while pol != 0: qr = pol.quo_rem(unram2) @@ -133,7 +184,8 @@ def eisensteinformlatex(pol, unram): newpol = newpol.replace('y', 'x') return newpol -def plot_polygon(verts, polys, inds, p): +def plot_ramification_polygon(verts, p, polys=None, inds=None): + # print("VERTS", verts) verts = [tuple(pt) for pt in verts] if not verts: # Unramified, so we won't be displaying the plot @@ -142,9 +194,9 @@ def plot_polygon(verts, polys, inds, p): ymax = verts[0][1] xmax = verts[-1][0] # How far we need to shift text depends on the scale - txshift = xmax / 60 + txshift = xmax / 80 tyshift = xmax / 48 - tick = xmax / 160 + #tick = xmax / 160 nextq = p L = Graphics() if ymax > 0: @@ -153,64 +205,81 @@ def plot_polygon(verts, polys, inds, p): # Add in silly white dot L += points([(0,1)], color="white") asp_ratio = (xmax + 2*txshift) / (8 + 16*tyshift) - L += line([(0,0), (0, ymax)], color="grey") - L += line([(0,0), (xmax, 0)], color="grey") - for i in range(1, ymax + 1): - L += line([(0, i), (tick, i)], color="grey") - for i in range(xmax + 1): - L += line([(i, 0), (i, tick/asp_ratio)], color="grey") - for P in verts: + for i in range(xmax+1): + L += line([(-i, 0), (-i, ymax)], color=(0.85,0.85,0.85), thickness=0.5) + for j in range(ymax+1): + L += line([(0,j), (-xmax, j)], color=(0.85,0.85,0.85), thickness=0.5) + #L += line([(0,0), (0, ymax)], color="grey") + #L += line([(0,0), (-xmax, 0)], color="grey") + #for i in range(1, ymax + 1): + # L += line([(0, i), (-tick, i)], color="grey") + #for i in range(0, xmax + 1): + # L += line([(-i, 0), (-i, tick/asp_ratio)], color="grey") + xticks = set(P[0] for P in verts) + yticks = set(P[1] for P in verts) + if inds is not None: + xticks = xticks.union(p**i for i in range(len(inds))) + yticks = yticks.union(ind for ind in inds) + for x in xticks: L += text( - f"${P[0]}$", (P[0], -tyshift/asp_ratio), + f"${-x}$", (-x, -tyshift/asp_ratio), color="black") + for y in yticks: L += text( - f"${P[1]}$", (-txshift, P[1]), - horizontal_alignment="right", + f"${y}$", (txshift, y), + horizontal_alignment="left", color="black") - R = ZZ["t"]["z"] - polys = [R(poly) for poly in polys] - - def restag(c, a, b): - return text(f"${latex(c)}$", (a + txshift, b + tyshift/asp_ratio), - horizontal_alignment="left", - color="black") - L += restag(polys[0][0], 1, ymax) + + if polys is not None: + R = ZZ["t"]["z"] + polys = [R(poly) for poly in reversed(polys)] + # print("POLYS", polys) + + def restag(c, a, b): + return text(f"${latex(c)}$", (-a - txshift, b + tyshift/asp_ratio), + horizontal_alignment="left", + color="black") + L += restag(polys[0][0], 1, ymax) for i in range(len(verts) - 1): P = verts[i] Q = verts[i+1] slope = ZZ(P[1] - Q[1]) / ZZ(Q[0] - P[0]) # actually the negative of the slope d = slope.denominator() - poly = polys[i] if slope != 0: - while nextq <= Q[0]: - i = (nextq - P[0]) / d - if i in ZZ and poly[i]: - L += restag(poly[i], nextq, P[1] - (nextq - P[0]) * slope) - nextq *= p + if polys is not None: + # Need to check that this is compatible with the residual polynomial normalization + while nextq <= Q[0]: + j = (nextq - P[0]) / d + if j in ZZ and polys[i][j]: + L += restag(polys[i][j], nextq, P[1] - (nextq - P[0]) * slope) + nextq *= p L += text( - f"${slope}$", (P[0] - txshift, (P[1] + Q[1]) / 2), - horizontal_alignment="right", + f"${slope}$", (-(P[0] + Q[0]) / 2 + txshift, (P[1] + Q[1]) / 2 - tyshift/(2*asp_ratio)), + horizontal_alignment="left", color="blue") - for x in range(P[0], Q[0] + 1): - L += line( - [(x, Q[1]), (x, P[1] - (x - P[0]) * slope)], - color="grey", - ) - for y in range(Q[1], P[1]): - L += line( - [(P[0] - (y - P[1]) / slope, y), (P[0], y)], - color="grey", - ) - else: + #for x in range(P[0], Q[0] + 1): + # L += line( + # [(-x, Q[1]), (-x, P[1] - (x - P[0]) * slope)], + # color="grey", + # ) + #for y in range(Q[1], P[1]): + # L += line( + # [(-P[0] + (y - P[1]) / slope, y), (-P[0], y)], + # color="grey", + # ) + elif polys: # For tame inertia, the coefficients can occur at locations other than powers of p - for i, c in enumerate(poly): - if i and c: - L += restag(c, P[0] + i, P[1]) - L += line(verts, thickness=2) - L += points([(p**i, ind) for i, ind in enumerate(inds)], size=30, color="black") + for j, c in enumerate(polys[i]): + if j and c: + L += restag(c, P[0] + j, P[1]) + L += line([(-x,y) for (x,y) in verts], thickness=2) + L += polygon([(-x,y) for (x,y) in verts] + [(-xmax, ymax)], alpha=0.08) + if inds is not None: + # print("INDS", inds) + L += points([(-p**i, ind) for (i, ind) in enumerate(inds)], size=30, color="black", zorder=5) L.axes(False) L.set_aspect_ratio(asp_ratio) - return encode_plot(L, pad=0, pad_inches=0, bbox_inches="tight", figsize=(8,4)) + return encode_plot(L, pad=0, pad_inches=0, bbox_inches="tight", figsize=(8,4), dpi=300) @app.context_processor @@ -221,7 +290,10 @@ def ctx_local_fields(): # Utilities for subfield display def format_lfield(label, p): - data = db.lf_fields.lookup(label) + if OLD_LF_RE.fullmatch(label): + data = db.lf_fields.lucky({"old_label": label}, ["n", "p", "rf", "old_label", "new_label"]) + else: + data = db.lf_fields.lucky({"new_label": label}, ["n", "p", "rf", "old_label", "new_label"]) return lf_display_knowl(label, name=prettyname(data)) @@ -259,24 +331,48 @@ def show_slopes2(sl): def show_slope_content(sl,t,u): if sl is None or t is None or u is None: - return ' $not computed$ ' # actually killing math mode + return 'not computed' sc = str(sl) - if sc == '[]': - sc = r'[\ ]' if t > 1: sc += '_{%d}' % t if u > 1: sc += '^{%d}' % u - return (sc) + return latex_content(sc) + +relative_columns = ["base", "n0", "e0", "f0", "c0", "label_absolute", "n_absolute", "e_absolute", "f_absolute", "c_absolute"] @local_fields_page.route("/") def index(): bread = get_bread() info = to_dict(request.args, search_array=LFSearchArray(), stats=LFStats()) + if any(col in info for col in relative_columns): + info["relative"] = 1 if len(request.args) != 0: - return local_field_search(info) + info["search_type"] = search_type = info.get("search_type", info.get("hst", "")) + if search_type in ['Families', 'FamilyCounts']: + info['search_array'] = FamiliesSearchArray(relative=("relative" in info)) + if search_type in ['Counts', 'FamilyCounts']: + return local_field_count(info) + elif search_type in ['Families', 'RandomFamily']: + return families_search(info) + elif search_type in ['List', '', 'Random']: + return local_field_search(info) + else: + flash_error("Invalid search type; if you did not enter it in the URL please report") + info["field_count"] = db.lf_fields.stats.column_counts(["n", "p"]) + info["family_count"] = db.lf_families.count({"n0":1}, groupby=["n", "p"]) return render_template("lf-index.html", title="$p$-adic fields", titletag="p-adic fields", bread=bread, info=info, learnmore=learnmore_list()) +@local_fields_page.route("/families/") +def family_redirect(): + info = to_dict(request.args) + info["search_type"] = "Families" + if "relative" not in info: + # Check for the presence of any relative-only arguments + if any(x in info for x in relative_columns): + info["relative"] = 1 + return redirect(url_for(".index", **info)) + @local_fields_page.route("/

+ Fields with $n>16$ and $p \mid n$ have not yet been added, except $n=p$ and $(p,n) = (2,18)$. Not all Galois groups and hidden slope information has been computed for $(p,n) = (2,16)$. +

+{% endblock %} + +{% block show_results %} + +{% include 'count_results.html' %} + +{% endblock %} diff --git a/lmfdb/local_fields/templates/lf-families.html b/lmfdb/local_fields/templates/lf-families.html new file mode 100644 index 0000000000..e825a86fcd --- /dev/null +++ b/lmfdb/local_fields/templates/lf-families.html @@ -0,0 +1,32 @@ +{% extends "homepage.html" %} + +{% block content %} + + + + + + + + + + + + + + {% for family in families %} + + + + + + + + + + + + {% endfor %} +
Label$e$$f$$c$Visible SlopesHeightsRamsNum. PolyNum. Fields
{{family.link|safe}}${{family.e}}$${{family.f}}$${{family.c}}$${{family.artin_slopes}}$${{family.heights}}$${{family.rams}}$${{family.poly_count}}$${{family.field_count}}$
+ +{% endblock %} diff --git a/lmfdb/local_fields/templates/lf-family.html b/lmfdb/local_fields/templates/lf-family.html new file mode 100644 index 0000000000..9728bf1911 --- /dev/null +++ b/lmfdb/local_fields/templates/lf-family.html @@ -0,0 +1,141 @@ +{% extends 'embedded_results.html' %} + +{% block embedding_content %} + +{% if family.e > 1 %} +

{{ KNOWL('lf.family_polynomial', 'Defining polynomial') }}{% if family.f > 1 %} over unramified subextension{% endif %}

+ + + +
${{ family.polynomial._latex_() }}$
+{% endif %} + +

{{ KNOWL('lf.family_invariants', 'Invariants') }}

+ + + + + + + + + + + + + + + +
{{ KNOWL('lf.residue_field', 'Residue field characteristic') }}: ${{family.p}}$
{{ KNOWL('lf.degree', 'Degree') }}: ${{family.n}}$
{{ KNOWL('lf.family_base', 'Base field') }}: {{family.base_link | safe}}
{{ KNOWL('lf.ramification_index', 'Ramification index') }} $e$: ${{family.e}}$
{{ KNOWL('lf.residue_field_degree', 'Residue field degree') }} $f$: ${{family.f}}$
{{ KNOWL('lf.discriminant_exponent', 'Discriminant exponent') }} $c$: ${{family.c}}$
{{ KNOWL('lf.slopes', 'Absolute Artin slopes' if family.n0 > 1 else 'Artin slopes') }}: {{info.latex_content(family.artin_slopes)}}
{{ KNOWL('lf.slopes', 'Swan slopes') }}: {{info.latex_content(family.slopes)}}
{{ KNOWL('lf.means', 'Means') }}: {{family.means_display}}
{{ KNOWL('lf.rams', 'Rams') }}: {{family.rams_display}}
{{ KNOWL('lf.family_field_count', 'Field count') }}: ${{family.field_count}}$ {% if family.all_stored %} (complete) {% else %} (incomplete) {% endif %}
{{ KNOWL('lf.family_ambiguity', 'Ambiguity') }}: ${{family.ambiguity}}$
{{ KNOWL('lf.family_mass', 'Mass') }}: ${{family.mass_relative_display}}$
{{ KNOWL('lf.family_mass', 'Absolute Mass') }}: ${{family.mass_absolute_display}}$ {% if not family.all_stored %}(${{family.mass_stored}}$ currently in the LMFDB){% endif %}
+ +{% if family.w > 0 %} +

{{ KNOWL('lf.family_diagrams', 'Diagrams') }}

+ +
+ + + +
+
+ +
+ + +{% endif %} + +{% if family.field_count > 0 %} + +

{{ KNOWL('lf.family_varying', 'Varying') }}

+{% if not family.all_stored %} +

The following invariants arise for fields within the LMFDB; since not all fields in this family are stored, it may be incomplete.

+{% endif %} +{% if family.n0 > 1 %} +

These invariants are all associated to absolute extensions of $\Q_{ {{family.p}} }$ within this relative family, not the relative extension.

+{% endif %} + + {% if family.n0 > 1 and family.some_hidden_data_available %} + + + {% endif %} + + + +
{{ KNOWL('nf.galois_group', 'Galois group') }}: {{ family.galois_groups | safe }}
{{ KNOWL('lf.hidden_slopes', 'Hidden Artin slopes') }}: {{ family.hidden_slopes | safe }}
{{ KNOWL('lf.indices_of_inseparability', 'Indices of inseparability') }}: {{ family.indices_of_insep | safe }}
{{ KNOWL('lf.associated_inertia', 'Associated inertia') }}: {{ family.associated_inertia | safe }}
{{ KNOWL('lf.jump_set', 'Jump Set') }}: {{ family.jump_set | safe }}
+ +{% if family.n0 == 1 and family.some_hidden_data_available %} +

{{ KNOWL('lf.packet', 'Galois groups and Hidden Artin slopes') }}

+ +{% if (family.gal_slope_tables|length) > 1 %} +

Select desired size of Galois group.{% if not family.all_hidden_data_available %} Note that the following data has not all been computed for fields in this family, so the tables below are incomplete.{% endif %}

+
+ {% for N, d in family.gal_slope_tables %} + + {% endfor %} + +
+{% elif not family.all_hidden_data_available %} +

Note that the following data has not all been computed for fields in this family, so the table below is incomplete.

+{% endif %} + +{% for N, d in family.gal_slope_tables %} + +{% endfor %} + +{% endif %} {# family.n0 == 1 #} + +

{{ KNOWL('lf.family_polynomial', 'Fields') }}

+ +{% else %} {# family.field_count = 0 #} + +

+ The LMFDB does not contain any fields from this family. +

+ +{% endif %} + + + +{% endblock %} + +{% block post_results_content %} + +{# +{% if family.poly_count < 100 %} +

Polynomials

+ + + {% for poly in family %} + + {% endfor %} +
${{ poly._latex_() }}$
+ +{% endif %} +#} + +{% endblock %} diff --git a/lmfdb/local_fields/templates/lf-index.html b/lmfdb/local_fields/templates/lf-index.html index 99a8eb83fb..4c3ddbcd92 100644 --- a/lmfdb/local_fields/templates/lf-index.html +++ b/lmfdb/local_fields/templates/lf-index.html @@ -1,3 +1,4 @@ + {% extends "homepage.html" %} {% block content %} @@ -6,85 +7,58 @@ {{ info.stats.short_summary | safe }} -

Browse

+

Browse fields

-

-The table gives for each $p$ and $n$ shown, the number of degree $n$ extension -fields -of $\Q_p$ up to isomorphism. +

+This table gives, for each $p$ and $n$ shown, the number of degree $n$ extension +fields of $\Q_p$ up to isomorphism. Here are more counts for larger $p$ and $n$.

-

- +{# We want the columns in the field and family tables to align, which requires some circumlocutions (we end up making them one table, but have to mess with the layout to insert a header and paragraph in between) #} + +
- - - - - - + + + {% for n in range(2,24) %} + + {% endfor %} + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + {% for p in [2,3,5,7] %} + + + {% for n in range(2,24) %} + + {% endfor %} + + {% endfor %} + + + + + + {% for n in range(2,24) %} + + {% endfor %} + + {% for p in [2,3,5,7] %} + + + {% for n in range(2,24) %} + + {% endfor %} + + {% endfor %} +
$p$ \ $n$2 3 4 5 67 8 9 10 1112 13 14 15
$p$ \ $n${{n}}
2 7 2 59 2 47 2 1823 3 158 2 549325904
33 10 5 2 75 2 8 795 6 2 785 261172
53 2 7 26 7 2 11 3 258 2 17 261012
73 4 5 2 12 50 8 7 6 2 20 26548
113 2 5 6 7 2 8 3 18 122 13 2612
133 4 7 2 12 2 11 7 6 2 28 17098
173 2 7 2 7 2 15 3 6 2 17 264
193 4 5 2 12 2 8 13 8 2 20 268
233 2 5 2 7 2 8 3 6 12 13 264
{{p}}{{info.field_count[n,p]}}
+

Browse families

+

This table gives the number of degree $n$ {{KNOWL('lf.family_polynomial', 'families')}} over $\Q_p$. Here are more counts for larger $p$ and $n$, and search pages for absolute families and relative families.

+
$p$ \ $n${{n}}
{{p}}{{info.family_count[n,p]}}
-

-Some interesting $p$-adic fields or a random $p$-adic field +Some interesting $p$-adic fields, a random $p$-adic field, a random absolute family, a random relative family

{{KNOWL('intro.search', 'Search')}}

diff --git a/lmfdb/local_fields/templates/lf-show-field.html b/lmfdb/local_fields/templates/lf-show-field.html index 062be05c83..b7d34ff596 100644 --- a/lmfdb/local_fields/templates/lf-show-field.html +++ b/lmfdb/local_fields/templates/lf-show-field.html @@ -12,7 +12,7 @@

{{ KNOWL('lf.invariants', title='Invariants') }}

- + @@ -37,7 +37,12 @@

{{ KNOWL('lf.invariants', title='Invariants') }}

{% endif %} - + + + + + +
Base field: {{info.base|safe}}
{{ KNOWL('lf.degree', title='Degree') }} $d$: ${{info.n}}$
{{ KNOWL('lf.ramification_index', title='Ramification exponent') }} $e$: ${{info.e}}$
{{ KNOWL('lf.ramification_index', title='Ramification index') }} $e$: ${{info.e}}$
{{ KNOWL('lf.residue_field_degree', title='Residue field degree') }} $f$: ${{info.f}}$
{{ KNOWL('lf.discriminant_exponent', title='Discriminant exponent') }} $c$: ${{info.c}}$
{{ KNOWL('lf.discriminant_root_field', title='Discriminant root field') }}: {{info.rf|safe}}
{{info.galphrase}}
{{ KNOWL('lf.visible_slopes', title='Visible slopes')}}:{{info.visible}}
{{ KNOWL('lf.slopes', title='Visible Artin slopes')}}:{{info.visible}}
{{ KNOWL('lf.slopes', title='Visible Swan slopes')}}:{{info.visible_swan}}
{{ KNOWL('lf.means', title='Means')}}:{{info.means}}
{{ KNOWL('lf.rams', title='Rams')}}:{{info.rams}}
{{ KNOWL('lf.jump_set', title='Jump set')}}:{{info.jump_set}}
{{ KNOWL('lf.roots_of_unity', title='Roots of unity') }}:{{info.roots_of_unity}}

{{ KNOWL('lf.intermediate_fields', title='Intermediate fields') }}

@@ -60,7 +65,7 @@

{{ KNOWL('lf.intermediate_fields', title='Intermediate fields') }}

{% endif %} -

{{ KNOWL('lf.unramified_totally_ramified_tower', title='Unramified/totally ramified tower') }}

+

{{ KNOWL('lf.unramified_totally_ramified_tower', title='Canonical tower') }}

@@ -71,7 +76,7 @@

{{ KNOWL('lf.ramification_polygon_display', title='Ramification polygon') }} {% if info.e > 1 %} - {% if info.ram_poly_polt %} + {% if info.ram_polygon_plot %}

{{ KNOWL('lf.unramified_subfield', title='Unramified subfield')}}:{{info.unram|safe}}
Relative {{ KNOWL('lf.eisenstein_polynomial', 'Eisenstein polynomial')}}:{{info.eisen|safe}}
@@ -80,7 +85,7 @@

{{ KNOWL('lf.ramification_polygon_display', title='Ramification polygon') }}

{{ KNOWL('lf.indices_of_inseparability', 'Indices of inseparability')}}:{{info.ind_insep|safe}}
{% else %} - Not computed + not computed {% endif %} @@ -90,55 +95,68 @@

{{ KNOWL('lf.ramification_polygon_display', title='Ramification polygon') }} {% endif %} -

{{ KNOWL('lf.galois_invariants', title='Invariants of the Galois closure') }}

+

{{ KNOWL('lf.galois_closure_invariants', title='Invariants of the Galois closure') }}

+ - - - diff --git a/lmfdb/local_fields/templates/padic-refine-search.html b/lmfdb/local_fields/templates/padic-refine-search.html new file mode 100644 index 0000000000..0d95f997d9 --- /dev/null +++ b/lmfdb/local_fields/templates/padic-refine-search.html @@ -0,0 +1,18 @@ +{% extends 'homepage.html' %} + +{% block content %} + +{% if info.stats %} +

+{{info.stats|safe}} +

+{% endif %} + +{% block top_matter %}{% endblock %} + +{% include 'refine_search_form.html' %} + +{% block show_results %}{% endblock %} +{% include 'debug_info.html' %} + +{% endblock %} diff --git a/lmfdb/local_fields/test_localfields.py b/lmfdb/local_fields/test_localfields.py index f7650545c9..7653e79734 100644 --- a/lmfdb/local_fields/test_localfields.py +++ b/lmfdb/local_fields/test_localfields.py @@ -12,50 +12,50 @@ def test_search_ramif_cl_deg(self): def test_search_f(self): L = self.tc.get('/padicField/?n=6&p=2&f=3') dat = L.get_data(as_text=True) - assert '2.6.4.1' not in dat - assert '2.6.6.2' in dat + assert '2.2.3.4a1.1' not in dat + assert '2.3.2.6a1.2' in dat def test_search_top_slope(self): L = self.tc.get('/padicField/?p=2&topslope=3.5') - assert '2.4.9.1' in L.get_data(as_text=True) # number of matches + assert '2.1.4.9a1.1' in L.get_data(as_text=True) # number of matches L = self.tc.get('/padicField/?p=2&topslope=3.4..3.55') - assert '2.4.9.1' in L.get_data(as_text=True) # number of matches + assert '2.1.4.9a1.1' in L.get_data(as_text=True) # number of matches L = self.tc.get('/padicField/?p=2&topslope=7/2') - assert '2.4.9.1' in L.get_data(as_text=True) # number of matches + assert '2.1.4.9a1.1' in L.get_data(as_text=True) # number of matches def test_field_page(self): - L = self.tc.get('/padicField/11.6.4.2') - assert '11.6.4.2' in L.get_data(as_text=True) - #assert 'x^{2} - x + 7' in L.get_data(as_text=True) # bad (not robust) test, but it's the best i was able to find... - #assert 'x^{3} - 11 t' in L.get_data(as_text=True) # bad (not robust) test, but it's the best i was able to find... + L = self.tc.get('/padicField/11.6.4.2', follow_redirects=True) + assert '11.2.3.4a1.1' in L.get_data(as_text=True) + assert 'x^{2} + 7 x + 2' in L.get_data(as_text=True) # bad (not robust) test, but it's the best i was able to find... + assert 'x^{3} + 44 t + 99' in L.get_data(as_text=True) # bad (not robust) test, but it's the best i was able to find... def test_global_splitting_models(self): # The first one will have to change if we compute a GSM for it - L = self.tc.get('/padicField/163.8.7.2') - assert 'Not computed' in L.get_data(as_text=True) - L = self.tc.get('/padicField/2.8.0.1') + L = self.tc.get('/padicField/163.1.8.7a1.2') + assert 'not computed' in L.get_data(as_text=True) + L = self.tc.get('/padicField/2.8.1.0a1.1') assert 'Does not exist' in L.get_data(as_text=True) def test_underlying_data(self): - page = self.tc.get('/padicField/11.6.4.2').get_data(as_text=True) - assert 'Underlying data' in page and 'data/11.6.4.2' in page + page = self.tc.get('/padicField/11.2.3.4a1.2').get_data(as_text=True) + assert 'Underlying data' in page and 'data/11.2.3.4a1.2' in page def test_search_download(self): page = self.tc.get('/padicField/?Submit=gp&download=1&query=%7B%27p%27%3A+2%2C+%27n%27%3A+2%7D&n=2&p=2').get_data(as_text=True) - assert '''columns = ["label", "coeffs", "p", "e", "f", "c", "gal", "slopes"]; + assert '''columns = ["label", "coeffs", "p", "f", "e", "c", "gal", "slopes"]; data = {[ -["2.2.0.1", [1, 1, 1], 2, 1, 2, 0, [2, 1], [[], 1, 2]], -["2.2.2.1", [2, 2, 1], 2, 2, 1, 2, [2, 1], [[2], 1, 1]], -["2.2.2.2", [6, 2, 1], 2, 2, 1, 2, [2, 1], [[2], 1, 1]], -["2.2.3.1", [2, 4, 1], 2, 2, 1, 3, [2, 1], [[3], 1, 1]], -["2.2.3.2", [10, 4, 1], 2, 2, 1, 3, [2, 1], [[3], 1, 1]], -["2.2.3.3", [2, 0, 1], 2, 2, 1, 3, [2, 1], [[3], 1, 1]], -["2.2.3.4", [10, 0, 1], 2, 2, 1, 3, [2, 1], [[3], 1, 1]] +["2.2.1.0a1.1", [1, 1, 1], 2, 2, 1, 0, [2, 1], [[], 1, 2]], +["2.1.2.2a1.1", [2, 2, 1], 2, 1, 2, 2, [2, 1], [[2], 1, 1]], +["2.1.2.2a1.2", [6, 2, 1], 2, 1, 2, 2, [2, 1], [[2], 1, 1]], +["2.1.2.3a1.1", [2, 0, 1], 2, 1, 2, 3, [2, 1], [[3], 1, 1]], +["2.1.2.3a1.2", [10, 0, 1], 2, 1, 2, 3, [2, 1], [[3], 1, 1]], +["2.1.2.3a1.3", [2, 4, 1], 2, 1, 2, 3, [2, 1], [[3], 1, 1]], +["2.1.2.3a1.4", [10, 4, 1], 2, 1, 2, 3, [2, 1], [[3], 1, 1]] ]}; create_record(row) = { - out = Map(["label",row[1];"coeffs",row[2];"p",row[3];"e",row[4];"f",row[5];"c",row[6];"gal",row[7];"slopes",row[8]]); + out = Map(["label",row[1];"coeffs",row[2];"p",row[3];"f",row[4];"e",row[5];"c",row[6];"gal",row[7];"slopes",row[8]]); field = Polrev(mapget(out, "coeffs")); mapput(~out, "field", field); return(out);''' in page diff --git a/lmfdb/number_fields/templates/nf-show-field.html b/lmfdb/number_fields/templates/nf-show-field.html index d1787ab36d..cb4ca30a9b 100644 --- a/lmfdb/number_fields/templates/nf-show-field.html +++ b/lmfdb/number_fields/templates/nf-show-field.html @@ -278,7 +278,7 @@

{{ KNOWL('nf.sibling', title='Sibling') }} fields

- + {{ info.loc_alg | safe }} diff --git a/lmfdb/static/groups.js b/lmfdb/static/groups.js index ecf99ddbf2..a16d8a9310 100644 --- a/lmfdb/static/groups.js +++ b/lmfdb/static/groups.js @@ -159,18 +159,6 @@ function getpositions() { } var styles=['subgroup_diagram', 'subgroup_profile', 'subgroup_autdiagram', 'subgroup_autprofile', 'normal_diagram', 'normal_profile', 'normal_autdiagram', 'normal_autprofile']; -function button_on(who) { - $('button.'+who).css('background', '{{color.lf_an_button_bkg}}'); - $('button.'+who).css('border', '2px solid {{color.lf_an_button_brd}}'); -} -function other_buttons_off(keep) { - for (var i = 0; i < styles.length; i++) { - if(styles[i] != keep) { - $('button.'+styles[i]).css('background', '{{color.lf_ar_button_bkg}}'); - $('button.'+styles[i]).css('border', '1px solid {{color.lf_ar_button_brd}}'); - } - } -} var mode_pairs = [['subgroup', 'normal'], ['', 'aut'], ['diagram', 'profile']]; function select_subgroup_mode(mode) { var cls, thismode, opposite_mode, piece; diff --git a/lmfdb/static/lmfdb.js b/lmfdb/static/lmfdb.js index 68eabf5489..79429351ca 100644 --- a/lmfdb/static/lmfdb.js +++ b/lmfdb/static/lmfdb.js @@ -358,7 +358,11 @@ function get_count_of_results() { $("span.download-msg").html("Computing number of results..."); if (address.slice(-1) === "#") address = address.slice(0,-1); - address += "&result_count=1"; + if (address.includes("?")) { + address += "&result_count=1"; + } else { + address += "?result_count=1"; + } $.ajax({url: address, success: get_count_callback}); }; diff --git a/lmfdb/templates/count_results.html b/lmfdb/templates/count_results.html index 8b3c3d2ce9..049b5dbb20 100644 --- a/lmfdb/templates/count_results.html +++ b/lmfdb/templates/count_results.html @@ -26,8 +26,12 @@ n/a {% else %} {% set url = info.url_func(row, col) %} + {% if url is none %} + {{size}} + {% else %} {{size}} - {% endif %} + {% endif %} {# url none #} + {% endif %} {# size none #} {% endfor %} diff --git a/lmfdb/templates/embedded_results.html b/lmfdb/templates/embedded_results.html index a628c9953a..66be3e4a81 100644 --- a/lmfdb/templates/embedded_results.html +++ b/lmfdb/templates/embedded_results.html @@ -5,6 +5,7 @@ {% endblock %} +{% if info.number > 0 %} {{ info.columns.above_results | safe }} {% if info.columns.languages %} {% set languages = info.columns.languages %} @@ -27,10 +28,17 @@ {% endif %}
- {% if info.number > info.count or info.start != 0 -%} + {% if info.number > info.count or info.start != 0 or info.show_count -%}

- Showing {{ info.start + 1 }}-{{ upper_count }} of {{ info.number }} -

+ Showing {% if info.number <= info.count and info.start == 0 -%} + all {{ info.number }} + {% elif info.exact_count -%} + {{ info.start + 1 }}-{{ upper_count }} of {{ info.number }} + {% else -%} + {{ info.start + 1 }}-{{ upper_count }} of + at least {{ info.number }} + {% endif %} + {% endif %} {% include 'forward_back.html' %} {% include 'download_search_results.html' %} @@ -45,7 +53,13 @@

{% for col in C.columns_shown(info, rank) %} -

+ {% endfor %} {% endfor %} diff --git a/lmfdb/templates/stat_2d.html b/lmfdb/templates/stat_2d.html index 374cb47b10..9533fc32a0 100644 --- a/lmfdb/templates/stat_2d.html +++ b/lmfdb/templates/stat_2d.html @@ -1,5 +1,5 @@
{{ KNOWL('nf.galois_group', title='Galois degree')}}: + {% if info.galois_degree %} + ${{info.galois_degree}}$ + {% else %} + not computed + {% endif %} +
{{ KNOWL('nf.galois_group', title='Galois group')}}: {% if info.gal %} - {{info.gal|safe}} + {{info.gal|safe}} {% else %} - Not computed + not computed {% endif %}
{{ KNOWL('lf.inertia_group', title='Inertia group')}}: {% if info.inertia %} {{info.inertia|safe}} {% else %} - Not computed + not computed {% endif %}
{{ KNOWL('lf.wild_inertia_group', title='Wild inertia group')}}: {% if info.wild_inertia %} {{info.wild_inertia|safe}} {% else %} - Not computed + not computed {% endif %}
{{ KNOWL('lf.unramified_degree', title='Unramified degree')}}: +
{{ KNOWL('lf.unramified_degree', title='Galois unramified degree')}}: {% if info.u %} ${{info.u}}$ {% else %} - Not computed + not computed {% endif %}
{{ KNOWL('lf.tame_degree', title='Tame degree')}}: +
{{ KNOWL('lf.tame_degree', title='Galois tame degree')}}: {% if info.t %} ${{info.t}}$ {% else %} - Not computed + not computed {% endif %}
{{ KNOWL('lf.wild_slopes', title='Wild slopes')}}: +
{{ KNOWL('lf.slopes', title='Galois Artin slopes')}}: {% if info.slopes %} {{info.slopes}} {% else %} - Not computed + not computed + {% endif %} +
{{ KNOWL('lf.slopes', title='Galois Swan slopes')}}: + {% if info.slopes %} + {{info.swanslopes}} + {% else %} + not computed {% endif %}
{{ KNOWL('lf.galois_mean_slope', title='Galois mean slope')}}: {% if info.gms %} ${{info.gms}}$ {% else %} - Not computed + not computed {% endif %}
{{KNOWL('lf.galois_splitting_model', title='Galois splitting model')}}:{{info.gsm|safe}}
{{KNOWL("lf.residue_field_degree",title="$f$")}} {{KNOWL("lf.discriminant_exponent",title="$c$")}} {{KNOWL("nf.galois_group",title="Galois group")}}{{KNOWL("lf.slope_content",title="Slope content")}}{{KNOWL("lf.slopes",title="Slope content")}}
{{ col.display_knowl() | safe }}{{ col.display_knowl(info) | safe }}
- + @@ -12,26 +12,26 @@ {% if loop.index == 1 %} {% endif %} - + {% for c in row %} {% if c.count %} {% if c.query %} - + {% else %} - + {% endif %} {% else %} - + {% endif %} {% endfor %} {% for c in row %} - + {% endfor %} {% endfor %} {# d.grid #} diff --git a/lmfdb/templates/style.css b/lmfdb/templates/style.css index 0adf995ec6..09325378e7 100644 --- a/lmfdb/templates/style.css +++ b/lmfdb/templates/style.css @@ -1128,6 +1128,16 @@ table.statgrid th.chead { border: 1px solid #ccc; border-bottom: 2px solid {{color.table_ntdata_border}}; } +table.statgrid .totalrow { + border-top: 2px solid {{color.table_ntdata_border}}; +} +table.statgrid .totalcol { + border-left: 2px solid {{color.table_ntdata_border}}; +} +table.statgrid .totalcorner { + border-top: 2px solid {{color.table_ntdata_border}}; + border-left: 2px solid {{color.table_ntdata_border}}; +} table.statgrid td.prop { border-bottom: 1px solid #ccc; border-right: 1px solid #ccc; @@ -1950,7 +1960,7 @@ div.table-scroll-wrapper table.onesticky th:first-child::after, div.table-scroll table.nowrap td { white-space:nowrap; } -td.nowrap { +td.nowrap, th.nowrap, span.nowrap { white-space:nowrap; } table.ntdata td.nowrap a { @@ -2141,13 +2151,13 @@ div.sub_divider { margin-right: 20px; } -button.sub_active { +button.sub_active, button.dia_active, button.gs_active { background: {{color.lf_an_button_bkg}}; border: 2px solid {{color.lf_an_button_brd}}; cursor: pointer; } -button.sub_inactive { +button.sub_inactive, button.dia_inactive, button.gs_inactive { background: {{color.lf_ar_button_bkg}}; border: 1px solid {{color.lf_ar_button_brd}}; } diff --git a/lmfdb/utils/__init__.py b/lmfdb/utils/__init__.py index f0227b5cde..957e8c3f20 100644 --- a/lmfdb/utils/__init__.py +++ b/lmfdb/utils/__init__.py @@ -31,8 +31,9 @@ 'parse_galgrp', 'parse_nf_string', 'parse_subfield', 'parse_nf_elt', 'parse_nf_jinv', 'parse_container', 'parse_hmf_weight', 'parse_count', 'parse_newton_polygon', 'parse_start', 'parse_ints_to_list_flash', 'integer_options', + 'unparse_range', 'nf_string_to_label', 'clean_input', 'prep_ranges', - 'search_wrap', 'count_wrap', 'embed_wrap', + 'search_wrap', 'count_wrap', 'embed_wrap', 'yield_wrap', 'SearchArray', 'EmbeddedSearchArray', 'TextBox', 'TextBoxNoEg', 'TextBoxWithSelect', 'BasicSpacer', 'SkipBox', 'CheckBox', 'CheckboxSpacer', 'DoubleSelectBox', 'HiddenBox', 'SearchButton', 'SearchButtonWithSelect', 'RowSpacer', @@ -147,9 +148,10 @@ parse_nf_elt, parse_nf_jinv, parse_container, parse_hmf_weight, parse_count, parse_start, parse_ints_to_list_flash, integer_options, nf_string_to_label, parse_subfield, parse_interval, + unparse_range, clean_input, prep_ranges, input_string_to_poly) -from .search_wrapper import search_wrap, count_wrap, embed_wrap +from .search_wrapper import search_wrap, count_wrap, embed_wrap, yield_wrap from .search_boxes import ( SearchArray, EmbeddedSearchArray, TextBox, TextBoxNoEg, TextBoxWithSelect, BasicSpacer, SkipBox, CheckBox, CheckboxSpacer, DoubleSelectBox, HiddenBox, diff --git a/lmfdb/utils/display_stats.py b/lmfdb/utils/display_stats.py index 2e6dd3f047..50e053099b 100644 --- a/lmfdb/utils/display_stats.py +++ b/lmfdb/utils/display_stats.py @@ -291,7 +291,7 @@ def __call__(self, grid, row_headers, col_headers, stats): # Make the sums available for the column proportions stats._total_grid[i].append({'count':overall}) proportion = _format_percentage(total, overall) if col_proportions else '' - D = {'count':total, 'query':query, 'proportion':proportion} + D = {'count':total, 'query':query, 'proportion':proportion, 'extraclass':'totalcol', 'propclass':'totalcol'} row.append(D) if col_counts: row_headers.append(col_total_label) @@ -300,8 +300,12 @@ def __call__(self, grid, row_headers, col_headers, stats): row = [] for i, col in enumerate(zip(*grid)): # We've already totaled rows, so have to skip if we don't want the corner - if not corner_count and i == num_cols: - break + if i == num_cols: + if not corner_count: + break + extraclasses = {'extraclass': 'totalcorner', 'propclass': 'totalcol'} + else: + extraclasses = {'extraclass': 'totalrow'} total = sum(elt['count'] for elt in col) if total == 0: query = None @@ -313,6 +317,7 @@ def __call__(self, grid, row_headers, col_headers, stats): overall = sum(D['count'] for D in total_grid_cols[i]) proportion = _format_percentage(total, overall) if (col_proportions and i != num_cols or corner_prop and i == num_cols) else '' D = {'count':total, 'query':query, 'proportion':proportion} + D.update(extraclasses) row.append(D) grid.append(row) #if corner_count and row_counts and not col_counts: @@ -629,8 +634,9 @@ def prep(self, attr): data = self.display_data(**attr) attr['intro'] = attr.get('intro',[]) data['attribute'] = attr - if len(cols) == 1: + if 'row_title' not in attr: attr['row_title'] = self._short_display[cols[0]] + if len(cols) == 1: max_rows = attr.get('max_rows',6) counts = data['counts'] rows = [counts[i:i+10] for i in range(0, len(counts), 10)] @@ -641,8 +647,8 @@ def prep(self, attr): else: data['divs'] = [(rows, "short_table", "none")] elif len(cols) == 2: - attr['row_title'] = self._short_display[cols[0]] - attr['col_title'] = self._short_display[cols[1]] + if 'col_title' not in attr: + attr['col_title'] = self._short_display[cols[1]] return data @lazy_attribute diff --git a/lmfdb/utils/downloader.py b/lmfdb/utils/downloader.py index f0286d1b3b..00517f66b5 100644 --- a/lmfdb/utils/downloader.py +++ b/lmfdb/utils/downloader.py @@ -745,7 +745,7 @@ def __call__(self, info): seen.add(name) cols = [cols[i] for i in include] column_names = [column_names[i] for i in include] - data_format = [col.title for col in cols] + data_format = [(col.title if isinstance(col.title, str) else col.title(info)) for col in cols] first50 = [[col.download(rec) for col in cols] for rec in first50] if num_results > 10000: # Estimate the size of the download file. This won't necessarily be a great estimate @@ -820,10 +820,14 @@ def knowl_subber(match): else: knowl = col.download_desc if knowl: - if name.lower() == col.title.lower(): - yield lang.comment(f" {col.title} --\n") + if isinstance(col.title, str): + title = col.title else: - yield lang.comment(f"{col.title} ({name}) --\n") + title = col.title(info) + if name.lower() == title.lower(): + yield lang.comment(f" {title} --\n") + else: + yield lang.comment(f"{title} ({name}) --\n") for line in knowl.split("\n"): if line.strip(): yield lang.comment(" " + line.rstrip() + "\n") diff --git a/lmfdb/utils/search_boxes.py b/lmfdb/utils/search_boxes.py index b0df4c6f06..fd53d0c3ba 100644 --- a/lmfdb/utils/search_boxes.py +++ b/lmfdb/utils/search_boxes.py @@ -537,7 +537,11 @@ def _input(self, info): for col in C.columns_shown(info, use_rank): if col.short_title is None: # probably a spacer column: continue - title = col.short_title.replace("$", "").replace(r"\(", "").replace(r"\)", "").replace("\\", "") + if isinstance(col.short_title, str): + short_title = col.short_title + else: + short_title = col.short_title(info) + title = short_title.replace("$", "").replace(r"\(", "").replace(r"\)", "").replace("\\", "") if col.default(info): disp = "✓ " + title # The space is a unicode space the size of an emdash else: @@ -677,8 +681,11 @@ def sort_order(self, info): def _search_again(self, info, search_types): if info is None: return search_types - st = self._st(info) - return [(st, "Search again")] + [(v, d) for v, d in search_types if v != st] + mst = st = self._st(info) + # Sometimes need to treat empty string as equal to "List" + if not st and any(v == "List" for (v,d) in search_types): + mst = "List" + return [(st, "Search again")] + [(v, d) for v, d in search_types if v != mst] def search_types(self, info): # Override this method to change the displayed search buttons @@ -745,7 +752,7 @@ def _st(self, info): if info is not None: search_type = info.get("search_type", info.get("hst", "")) if search_type == "List": - # Backward compatibility + # Want to avoid including search_type in URL when possible search_type = "" return search_type diff --git a/lmfdb/utils/search_columns.py b/lmfdb/utils/search_columns.py index bb79312be5..75bae85c1b 100644 --- a/lmfdb/utils/search_columns.py +++ b/lmfdb/utils/search_columns.py @@ -18,6 +18,7 @@ (in the case of column groups). """ +import re from .web_display import display_knowl from lmfdb.utils import coeff_to_poly from sage.all import Rational, latex @@ -58,7 +59,7 @@ class SearchCol: and the default key used when extracting data from a database record. - ``knowl`` -- a knowl identifier, for displaying the column header as a knowl - ``title`` -- the string shown for the column header, also included when describing the column - in a download file. + in a download file. Alternatively, you can provide a function of info that produces such a string. - ``default`` -- either a boolean or a function taking an info dictionary as input and returning a boolean. In either case, this determines whether the column is displayed initially. See the ``get_default_func`` above. @@ -98,7 +99,12 @@ def __init__(self, name, knowl, title, default=True, align="left", self.knowl = knowl self.title = title if short_title is None: - short_title = None if title is None else title.lower() + if title is None: + short_title = None + elif isinstance(title, str): + short_title = title.lower() + else: + short_title = lambda info: title(info).lower() self.short_title = short_title self.default = get_default_func(default, name) self.mathmode = mathmode @@ -178,13 +184,17 @@ def display(self, rec): s = f"${s}$" return s - def display_knowl(self): + def display_knowl(self, info): """ Displays the column header contents. """ + if isinstance(self.title, str): + title = self.title + else: + title = self.title(info) if self.knowl: - return display_knowl(self.knowl, self.title) - return self.title + return display_knowl(self.knowl, title) + return title def show(self, info, rank=None): """ @@ -229,7 +239,7 @@ def __init__(self, name, **kwds): def display(self, rec): return "" - def display_knowl(self): + def display_knowl(self, info): return "" def show(self, info, rank=None): @@ -578,8 +588,44 @@ def split(x): class ListCol(ProcessedCol): """ + Used for lists that may be empty. + + The list may be stored in a postgres array or a postgres string + """ + def __init__(self, *args, **kwds): + if "delim" in kwds: + self.delim = kwds.pop("delim") + assert len(self.delim) == 2 + else: + self.delim = None + super().__init__(*args, **kwds) + + def display(self, rec): + s = str(self.func(self.get(rec))) + if s == "[]": + s = "[ ]" + if self.delim: + s = s.replace("[", self.delim[0]).replace("]", self.delim[1]) + if s and self.mathmode: + s = f"${s}$" + return s + +class RationalListCol(ListCol): + """ + For lists of rational numbers. + Uses the ``eval_rational_list`` function to process the column for downloading. """ + def __init__(self, name, knowl, title, func=None, apply_download=False, mathmode=True, use_frac=True, **kwds): + self.use_frac = use_frac + super().__init__(name, knowl, title, func=func, apply_download=apply_download, mathmode=mathmode, **kwds) + + def display(self, rec): + s = super().display(rec) + if self.use_frac: + s = re.sub(r"(\d+)/(\d+)", r"\\frac{\1}{\2}", s) + return s.replace("'", "").replace('"', '') + def download(self, rec): s = super().download(rec) return eval_rational_list(s) diff --git a/lmfdb/utils/search_parsing.py b/lmfdb/utils/search_parsing.py index 020e818349..295fb7af3d 100644 --- a/lmfdb/utils/search_parsing.py +++ b/lmfdb/utils/search_parsing.py @@ -37,7 +37,6 @@ FLOAT_RE = re.compile("^" + FLOAT_STR + "$") BRACKETING_RE = re.compile(r"(\[[^\]]*\])") # won't work for iterated brackets [[a,b],[c,d]] PREC_RE = re.compile(r"^-?((?:\d+(?:[.]\d*)?)|(?:[.]\d+))(?:e([-+]?\d+))?$") -LF_LABEL_RE = re.compile(r"^\d+\.\d+\.\d+\.\d+$") MULTISET_RE = re.compile(r"^(\d+)(\^(\d+))?(,(\d+)(\^(\d+))?)*$") class PowMulNodeVisitor(ast.NodeTransformer): @@ -70,6 +69,7 @@ def __init__( default_qfield, error_is_safe, clean_spaces, + angle_to_curly, ): self.f = f self.clean_info = clean_info @@ -81,6 +81,7 @@ def __init__( self.default_qfield = default_qfield self.error_is_safe = error_is_safe # Indicates that the message in raised exception contains no user input, so it is not escaped self.clean_spaces = clean_spaces + self.angle_to_curly = angle_to_curly def __call__(self, info, query, field=None, name=None, qfield=None, *args, **kwds): try: @@ -97,7 +98,7 @@ def __call__(self, info, query, field=None, name=None, qfield=None, *args, **kwd inp = str(inp) if SPACES_RE.search(inp): raise SearchParsingError("You have entered spaces in between digits. Please add a comma or delete the spaces.") - inp = clean_input(inp, self.clean_spaces) + inp = clean_input(inp, self.clean_spaces, self.angle_to_curly) if qfield is None: if field is None: qfield = self.default_qfield @@ -147,6 +148,7 @@ def search_parser( default_qfield=None, error_is_safe=False, clean_spaces=True, + angle_to_curly=False, ): return SearchParser( f, @@ -159,15 +161,19 @@ def search_parser( default_qfield, error_is_safe, clean_spaces, + angle_to_curly, ) # Remove whitespace for simpler parsing # Remove brackets to avoid tricks (so we can echo it back safely) -def clean_input(inp, clean_spaces=True): +def clean_input(inp, clean_spaces=True, angle_to_curly=False): if inp is None: return None if clean_spaces: - return re.sub(r"[\s<>]", "", str(inp)) + inp = re.sub(r"\s", "", str(inp)) + if angle_to_curly: + inp = re.sub("<", "{", inp) + return re.sub(">", "}", inp) else: return re.sub(r"[<>]", "", str(inp)) @@ -355,6 +361,21 @@ def parse_range2(arg, key, parse_singleton=int, parse_endpoint=None, split_minus else: return [key, parse_singleton(arg)] +def unparse_range(query_part, col_name=None): + """ + Given the output of parse_ints or other parser based on parse_range2, + return a lower and upper bound for the result. Either being None indicates no limit. + $or is not supported + """ + if isinstance(query_part, dict): + if "$or" in query_part: + msg = "Multiple ranges not supported" + if col_name: + msg += f" for {col_name}" + raise ValueError(msg) + return query_part.get("$gte"), query_part.get("$lte") + return query_part, query_part + # Like parse_range2, but to deal with strings which could be rational numbers # process is a function to apply to arguments after they have been parsed def parse_range2rat(arg, key, process): @@ -1155,12 +1176,16 @@ def parse_inertia(inp, query, qfield, err_msg=None): nt = aliases[inp2][0] query[iner_gap] = nt2abstract(nt[0], nt[1]) else: - # Check for Gap code - rematch = re.match(r"^\[(\d+),(\d+)\]$", inp) + # Check for Gap code using [a,b] or a.b notation + rematch = re.fullmatch(r"\[(\d+),(\d+)\]", inp) if rematch: query[iner_gap] = [int(rematch.group(1)), int(rematch.group(2))] else: - raise NameError + rematch = re.fullmatch(r"(\d+)\.(\d+)", inp) + if rematch: + query[iner_gap] = [int(rematch.group(1)), int(rematch.group(2))] + else: + raise NameError except NameError: if re.match(r"^[ACDFMQS]\d+$", inp): @@ -1168,18 +1193,20 @@ def parse_inertia(inp, query, qfield, err_msg=None): if err_msg: raise SearchParsingError(err_msg) else: - raise SearchParsingError("It needs to be a GAP id, such as [4,1] or [12,5], ia transitive group in nTj notation, such as 5T1, or a group label") + raise SearchParsingError("It needs to be a small group id, such as [4,1] or 12.5, ia transitive group in nTj notation, such as 5T1, or a group label") # see SearchParser.__call__ for actual arguments when calling @search_parser(clean_info=True, error_is_safe=True) def parse_padicfields(inp, query, qfield, flag_unramified=False): + from lmfdb.local_fields.main import NEW_LF_RE, OLD_LF_RE labellist = inp.split(",") doflash = False for label in labellist: - if not LF_LABEL_RE.match(label): + if not NEW_LF_RE.fullmatch(label) and not OLD_LF_RE.fullmatch(label): raise SearchParsingError('It needs to be a $p$-adic field label or a list of local field labels') splitlab = label.split('.') - if splitlab[2] == '0': + if (OLD_LF_RE.fullmatch(label) and splitlab[2] == '0' or + NEW_LF_RE.fullmatch(label) and splitlab[3][0] == '0'): doflash = True if flag_unramified and doflash: flash_info("Search results may be incomplete. Given $p$-adic completions contain an unramified field and completions are only searched for ramified primes.") diff --git a/lmfdb/utils/search_wrapper.py b/lmfdb/utils/search_wrapper.py index 8b3b49bf09..760c7365be 100644 --- a/lmfdb/utils/search_wrapper.py +++ b/lmfdb/utils/search_wrapper.py @@ -333,12 +333,18 @@ def __call__(self, info): if not isinstance(data, tuple): return data # error page query, sort, table, title, err_title, template, one_per = data + groupby = query.pop("__groupby__", self.groupby) template_kwds = {key: info.get(key, val()) for key, val in self.kwds.items()} try: if query: - res = table.count(query, groupby=self.groupby) + res = table.count(query, groupby=groupby) else: - res = table.stats.column_counts(self.groupby) + # We want to use column_counts since it caches results, but it also sorts the input columns and doesn't adjust the results + res = table.stats.column_counts(groupby) + sgroupby = sorted(groupby) + if sgroupby != groupby: + perm = [sgroupby.index(col) for col in groupby] + res = {tuple(key[i] for i in perm): val for (key, val) in res.items()} except QueryCanceledError as err: return self.query_cancelled_error( info, query, err, err_title, template, template_kwds @@ -355,7 +361,7 @@ def __call__(self, info): res[row, col] = 0 else: res[row, col] = None - info['count'] = 50 # put count back in so that it doesn't show up as none in url + info['count'] = 50 # put count back in so that it doesn't show up as none in url except ValueError as err: # Errors raised in postprocessing @@ -404,6 +410,12 @@ def __call__(self, info): proj = query.pop("__projection__", self.projection) if isinstance(proj, list): proj = [col for col in proj if col in table.search_cols] + if "result_count" in info: + if one_per: + nres = table.count_distinct(one_per, query) + else: + nres = table.count(query) + return jsonify({"nres": str(nres)}) count = parse_count(info, self.per_page) start = parse_start(info) try: @@ -414,6 +426,7 @@ def __call__(self, info): offset=start, sort=sort, info=info, + one_per=one_per ) except QueryCanceledError as err: return self.query_cancelled_error(info, query, err, err_title, template, template_kwds) @@ -424,9 +437,95 @@ def __call__(self, info): # This is caused when a user inputs a number that's too large for a column search type return self.oob_error(info, query, err, err_title, template, template_kwds) else: + try: + if self.postprocess is not None: + res = self.postprocess(res, info, query) + except ValueError as err: + raise + flash_error(str(err)) + info["err"] = str(err) + return render_template(template, info=info, title=err_title, **template_kwds) info["results"] = res return render_template(template, info=info, title=title, **template_kwds) +class YieldWrapper(Wrapper): + """ + A variant on search wrapper that is intended to replace the database table with a Python function + that yields rows. + + The Python function should also accept a boolean random keyword (though it's allowed to raise an error) + """ + def __init__( + self, + f, # still a function that parses info into a query dictionary + template="search_results.html", + yielder=None, + title=None, + err_title=None, + per_page=50, + columns=None, + url_for_label=None, + **kwds + ): + Wrapper.__init__( + self, f, template, yielder, title, err_title, postprocess=None, **kwds + ) + self.per_page = per_page + self.columns = columns + self.url_for_label = url_for_label + + def __call__(self, info): + info = to_dict(info) + # if search_type starts with 'Random' returns a random label + search_type = info.get("search_type", info.get("hst", "")) + info["search_type"] = search_type + info["columns"] = self.columns + random = info["search_type"].startswith("Random") + template_kwds = {key: info.get(key, val()) for key, val in self.kwds.items()} + data = self.make_query(info, random) + if not isinstance(data, tuple): + return data + query, sort, yielder, title, err_title, template, one_per = data + if "result_count" in info: + if one_per: + nres = yielder(query, one_per=one_per, count=True) + else: + nres = yielder(query, count=True) + return jsonify({"nres": str(nres)}) + count = parse_count(info, self.per_page) + start = parse_start(info) + try: + if random: + label = yielder(query, random=True) + if label is None: + res = [] + # ugh; we have to set these manually + info["query"] = dict(query) + info["number"] = 0 + info["count"] = count + info["start"] = start + info["exact_count"] = True + else: + return redirect(self.url_for_label(label), 307) + else: + res = yielder( + query, + limit=count, + offset=start, + sort=sort, + info=info, + one_per=one_per, + ) + except ValueError as err: + flash_error(str(err)) + info["err"] = str(err) + title = err_title + raise + else: + info["results"] = res + return render_template(template, info=info, title=title, **template_kwds) + + @decorator_keywords def search_wrap(f, **kwds): return SearchWrapper(f, **kwds) @@ -438,3 +537,7 @@ def count_wrap(f, **kwds): @decorator_keywords def embed_wrap(f, **kwds): return EmbedWrapper(f, **kwds) + +@decorator_keywords +def yield_wrap(f, **kwds): + return YieldWrapper(f, **kwds) diff --git a/lmfdb/utils/web_display.py b/lmfdb/utils/web_display.py index 0a2b60b79e..d4a0be49d9 100644 --- a/lmfdb/utils/web_display.py +++ b/lmfdb/utils/web_display.py @@ -708,10 +708,16 @@ def frac_string(frac): # copied here from hilbert_modular_forms.hilbert_modular_form as it # started to cause circular imports: -def teXify_pol(pol_str): # TeXify a polynomial (or other string containing polynomials) +def teXify_pol(pol_str, greek_vars=False, subscript_vars=False): # TeXify a polynomial (or other string containing polynomials) if not isinstance(pol_str, str): pol_str = str(pol_str) - o_str = pol_str.replace('*', '') + if greek_vars: + greek_re = re.compile(r"\b(alpha|beta|gamma|delta|epsilon|zeta|eta|theta|iota|kappa|lambda|mu|nu|xi|omicron|pi|rho|sigma|tau|upsilon|phi|chi|psi|omega)\b") + pol_str = greek_re.sub(r"\\\g<1>", pol_str) + if subscript_vars: + subscript_re = re.compile(r"([A-Za-z]+)(\d+)") + pol_str = subscript_re.sub(r"\g<1>_{\g<2>}", pol_str) + o_str = pol_str.replace('*', ' ') ind_mid = o_str.find('/') while ind_mid != -1: ind_start = ind_mid - 1
{{info.stats._short_display[d.attribute.cols[1]]}}
{{d.attribute.col_title | safe}}
- {{info.stats._short_display[d.attribute.cols[0]] | safe}} + {{d.attribute.row_title | safe}}
{{ rhead | safe}}{{ rhead | safe}}{{c.count}}{{c.count}}{{c.count}}{{c.count}}
{{c.proportion}}{{c.proportion}}