Skip to content

Integral elliptic-quartic Magma adapter (IntegralQuarticPoints) #78

Description

@roed-math

Goal

Add the elliptic-quartic Magma adapter on top of the core runner (#76): for v^2 = Q(u) with Q a quartic with concrete integer coefficients, find a deterministic integral seed point P on the model, run Magma's two-argument IntegralQuarticPoints(Q, P) on the encoded coefficient sequence and that seed, parse the returned integral points into a SolutionSet whose completeness, scope and assumptions state exactly what that routine establishes, verify every point against the original equation, and fall back to today's SolverUnavailable-with-templates on any failure — including a failed seed search. Opt-in only (solve(..., use_magma=True) / --use-magma).

This is the adapter that gives #50 a route to integral points: #65 lists it as the integral-points route, under the assumptions recorded here and subject to the seed applicability guard specified below.

Target base

Dependencies

Pinned external API

Magma version floor: V2.29 (or an earlier version on which the two
signatures below have been specifically verified by running them; there is
no other supported configuration). The adapter records the Magma version
reported by the run in its Evidence.

The following is quoted verbatim from the Magma V2.29 Handbook (footer:
V2.29, 3 August 2026), chapter Elliptic Curves over Q and Number Fields,
section Curves over the Rationals, subsection Integral and S-integral
Points
, at
https://magma.maths.usyd.edu.au/magma/handbook/text/1567
(fetched and read 2026-08-09). Cite the section title, not only the page
number: in the current V2.29 printing text/1566 is the Introduction of the
same chapter and the intrinsics below sit one page later, so a bare page
number drifts between printings.

IntegralQuarticPoints(Q) : [ RngIntElt ] -> [ SeqEnum ]

If Q is a sequence of five integers [a, b, c, d, e] where e is a
square, this function returns all integral points (modulo negation)
on the curve y^2 = a x^4 + b x^3 + c x^2 + d x + e.

IntegralQuarticPoints(Q, P) : [ RngIntElt ], [ RngIntElt ] -> [ SeqEnum ]

If Q is a list of five integers [a, b, c, d, e] defining the
hyperelliptic quartic y^2 = a x^4 + b x^3 + c x^2 + d x + e and P is a
sequence representing a rational point [x, y], this function
returns all integral points on Q.

Three consequences are binding for this adapter.

  1. The coefficient sequence is descending, and e is the constant
    coefficient.
    That is exactly the qcoeffs convention of T3: elliptic quartics — Jacobian, two-covers, and transported point computations #50, so no
    reordering is performed anywhere.
  2. The one-argument form is not usable here. Its documented hypothesis is
    that the constant coefficient e is a square. The fixture equation has
    e = 7, which is not a square, so the one-argument call is outside the
    documented hypothesis; and even where it does apply it returns points only
    modulo negation, which would silently halve a solution set this
    repository is contractually required to return in full. This adapter
    never calls the one-argument form.
  3. The two-argument form needs a rational point P = [x, y] on the
    quartic
    and then returns all integral points (no modulo-negation
    qualifier). Supplying that point is this adapter's job; see Deterministic
    seed search
    .

The same handbook subsection states the provenance and method of the routine
(verbatim):

The following algorithms use the technique of linear forms in complex
and p-adic elliptic logarithms. They were initially implemented by
Emmanuel Hermann in the late 90s, and then Steve Donnelly made various
improvements. The main theoretical reference is Stroeker and Tzanakis
[ST94], and Tzanakis [ST96] for the case of a quartic equation.

The main routine here is (S)IntegralPoints for an elliptic curves.
The functions listed afterwards, for determining integral points on
various other kinds of genus one curves, are applications of the main
routine.

and, for that main routine, IntegralPoints:

The algorithm involves first computing generators of the Mordell--Weil group,
using the tools available in Magma for this.

Since the handbook says in so many words that the quartic functions are
applications of IntegralPoints, this is the documentary basis for the two
assumptions recorded under Guarantee, scope and assumptions below: the
Mordell–Weil computation is performed by Magma itself and is not re-derived by
this package.

The handbook's own worked example on the same page fixes the return element
shape
— a sequence of two-element integer sequences [x, y] (verbatim):

> IntegralQuarticPoints([1, 0, -8, 8, 1]);
[
    [ 2, -1 ],
    [ -6, 31 ],
    [ 0, 1 ]
]

(That example uses the one-argument form legitimately: its constant
coefficient is 1, a square. It is quoted here only to pin p[1], p[2].)

Supported inputs

Registered as MAGMA_SOLVERS["elliptic-quartic"] with

MAGMA_CAPABILITIES["elliptic-quartic"] = SolverCapability(
    slug="elliptic-quartic", domains=frozenset(("ZZ", "NN")),
    goals=frozenset(("enumerate", "exists", "find-one")),
    kinds=frozenset(("finite-complete", "empty")),
    completeness=frozenset(("conditional",)),
    guards=(see "Guards" below))

elliptic-quartic has no entry in CAPABILITIES/SOLVERS today and gains
none: with use_magma=False the family still has no wired solver.

Input regime

v^2 = Q(u), deg Q = 4, all coefficients concrete integers; integral
points, i.e. effective domain ZZ (or NN, handled by the existing
_restrict_nonneg post-pass). Verified today (re-run against Sage 10.7 on
85f8e0a):

classify("v^2 = 2*u^4 - 3*u^3 + u^2 - 5*u + 7").data
  == {'q': '2*u^4 - 3*u^3 + u^2 - 5*u + 7', 'x': 'u', 'y': 'v',
      'qcoeffs': '[2, -3, 1, -5, 7]'}
classify("v^2 = 2*u^4 - 3*u^3 + u^2 - 5*u + 7").parsed.unknowns == ('v', 'u')

qcoeffs is [a, b, c, d, e] descending: Q(u) = a*u^4 + b*u^3 + c*u^2 + d*u + e (the convention used throughout #50, and — see above — exactly the
sequence order the pinned intrinsic documents). Match data values are
stringified, so the list is read with ast.literal_eval and each entry
converted exactly with ZZ(...); a non-integer entry declines the adapter.
The resulting list goes through encode_int_list, which is the only path
into the script. data["x"]/data["y"] give
the user's names for the curve's coordinates — the assignment is built from
those and then ordered with _ordered(cls, assignment), so the returned
tuples follow the unknowns' order of first appearance: for the fixture
equation that is (v, u), not (u, v).

Domain gate

The rational case is a different problem and this adapter never touches it.
With domain="QQ" the Magma route is skipped by the capability check and
solve behaves exactly as today (verified: SolverUnavailable("no automatic solver for family 'elliptic-quartic' yet — ...")). Rational points on
genus-one quartics are #50's business, not this adapter's.

Guards (each one declines: MagmaFailure, then the standard fallback)

  • any coefficient not a concrete integer (parametric input, or a rational
    coefficient — encode_int_list rejects both);
  • leading coefficient zero, i.e. deg Q != 4;
  • Q not separable (zero discriminant): the model is then not a smooth
    genus-one curve and the routine's hypotheses do not apply;
  • the effective domain not in ("ZZ", "NN");
  • no integral seed point found within INTEGRAL_QUARTIC_SEED_BOUND (the
    new guard; see immediately below). This guard declines before any Magma
    process is started
    .

Deterministic seed search

The two-argument intrinsic requires a rational point on the quartic. This
adapter supplies an integral one, found by an exact bounded enumeration in
Sage before Magma is invoked. The helper is fully specified here; there is
nothing to choose at implementation time.

INTEGRAL_QUARTIC_SEED_BOUND = 50   # module-level constant of the adapter


def _integral_quartic_seed(coeffs):
    """
    Return an exact integral point ``(u, v)`` with ``v^2 == Q(u)``, or
    ``None``.

    ``coeffs`` is ``[a, b, c, d, e]`` descending, all ``ZZ``.
    ``Q(u) = a*u^4 + b*u^3 + c*u^2 + d*u + e``.

    The abscissas are tried in exactly this order::

        u = 0, 1, -1, 2, -2, ..., B, -B      with B = INTEGRAL_QUARTIC_SEED_BOUND

    For each ``u`` the value ``Q(u)`` is computed exactly in ``ZZ`` and
    accepted iff ``Q(u) >= 0`` and ``ZZ(Q(u)).is_square()``.  The first
    accepted ``u`` yields ``(u, ZZ(Q(u)).sqrt())`` — the **nonnegative**
    square root — and the enumeration stops.  If no ``u`` in the list is
    accepted the helper returns ``None``.
    """

Binding properties of this helper:

  • The enumeration order above is part of the contract, not an implementation
    detail: it makes the seed a deterministic function of coeffs, so the
    generated script is reproducible and snapshot-testable. The list has
    1 + 2*B = 101 entries at the shipped bound.
  • The search is model-level. It enumerates over ZZ and ignores the
    user's requested domain entirely; in particular a domain="NN" request may
    still be seeded by a negative u. The seed is not a solution being
    reported, it is an argument being handed to Magma, and the NN
    restriction is applied to the result afterwards (see Parsing).
  • INTEGRAL_QUARTIC_SEED_BOUND is an applicability guard, not a
    completeness bound
    . It bounds how hard the adapter looks for a way in;
    it says nothing about how far IntegralQuarticPoints then searches, and it
    never appears in a scope string.
  • A failed seed search is not an emptiness claim. If the helper returns
    None the adapter raises MagmaFailure(..., outcome="cas-failure") with
    reason "no-seed-point" and the standard fallback runs, exactly as for any
    other declined guard. The adapter must never turn "I could not find a point
    to start from" into kind="empty", and must not start a Magma process in
    this case.
  • The seed is re-verified exactly (v^2 == Q(u) in ZZ) immediately before
    encoding, so a bug in the enumerator cannot put a non-point into the script.

The fixture's seed, computed by hand and machine-verified. For
Q = [2, -3, 1, -5, 7], i.e. Q(u) = 2*u^4 - 3*u^3 + u^2 - 5*u + 7, the
declared enumeration order gives

step u Q(u) exact square?
1 0 7 no
2 1 2 - 3 + 1 - 5 + 7 = 2 no
3 -1 2 + 3 + 1 + 5 + 7 = 18 no
4 2 32 - 24 + 4 - 10 + 7 = 9 yes, 9 = 3^2

so the helper returns exactly (2, 3) and stops; -2 is never reached. In
words, and this is the statement the test pins: Q(0) = 7 is not a square,
Q(1) = 2 is not a square, Q(-1) = 18 is not a square, and Q(2) = 9 = 3^2
is.
(Verified against Sage 10.7 during this edit: the declared enumeration
was run and the first hit is (2, 3), with both entries ZZ; disc(Q) = 147348 != 0, so the separability guard passes.) P = [2, 3] is the seed the
pinned fixture asserts.

Output and API contract

Script template

diophantine_classifier/data/magma/integral_quartic_points.m, assembled by

build_script("integral_quartic_points",
             coeffs=encode_int_list([a, b, c, d, e]),
             seed=encode_int_list([u, v]))

on top of prelude.m. Both placeholders are filled only with typed
integer-encoder output; there is no other path into the script.

// integral_quartic_points.m  (prelude.m is prepended)
Q := <<coeffs>>;                      // [a, b, c, d, e], descending
P := <<seed>>;                        // [u, v] with v^2 = Q(u), v >= 0

try
    pts := IntegralQuarticPoints(Q, P);
    entries := [ JSONList([JSONInt(p[1]), JSONInt(p[2])]) : p in pts ];
    DCEmit("ok", "", JSONObj(["points"], [JSONList(entries)]));
catch e
    DCEmit("error", "routine-error", JSONObj([], []));
end try;
quit;

For the fixture this renders literally and completely as

Q := [2, -3, 1, -5, 7];
P := [2, 3];

try
    pts := IntegralQuarticPoints(Q, P);
    entries := [ JSONList([JSONInt(p[1]), JSONInt(p[2])]) : p in pts ];
    DCEmit("ok", "", JSONObj(["points"], [JSONList(entries)]));
catch e
    DCEmit("error", "routine-error", JSONObj([], []));
end try;
quit;

p[1], p[2] is the documented element shape: the intrinsic's return type is
[ SeqEnum ], a sequence of two-element integer sequences [x, y], as the
handbook's own worked example quoted above shows.

The try/catch wrapper stays mandatory: a Magma-side error (a rank
condition the routine cannot settle, a Mordell–Weil computation that fails,
an unexpected model rejection) surfaces as status: "error" and takes the
fallback path — never a partial parse and never a completeness claim.

Payload schema (data)

Unchanged from the previous design: the two-argument intrinsic returns
the same shape as the one-argument one, so the payload stays a flat list of
(u, v) pairs.

field type meaning
points array of 2-element arrays of decimal strings the integral points, as (u, v) in the curve's own coordinates

Integers are decimal strings; bare JSON numbers are rejected by the protocol
(#76 §2). The seed itself is not echoed in the payload — it is already
recorded in the evidence command string.

Parsing into a SolutionSet

  1. decode_int each coordinate; build the assignment
    {data["x"]: u, data["y"]: v} and order it with _ordered(cls, ...).
  2. sorted(set(...)) — deduplicate and sort. (The two-argument form returns
    all integral points rather than one of each ± pair, so (u, v) and
    (u, -v) both arrive; deduplication is still applied because nothing in
    the intrinsic's contract promises a duplicate-free sequence.)
  3. No silent prefilter. The adapter does not drop, correct or quietly
    discard any returned pair for any reason. solvers._verified is the
    firewall; a point that fails ParsedEquation.accepts raises
    InternalConsistencyError (this equation carries no nonvanishing
    conditions, so silent dropping is not permitted).
  4. The ordinary NN restriction is applied afterwards, by the existing
    _restrict_nonneg post-pass — never as a prefilter inside the adapter,
    and never inside the seed search.
  5. kind = "finite-complete" when nonempty, "empty" otherwise.
  6. representation is left to _derived_representation, which gives
    PartialSearch(solutions, scope) for a finite-complete result that is
    not proved. It must not be overridden with a FiniteSet: that would
    assert unconditional completeness.
  7. scope_info = Scope(domain_scope="integral points of the affine model", ordering_convention="unknowns in order of first appearance in the input equation").

Guarantee, scope and assumptions

IntegralQuarticPoints bounds the integral points by elliptic-logarithm
estimates on the associated elliptic curve (handbook quote above), and those
estimates are computed from a Mordell–Weil basis that Magma itself
computes
. The honest report is therefore:

completeness="conditional"
scope="all integral points"
assumptions=("Magma's Mordell-Weil computation for the associated curve "
             "is correct and saturated",
             "the external Magma routine computed correctly")

conditional requires nonempty assumptions (docs/SEMANTICS.md §3), and
these are the two things the classifier cannot re-derive: it re-checks every
returned point against the user's equation, but not that the search bound was
valid or that the basis was saturated. This mirrors how
_solve_weierstrass already reports a gens(proof=False) fallback
(assumptions=("the unproved Mordell-Weil basis is correct and saturated",)).

The seed bound does not enter scope. scope describes the set the
routine reports on; INTEGRAL_QUARTIC_SEED_BOUND only decides whether the
routine is invoked at all.

No statement is made — here or anywhere — that a Magma-backed answer settles
the family in general: the guarantee above belongs to this routine on this
input regime.

Evidence

result.as_evidence(
    "Magma IntegralQuarticPoints(Q, P) (elliptic-logarithm bounds on the "
    "associated elliptic curve, seeded with an exact integral point)",
    command="integral_quartic_points([2, -3, 1, -5, 7], seed=[2, 3])",
    reference=("Tzanakis2013", "StroekerTzanakis1994"))

The command string records the seed, so a certificate identifies the exact
call that was made. Both keys already resolve in data/references.bib
(verified today): Tzanakis2013 is already cited by the elliptic-quartic
family (why: "solving quartic elliptic equations via elliptic logarithms"),
and StroekerTzanakis1994 (cited today by elliptic-weierstrass) is
precisely the [ST94] the handbook names as the routine's main theoretical
reference in the quotation above. No bibliography change is required. If the
implementation adds one, it follows the standard
rules (author/title/journal/volume/year, nonempty why, doi verified at
commit time, url only for legally free copies, make references passing).

Failure and fallback semantics

Guard declined (including no seed point found), executable missing,
timeout, nonzero exit, status != "ok" (including an error raised on rank
conditions), oversized output or unparseable payload all fall through to
today's behavior for this family: _local_emptiness still runs first, and
then

SolverUnavailable("no automatic solver for family 'elliptic-quartic' yet — "
                  "magma: IntegralQuarticPoints([a4,...,a0]); sage: Jacobian "
                  "of genus-one models; code[magma (elliptic-quartic)]: "
                  "IntegralQuarticPoints([2, -3, 1, -5, 7]);; no small local "
                  "obstruction found either",
                  family="elliptic-quartic")     # outcome unsupported-family

is raised with "; the Magma route was attempted and did not produce a result (<outcome>): <reason>" appended (#76). The message above is today's
verbatim output for the fixture equation (re-verified during this edit) and is
what the equivalence test pins.

Note the ordering, because the no-seed test depends on it: _local_emptiness
runs before the family's SolverUnavailable is raised, so an equation with
a real or small-modular obstruction never reaches this adapter at all. That is
why the no-seed fixture below is chosen to be an equation the local layer
cannot settle.

Acceptance criteria

Fixture (deterministic, fake runner, no Magma)

Equation: v^2 = 2*u^4 - 3*u^3 + u^2 - 5*u + 7 (the quartic already used as
the #50 fixture; qcoeffs == [2, -3, 1, -5, 7], verified).

Fake payload data["points"] = [["2", "3"], ["2", "-3"]] — genuine integral
points, since Q(2) = 2*16 - 3*8 + 4 - 10 + 7 = 9 = (±3)^2 (verified).

Assertions on solve("v^2 = 2*u^4 - 3*u^3 + u^2 - 5*u + 7", use_magma=True)
with the fake runner:

  1. result.variables == ('v', 'u');
  2. result.solutions == [(-3, 2), (3, 2)] — the (u, v) payload pairs
    reordered to the unknowns' order of first appearance and sorted;
  3. result.kind == "finite-complete", result.completeness == "conditional", result.complete is False;
  4. result.scope == "all integral points" and result.assumptions == ("Magma's Mordell-Weil computation for the associated curve is correct " "and saturated", "the external Magma routine computed correctly");
    "50" does not appear in result.scope (the seed bound is not a
    completeness bound);
  5. exactly one Evidence with kind == "external-computation",
    software == "magma", version equal to the fake payload's version, and
    every reference key resolvable in bibliography();
  6. result.representation.form == "partial-search";
  7. check_solver_contract(cls, result) (Core opt-in Magma process runner #76) passes, and
    json.dumps(result.to_dict()) succeeds with schema_version == 3 and
    exact tagged integers (decode_exact round-trips each coordinate);
  8. an empty payload (data["points"] == []) yields kind == "empty",
    completeness == "conditional", the same assumptions, and
    representation.form == "empty";
  9. domain="NN" on the same payload yields only (3, 2) (the existing
    _restrict_nonneg post-pass), with everything else unchanged — and the
    generated script is byte-identical to the ZZ one, proving the seed
    search is model-level and unaffected by the requested domain.

The five required seed/script/signature tests

  1. Script snapshot — the generated script contains both Q and P.
    Assert all of:
    "Q := [2, -3, 1, -5, 7];" in script,
    "P := [2, 3];" in script,
    "IntegralQuarticPoints(Q, P)" in script, and — guarding against a
    regression to the unusable form —
    "IntegralQuarticPoints(Q);" not in script. Compare the whole script
    against the literal rendering shown above, not only these substrings.

  2. The fixture seed is exactly (2, 3).
    _integral_quartic_seed([2, -3, 1, -5, 7]) == (2, 3) exactly, with both
    entries ZZ. Assert also the intermediate facts that pin the enumeration
    order: Q(0) == 7, Q(1) == 2 and Q(-1) == 18 are each not squares
    while Q(2) == 9 is, so no earlier abscissa can be returned and -2 is
    never examined.

  3. No seed → decline without starting Magma. Fixture equation
    v^2 = u^4 + 5203, i.e. qcoeffs == [1, 0, 0, 0, 5203] (verified today:
    classify gives that qcoeffs with unknowns == ('v', 'u'), and
    disc(Q) = 36057984109312 != 0, so every other guard passes). Verified
    facts that make this fixture exactly right:

    • Q(u) is not a nonnegative square for any |u| <= 50, so
      _integral_quartic_seed([1, 0, 0, 0, 5203]) is None after examining all
      101 declared abscissas;
    • the equation nevertheless has integral solutions — Q(51) = 6770404 = 2602^2, so (u, v) = (±51, ±2602) are genuine points —
      which is what makes "a failed seed search is not an emptiness claim" a
      real assertion rather than a vacuous one;
    • solve("v^2 = u^4 + 5203") today raises the family's
      SolverUnavailable with "no small local obstruction found either"
      (verified), so the local layer is inconclusive and the adapter really is
      reached.

    Assert: _integral_quartic_seed(...) is None; the documented fallback runs
    with reason "no-seed-point" and outcome == "cas-failure" recorded,
    ending in the family's SolverUnavailable; no process is started at
    all
    — assert this with a fake runner that fails the test if it is
    invoked (a stub raising AssertionError when called), not merely by
    inspecting output; and kind is never "empty" on this path.

    (Do not use v^2 = -u^4 - 1 for this test. Verified today: it is
    intercepted earlier by the real-obstruction layer, which returns
    kind="empty", completeness="proved" with the description "the equation
    has no real solutions ...", so the adapter is never consulted and the test
    would assert nothing about the seed guard.)

  4. Invalid returned point trips InternalConsistencyError. Payload
    [["3", "5"]] on the main fixture — Q(3) = 82 and 5^2 = 25, so the
    pair is not a point of the curve (verified) — makes solvers._verified
    raise InternalConsistencyError; the bad point appears in no returned
    solution set, and the adapter does not pre-filter it away.

  5. Real-Magma test confirming the V2.29 signature. A magma-marked test,
    skipped by default, runs the fixture against a real install and asserts
    that the two-argument call is the one that works on the pinned version:
    it calls IntegralQuarticPoints(Q, P) and requires success, and
    separately asserts that the run's reported Magma version is V2.29 or
    higher (or is an explicitly allow-listed earlier version recorded in the
    test). Every returned point must verify against the equation. This test is
    never a substitute for the deterministic tests above.

Failure-path tests (main fixture, fake runner)

  1. malformed payload → the verbatim SolverUnavailable above, with the
    reason appended; outcome == "unsupported-family", family == "elliptic-quartic";
  2. timeout (magma_timeout=1, sleeping fake) → same, reason mentions
    resource-exceeded;
  3. status: "error" payload (standing in for a rank-condition error) →
    same;
  4. domain="QQ" with use_magma=True behaves exactly as today (the
    capability gate skips the adapter; no process is started).

Unconditional and repository-wide

  1. With use_magma=False, and with DIOPHANTINE_CLASSIFIER_MAGMA pointing
    at the fake, solve("v^2 = 2*u^4 - 3*u^3 + u^2 - 5*u + 7") raises the
    same SolverUnavailable message as on the pre-epic tree. Runs
    unconditionally.
  2. A new corpus row is not required (elliptic-quartic is already matched);
    the adapter's fixture equations are added to the Magma adapter test
    catalogue introduced by Core opt-in Magma process runner #76.
  3. make test, make doctest, make coverage (100%) and
    make registry-docs clean without Magma installed; every new
    function has a Sage-convention docstring whose EXAMPLES pass sage -t
    without Magma. _integral_quartic_seed in particular has an EXAMPLES
    block exhibiting the (2, 3) seed and a None return.

Out of scope

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions