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
16 changes: 13 additions & 3 deletions lmfdb/groups/abstract/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -1186,6 +1186,16 @@ def normal_autdiagram(label):
title = f"Diagram of normal subgroups up to automorphism for group {label}"
return _subgroup_diagram(label, title, only=("normal", "aut"), style="normal_autdiagram")

@abstract_page.route("/maximal_diagram/<label>")
def maximal_diagram(label):
title = f"Diagram of maximal subgroups up to conjugation for group {label}"
return _subgroup_diagram(label, title, only=("maximal", ""), style="maximal_diagram")

@abstract_page.route("/maximal_autdiagram/<label>")
def maximal_autdiagram(label):
title = f"Diagram of maximal subgroups up to automorphism for group {label}"
return _subgroup_diagram(label, title, only=("maximal", "aut"), style="maximal_autdiagram")

def show_type(ab, nil, solv, smith, nilcls, dlen, clen):
# arguments - ["abelian", "nilpotent", "solvable", "smith_abelian_invariants", "nilpotency_class", "derived_length", "composition_length"]
if ab:
Expand Down Expand Up @@ -2023,11 +2033,11 @@ def diagram_js(gp, layers, display_opts, aut=False, normal=False):
return [ll, layers[1]], order_lookup

def diagram_js_string(gp, only=None):
glist = [[], [], [], []]
order_lookup = [[], [], [], []]
glist = [[], [], [], [], [], []]
order_lookup = [[], [], [], [], [], []]
display_opts = defaultdict(int)
limit = (100 if only is None else 0)
for i, pair in enumerate([("subgroup", ""), ("subgroup", "aut"), ("normal", ""), ("normal", "aut")]):
for i, pair in enumerate([("subgroup", ""), ("subgroup", "aut"), ("normal", ""), ("normal", "aut"), ("maximal", ""), ("maximal", "aut")]):
sub_all, sub_aut = pair
if (only is None or only == pair) and gp.diagram_count(sub_all, sub_aut, limit=limit):
glist[i], order_lookup[i] = diagram_js(gp, gp.subgroup_lattice(sub_all, sub_aut), display_opts, aut=bool(sub_aut), normal=(sub_all == "normal"))
Expand Down
5 changes: 3 additions & 2 deletions lmfdb/groups/abstract/templates/abstract-show-group.html
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,7 @@ <h3>{{KNOWL('group.subgroup.diagram.lmfdb','Subgroup diagram')}} and profile</h3
<div class="sub_divider">
<button class='sub_subgroup sub_active' onclick="select_subgroup_mode('subgroup'); return false">all subgroups</button>
<button class='sub_normal sub_inactive' onclick="select_subgroup_mode('normal'); return false">normal subgroups</button>
<button class='sub_maximal sub_inactive' onclick="select_subgroup_mode('maximal'); return false">maximal subgroups</button>
</div>
<div class="sub_divider">
<button class='sub_ sub_active' onclick="select_subgroup_mode(''); return false">up to conjugacy</button>
Expand All @@ -412,7 +413,7 @@ <h3>{{KNOWL('group.subgroup.diagram.lmfdb','Subgroup diagram')}} and profile</h3

{# Just header information for the diagrams #}

{% for sub_all in ["subgroup", "normal"] %}
{% for sub_all in ["subgroup", "normal", "maximal"] %}
{% for sub_aut in ["", "aut"] %}
{% set sub_desc, sub_summary, sub_cnt = gp.get_diagram_info(sub_all, sub_aut, limit=100) %}
<div class="{{sub_all}}_{{sub_aut}}diagram" style="display:none">
Expand Down Expand Up @@ -450,7 +451,7 @@ <h4>{{sub_desc}}</h4>
{{ info.dojs|safe }}
</script>

{% for sub_all in ["subgroup", "normal"] %}
{% for sub_all in ["subgroup", "normal", "maximal"] %}
{% for sub_aut in ["", "aut"] %}
{% set sub_profile, sub_desc, sub_summary = gp.get_profile(sub_all, sub_aut) %}
<div class="{{sub_all}}_{{sub_aut}}profile" style="display: none">
Expand Down
158 changes: 158 additions & 0 deletions lmfdb/groups/abstract/test_abstract_groups.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,42 @@
import re
from ast import literal_eval

from lmfdb.tests import LmfdbTest

# The graphs that diagram_js_string builds for a group page, in order
DIAGRAM_MODES = [("subgroup", ""), ("subgroup", "aut"), ("normal", ""), ("normal", "aut"),
("maximal", ""), ("maximal", "aut")]


def diagram_graphs(page, label):
r"""
The graphs passed to make_sdiagram on a group page or a fullpage diagram: one
entry for each mode of DIAGRAM_MODES, empty when that diagram is not drawn.
Returns None when the page contains no subgroup diagram at all.
"""
m = re.search(r'make_sdiagram\("subdiagram", "%s", (.*)\);' % re.escape(label), page)
if m is None:
return None
graphs, order_lookup, layers = literal_eval("(" + m.group(1) + ")")
assert len(graphs) == len(DIAGRAM_MODES)
return graphs


def graph_content(graph):
r"""
The short labels of the nodes and the edges of one graph from diagram_graphs
"""
nodes, edges = graph
return {node[1] for node in nodes}, {tuple(edge) for edge in edges}


def profile_content(page, cls):
r"""
The contents of one of the subgroup profile divs on a group page
"""
return re.search(r'<div class="%s"[^>]*>(.*?)</div>' % cls, page, re.DOTALL).group(1)


class AbGpsTest(LmfdbTest):
# All tests should pass

Expand Down Expand Up @@ -90,3 +127,124 @@ def test_underlying_data(self):
def test_subgroups(self):
self.check_args("/Groups/Abstract/sub/78125.1385.15625.A","Group of order 31250000")
self.check_args("/Groups/Abstract/sub/16384.mv.8._.BQX",'The ambient group is <a title="Abelian group [group.abelian]"')

def test_maximal_subgroups(self):
r"""
Check the maximal subgroup mode of the subgroup diagram/profile
"""
# S4 has maximal subgroups A4 (2.a1.a1), D4 (3.a1.a1) and S3 (4.a1.a1),
# none of which are related by an automorphism; 1.a1.a1 is S4 itself,
# which sits at the top of the maximal diagram
page = self.tc.get("/Groups/Abstract/24.12").get_data(as_text=True)
assert "maximal subgroups</button>" in page
profile = profile_content(page, "maximal_profile")
assert "Classes of maximal subgroups up to conjugation" in profile
assert "A_4" in profile and "D_4" in profile and "S_3" in profile
autprofile = profile_content(page, "maximal_autprofile")
assert "Classes of maximal subgroups up to automorphism" in autprofile
assert "A_4" in autprofile and "D_4" in autprofile and "S_3" in autprofile
graphs = diagram_graphs(page, "24.12")
for mode in [("maximal", ""), ("maximal", "aut")]:
nodes, edges = graph_content(graphs[DIAGRAM_MODES.index(mode)])
assert nodes == {"1.a1.a1", "2.a1.a1", "3.a1.a1", "4.a1.a1"}
assert edges == {("2.a1.a1", "1.a1.a1"), ("3.a1.a1", "1.a1.a1"), ("4.a1.a1", "1.a1.a1")}

def test_maximal_subgroups_diagram(self):
r"""
Check that the maximal subgroup diagram is a star with the whole group on
top, even when the full subgroup diagram is too large to be displayed
"""
from lmfdb.groups.abstract.web_groups import WebAbstractGroup
# 32.45 has 118 classes of subgroups, too many to draw, but only 15 classes
# of maximal subgroups, which fuse into 2 classes up to automorphism
G = WebAbstractGroup("32.45")
assert G.diagram_count("subgroup", "", limit=100) == 0
assert G.diagram_count("maximal", "", limit=100) == 16
assert G.diagram_count("maximal", "aut", limit=100) == 3
nodes, edges = G.subgroup_lattice("maximal", "")
top = [H for H in nodes if H.quotient_order == 1]
maxima = [H for H in nodes if H.maximal]
assert len(top) == 1
assert len(maxima) == 15
assert len(nodes) == len(maxima) + 1
assert {tuple(edge) for edge in edges} == {(H.short_label, top[0].short_label) for H in maxima}
# Up to automorphism there is exactly one node for each autjugacy class
aut_nodes, aut_edges = G.subgroup_lattice("maximal", "aut")
assert len(aut_nodes) == G.diagram_count("maximal", "aut")
assert len({H.aut_label for H in aut_nodes}) == len(aut_nodes)
assert {H.aut_label for H in aut_nodes} == {H.aut_label for H in nodes}
aut_top = [H for H in aut_nodes if H.quotient_order == 1]
assert len(aut_top) == 1
assert len(aut_edges) == len(aut_nodes) - 1
assert {target for source, target in aut_edges} == {aut_top[0].short_label}
# Only the two maximal diagrams and the normal diagram up to automorphism
# are small enough to be drawn on the group page
page = self.tc.get("/Groups/Abstract/32.45").get_data(as_text=True)
graphs = diagram_graphs(page, "32.45")
assert [DIAGRAM_MODES[i] for i, graph in enumerate(graphs) if graph] == [
("normal", "aut"), ("maximal", ""), ("maximal", "aut")]
assert graph_content(graphs[DIAGRAM_MODES.index(("maximal", ""))]) == (
{H.short_label for H in nodes},
{(H.short_label, top[0].short_label) for H in maxima})
assert graph_content(graphs[DIAGRAM_MODES.index(("maximal", "aut"))]) == (
{H.short_label for H in aut_nodes},
{tuple(edge) for edge in aut_edges})
profile = profile_content(page, "maximal_profile")
assert "x 14" in profile # 14 conjugate copies of C_2^2 x C_4

def test_maximal_subgroups_fullpage(self):
r"""
Check that the fullpage maximal subgroup diagrams contain the right graph
"""
from lmfdb.groups.abstract.web_groups import WebAbstractGroup
for label, title, mode in [
("24.12", "Diagram of maximal subgroups up to conjugation", ("maximal", "")),
("24.12", "Diagram of maximal subgroups up to automorphism", ("maximal", "aut")),
("32.45", "Diagram of maximal subgroups up to conjugation", ("maximal", "")),
("32.45", "Diagram of maximal subgroups up to automorphism", ("maximal", "aut"))]:
sub_all, sub_aut = mode
G = WebAbstractGroup(label)
shown = [H for H in G.subgroups.values() if H.maximal or H.quotient_order == 1]
response = self.tc.get(f"/Groups/Abstract/{sub_all}_{sub_aut}diagram/{label}")
assert response.status_code == 200
page = response.get_data(as_text=True)
assert title in page
assert f'show_info("{sub_all}_{sub_aut}diagram")' in page
graphs = diagram_graphs(page, label)
assert graphs is not None, f"no diagram on the fullpage {sub_all} {sub_aut} diagram for {label}"
# The fullpage version contains only the requested diagram
assert [DIAGRAM_MODES[i] for i, graph in enumerate(graphs) if graph] == [mode]
drawn, edges = graph_content(graphs[DIAGRAM_MODES.index(mode)])
whole_group, = [H.short_label for H in shown if H.quotient_order == 1]
assert edges == {(short_label, whole_group) for short_label in drawn - {whole_group}}
if sub_aut:
# one representative from each autjugacy class, so 3 nodes for 32.45
assert drawn <= {H.short_label for H in shown}
assert len(drawn) == G.diagram_count(sub_all, sub_aut)
assert ({H.aut_label for H in shown if H.short_label in drawn}
== {H.aut_label for H in shown})
else:
assert drawn == {H.short_label for H in shown}

def test_maximal_subgroups_without_inclusions(self):
r"""
Check that maximal subgroups are shown for a group whose subgroup
inclusions were never computed
"""
from lmfdb.groups.abstract.web_groups import WebAbstractGroup
G = WebAbstractGroup("1024.dip")
assert G.maximal_subgroups_known
assert not G.subgroup_inclusions_known
assert G.maximal_profile
# No diagram can be drawn without the inclusions, but the profile is still shown
assert G.diagram_count("maximal", "", limit=100) == 0
page = self.tc.get("/Groups/Abstract/1024.dip").get_data(as_text=True)
assert diagram_graphs(page, "1024.dip") is None
profile = profile_content(page, "maximal_profile")
assert "not computed" not in profile
# 255 classes of maximal subgroups, all of order 512
assert "Order 512:" in profile
assert r"$C_2^6:D_4$</a> x 248" in profile
assert r"$D_4\times C_2^6$</a> x 3" in profile
assert r"$C_2^8.C_2$</a> x 3" in profile
assert r"$C_2^9$</a>" in profile
69 changes: 64 additions & 5 deletions lmfdb/groups/abstract/web_groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -1274,6 +1274,26 @@ def normal_autprofile(self):
seen.add(s.aut_label)
return self._finalize_profile(by_order)

@lazy_attribute
def maximal_profile(self):
if self.has_subgroups and self.maximal_subgroups_known:
by_order = defaultdict(Counter)
for s in self.subgroups.values():
if s.maximal:
by_order[s.subgroup_order][s.subgroup, s.subgroup_hash, s.subgroup_tex] += s.conjugacy_class_count
return self._finalize_profile(by_order)

@lazy_attribute
def maximal_autprofile(self):
if self.has_subgroups and self.maximal_subgroups_known and (self.all_subgroups_known or self.complements_known or self.outer_equivalence):
seen = set()
by_order = defaultdict(Counter)
for s in self.subgroups.values():
if s.maximal and s.aut_label not in seen:
by_order[s.subgroup_order][s.subgroup, s.subgroup_hash, s.subgroup_tex] += 1
seen.add(s.aut_label)
return self._finalize_profile(by_order)

def _display_profile(self, profile, aut):
def display_profile_line(order, subs):
l = []
Expand Down Expand Up @@ -1312,6 +1332,12 @@ def _normal_summary(self):
return f"All normal subgroups of index up to {self.normal_index_bound} or order up to {self.normal_order_bound} are shown. <br>"
return ""

@cached_method
def _maximal_summary(self):
if not self.maximal_subgroups_known:
return "Maximal subgroups have not been computed. <br>"
return ""

@lazy_attribute
def subgroup_order_bound(self):
if self.subgroup_index_bound == 0 or self.subgroup_index_bound is None:
Expand Down Expand Up @@ -1342,6 +1368,14 @@ def get_profile(self, sub_all, sub_aut):
profile = self.subgroup_profile
desc = "Classes of subgroups up to conjugation"
summary = self._subgroup_summary(in_profile=True)
elif sub_all == "maximal":
if sub_aut:
profile = self.maximal_autprofile
desc = "Classes of maximal subgroups up to automorphism"
else:
profile = self.maximal_profile
desc = "Classes of maximal subgroups up to conjugation"
summary = self._maximal_summary()
else:
if sub_aut:
profile = self.normal_autprofile
Expand All @@ -1356,7 +1390,7 @@ def get_profile(self, sub_all, sub_aut):

@cached_method
def diagram_count(self, sub_all, sub_aut, limit=0):
# The number of subgroups shown in the diagram of this type; sub_all can be "subgroup" or "normal" and sub_aut can be "aut" or ""
# The number of subgroups shown in the diagram of this type; sub_all can be "subgroup", "normal" or "maximal" and sub_aut can be "aut" or ""
# If limit is nonzero, then a count of 0 is returned (indicating that the diagram should not be shown) when there would be more nodes than the limit.
if not self.subgroup_inclusions_known:
return 0
Expand All @@ -1382,7 +1416,13 @@ def impose_limit(n):
return 0
return impose_limit(len(subs))
else:
subs = [H for H in self.subgroups.values() if H.normal]
if sub_all == "maximal":
if not self.maximal_subgroups_known:
return 0
# We include the whole group at the top of the diagram
subs = [H for H in self.subgroups.values() if H.maximal or H.quotient_order == 1]
else:
subs = [H for H in self.subgroups.values() if H.normal]
if sub_aut:
if any(H.aut_label is None or H.diagramx is None for H in subs):
# We don't know subgroups up to automorphism or can't lay out the subgroups
Expand All @@ -1402,6 +1442,12 @@ def get_diagram_info(self, sub_all, sub_aut, limit=0):
else:
desc = "Classes of subgroups up to conjugation"
summary = self._subgroup_summary(in_profile=False)
elif sub_all == "maximal":
if sub_aut:
desc = "Classes of maximal subgroups up to automorphism"
else:
desc = "Classes of maximal subgroups up to conjugation"
summary = self._maximal_summary()
else:
if sub_aut:
desc = "Normal subgroups up to automorphism"
Expand All @@ -1425,10 +1471,10 @@ def get_diagram_info(self, sub_all, sub_aut, limit=0):
return desc, summary, count

def diagram_classes(self):
# Which combinations of subgroup/normal and conj/aut have a diagram
# Which combinations of subgroup/normal/maximal and conj/aut have a diagram
# Note that it's possible that the only diagrams "shown" will be for cases where there's a link to a fullpage version.
classes = []
for sub_all in ["subgroup", "normal"]:
for sub_all in ["subgroup", "normal", "maximal"]:
for sub_aut in ["", "aut"]:
if self.diagram_count(sub_all, sub_aut) > 0:
classes.append(f"{sub_all}_{sub_aut}diagram")
Expand All @@ -1451,6 +1497,19 @@ def test(H):

def contains(G):
return [h for h in G.contains if test(subs[h])]
elif sub_all == "maximal":
def test(H):
# We include the whole group so that the diagram is connected
if H.maximal or H.quotient_order == 1:
by_aut[H.aut_label].add(H.short_label)
return True

def contains(G):
# Every maximal subgroup is covered by the whole group at the top;
# no inclusions among maximal subgroups themselves
if G.quotient_order == 1:
return [H.short_label for H in nodes if H is not G]
return []
else:
def test(H):
if H.normal:
Expand Down Expand Up @@ -1493,7 +1552,7 @@ def contains(G):

def fullpage_links(self, getpositions=False):
s = ""
for sub_all in ["subgroup", "normal"]:
for sub_all in ["subgroup", "normal", "maximal"]:
for sub_aut in ["", "aut"]:
cls = f'{sub_all}_{sub_aut}diagram'
s += f'<div class="{cls}">\n'
Expand Down
Loading
Loading