diff --git a/.github/workflows/matrix_includes.json b/.github/workflows/matrix_includes.json index f8f142289e..830ed54e2c 100644 --- a/.github/workflows/matrix_includes.json +++ b/.github/workflows/matrix_includes.json @@ -30,12 +30,12 @@ "server": "devmirror" }, { - "files": "lmfdb/tests/test_dynamic_knowls.py lmfdb/tests/test_root.py lmfdb/hecke_algebras/test_hecke_algebras.py lmfdb/tests/test_homepage.py lmfdb/elliptic_curves/test_ell_curves.py lmfdb/elliptic_curves/test_browse_page.py", + "files": "lmfdb/tests/test_dynamic_knowls.py lmfdb/tests/test_root.py lmfdb/hecke_algebras/test_hecke_algebras.py lmfdb/tests/test_homepage.py lmfdb/tests/test_random_redirects.py lmfdb/elliptic_curves/test_ell_curves.py lmfdb/elliptic_curves/test_browse_page.py", "folders": "elliptic_curves hecke_algebras tests", "server": "proddb" }, { - "files": "lmfdb/tests/test_dynamic_knowls.py lmfdb/tests/test_root.py lmfdb/hecke_algebras/test_hecke_algebras.py lmfdb/tests/test_homepage.py lmfdb/elliptic_curves/test_ell_curves.py lmfdb/elliptic_curves/test_browse_page.py", + "files": "lmfdb/tests/test_dynamic_knowls.py lmfdb/tests/test_root.py lmfdb/hecke_algebras/test_hecke_algebras.py lmfdb/tests/test_homepage.py lmfdb/tests/test_random_redirects.py lmfdb/elliptic_curves/test_ell_curves.py lmfdb/elliptic_curves/test_browse_page.py", "folders": "elliptic_curves hecke_algebras tests", "server": "devmirror" }, diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index fcb1e9ac13..79407bcda7 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -90,7 +90,7 @@ jobs: - name: checking that we didn't miss any test files shell: bash -l {0} # If this fails you need to update the file list above and file count - run: test $(find lmfdb -name 'test_*.py' -or -name '*_test.py' | wc -l) -eq 40 + run: test $(find lmfdb -name 'test_*.py' -or -name '*_test.py' | wc -l) -eq 41 - name: Config LMFDB to run tests against proddb if: matrix.files != 'lint' && matrix.server == 'proddb' diff --git a/lmfdb/bianchi_modular_forms/bianchi_modular_form.py b/lmfdb/bianchi_modular_forms/bianchi_modular_form.py index 4f9dab6dd5..eba31379b4 100644 --- a/lmfdb/bianchi_modular_forms/bianchi_modular_form.py +++ b/lmfdb/bianchi_modular_forms/bianchi_modular_form.py @@ -8,7 +8,7 @@ to_dict, web_latex_ideal_fact, flash_error, comma, display_knowl, nf_string_to_label, parse_nf_string, parse_noop, parse_start, parse_count, parse_ints, parse_primes, SearchArray, TextBox, SelectBox, ExcludeOnlyBox, CountBox, SubsetBox, TextBoxWithSelect, - teXify_pol, search_wrap, Downloader) + teXify_pol, search_wrap, Downloader, redirect_no_cache) from lmfdb.utils.display_stats import StatsDisplay, totaler, proportioners from lmfdb.utils.interesting import interesting_knowls from lmfdb.utils.search_columns import SearchColumns, ProcessedCol, MultiProcessedCol @@ -86,12 +86,13 @@ def index(): return bianchi_modular_form_search(info) @bmf_page.route("/random") +@redirect_no_cache def random_bmf(): # Random Bianchi modular form res = db.bmf_forms.random(projection=['field_label', 'level_label', 'label_suffix']) - return redirect(url_for(".render_bmf_webpage", + return url_for(".render_bmf_webpage", field_label=res['field_label'], level_label=res['level_label'], - label_suffix=res['label_suffix']), 307) + label_suffix=res['label_suffix']) @bmf_page.route("/interesting") def interesting(): diff --git a/lmfdb/characters/main.py b/lmfdb/characters/main.py index b835dab588..34bfcfcfdb 100644 --- a/lmfdb/characters/main.py +++ b/lmfdb/characters/main.py @@ -6,7 +6,7 @@ from lmfdb.utils import ( to_dict, flash_error, SearchArray, YesNoBox, display_knowl, ParityBox, TextBox, CountBox, parse_bool, parse_ints, search_wrap, raw_typeset_poly, - StatsDisplay, totaler, proportioners, comma, flash_warning, Downloader) + StatsDisplay, totaler, proportioners, comma, flash_warning, Downloader, redirect_no_cache) from lmfdb.utils.interesting import interesting_knowls from lmfdb.utils.search_parsing import parse_range3 from lmfdb.utils.search_columns import SearchColumns, MathCol, LinkCol, CheckCol, ProcessedCol, MultiProcessedCol @@ -22,7 +22,6 @@ WebSmallDirichletGroup, WebDBDirichletOrbit ) -from lmfdb.characters.ListCharacters import get_character_modulus from lmfdb.characters import characters_page from lmfdb import db @@ -312,24 +311,6 @@ def dirichlet_character_search(info, query): @characters_page.route("/Dirichlet") @characters_page.route("/Dirichlet/") def render_DirichletNavigation(): - try: - if 'modbrowse' in request.args: - arg = request.args['modbrowse'] - arg = arg.split('-') - modulus_start = int(arg[0]) - modulus_end = int(arg[1]) - info = {'args': request.args} - info['title'] = 'Dirichlet characters of modulus ' + str(modulus_start) + '-' + str(modulus_end) - info['bread'] = bread('Modulus') - info['learnmore'] = learn() - headers, entries, rows, cols = get_character_modulus(modulus_start, modulus_end, limit=8) - info['entries'] = entries - info['rows'] = list(range(modulus_start, modulus_end + 1)) - info['cols'] = sorted({r[1] for r in entries}) - return render_template("ModulusList.html", **info) - except (ValueError, IndexError) as err: - flash_error("Error raised in parsing: %s", err) - if request.args: # hidden_search_type for prev/next buttons info = to_dict(request.args, search_array=DirichSearchArray()) @@ -640,8 +621,9 @@ def ctx_dirchar(): @characters_page.route('/Dirichlet/random') +@redirect_no_cache def random_Dirichletwebpage(): - return redirect(url_for('.render_DirichletNavigation', search_type="Random")) + return url_for('.render_DirichletNavigation', search_type="Random") @characters_page.route('/Dirichlet/interesting') diff --git a/lmfdb/characters/templates/CharacterNavigate.html b/lmfdb/characters/templates/CharacterNavigate.html index ef459eed65..e38fcfee1a 100644 --- a/lmfdb/characters/templates/CharacterNavigate.html +++ b/lmfdb/characters/templates/CharacterNavigate.html @@ -12,7 +12,7 @@

Browse

- + diff --git a/lmfdb/characters/test_characters.py b/lmfdb/characters/test_characters.py index 792c8178eb..485f9434e2 100644 --- a/lmfdb/characters/test_characters.py +++ b/lmfdb/characters/test_characters.py @@ -31,8 +31,8 @@ def test_even_odd(self): assert '>%sfurther statistics.' % (latex_comma(self.ngroups), url_for(".statistics")) + return r'The database currently contains %s groups, including all transitive subgroups of $S_n$ (up to conjugacy) for $n \le 47$ and $n \ne 32$. Here are some further statistics.' % (comma(self.ngroups), url_for(".statistics")) diff --git a/lmfdb/groups/abstract/main.py b/lmfdb/groups/abstract/main.py index a8d01f1cd4..73090a966f 100644 --- a/lmfdb/groups/abstract/main.py +++ b/lmfdb/groups/abstract/main.py @@ -48,6 +48,7 @@ pos_int_and_factor, sparse_cyclotomic_to_mathml, integer_to_mathml, + redirect_no_cache, ) from lmfdb.utils.search_parsing import (parse_multiset, search_parser, collapse_ors) from lmfdb.utils.interesting import interesting_knowls @@ -841,11 +842,10 @@ def dynamic_statistics(): @abstract_page.route("/random") +@redirect_no_cache def random_abstract_group(): label = db.gps_groups.random(projection="label") - response = make_response(redirect(url_for(".by_label", label=label), 307)) - response.headers["Cache-Control"] = "no-cache, no-store" - return response + return url_for(".by_label", label=label) @abstract_page.route("/interesting") diff --git a/lmfdb/groups/abstract/web_groups.py b/lmfdb/groups/abstract/web_groups.py index 94ebb57197..4056a923c8 100644 --- a/lmfdb/groups/abstract/web_groups.py +++ b/lmfdb/groups/abstract/web_groups.py @@ -2335,7 +2335,8 @@ def representation_line(self, rep_type, skip_head=False): if rep_type == "Lie": desc = "Groups of " + display_knowl("group.lie_type", "Lie type") reps = ", ".join(fr"$\{rep['family']}({rep['d']},{rep['q']})$" for rep in rdata) - return f'' + code_cmd = " ".join([self.create_lie_type_snippet(rep['family']) for rep in rdata]) + return f'' elif rep_type == "PC": pres = self.presentation() if not skip_head: #add copy button in certain cases @@ -2841,6 +2842,32 @@ def create_snippet(self,item): post="") return snippet.place_code() + # Used for creating code snipets for Lie type representations + def create_lie_type_snippet(self,item): + snippet = CodeSnippet(self.code_snippets(), item) + return snippet.place_code() + + @lazy_attribute + def lie_representations(self): + # Ideally we should get the Lie-type representations from the "gps_special_names" table: + # lie_reps = list(db.gps_special_names.search({'label':self.label}, projection={'family','gens','parameters'})) + + # TODO: We should update groups data to use new family names + # For now, we'll implement a "old to new" family dictionary (can delete once groups data is updated) + old_to_new_family_name = {"GO":"Orth", "GOPlus":"OrthPlus", "GOMinus":"OrthMinus", "GU":"Unitary", "PGO":"PO", + "PGOPlus":"POPlus", "PGOMinus":"POMinus", "PGU":"PU", "CSp":"GSp", "CSO":"GSO", "CSOPlus":"GSOPlus", + "CSOMinus":"GSOMinus", "CSU":"GSU", "CO":"GOrth", "COPlus":"GOrthPlus", "COMinus":"GOrthMinus", + "CU":"GUnitary"} + + # Temporary solution: Get Lie type representations from gps_groups table (the order here is important!) + if "Lie" in self.representations: + lie_reps = self.representations["Lie"] + for i in range(len(lie_reps)): + if lie_reps[i]['family'] in old_to_new_family_name: + lie_reps[i]['family'] = old_to_new_family_name[lie_reps[i]['family']] + return lie_reps + return None + @cached_method def code_snippets(self): if self.live(): @@ -2899,7 +2926,7 @@ def code_snippets(self): LZqsage = "["+", ".join(["MS("+str(split_matrix_list(self.decode_as_matrix(g, "GLZq", ListForm=True), nZq))+")" for g in self.representations["GLZq"]["gens"]])+"]" else: nZq, Zq, LZq, LZqsplit, LZqsage = None, None, None, None, None -# add below for GLFq implementation + # add below for GLFq implementation if "GLFq" in self.representations: nFq = self.representations["GLFq"]["d"] Fq = self.representations["GLFq"]["q"] @@ -2922,6 +2949,72 @@ def code_snippets(self): 'LZsage': LZsage, 'LFpsage': LFpsage, 'LZNsage': LZNsage, 'LZqsage': LZqsage, 'LFqsage': LFqsage, } + # The following code implements code snippets for the Lie-type matrix representations + magma_top_lie, gap_top_lie, sage_top_lie = None, None, None # Keep track of Lie-type snippet for use as a top code snippet + gap_used_lie_gens, sage_used_lie_gens = False, False # Track whether we need to use generators + if self.lie_representations is not None: + # Get Magma commands for all the Lie type families + gps_families_data = list(db.gps_families.search(projection={'family','magma_cmd'})) + magma_commands = {d['family']: d['magma_cmd'] for d in gps_families_data} + # Hardcoded list of Lie Type families available in GAP and Sage (NB: Must ensure their implementation agrees with our definition!) + gap_families = ['GL','SL','PSL','PGL','Sp','SO','SU','PSp','PSO','PSU','Orth','Unitary','Omega','PO','PU','POmega','PGammaL','PSigmaL'] + sage_families = ['GL','SL','PSL','PGL','PSp','PSU','Orth','Unitary','PU'] + + # Prioritise displaying the first Lie type representation which is implemented in the language + for lie_rep in self.lie_representations: + code[lie_rep['family']] = dict() + nLie, qLie = ZZ(lie_rep['d']), ZZ(lie_rep['q']) + + code[lie_rep['family']]['magma'] = "G := "+magma_commands[lie_rep['family']].replace("n,q", str(nLie)+","+str(qLie))+";" + if magma_top_lie is None: + magma_top_lie = code[lie_rep['family']]['magma'] + + if lie_rep['family'] in gap_families: + code[lie_rep['family']]['gap'] = code[lie_rep['family']]['magma'] + if (gap_top_lie is None) or gap_used_lie_gens: + gap_top_lie = code[lie_rep['family']]['gap'] + gap_used_lie_gens = False + elif "gens" in lie_rep: + lie_mats = [self.decode_as_matrix(g, "Lie", ListForm=True) for g in lie_rep["gens"]] + if qLie.is_prime(): + e = libgap.One(GF(qLie)) + lie_gap_mats = [split_matrix_list_Fp(mat, nLie, e) for mat in lie_mats] + gap_lie_code_snippet = code['GLFp']['gap'].format(**{'LFpsplit':lie_gap_mats}) + else: + lie_gap_mats = "[" + ",".join(split_matrix_list_Fq(mat, nLie, qLie) for mat in lie_mats) + "]" + gap_lie_code_snippet = code['GLFq']['gap'].format(**{'LFqsplit':lie_gap_mats}) + if gap_top_lie is None: + gap_top_lie, gap_used_lie_gens = gap_lie_code_snippet, True + + if lie_rep['family'] in sage_families: + code[lie_rep['family']]['sage'] = "G = "+magma_commands[lie_rep['family']].replace("n,q", str(nLie)+","+str(qLie)) + if (sage_top_lie is None) or sage_used_lie_gens: + sage_top_lie = code[lie_rep['family']]['sage'] + sage_used_lie_gens = False + elif "gens" in lie_rep: + lie_mats = [self.decode_as_matrix(g, "Lie", ListForm=True) for g in lie_rep["gens"]] + if qLie.is_prime(): + lie_sage_mats = "["+", ".join(["MS("+str(split_matrix_list(self.decode_as_matrix(g, "Lie", ListForm=True),nLie))+")" for g in lie_rep["gens"]])+"]" + sage_lie_code_snippet = code['GLFp']['sage'].format(**{'LFpsage':lie_sage_mats, 'nFp':nLie, 'Fp':qLie}) + else: + lie_sage_mats = "["+", ".join(["MS("+str(split_matrix_Fq_add_al(mat, nLie))+")" for mat in lie_mats])+"]" + sage_lie_code_snippet = code['GLFq']['sage'].format(**{'LFqsage':lie_sage_mats, 'nFq':nLie, 'Fq':qLie}) + if sage_top_lie is None: + sage_top_lie, sage_used_lie_gens = sage_lie_code_snippet, True + + # In the case no Lie type representation is implemented natively in Sage or GAP, we display the first one with gens as a matrix group + # as a code snippet in the "Constructions" header (and as a possible candidate for the top code snippet) + if (gap_top_lie is not None) and gap_used_lie_gens: + for lie_rep in self.lie_representations: + if "gens" in lie_rep: + code[lie_rep['family']]['gap'] = gap_top_lie + break + if (sage_top_lie is not None) and sage_used_lie_gens: + for lie_rep in self.lie_representations: + if "gens" in lie_rep: + code[lie_rep['family']]['sage'] = sage_top_lie + break + # Here, we add the (perhaps subjectively?) "best" implementation of this group as a code snippet in Magma/GAP/SageMath, # to display at the top of each group page. This is computed and stored in code['code_description']. # If the group is a member of a special family (i.e. Cyclic,Symmetric,Dihedral,Alternating,Dicyclic,LieType,Chevalley), @@ -2959,27 +3052,13 @@ def code_snippets(self): code['code_description']['gap'] = "G := DicyclicGroup("+str(self.order)+");" # GAP Dic(n) has order n code['code_description']['sage'] = "G = DiCyclicGroup("+str(self.order/4)+")" # Sage Dic(n) has order 4n else: - # List of Lie Type families available in Magma (NB: Must ensure the Magma implementation agrees with our definition!) - for f in ['GL', 'SL', 'PSL', 'PGL', 'Sp', 'SO', 'SU', 'PSp', 'PSO', 'PSU', 'SOPlus', 'SOMinus']: - if f in [t['family'] for t in self_families]: - fam_index = [t['family'] for t in self_families].index(f) - lie_params = str(self_families[fam_index]['parameters']['n'])+", "+str(self_families[fam_index]['parameters']['q']) - code['code_description']['magma'] = "G := "+f+"("+lie_params+");" - break - # List of Lie Type families available in GAP (NB: Must ensure the GAP implementation agrees with our definition!) - for f in ['GL', 'SL', 'PSL', 'PGL', 'Sp', 'SO', 'SU', 'PSp', 'PSO', 'PSU']: - if f in [t['family'] for t in self_families]: - fam_index = [t['family'] for t in self_families].index(f) - lie_params = str(self_families[fam_index]['parameters']['n'])+", "+str(self_families[fam_index]['parameters']['q']) - code['code_description']['gap'] = "G := "+f+"("+lie_params+");" - break - # List of Lie Type families available in Sage (NB: Must ensure the Sage implementation agrees with our definition!) - for f in ['GL', 'SL', 'PSL', 'PGL']: - if f in [t['family'] for t in self_families]: - fam_index = [t['family'] for t in self_families].index(f) - lie_params = str(self_families[fam_index]['parameters']['n'])+", "+str(self_families[fam_index]['parameters']['q']) - code['code_description']['sage'] = "G = "+f+"("+lie_params+")" - break + # Use a Lie Type matrix construction (if it exists and the Lie type generators were not used) + if magma_top_lie is not None: + code['code_description']['magma'] = magma_top_lie + if (gap_top_lie is not None) and (not gap_used_lie_gens): + code['code_description']['gap'] = gap_top_lie + if (sage_top_lie is not None) and (not sage_used_lie_gens): + code['code_description']['sage'] = sage_top_lie # Checking if group is in the Chevalley or Twisted Chevalley family if ('Chev' in [t['family'] for t in self_families]) and ('magma' not in code['code_description']): chev_index = [t['family'] for t in self_families].index("Chev") @@ -3018,6 +3097,11 @@ def code_snippets(self): for lang in code[code_rep]: if lang not in code['code_description']: code['code_description'][lang] = code[code_rep][lang] + # Finally, try using Lie constructions which required use of the generators + if (gap_top_lie is not None) and ('gap' not in code['code_description']): + code['code_description']['gap'] = gap_top_lie + if (sage_top_lie is not None) and ('sage' not in code['code_description']): + code['code_description']['sage'] = sage_top_lie # Otherwise, if absolutely all else fails, we display no code snippet at the top :( # If no Sage top code snippet, then we resort to implementing the group G using the GAP interface in Sage diff --git a/lmfdb/groups/glnC/main.py b/lmfdb/groups/glnC/main.py index 1e12089595..bfc9e79174 100644 --- a/lmfdb/groups/glnC/main.py +++ b/lmfdb/groups/glnC/main.py @@ -9,7 +9,7 @@ flash_error, SearchArray, TextBox, CountBox, YesNoBox, parse_ints, parse_bool, clean_input, to_dict, sparse_cyclotomic_to_latex, # parse_gap_id, parse_bracketed_posints, - search_wrap) + search_wrap, redirect_no_cache) from lmfdb.utils.search_columns import SearchColumns, LinkCol, MathCol from lmfdb.groups.abstract.web_groups import group_names_pretty from lmfdb.groups.abstract.main import abstract_group_display_knowl, abstract_subgroup_label_regex @@ -58,9 +58,10 @@ def index(): @glnC_page.route("/random") +@redirect_no_cache def random_glnC_group(): label = db.gps_crep.random(projection='label') - return redirect(url_for(".by_label", label=label)) + return url_for(".by_label", label=label) @glnC_page.route("/
By {{ KNOWL('character.dirichlet.modulus', title="modulus") }}:{% for rnge in info.modulus_list %} {{rnge}}   {% endfor %} $\cdots${% for rnge in info.modulus_list %} {{rnge}}   {% endfor %} $\cdots$
By {{ KNOWL('character.dirichlet.conductor', title="conductor") }}: {% for rnge in info.conductor_list %} {{rnge}}   {% endfor %} $\cdots$
{desc}:{reps}
{desc}:{reps}
{code_cmd}