From b22bbae68e3fdf06f0993742918fcc1b5bf0e82b Mon Sep 17 00:00:00 2001 From: rvisser7 Date: Thu, 14 May 2026 23:09:24 -0400 Subject: [PATCH 1/5] First pass at speeding up the multiquadratic case for field_pretty --- lmfdb/number_fields/web_number_field.py | 113 ++++++++++++++++++++++-- 1 file changed, 106 insertions(+), 7 deletions(-) diff --git a/lmfdb/number_fields/web_number_field.py b/lmfdb/number_fields/web_number_field.py index 2fae845ca0..1a4b1228bd 100644 --- a/lmfdb/number_fields/web_number_field.py +++ b/lmfdb/number_fields/web_number_field.py @@ -333,6 +333,9 @@ def _sqrt_symbol(z): D = D.sqrt() return r'\(\Q(\sqrt[3]{%d})\)' % D + import time + + """ # Case 7: General multi-quadratic fields: Q(\sqrt{D_1}, ..., \sqrt{D_k}) if ZZ(d).is_power_of(2): k = ZZ(d).valuation(2) @@ -351,25 +354,121 @@ def _sqrt_symbol(z): # Compute set of all primes dividing the Ds primes = sorted({int(p) for D in all_Ds for p in ZZ(abs(D)).prime_divisors()}) - # Keep track of prime exponents and row space used so far + # Keep track of prime exponents and row space (over F_2) used so far + # For efficiency, store row_space as a set of integers, considered as bit vectors. all_prime_exponents = [] - row_space = matrix(GF(2), all_prime_exponents).row_space() + row_space = {0} # The trivial space for D in sorted_Ds: - # Convert D to a vector of prime exponents mod 2 (including sign) - prime_exp = [int(D < 0)]+[D.valuation(p)%2 for p in primes] - if vector(prime_exp) not in row_space: + # Convert D to a vector of prime exponents mod 2 (including sign), stored with bits as an integer + prime_exp = int(D < 0) + for i in range(len(primes)): + prime_exp += (D.valuation(primes[i])%2) << (i+1) + if prime_exp not in row_space: final_Ds.append(D) # Break out once rank is full if len(final_Ds) == k: break - # Recompute row space + # Recompute the new row space (take prime_exp XOR everything else in row_space) all_prime_exponents.append(prime_exp) - row_space = matrix(GF(2), all_prime_exponents).row_space() + old_row_space = row_space.copy() + for v in old_row_space: + row_space.add(v^prime_exp) return r'\(\Q('+', '.join([_sqrt_symbol(D) for D in final_Ds])+r')\)' + """ + + + # Case 7: General multi-quadratic fields: Q(\sqrt{D_1}, ..., \sqrt{D_k}) + if ZZ(d).is_power_of(2): + t_case = time.perf_counter() + + k = ZZ(d).valuation(2) + + t = time.perf_counter() + wnf = WebNumberField(label) + print(f"WebNumberField: {time.perf_counter() - t:.4f}s") + + t = time.perf_counter() + all_subs = wnf.subfields() + print(f"subfields(): {time.perf_counter() - t:.4f}s") + + t = time.perf_counter() + quad_subs = [s[0] for s in all_subs if s[0].count(',') == 2] + num_quad_subs = len(quad_subs) + print(f"extract quadratic subfields: {time.perf_counter() - t:.4f}s") + + if num_quad_subs == int(d) - 1: + + t = time.perf_counter() + quad_labels = [ + str(wnf.from_coeffs(string2list(str(z))).get_label()) + for z in quad_subs + ] + print(f"convert to labels: {time.perf_counter() - t:.4f}s") + + t = time.perf_counter() + all_Ds = [_quad_label_to_D(qlabel) for qlabel in quad_labels] + print(f"_quad_label_to_D: {time.perf_counter() - t:.4f}s") + + t = time.perf_counter() + sorted_Ds = sorted(all_Ds, key=lambda x: (abs(x), -x)) + print(f"sorting Ds: {time.perf_counter() - t:.4f}s") + + final_Ds = [] + + t = time.perf_counter() + primes = sorted({ + int(p) + for D in all_Ds + for p in ZZ(abs(D)).prime_divisors() + }) + print(f"compute prime support: {time.perf_counter() - t:.4f}s") + + all_prime_exponents = [] + row_space = {0} + + t_loop = time.perf_counter() + + for D in sorted_Ds: + t_row = time.perf_counter() + + # build bitmask + prime_exp = int(D < 0) + for i in range(len(primes)): + prime_exp += (D.valuation(primes[i]) % 2) << (i + 1) + + print(f" build bitmask for {D}: {time.perf_counter() - t_row:.6f}s") + + if prime_exp not in row_space: + final_Ds.append(D) + + if len(final_Ds) == k: + break + + t_update = time.perf_counter() + all_prime_exponents.append(prime_exp) + + old_row_space = row_space.copy() + for v in old_row_space: + row_space.add(v ^ prime_exp) + + print( + f" update row space: " + f"{time.perf_counter() - t_update:.6f}s" + ) + + print(f"main loop total: {time.perf_counter() - t_loop:.4f}s") + print(f"total case 7: {time.perf_counter() - t_case:.4f}s") + + return ( + r'\(\Q(' + + ', '.join([_sqrt_symbol(D) for D in final_Ds]) + + r')\)' + ) + # Otherwise, if no latex form found, just return the LMFDB label return label From f0beada14eae243446847a1333b2b11110216b4e Mon Sep 17 00:00:00 2001 From: rvisser7 Date: Fri, 15 May 2026 19:33:59 -0400 Subject: [PATCH 2/5] Further optimisations to field_pretty, and add degree 32 multiquad latex label test --- lmfdb/number_fields/test_numberfield.py | 1 + lmfdb/number_fields/web_number_field.py | 129 ++++-------------------- 2 files changed, 19 insertions(+), 111 deletions(-) diff --git a/lmfdb/number_fields/test_numberfield.py b/lmfdb/number_fields/test_numberfield.py index cc370e2779..850f340559 100644 --- a/lmfdb/number_fields/test_numberfield.py +++ b/lmfdb/number_fields/test_numberfield.py @@ -111,6 +111,7 @@ def test_pretty_labels(self): self.check_args('/NumberField/4.0.2048.2', r'\Q(\sqrt{-2 + \sqrt{2}})') self.check_args('/NumberField/8.8.3317760000.1', r'\Q(\sqrt{2}, \sqrt{3}, \sqrt{5})') self.check_args('/NumberField/16.0.11007531417600000000.1', r'\Q(i, \sqrt{2}, \sqrt{3}, \sqrt{5})') + self.check_args('/NumberField/32.0.4026692887688564776141139207792885760000000000000000.1', r'\Q(i, \sqrt{2}, \sqrt{3}, \sqrt{5}, \sqrt{7})') def test_signature_search(self): # Square brackets diff --git a/lmfdb/number_fields/web_number_field.py b/lmfdb/number_fields/web_number_field.py index 1a4b1228bd..4c66430949 100644 --- a/lmfdb/number_fields/web_number_field.py +++ b/lmfdb/number_fields/web_number_field.py @@ -263,17 +263,15 @@ def _sqrt_symbol(z): # Case 5a: Biquadratic fields Q(\sqrt{A}, \sqrt{B}) if len(subs) == 3: # only for V_4 fields - subs = [wnf.from_coeffs(string2list(str(z[0]))) for z in subs] - # Abort if we don't know one of these fields - if not any(z._data is None for z in subs): - labels_str = [str(z.get_label()) for z in subs] - labels_split = [z.split('.') for z in labels_str] - # extract abs disc and signature to be good for sorting - labels = sorted([[integer_squarefree_part(ZZ(z[2])), - int(z[1])] for z in labels_split]) - # put in +/- sign - labels_values = [z[0] * (-1)**(1 + z[1] / 2) for z in labels] - labels_str = [_sqrt_symbol(z) for z in labels_values] - return r'\(\Q(%s, %s)\)' % (labels_str[0], labels_str[1]) + all_Ds = [] + for sub in subs: + qs = sub[0].split(',') + all_Ds.append(integer_squarefree_part(ZZ(qs[1])**2 - 4*ZZ(qs[0])*ZZ(qs[2]))) + + # Sort the Ds by absolute value (in case of tie, put positive Ds first) + labels_values = sorted(all_Ds, key=lambda x: (abs(x), -x)) + labels_str = [_sqrt_symbol(z) for z in labels_values] + return r'\(\Q(%s, %s)\)' % (labels_str[0], labels_str[1]) # Case 5b: Imprimitive quartic fields of type Q(\sqrt(A + B*\sqrt(D))) if len(subs) == 1: @@ -333,9 +331,6 @@ def _sqrt_symbol(z): D = D.sqrt() return r'\(\Q(\sqrt[3]{%d})\)' % D - import time - - """ # Case 7: General multi-quadratic fields: Q(\sqrt{D_1}, ..., \sqrt{D_k}) if ZZ(d).is_power_of(2): k = ZZ(d).valuation(2) @@ -344,15 +339,17 @@ def _sqrt_symbol(z): quad_subs = [s[0] for s in all_subs if s[0].count(',') == 2] num_quad_subs = len(quad_subs) if num_quad_subs == int(d) - 1: - quad_labels = [str(wnf.from_coeffs(string2list(str(z))).get_label()) for z in quad_subs] - all_Ds = [_quad_label_to_D(qlabel) for qlabel in quad_labels] + all_Ds = [] + for quad_sub in quad_subs: + qs = quad_sub.split(',') + all_Ds.append(integer_squarefree_part(ZZ(qs[1])**2 - 4*ZZ(qs[0])*ZZ(qs[2]))) # Sort the Ds by absolute value (in case of tie, put positive Ds first) sorted_Ds = sorted(all_Ds, key=lambda x: (abs(x), -x)) final_Ds = [] - # Compute set of all primes dividing the Ds - primes = sorted({int(p) for D in all_Ds for p in ZZ(abs(D)).prime_divisors()}) + # Compute set of all primes dividing the Ds (can take prime divisors of discriminant) + primes = ZZ(D).prime_divisors() # Keep track of prime exponents and row space (over F_2) used so far # For efficiency, store row_space as a set of integers, considered as bit vectors. @@ -361,9 +358,10 @@ def _sqrt_symbol(z): for D in sorted_Ds: # Convert D to a vector of prime exponents mod 2 (including sign), stored with bits as an integer + # D is already squarefree, so all prime exponents either 0 or 1 prime_exp = int(D < 0) for i in range(len(primes)): - prime_exp += (D.valuation(primes[i])%2) << (i+1) + prime_exp += int((D%primes[i]) == 0) << (i+1) if prime_exp not in row_space: final_Ds.append(D) @@ -375,101 +373,10 @@ def _sqrt_symbol(z): all_prime_exponents.append(prime_exp) old_row_space = row_space.copy() for v in old_row_space: - row_space.add(v^prime_exp) + row_space.add(v^prime_exp) # here ^ is bitwise XOR return r'\(\Q('+', '.join([_sqrt_symbol(D) for D in final_Ds])+r')\)' - """ - - # Case 7: General multi-quadratic fields: Q(\sqrt{D_1}, ..., \sqrt{D_k}) - if ZZ(d).is_power_of(2): - t_case = time.perf_counter() - - k = ZZ(d).valuation(2) - - t = time.perf_counter() - wnf = WebNumberField(label) - print(f"WebNumberField: {time.perf_counter() - t:.4f}s") - - t = time.perf_counter() - all_subs = wnf.subfields() - print(f"subfields(): {time.perf_counter() - t:.4f}s") - - t = time.perf_counter() - quad_subs = [s[0] for s in all_subs if s[0].count(',') == 2] - num_quad_subs = len(quad_subs) - print(f"extract quadratic subfields: {time.perf_counter() - t:.4f}s") - - if num_quad_subs == int(d) - 1: - - t = time.perf_counter() - quad_labels = [ - str(wnf.from_coeffs(string2list(str(z))).get_label()) - for z in quad_subs - ] - print(f"convert to labels: {time.perf_counter() - t:.4f}s") - - t = time.perf_counter() - all_Ds = [_quad_label_to_D(qlabel) for qlabel in quad_labels] - print(f"_quad_label_to_D: {time.perf_counter() - t:.4f}s") - - t = time.perf_counter() - sorted_Ds = sorted(all_Ds, key=lambda x: (abs(x), -x)) - print(f"sorting Ds: {time.perf_counter() - t:.4f}s") - - final_Ds = [] - - t = time.perf_counter() - primes = sorted({ - int(p) - for D in all_Ds - for p in ZZ(abs(D)).prime_divisors() - }) - print(f"compute prime support: {time.perf_counter() - t:.4f}s") - - all_prime_exponents = [] - row_space = {0} - - t_loop = time.perf_counter() - - for D in sorted_Ds: - t_row = time.perf_counter() - - # build bitmask - prime_exp = int(D < 0) - for i in range(len(primes)): - prime_exp += (D.valuation(primes[i]) % 2) << (i + 1) - - print(f" build bitmask for {D}: {time.perf_counter() - t_row:.6f}s") - - if prime_exp not in row_space: - final_Ds.append(D) - - if len(final_Ds) == k: - break - - t_update = time.perf_counter() - all_prime_exponents.append(prime_exp) - - old_row_space = row_space.copy() - for v in old_row_space: - row_space.add(v ^ prime_exp) - - print( - f" update row space: " - f"{time.perf_counter() - t_update:.6f}s" - ) - - print(f"main loop total: {time.perf_counter() - t_loop:.4f}s") - print(f"total case 7: {time.perf_counter() - t_case:.4f}s") - - return ( - r'\(\Q(' - + ', '.join([_sqrt_symbol(D) for D in final_Ds]) - + r')\)' - ) - - # Otherwise, if no latex form found, just return the LMFDB label return label From 90986dbece941bb1095a18b1221be684e873c8d2 Mon Sep 17 00:00:00 2001 From: rvisser7 Date: Fri, 15 May 2026 19:41:16 -0400 Subject: [PATCH 3/5] Remove all_prime_exponents from field_pretty --- lmfdb/number_fields/web_number_field.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lmfdb/number_fields/web_number_field.py b/lmfdb/number_fields/web_number_field.py index 4c66430949..8ecd6ee257 100644 --- a/lmfdb/number_fields/web_number_field.py +++ b/lmfdb/number_fields/web_number_field.py @@ -352,8 +352,7 @@ def _sqrt_symbol(z): primes = ZZ(D).prime_divisors() # Keep track of prime exponents and row space (over F_2) used so far - # For efficiency, store row_space as a set of integers, considered as bit vectors. - all_prime_exponents = [] + # For fast computations, store row_space just as a set of integers, considered as vectors of bits. row_space = {0} # The trivial space for D in sorted_Ds: @@ -370,13 +369,12 @@ def _sqrt_symbol(z): break # Recompute the new row space (take prime_exp XOR everything else in row_space) - all_prime_exponents.append(prime_exp) old_row_space = row_space.copy() for v in old_row_space: row_space.add(v^prime_exp) # here ^ is bitwise XOR return r'\(\Q('+', '.join([_sqrt_symbol(D) for D in final_Ds])+r')\)' - + # Otherwise, if no latex form found, just return the LMFDB label return label From 35e16c9b220b064b4283cd00389fc1830507f884 Mon Sep 17 00:00:00 2001 From: rvisser7 Date: Fri, 15 May 2026 20:07:46 -0400 Subject: [PATCH 4/5] Fix linting for field_pretty --- lmfdb/number_fields/web_number_field.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lmfdb/number_fields/web_number_field.py b/lmfdb/number_fields/web_number_field.py index 8ecd6ee257..3f9d26643d 100644 --- a/lmfdb/number_fields/web_number_field.py +++ b/lmfdb/number_fields/web_number_field.py @@ -4,7 +4,7 @@ from flask import url_for from sage.all import ( - Set, ZZ, RR, pi, gcd, euler_phi, CyclotomicField, gap, RealField, sqrt, prod, matrix, vector, GF, + Set, ZZ, RR, pi, gcd, euler_phi, CyclotomicField, gap, RealField, sqrt, prod, QQ, NumberField, QuadraticField, PolynomialRing, latex, pari, cached_function, Permutation) from lmfdb import db From 281e4cb602e0edd572ea80b7d09fcce6f86a209c Mon Sep 17 00:00:00 2001 From: rvisser7 Date: Wed, 3 Jun 2026 15:32:36 -0400 Subject: [PATCH 5/5] Change discriminant variable to disc in field_pretty --- lmfdb/number_fields/web_number_field.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lmfdb/number_fields/web_number_field.py b/lmfdb/number_fields/web_number_field.py index 3f9d26643d..e9ab44c86b 100644 --- a/lmfdb/number_fields/web_number_field.py +++ b/lmfdb/number_fields/web_number_field.py @@ -221,7 +221,7 @@ def field_pretty(label): - cyclotomic fields and their maximal real subfields, and general multi-quadratic fields. """ - d, r, D, _ = label.split('.') + d, r, disc, _ = label.split('.') # Case 1: The rationals Q if d == '1': # Q @@ -239,8 +239,9 @@ def _sqrt_symbol(z): return 'i' if z == -1 else r'\sqrt{%d}' % z # Case 2: Quadratic fields Q(\sqrt{D}) + # (note that we give the pretty name for 2.0.4.1 as \Q(\sqrt{-1}), and not \Q(i)) if d == '2': - D = ZZ(int(D)) + D = ZZ(disc) if r == '0': D = -D # Don't prettify invalid quadratic field labels @@ -349,7 +350,7 @@ def _sqrt_symbol(z): final_Ds = [] # Compute set of all primes dividing the Ds (can take prime divisors of discriminant) - primes = ZZ(D).prime_divisors() + primes = ZZ(disc).prime_divisors() # Keep track of prime exponents and row space (over F_2) used so far # For fast computations, store row_space just as a set of integers, considered as vectors of bits.