Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions lmfdb/belyi/templates/belyi_galmap.html
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,8 @@ <h2>{{ KNOWL('nf.embedding', title="Embeddings") }}</h2>
<div class="knowl"><div><div class="knowl-content">
<strong>Dessin d&rsquo;enfant</strong>
{% for g in data.embeddings_and_triples %}
{% set svg_idx = loop.index0 if loop.index0 < data.dessin_svgs|length else 0 %}
<div id="dessin-svg-{{ loop.index0 }}" class="dessin-svg-item" style="display:none;">
{{ data.dessin_svgs[svg_idx]|safe }}
{{ data.dessin_svgs[loop.index0]|safe }}
</div>
{% endfor %}
<p style="font-size:0.85em; margin-top:0.75em;">
Expand Down
86 changes: 86 additions & 0 deletions lmfdb/belyi/test_belyi.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import gzip
import os
from collections import defaultdict

from lmfdb.tests import LmfdbTest
from lmfdb import db
from lmfdb.belyi import web_belyi
from lmfdb.belyi.web_belyi import get_belyi_images, _belyidb_to_lmfdb_plabel


class BelyiTest(LmfdbTest):
Expand All @@ -12,6 +19,85 @@ def test_random(self):
def test_by_galmap_label(self):
self.check_args("/Belyi/6T15-5.1_4.2_4.2-b", "A_6")

# dessin images (#7026)

def test_belyidb_to_lmfdb_plabel_orbit_letters(self):
# missing orbit-letter component defaults to "a"
assert _belyidb_to_lmfdb_plabel("4T2-[2,2,2]-22-22-22-g0") == "4T2-2.2_2.2_2.2-a"
# distinct orbit letters must map to distinct labels, across a
# spread of degrees, genera and letters (not just a single example)
cases = [
("4T2-[2,2,2]-22-22-22-g0-a", "4T2-2.2_2.2_2.2-a"),
("5T1-[5,5,5]-5-5-5-g0-c", "5T1-5_5_5-c"),
("6T15-[5,5,5]-51-51-51-g1-d", "6T15-5.1_5.1_5.1-d"),
("7T6-[7,7,322]-7-7-322-g1-c", "7T6-7_7_3.2.2-c"),
("9T23-[6,6,6]-621-621-621-g1-a", "9T23-6.2.1_6.2.1_6.2.1-a"),
("9T23-[6,6,6]-621-621-621-g1-b", "9T23-6.2.1_6.2.1_6.2.1-b"),
]
for belyidb_label, expected in cases:
assert _belyidb_to_lmfdb_plabel(belyidb_label) == expected
# too few components is not a valid label
assert _belyidb_to_lmfdb_plabel("4T2-[2,2,2]") is None

def test_belyidb_to_lmfdb_plabel_roundtrips_for_every_shipped_image(self):
# regression test for #7026: parse every raw BelyiDB label in the
# shipped image dump and check that the conversion is (a) injective
# -- distinct orbits must never collide onto the same LMFDB label,
# the root cause of #7026 -- and (b) actually resolves to a real
# galmap in the database, for the whole dataset, not just an example
images_path = os.path.abspath(os.path.join(
os.path.dirname(web_belyi.__file__), "..", "static", "images", "belyi_images.txt.gz"))
raw_labels = []
with gzip.open(images_path, "rt", encoding="utf-8") as f:
for i, line in enumerate(f):
if i < 2: # header rows
continue
parts = line.rstrip("\n").split("|", 2)
if len(parts) == 3:
raw_labels.append(parts[1])
assert len(raw_labels) > 1000 # sanity: dump did not go missing/truncated

converted = [_belyidb_to_lmfdb_plabel(label) for label in raw_labels]
assert all(c is not None for c in converted)
assert len(set(converted)) == len(converted), "orbit collision: two BelyiDB labels mapped to the same LMFDB label"

real_labels = set(db.belyi_galmaps.search({}, projection="label"))
missing = [c for c in converted if c not in real_labels]
assert not missing

def test_dessin_images_distinct_across_orbits(self):
# regression test for #7026: galmaps sharing a passport but in
# different Galois orbits must not be served the same dessin
# images. Check every multi-orbit passport in the database (not
# just one hand-picked example), pairwise within each passport.
orbits_by_passport = defaultdict(list)
for galmap in db.belyi_galmaps.search({}, projection=["label", "plabel"]):
orbits_by_passport[galmap["plabel"]].append(galmap["label"])
multi_orbit_passports = {p: labels for p, labels in orbits_by_passport.items() if len(labels) > 1}
assert len(multi_orbit_passports) > 50 # sanity: this is exercising a lot of passports

checked_pairs = 0
for plabel, labels in multi_orbit_passports.items():
images = {label: get_belyi_images(label) for label in labels}
present = [label for label in labels if images[label]]
for i in range(len(present)):
for j in range(i + 1, len(present)):
a, b = present[i], present[j]
assert images[a] != images[b], "orbits {} and {} of passport {} share dessin images".format(a, b, plabel)
checked_pairs += 1
assert checked_pairs > 50 # sanity: the loop actually compared something

def test_dessin_image_counts_match_embeddings(self):
# every galmap that has dessin images must have exactly one image
# per embedding, since the template indexes into the image list by
# embedding position with no fallback (see PR #7189 review)
mismatches = []
for galmap in db.belyi_galmaps.search({}, projection=["label", "triples_cyc"]):
images = get_belyi_images(galmap["label"])
if images and len(images) != len(galmap["triples_cyc"]):
mismatches.append((galmap["label"], len(images), len(galmap["triples_cyc"])))
assert not mismatches, mismatches

def test_passport_label(self):
self.check_args("/Belyi/5T4-5_3.1.1_3.1.1-a", "5T4-5_3.1.1_3.1.1")

Expand Down
34 changes: 24 additions & 10 deletions lmfdb/belyi/web_belyi.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
from lmfdb.utils import (names_and_urls, prop_int_pretty, raw_typeset,
web_latex, compress_expression)
from flask import url_for
import gzip
import re
import os

from lmfdb import db


###############################################################################
# Belyi dessin images from belyi_images.txt
# Belyi dessin images from belyi_images.txt.gz
###############################################################################

_belyi_images = None
Expand All @@ -22,11 +23,11 @@ def _load_belyi_images():
if _belyi_images is not None:
return _belyi_images
_belyi_images = {}
txt_path = os.path.join(os.path.dirname(__file__), '..', 'static', 'images', 'belyi_images.txt')
txt_path = os.path.join(os.path.dirname(__file__), '..', 'static', 'images', 'belyi_images.txt.gz')
txt_path = os.path.abspath(txt_path)
if not os.path.exists(txt_path):
return _belyi_images
with open(txt_path, encoding='utf-8') as f:
with gzip.open(txt_path, 'rt', encoding='utf-8') as f:
Comment on lines +26 to +30
for i, line in enumerate(f):
if i < 2: # skip header rows
continue
Expand Down Expand Up @@ -76,7 +77,10 @@ def _new_height(m):


def _belyidb_to_lmfdb_plabel(belyidb_label):
# "4T2-[2,2,2]-22-22-22-g0" -> "4T2-2.2_2.2_2.2"
# "4T2-[2,2,2]-22-22-22-g0" -> "4T2-2.2_2.2_2.2-a"
# "9T23-[6,6,6]-621-621-621-g1-b" -> "9T23-6.2.1_6.2.1_6.2.1-b"
# (the optional 7th, orbit-letter component distinguishes galmaps within
# a passport; passports with a single orbit omit it, defaulting to "a")
parts = belyidb_label.split('-')
if len(parts) < 5:
return None
Expand All @@ -85,12 +89,13 @@ def _belyidb_to_lmfdb_plabel(belyidb_label):
sigma0 = '.'.join(list(parts[2]))
sigma1 = '.'.join(list(parts[3]))
sigmaoo = '.'.join(list(parts[4]))
return '{}-{}_{}_{}'.format(group, sigma0, sigma1, sigmaoo)
letter = parts[6] if len(parts) >= 7 else 'a'
return '{}-{}_{}_{}-{}'.format(group, sigma0, sigma1, sigmaoo, letter)


def get_belyi_images(plabel):
"""Return list of SVG strings for the given LMFDB passport label, or []."""
return _load_belyi_images().get(plabel, [])
def get_belyi_images(label):
"""Return list of SVG strings for the given LMFDB galmap label, or []."""
return _load_belyi_images().get(label, [])
Comment on lines +92 to +98


###############################################################################
Expand Down Expand Up @@ -383,8 +388,17 @@ def __init__(self, galmap, triple=None):
if galmap.get('plane_map_constant_factored'):
data['plane_map_constant_factored'] = galmap['plane_map_constant_factored']

# Dessin images (one per embedding, or a single shared one)
data['dessin_svgs'] = get_belyi_images(galmap['plabel'])
# Dessin images (one per embedding), looked up per galmap orbit so
# that different orbits within the same passport don't share images.
# An empty list means no images are available for this galmap; a
# nonempty list must have exactly one SVG per embedding, since the
# template indexes into it by embedding position with no fallback.
data['dessin_svgs'] = get_belyi_images(galmap['label'])
if data['dessin_svgs']:
assert len(data['dessin_svgs']) == len(data['embeddings_and_triples']), (
"dessin image count ({}) does not match embedding count ({}) for {}".format(
len(data['dessin_svgs']), len(data['embeddings_and_triples']), galmap['label'])
)
Comment on lines +397 to +401

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the assertion fails here, I think the whole website is going to crash (or at least it will show an error page, instead of displaying the rest of the interesting mathematical invariants). Even though assertions are made on some other pages, I would recommend just setting data['dessin_svgs']=[] or something if there's a mismatch.


# Properties
self.plot = db.belyi_galmap_portraits.lucky({"label": galmap['label']},
Expand Down
Loading
Loading