Skip to content

Add Szpiro ratio for elliptic curves over number fields - #33

Open
roed-math wants to merge 3 commits into
mainfrom
ai/t15-ecnf-szpiro
Open

Add Szpiro ratio for elliptic curves over number fields#33
roed-math wants to merge 3 commits into
mainfrom
ai/t15-ecnf-szpiro

Conversation

@roed-math

@roed-math roed-math commented Jul 19, 2026

Copy link
Copy Markdown
Owner

Adds the classical Szpiro ratio $\sigma = \log|N(\mathfrak{D}_{\mathrm{min}})|/\log N(\mathfrak{N})$ (Hindry) to the ECNF section: curve homepage display, search box, results column and sort order. All uses are guarded by a check that the szpiro_ratio column exists in ec_nfcurves, so this is safe to deploy before the data upload.

That check is a startup-time compatibility guard, not a live one: it, the result columns, the search array and the sort choices are all built when lmfdb.ecnf.main is imported, so adding the column does not make the feature appear in a running web worker (and neither does db.refresh_tables()). The migration therefore ends with a restart. The full order (generate and verify the file, create the ec.szpiro_ratio knowl, add the column, upload, create the index backing the new range search and sort, restart every web worker, smoke test display/search/sort) is written out in the generator's docstring.

Includes scripts/ecnf/generate_szpiro_ratio.py, which computes the ratio for all 767518 curves from stored invariants in ~3 minutes (NULL for the 712 curves with everywhere good reduction, where the ratio is undefined), cross-checks two independent formulas on every non-minimal-model row plus exactly --sample-check minimal-model rows, and can re-verify random rows from scratch in Sage; values were additionally checked against base changes of Q-curves with known ratios. Being a one-time production migration it fails closed: every integrity problem raises with the offending label and exits nonzero, and rows are streamed to a temporary file that replaces the output path only once the row counts match and all cross-checks and the requested --verify have passed. Addresses LMFDB#6292.

🤖 Generated with Claude Code

roed314 and others added 3 commits July 19, 2026 12:29
Add a szpiro_ratio column (classical Szpiro ratio
log(Norm(D_min))/log(Norm(N)), Hindry p.8) to the ECNF section: curve
page display, search box, search-results column and sort order, all
guarded by a module-level flag so the site keeps working until the
column is added to ec_nfcurves.  The ratio is left NULL for the 712
curves with everywhere good reduction, for which it is not defined.

scripts/ecnf/generate_szpiro_ratio.py computes the column for all
767518 curves from stored data (conductor_norm, normdisc, non_min_p,
local_data) in about 3 minutes, cross-checking the two available
formulas against each other, and has a --verify mode that recomputes
random rows from scratch with Sage.  Verified: full generation run with
--verify 25 (all match to 1e-12); base changes of 11.a1/11.a2/37.a1
agree with the classical Q-side ratios including ramified-prime scaling;
lmfdb/ecnf/test_ecnf.py passes (13 tests) with the column absent;
pyflakes clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review follow-up on the Szpiro ratio for elliptic curves over number
fields.  The mathematical definition and the frontend are unchanged.

- main.py: document HAVE_SZPIRO_RATIO as a startup-time compatibility
  guard.  It, the result columns, the search array and the sort choices
  are all built at import, so adding the column does not activate the
  feature in a running worker; the workers have to be restarted.

- generate_szpiro_ratio.py: spell out the migration order (generate,
  knowl, add column, upload, index, restart, smoke test), including the
  ec_nfcurves_szpiro_ratio_sort btree matching the sort tuple, created
  after update_from_file so the swap does not build it twice.

- generate_szpiro_ratio.py: fail closed.  Every data-integrity assert is
  now an explicit ValueError/RuntimeError naming the curve; the two
  partition counts must add up to the table count and the rows written
  must match the expected number; rows stream to a temporary file that
  replaces the output path only after all cross-checks, the count
  checks and any --verify have passed, so no partial file is left where
  it could be uploaded.  --sample-check N now makes exactly N checks
  (positions drawn with random.sample over the rows actually processed,
  not a Bernoulli frequency from the full table count), the numeric
  flags must be nonnegative, --verify above the generated row count
  errors clearly, and --seed makes the sampling reproducible.

- test_ecnf.py: the positive search test no longer passes when the
  constraint is ignored.  With the column present the test asserts the
  displayed value 1.0 on the curve page, that a 0.5-1.5 range contains
  the full curve url while a disjoint 1.1-1.5 range does not, that
  sort_order=szpiro_ratio un-hides the column, and that an
  everywhere-good-reduction curve shows no ratio row; without it, the
  compatibility branch only claims that the pages still load.  New
  schema-independent tests cover the generator helpers.

Verified: pytest -k szpiro (2 passed) and pyflakes/pylint/ruff clean;
the curve-page markup and the everywhere-good-reduction omission checked
against a simulated column-present row, since devmirror has no
szpiro_ratio column yet; a bounded run (--limit 1000 --sample-check 100
--verify 20) wrote exactly 2000 rows and made exactly 100 cross-checks;
an injected mismatch exited nonzero leaving the previous output intact
and no temporary file behind.

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

Copy link
Copy Markdown
Owner Author

Pushed 1ab4984 with the review follow-up. The classical definition and the frontend design are unchanged; what changed is the deployment contract, the index, the tests and the generator's failure behaviour. The PR description has been corrected too.

1. Activation contract. HAVE_SZPIRO_RATIO is now documented as a startup-time compatibility guard: it, ecnf_columns, ECNFSearchArray.sorts and the search boxes are all built when lmfdb.ecnf.main is imported, so adding the column does not activate the feature in a running worker, and db.refresh_tables() does not either. The generator docstring carries the migration order: generate and verify the file, create the ec.szpiro_ratio knowl, add the column, upload and check the row counts, create the index, restart every web worker, smoke test display/search/sort. The description no longer promises automatic activation.

2. Index. Deployment step 5 creates ec_nfcurves_szpiro_ratio_sort, a btree on exactly the sort tuple registered for "Szpiro ratio" in ECNFSearchArray.sorts, through create_index so that it is recorded in meta_indexes and rebuilt by later reloads. It is created after update_from_file, since that merges into a new table and rebuilds every index during the swap, and is followed by list_indexes(verbose=True) plus two analyze(..., explain_only=True) checks (a ratio-ordered LIMIT 50 and a narrow range query). Note that none of the other ECNF float sorts (reg, sha, class_size, class_deg) has an index today, so this one is new ground rather than a restoration.

3. Tests. The old positive search proved nothing, since the same curve comes back when the constraint is ignored. With the column present, test_szpiro_ratio now asserts the displayed value 1.0 in the curve-page row, that szpiro_ratio=0.5-1.5 contains the full curve url /EllipticCurve/2.2.5.1/31.1/a/1 while the disjoint szpiro_ratio=1.1-1.5 does not, that sort_order=szpiro_ratio returns 200 and un-hides the column (its <th class="col-szpiro_ratio"> loses display:none), and that the everywhere-good-reduction curve 3.3.1369.1-1.1-a1 shows no ratio row. Without the column the compatibility branch only claims that the pages load and offer no ratio, and says so in a comment. A new test_szpiro_ratio_generator covers the generator helpers and runs whatever the schema is: szpiro_ratio(31, 31) == 1.0, szpiro_ratio(1, 1) is None, invalid Nnorm == 1 with Dnorm != 1 raising, a synthetic non-minimal row agreeing under both formulas, and missing local_data for a listed non_min_p raising with the label and the prime in the message.

One thing worth knowing for the test: the curve-page assertion matches the label text, not the knowl markup, because KNOWL() renders a bare label until ec.szpiro_ratio exists and an anchor afterwards. That is also why the knowl is step 2 of the migration.

4. Fail closed. Every data-integrity assert in generate, min_disc_norm_from_normdisc, szpiro_ratio and verify is now an explicit ValueError/RuntimeError naming the curve and the values (Dnorm > 0, Nnorm > 0, Nnorm == 1 => Dnorm == 1, every non_min_p prime present in local_data, divisibility by normp**12, agreement of the two formulas, the verification tolerance and the expected \N). The two partition counts must add up to count() before anything is written, and the rows written must equal min(limit, minimal) + min(limit, nonminimal); the warning-only completion path is gone. Rows stream into a temporary file next to the output path, and --verify runs against that temporary file, so the requested output is only replaced after every check has passed; on any failure the temporary file is deleted and any pre-existing output is left alone.

5. --sample-check N. Positions are now drawn once with random.sample over the minimal rows actually processed, so exactly min(N, processed_minimal) rows are cross-checked (the old Bernoulli frequency came from the full table count, so a small --limit usually checked none). The count is reported and enforced. --limit, --sample-check and --verify must be nonnegative, --verify above the generated row count gives a clear error naming the actual count, and a new --seed makes the sampling reproducible.

Verified

  • sage -python -m pytest lmfdb/ecnf/test_ecnf.py -k szpiro -> 2 passed; pyflakes, pylint -e W0129,W0108 and ruff --select=E722 clean.
  • Curve-page markup and the everywhere-good-reduction omission checked against a simulated column-present row (the page depends only on the attribute, not on the flag), including a negative control that the value regex does not match a wrong value.
  • --limit 1000 --sample-check 100 --verify 20 wrote exactly 2000 rows with exactly 100 cross-checks; the file is mode 0644 with no temporary left behind.
  • Injecting a mismatch between the two formulas partway through: exits nonzero with 2.0.68.1-66.1-a2: local_data gives Norm(D_min) = ... but normdisc gives ..., the pre-existing output is byte-identical afterwards and no temporary file survives. --verify 10 on a 4-row file and negative flags also exit nonzero without creating the output.
  • A full run against devmirror (--verify 25 --seed 7, ~3 min) exits 0 having written exactly 767518 rows, that is db.ec_nfcurves.count(), with exactly 712 \N rows, matching count({"conductor_norm": 1}), and exactly 1000 cross-checks. Its output has the same 767518 label|value pairs as the file generated before this rewrite (only the physical row order differs, sort=[]), so the hardening changed no values.

Not yet possible here: devmirror has no szpiro_ratio column, so the column-present branch of test_szpiro_ratio still skips over the real path. It has to be rerun against the data once the column is added, as the review asks, before the workers are restarted.

🤖 Generated with Claude Code

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.

2 participants