Skip to content

Display group hashes and identify groups from a description - #7177

Open
roed-math wants to merge 8 commits into
LMFDB:mainfrom
roed-math:ai/t23-group-hashes
Open

Display group hashes and identify groups from a description#7177
roed-math wants to merge 8 commits into
LMFDB:mainfrom
roed-math:ai/t23-group-hashes

Conversation

@roed-math

Copy link
Copy Markdown
Contributor

Closes #5556.

Surfaces the isomorphism-invariant group hash in the finite groups interface and adds a group-identification tool.

Abstract group homepages now show the stored hash (with a search link) whenever it is informative (present and unequal to the label counter), there is an optional off-by-default "Hash" search column, and the two hash placeholders in the subgroup data popups are linkified.

A new /Groups/Abstract/identify endpoint (with a form on the index page and jump-box wiring for permutation input) lets a user enter permutation generators, a PC code, or matrix generators; the server computes the LMFDB (Magma) hash in-process via libgap, a faithful port of the FiniteGroups Hash.m, using strict parsers that never eval user input, a 10^6 order cap, and alarm() timeouts. Results go through GAP identification (a proof), the complete gps_smallhash tables (a unique match is a proof), then an (order, hash) lookup in gps_groups, always with mathematically honest messaging that a hash match alone does not prove isomorphism.

All hash resolution lives in one new module, lmfdb/groups/abstract/hash_lookup.py, so that N#h means the structural hash in the find box, on group homepages, in the subgroup and quotient popups, in the identification tool and in the optional column. That matters because gps_groups.hash stores the label counter rather than a hash at six of the ten orders with a complete gps_smallhash table (512, 1152, 1536, 1920, 2187, 15625), so searches at those orders resolve through gps_smallhash, where the answer is provably complete and a unique match is a proof of isomorphism. A stored counter is never displayed as a hash.

Parsing, group construction and the order computation share one alarm, and the order cap is checked before the order is factored; GAP's own start-up is deliberately outside the alarm, since interrupting it leaves libgap unusable for the whole worker. Matrix entries over GF(p^e) are integer codes whose base-p digits are coordinates in the power basis of the Conway generator, matching the FiniteGroups DecodeMat convention, so all of GF(q) can be entered and Mat(1,4):[[2]] is the generator of GF(4).

Verified by reproducing many stored hashes against the database, exercising every input/lookup/error/timeout path through the Flask test client, and running the abstract-groups and browse-page test suites (pyflakes clean). Subgroup homepages are unchanged, because the subgroup/quotient hash columns are entirely NULL in the current data.


Ported from roed-math#44, where the full write-up and comment history live.

🤖 Generated with Claude Code

roed314 and others added 6 commits July 19, 2026 15:22
Part (a): surface the isomorphism-invariant hash in the finite groups
interface. Abstract group homepages show the stored hash (with a search
link) whenever it is informative, i.e. hash is present and differs from
the label counter; add an optional, off-by-default "Hash" search column;
and linkify the two hash TODO popups in group_data. Subgroup pages get
nothing (subgroup_hash/quotient_hash are entirely NULL in the DB).

Part (b): new /Groups/Abstract/identify endpoint plus a form on the index
page and jump-box wiring for permutation input. The server computes the
LMFDB (Magma) hash in-process via libgap -- a faithful port of Hash.m in
new file identify.py -- with strict, no-eval parsers for permutation
generators, PC codes and matrices, a 10^6 order cap, and cysignals
alarm() timeouts. Lookup goes IdGroup (proof) -> gps_smallhash (complete
tables) -> (order, hash) on gps_groups, with mathematically honest
messaging (a hash match is not an isomorphism proof).

Verified: libgap hash port reproduces every stored value tested (512.11,
1536.947, 2016.i, 6561.4, 5120.cs, 1024.a, M11, Sp(4,3), abelian cases);
identify redirects/lists/errors and the alarm timeout+recovery exercised
via the Flask test client; new pytest cases and the full
test_abstract_groups.py (12) and test_browse_page.py (71) pass; pyflakes
clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…FDB#5556)

Review of PR 44 raised three merge blockers; this addresses all three.

1. New lmfdb/groups/abstract/hash_lookup.py is the single place that turns
   order#hash into a set of groups and decides which stored value may be
   shown as a hash.  gps_groups.hash holds the label counter rather than the
   hash at six of the ten orders with a complete gps_smallhash table (512,
   1152, 1536, 1920, 2187, 15625; checked over every row), so searches at
   those orders resolve through gps_smallhash, where the answer is complete
   and a unique match is a proof of isomorphism.  The find box, the search
   page, group homepages, the subgroup and quotient popups, the identify
   tool and the optional column all go through the resolver, and a counter
   is never displayed under the group.hash knowl.  A computed hash missing
   from a complete table now reports a data problem instead of rendering an
   empty paragraph.

   gps_smallhash has 4.2*10^8 rows and only an (order, hash) index, so the
   real hash of a group at those six orders cannot be fetched for display
   ((order, counter) is a 6-minute sequential scan); the value is suppressed
   rather than replaced by the counter, and the module says why.

2. identify_group parses, builds the group and computes its order inside one
   alarm, turns parser-side GAP or Sage failures into an error page, and
   checks the order cap before factoring the order.  GAP's own start-up is
   deliberately outside the alarm: an alarm that fires during it leaves
   libgap unusable for the whole worker.

3. Matrix entries over GF(p^e) are integer codes 0 <= x < q whose base-p
   digits are coordinates in the power basis of the Conway generator, the
   convention of the FiniteGroups DecodeMat, so every element of GF(q) can
   be entered; Mat(1,4):[[2]] is the generator of GF(4) rather than zero.

Verified with sage -python -m pytest on lmfdb/groups/abstract:
test_abstract_groups.py 22 passed (8 new tests covering the three findings),
test_browse_page.py 71 passed, pyflakes clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…sh searches (LMFDB#5556)

Follow-up review of PR 44 found the hash search still rendered through
gps_groups, so it could not show what the complete tables know.

A new route, /Groups/Abstract/hash/<order>/<value>, answers "which groups
have this order and hash".  At the ten orders with a complete gps_smallhash
table it lists every one of them, including those with no database row
(78125#3521944227884464685 is 78125.82, 78125.335, 78125.340, and only the
last two are in gps_groups), says that the list is complete, links the ones
with a homepage and marks the rest as absent, and redirects when the cluster
determines a single group.  Other orders go on to the ordinary search page,
where the stored column is the hash.  Every "groups with this order and
hash" link now goes through the route: group homepages, subgroup and
quotient popups, the identification tool and the find box, as does a search
whose only constraints are an order and a hash.

The hash column of gps_groups is no longer consulted at the six orders where
it holds the label counter, whatever the order constraint: with no single
order, hash_constraint builds an $or of the column match away from those
orders together with one resolved (order, counter) branch per affected
order, from a single indexed gps_smallhash query.  So a search for the hash
11 no longer returns 512.11, whose stored 11 is its counter, while a search
for 1584677793794603025 finds it with no order given at all.

The optional column still cannot show a hash that only gps_smallhash knows,
which is now documented; where the search itself named one hash at one
order, that value is displayed.  Out-of-range values are refused by the new
route rather than reaching the database, which errors on them.

Verified with sage -python -m pytest on lmfdb/groups/abstract:
test_abstract_groups.py 25 passed (the hash tests now cover the complete
cluster page, the popup and identification links into it, the label with no
homepage, and the false-positive cases), test_browse_page.py 71 passed,
pyflakes clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…#5556)

collapse_ors() combines two $or conditions by updating each old branch with
each new one, which discards the old condition wherever the two name the same
column.  They do here: an order list parses to
{"$or": [{"order": 512}, {"order": 2016}]} and every branch of
hash_constraint() names an order too, so the foiled query asked for the hash at
every order except the six counter-storing ones, and looked up order-512
counters whether or not 512 was one of the orders asked for.  merge_constraint()
intersects the two conditions with $and instead, which is what SearchParser
already does for negated inputs.

searched_hash() read the order out of N#h without consulting the order box, so
the early redirect to the hash page answered a request that names two different
orders, which parse_hashes() rejects.  It now returns nothing unless the two
agree, and a range or a list of orders never agrees, so those go to the ordinary
parser and its error.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An order and a hash on their own are a lookup, not a search: the request the
test made was redirected to the hash page and on to the homepage of 512.11,
which shows no hash, and the assertion was met by the structural hash that the
subgroup popups carry in their profiledata.  A second condition keeps the
request a table of results, where the column really does supply the hash that
an order-512 row cannot.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
LMFDB uses American spelling, so "cancelled" becomes "canceled" in two
comments and one assertion message.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@jenpaulhus

jenpaulhus commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

The "Identify" button looks like it's not got the right style (font and color).

Screenshot 2026-08-06 at 17 21 25

Edit to add: maybe move the box over too so it lines up with the "Compute the hash.." line.

@jenpaulhus

Copy link
Copy Markdown
Contributor

Might be worth having knowls for permutation generators, PC code, and matrix generators which give more information on the main page. If I enter garbage, I do get a new page with a table which tells me what the options for entering data look like, but particularly with Matrices and PC code, I think it would be good to be more explicit up front with what those mean.

@jenpaulhus

jenpaulhus commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Am I understanding the behavior correctly here: if I enter something beyond small groups it seems to give me a nice summary page with the hash explicitly listed and then a list of all groups with that hash? (I got one of these pages by going to the group 100000.o and copying its perm generators and entering that into the hash search)

But if I enter a small group (something like (1,2),(3,4,5,6)) it just takes me to the page for 8.2, not the summary page. This might be ok but just wanted to confirm this was the behavior we wanted?

GitHub Actions was disabled during a recent outage; this empty commit
kicks the checks back into running.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add hashes to finite group interface

3 participants