Skip to content

Thue–Mahler Magma adapter (Gherga–Siksek) #77

Description

@roed-math

Goal

Add the first family adapter on top of the core runner (#76): a thue-mahler route that calls the Gherga–Siksek Magma implementation at a pinned immutable commit, parses its output into a SolutionSet whose completeness, scope and assumptions state exactly what that run establishes, and falls back to today's PARI box search on any failure. Opt-in only (solve(..., use_magma=True) / --use-magma); with the switch off, nothing about the thue-mahler solver changes.

Target base

Dependencies

Pinned external package

Everything in this section was read directly off the package at the pinned
commit on 2026-08-09 (repository tree and file contents fetched from
api.github.com / raw.githubusercontent.com); nothing here is transcribed
from memory, and there is nothing left to look up at implementation time.

item pinned value
canonical repository https://github.com/adelagherga/ThueMahler
default branch master
immutable commit 164a4eb9f53c4ccc565379594251c0503952a70b
commit date 2023-10-16T02:28:18Z (Updating paper examples)
package directory Code/TMSolver/
entry file (relative) Code/TMSolver/solveThueMahler.m
load mechanism Magma loadnot AttachSpec (see below)
entry point solveThueMahler(alist, a, primelist : verb, coprime)
return type SetEnum of sequences [X, Y, z_1, ..., z_v]
exponents returned? yes, one per prime, in primelist order
solutions returned primitive only: gcd(X, Y) = 1
license file none at the pinned commit (see Provenance)

The authors' own pointer to this directory is in the paper
(GhergaSiksek2022, arXiv:2207.14492, published as Algebra & Number Theory
19 (2025), no. 4, 667–714): "We have implemented the algorithm described
in this paper in the computer algebra system Magma; our implementation is
available from https://github.com/adelagherga/ThueMahler/tree/master/Code/TMSolver".

There is no spec file: the package is loaded, not AttachSpeced

The recursive tree at the pinned commit contains no *.spec file for
Code/TMSolver
. The only file named like one is Code/Old/spec.m, which
belongs to the superseded Code/Old/ tree and whose entire content is an
absolute path on the author's own machine pointing at a file that does not
exist in the repository:

// spec.m

/Users/adela016/Documents/Magma/ThueMahler/TMSolverInit.m;

So AttachSpec is not the entry route, and the previous draft of this issue
(which called AttachSpec(<<pkg>>)) could never have worked. The package's own
Code/TMSolver/README.txt documents the real usage verbatim:

// Below is an example of how to use the Thue--Mahler solver,
// for instance, to solve
// 3 X^3 + 2 X^2 Y + 7 X Y^2 + 2 Y^3 = 2^{z_1} 3^{z_2} 7^{z_3} 41^{z_4}
// under the assumptions that gcd(X,Y) = gcd(a_0,Y) = 1:

load "solveThueMahler.m";

alist:=[3,2,7,2];
a:=1;
primelist:=[2,3,7,41];
time sols:=solveThueMahler(alist,a,primelist);
sols;

and solveThueMahler.m itself begins with relative loads of its seven
siblings:

load "./multGroup.m";
load "./equationsInK.m";
load "./reducedBound.m";
load "./sieveInfo.m";
load "./sift.m";
load "./solutionVectors.m";
load "./parseIO.m";

Two binding consequences.

  1. The process's working directory must be the package directory, because
    those ./ loads resolve against it. The script therefore begins with
    ChangeDirectory(<pkgdir>);ChangeDirectory(s) : MonStgElt ->, "Change
    to the directory specified by the string s. Tilde expansion is allowed.",
    Magma V2.29 Handbook, chapter Input and Output, section System Calls,
    https://magma.maths.usyd.edu.au/magma/handbook/text/42 (fetched
    2026-08-09). Doing it in the script keeps Core opt-in Magma process runner #76's runner API unchanged: the
    runner still just writes one script and executes it.
  2. load is a top-level directive and cannot go inside try/catch. The
    handbook lists load "filename"; as a "Special top level directive"
    (chapter Input and Output, section Loading Program Files,
    https://magma.maths.usyd.edu.au/magma/handbook/text/38), and Load(F)
    "may only be used in a simple statement at the top level". The
    ChangeDirectory and load lines therefore sit before the try block.
    If the directory or the file is wrong, the script dies before emitting
    anything between the sentinels; Core opt-in Magma process runner #76's runner sees no payload and the
    standard fallback runs. The adapter additionally checks in Python that the
    entry file exists before starting a process, so the ordinary declined path
    never relies on that crash.

Exact callable contract

Quoted verbatim from Code/TMSolver/solveThueMahler.m at the pinned commit:

solveThueMahler:=function(alist,a,primelist : verb:=false,coprime:=true)
    /*
      Solves a_0 X^d + ... + a_d Y^d = a p_1^{z_1} ... p_v^{z_v}
      subject to the assumptions that X, Y are integers and
      gcd(X,Y) = 1, with a_0, Y optionally coprime.

      Parameters
          alist: SeqEnum
              A list of coefficients a_0, a_1,...,a_d.
          a: RngIntElt
          primelist: SeqEnum
              A list of rational primes p_1, p_2,...,p_v.
          verb: BoolElt
              A true/false value. If set to true, this function returns status
	      updates as it proceeds.
          coprime: BoolElt
              A true/false value. If set to true, this function returns all
	      solutions of the Thue--Mahler form under the added assumption that
	      gcd(a_0,Y) = 1.
      Returns
          sols: SetEnum
              A list of solutions [X,Y,z_1,...,z_v] to the Thue-Mahler
	      equation.
   */

Reading that off precisely:

  • Argument order and types. alist is a SeqEnum of the integer
    coefficients a_0, a_1, ..., a_d of F(X, Y) = a_0 X^d + a_1 X^{d-1} Y + ... + a_d Y^d, i.e. descending in X — the same convention Core opt-in Magma process runner #76's
    encode_int_list will emit from the match data. a is a single
    RngIntElt. primelist is a SeqEnum of rational primes.
  • Return type. A SetEnum whose elements are sequences of length
    2 + v: [X, Y, z_1, ..., z_v]. Exponents are returned, and z_i
    belongs to primelist[i] — the pairing is positional, so the order in which
    this adapter encodes primelist is load-bearing (pinned below).
  • coprime must be passed as false. With its default true the routine
    imposes the extra hypothesis gcd(a_0, Y) = 1 and therefore returns only
    a subset of the primitive solutions when |a_0| > 1. With coprime := false the routine runs makeMonic/recoverXY and returns the primitive
    solutions of the original form. This adapter always passes coprime := false, so its scope statement is correct for every accepted input. (For the
    catalogue fixture a_0 = 1, so the two settings agree; the adapter does not
    special-case that.)
  • Primitivity is enforced inside the package. recoverXY rejects any
    candidate with GCD(sol[1], sol[2]) ne 1, so the returned set contains
    primitive pairs only.
  • Sign handling is the package's, not ours. recoverXY returns {sol} or
    {[-X, -Y]} for odd d (whichever makes the right-hand side positive) and
    both {sol, [-X, -Y]} for even d. The adapter therefore must not assume
    that returned pairs come in ± couples, and must not add or remove any.

Documented hypotheses (the completeness contract)

The paper states the problem it solves as F(X,Y) = a · p_1^{z_1} ⋯ p_v^{z_v}
with X, Y ∈ Z, gcd(X,Y) = 1, F "an irreducible binary form of degree at
least 3 with integer coefficients", a "a non-zero integer", p_1, …, p_v
rational primes, and the condition p_i ∤ a; the working form of the equation
adds gcd(a_0, Y) = 1, "where a₀ is the leading coefficient of F"
(arXiv:2207.14492, Introduction, fetched 2026-08-09).

The package enforces exactly these hypotheses as asserts in
Code/TMSolver/equationsInK.m (verbatim, at the pinned commit):

    assert &and[IsPrime(p) : p in primelist];
    assert &and[Valuation(a,p) eq 0 : p in primelist];
    assert &and[a_i in Integers() : a_i in alist];
    a0:=Integers()!alist[1];
    assert a0 ne 0;
    d:=#alist-1;
    assert d ge 3;
    // [four lines omitted: they build QUV, Qx, the homogeneous form F, and
    //  f := a0^(d-1)*Evaluate(F,[x/a0,1]), then assert IsHomogeneous(F)]
    assert IsMonic(f);
    assert Degree(f) eq d;
    assert IsIrreducible(f);

A violated assert is a Magma runtime error, i.e. status: "error" and the
fallback — but this adapter guards on all of them in Python first, so the
guarded cases decline without starting a process. Note in particular
Valuation(a, p) eq 0: no listed prime may divide m. That is a new
guard relative to the previous draft of this issue.

Supported Magma versions, provenance, license

  • The package states no minimum Magma version anywhere — not in
    README.md, not in Code/TMSolver/README.txt, not in any source header.
    The only version-specific remark in the whole Code/TMSolver tree is a
    comment in reducedBound.m: // ClosestVectors is error-prone in Magma V2.26-10. (the code uses CloseVectorsProcess instead). That records a
    historical workaround, not a supported floor.
  • This adapter therefore pins the same V2.29 floor as the rest of the epic
    (Core opt-in Magma process runner #76, Integral elliptic-quartic Magma adapter (IntegralQuarticPoints) #78, Genus-2 rational-points Magma adapter (RationalPointsGenus2) #79) rather than inventing a package-specific one, and records
    the run's reported Magma version in its Evidence so a certificate always
    names what actually ran.
  • Provenance/license. At the pinned commit the repository has no license
    file
    and the GitHub API reports no license. The package is therefore
    located, never vendored, never fetched, never redistributed by this
    repository — consistent with the existing no-web-dependencies rule. The
    adapter reads a user-supplied checkout; nothing is downloaded, and no
    package source is copied into this repository. The pinned commit is
    recorded so that a certificate identifies which revision produced a result.

The package writes progress output unconditionally

coprimeThueMahler in solveThueMahler.m calls printf on every run —
banner lines, the equation being solved, the number of S-unit equations, the
ranks, per-equation progress and time lines — regardless of verb. It also
executes SetAutoColumns(false); SetColumns(235); at load time. Two binding
consequences for #76's protocol:

  • the payload extractor must locate the sentinels inside an arbitrarily long
    stream of preceding text, and must not assume the emitted object is the
    first or only thing on stdout;
  • the runner's oversized-output guard must be sized for this adapter (or
    applied to the extracted payload rather than to raw stdout); a large but
    successful run must not be discarded as oversized. A test with a fake runner
    that emits several kilobytes of banner noise before the sentinels pins this.

Supported inputs

The adapter is registered as MAGMA_SOLVERS["thue-mahler"] with
MAGMA_CAPABILITIES["thue-mahler"] = SolverCapability(slug="thue-mahler", domains=frozenset(("ZZ", "NN")), goals=frozenset(("enumerate", "exists", "find-one")), kinds=frozenset(("finite-complete", "empty")), completeness=frozenset(("conditional",)), guards=(see "Guards" below)).

CAPABILITIES["thue-mahler"] and its doctest (completeness == frozenset({'partial'})) are unchanged: they describe the PARI solver,
which keeps its own contract.

Input regime

An irreducible binary form of degree at least 3 with concrete integer
coefficients, a concrete nonzero integer m divisible by none of the listed
primes, and a concrete list of distinct rational primes. The matcher already
supplies exactly this. Verified today (re-run against Sage 10.7 on 85f8e0a)
for the catalogue row x^3 + 2*y^3 = 5^a * 11^b:

classify("x^3 + 2*y^3 = 5^a * 11^b").data
  == {'form': '1*x^3 + 2*y^3', 'm': '1', 'degree': 3,
      'primes': ['11', '5'], 'exponents': ['b', 'a']}
solve("x^3 + 2*y^3 = 5^a * 11^b").variables == ('x', 'y', 'a', 'b')

Note the three orderings and keep them apart:

  • data["primes"][i] is paired with data["exponents"][i] — here 11 ↔ b
    and 5 ↔ a;
  • the adapter encodes primelist in exactly the order of data["primes"],
    so the package's returned z_i is the exponent of data["primes"][i] and
    binds to data["exponents"][i]. For the fixture that means
    primelist = [11, 5], z_1 ↔ b, z_2 ↔ a. No sorting, ever;
  • the solution tuple is ordered by first appearance of the unknowns in the
    input, ('x', 'y', 'a', 'b'), and is built with _ordered(cls, assignment)
    — never by the order the primes happen to have in the match data.

alist for the fixture is [1, 0, 0, 2]: F(X, Y) = 1·X^3 + 0·X^2 Y + 0·X Y^2 + 2·Y^3 = x^3 + 2 y^3 (verified). Degree-d forms give a
(d + 1)-entry alist; missing monomials are explicit zeros.

Guards (each one declines: the adapter raises MagmaFailure and the PARI solver runs)

  • any form coefficient, or m, not a concrete integer (parametric input);
  • m == 0;
  • degree < 3;
  • the binary form not irreducible over QQ (the reducible case is a
    different problem and the external solver's hypotheses do not cover it);
  • any listed prime failing encode_prime_list (not a rational prime, or
    repeated);
  • any listed prime dividing m — the package asserts Valuation(a, p) eq 0 for every p in primelist and the paper hypothesizes p_i ∤ a; this is
    the new guard, and it declines rather than relying on a Magma assertion
    failure;
  • the Gherga–Siksek package not located (below).

Locating the external package

IntegralQuarticPoints and RationalPointsGenus2 are Magma intrinsics, but
the Thue–Mahler solver is a third-party Magma package (Gherga–Siksek,
cited in the registry as software.magma: "Gherga-Siksek ThueMahler (GitHub)") that must be present on the machine. Following the same rule the
runner uses for the executable: a second environment variable,
DIOPHANTINE_CLASSIFIER_THUEMAHLER, may only locate the checked-out
repository root. It never activates anything — with use_magma=False it is
not read at all.

Concretely, with root the value of that variable:

  • the package directory is os.path.join(root, "Code", "TMSolver");
  • the entry file that must exist is
    os.path.join(root, "Code", "TMSolver", "solveThueMahler.m");
  • the adapter checks that file with os.path.isfile before starting a
    process; if the variable is unset, or the file is absent, the adapter
    declines and today's PARI result is returned with the reason recorded
    ("package-not-found").

The variable and its "locates, never activates" role are documented in
docs/SEMANTICS.md next to DIOPHANTINE_CLASSIFIER_MAGMA, together with the
pinned commit above and the note that this repository never downloads it.

Output and API contract

Script template

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

build_script("thue_mahler",
             pkgdir=encode_magma_path(os.path.join(root, "Code", "TMSolver")),
             coeffs=encode_int_list(alist),
             m=encode_int(m),
             primes=encode_prime_list(primes))

on top of prelude.m. Placeholders are <<name>> and are filled only with
encoder output; encode_magma_path renders a Magma string literal and rejects
quotes, backslashes and newlines.

// thue_mahler.m  (prelude.m is prepended)
// Gherga-Siksek ThueMahler, github.com/adelagherga/ThueMahler
// pinned commit 164a4eb9f53c4ccc565379594251c0503952a70b
// `load` is a top-level directive: it cannot appear inside try/catch.
ChangeDirectory(<<pkgdir>>);
load "solveThueMahler.m";

alist  := <<coeffs>>;
a      := <<m>>;
primes := <<primes>>;

try
    sols := solveThueMahler(alist, a, primes : verb := false,
                                               coprime := false);
    entries := [ JSONList([JSONInt(s[i]) : i in [1..#s]]) : s in sols ];
    DCEmit("ok", "", JSONObj(["solutions"], [JSONList(entries)]));
catch e
    DCEmit("error", "solver-error", JSONObj([], []));
end try;
quit;

For the catalogue fixture this renders literally and completely as — taking
/opt/ThueMahler as the located checkout, i.e. the value of
DIOPHANTINE_CLASSIFIER_THUEMAHLER; only that one path varies between
machines, and it is the only part of the script the snapshot test substitutes:

ChangeDirectory("/opt/ThueMahler/Code/TMSolver");
load "solveThueMahler.m";

alist  := [1, 0, 0, 2];
a      := 1;
primes := [11, 5];

try
    sols := solveThueMahler(alist, a, primes : verb := false,
                                               coprime := false);
    entries := [ JSONList([JSONInt(s[i]) : i in [1..#s]]) : s in sols ];
    DCEmit("ok", "", JSONObj(["solutions"], [JSONList(entries)]));
catch e
    DCEmit("error", "solver-error", JSONObj([], []));
end try;
quit;

verb and coprime are passed explicitly even though verb := false is the
package's default, so that the generated script records the exact call and a
future change to the package's defaults cannot silently change what this
repository reports. The try/catch wrapper is mandatory for the call
itself: a violated assert inside the package, or any other runtime error,
becomes status: "error" and the standard fallback — never a crash and never
a partial parse.

Payload schema (data)

field type meaning
solutions array of (2 + v)-element arrays of decimal strings the sequences [X, Y, z_1, ..., z_v] the package returned, with z_i the exponent of primes[i] in the order the script sent them

That is the whole payload. Integers are decimal strings; no bare JSON numbers
(#76 §2 rejects them).

The previous draft's primitive and solver_complete fields are removed.
solveThueMahler returns neither: it exposes no completeness flag of its own,
and primitivity is a property of the pinned package contract (recorded in
Python, in scope and Scope.primitive), not a value the run reports. As in
#79, the payload carries only what the external routine actually returns.

Parsing into a SolutionSet

  1. Reject any entry whose length is not 2 + len(data["primes"]); that is a
    malformed payload, not a solution.
  2. Decode every entry with decode_int: X, Y, and the exponents
    z_1, ..., z_v.
  3. Recompute the exponents independently in Python and require exact
    agreement.
    Compute F(X, Y) / m in QQ; require it to be a positive
    integer whose prime factorization is supported on the listed primes; take
    z'_i = the valuation at primes[i]. If the quotient is not such an
    integer, or if z' != z, the payload is inconsistent with the equation:
    raise MagmaFailure(..., outcome="cas-failure") and fall back. The
    package's exponents are used as the reported values only after they have
    been re-derived here; the classifier never forwards an external number it
    has not itself checked.
  4. Require gcd(X, Y) == 1 for every returned pair, matching the package's
    own recoverXY check. A non-primitive pair is a payload inconsistency,
    handled exactly as in step 3.
  5. Build the assignment (the form's two unknowns x, y from the parsed
    equation, plus data["exponents"][i] ↦ z_i) and order it with
    _ordered(cls, assignment).
  6. sorted(set(...)), as the PARI solver already does.
  7. kind = "finite-complete" when nonempty, "empty" otherwise.
  8. Leave representation to _derived_representation: for a
    finite-complete result that is not proved, that is
    PartialSearch(solutions, scope). Do not override it with a
    FiniteSet, which would assert unconditional completeness.
  9. scope_info = Scope(domain_scope="integer solutions", primitive="gcd(x, y) = 1", ordering_convention="unknowns in order of first appearance in the input equation").

Guarantee, scope and assumptions

completeness="conditional"
assumptions=("the external Gherga-Siksek Magma implementation computed "
             "correctly",)

conditional requires nonempty assumptions (docs/SEMANTICS.md §3), and
this is the honest report for an uncertified external run: the classifier
re-checks each returned point against the equation, but it cannot re-derive
that nothing was missed.

Scope, stated precisely. The pinned package resolves the equation for
coprime (X, Y) — that is the hypothesis in its own docstring, in the
paper, and in the recoverXY gcd check. The qualifier is not decoration:
without it the claim would be false, because the full integer solution set can
be infinite while the primitive one is finite. For the catalogue equation,
(x, y, a, b) = (-5^k, 5^k, 3k, 0) is a solution for every k >= 0
(-5^k)^3 + 2·(5^k)^3 = 5^{3k} (verified for k = 0, 1, 2, 3) — and each has
gcd = 5^k, so exactly one of them (k = 0) is primitive. Today's PARI box
search returns 40 solutions for that equation, of which 7 are coprime
(re-verified today).

Therefore:

scope=("all integer solutions with gcd(x, y) = 1 and right-hand side "
       "supported on the prime set {5, 11}")   # primes filled from the match

and the description contains exactly this sentence about the rest:

This adapter enumerates only the primitive solutions covered by the external
package.  Imprimitive solutions are outside this SolutionSet scope.

Do not restore the previous, false characterization that every imprimitive
solution is a scaling of a primitive one by primes of the listed set. For a
homogeneous form F(g·X, g·Y) = g^degree · F(X, Y), an imprimitive solution
with gcd(X, Y) = g reduces to a primitive pair solving a different
equation, and g may involve primes dividing the fixed multiplier m.
Concrete counterexample, verified: for F(X, Y) = X^3 + 2 Y^3, m = 24 and
prime set {5}, the pair (2, 2) satisfies F(2, 2) = 24 = 24·5^0 and has
gcd = 2, while (1, 1) — the primitive pair it scales from — has
F(1, 1) = 3, which is not 24·5^z for any z; and 2 divides m, not the
prime set. A more detailed scaling characterization may be added later only if
it includes the support of m and exact exponent constraints.

Evidence

result.as_evidence(
    "Thue-Mahler resolution via the Gherga-Siksek Magma implementation "
    "(github.com/adelagherga/ThueMahler @ 164a4eb9f53c4ccc565379594251c0503952a70b, "
    "solveThueMahler(alist, a, primelist : coprime := false))",
    command="thue_mahler([1, 0, 0, 2], 1, [11, 5])",
    reference=("GhergaSiksek2022",))

plus a second Evidence(..., kind="theorem", reference=("GhergaSiksek2022",))
recording which algorithm was run. The evidence names the pinned commit, so a
certificate identifies the exact revision that produced the result.
GhergaSiksek2022 already resolves in data/references.bib and is already
cited by the thue-mahler family (why: "the modern efficient implementation
(Magma code)"), so no bibliography change is required. Its entry is currently
@unpublished{... note = {Preprint}, year = {2022}, eprint = {2207.14492}}
(verified today); it may optionally be refreshed to the published version —
Algebra & Number Theory 19 (2025), no. 4, 667–714 — under the standard
rules (author/title/journal/volume/year, nonempty why, doi verified at
commit time, url only for legally free copies, make references passing).
That refresh is not required by this issue.

Failure and fallback semantics

Any of: package not located (DIOPHANTINE_CLASSIFIER_THUEMAHLER unset, or
Code/TMSolver/solveThueMahler.m absent under it), guard declined, timeout,
nonzero exit, status != "ok", unparseable payload, wrong entry length,
exponent disagreement, a non-primitive returned pair — falls through to the
existing SOLVERS["thue-mahler"] (the PARI per-exponent box search), whose
result is returned unchanged except for
"; the Magma route was attempted and did not produce a result (<outcome>): <reason>" appended to its description (#76). That fallback result keeps
its own completeness="partial" and scope "solutions with all prime exponents in [0, 6]"; the box search stays the unconditional partial answer.

A point that survives exponent recovery but still fails
ParsedEquation.accepts is an internal-consistency error by the repository's
contract: solvers._verified raises InternalConsistencyError, and the
adapter must not pre-filter to hide it.

Acceptance criteria

Fixture (deterministic, fake runner, no Magma)

Equation: the existing catalogue row x^3 + 2*y^3 = 5^a * 11^b
(tests/test_classify.py, tests/test_solver_contract.py), so
alist = [1, 0, 0, 2], m = 1, primes = [11, 5].

Fake payload data["solutions"] = the seven primitive solutions in the
package's own [X, Y, z_1, z_2] shape, with z_1 the exponent of 11 and
z_2 the exponent of 5 (every line re-verified today):

["-1",  "1", "0", "0"]     F(-1, 1)  = 1     = 11^0 * 5^0
[ "1",  "0", "0", "0"]     F(1, 0)   = 1     = 11^0 * 5^0
[ "1",  "3", "1", "1"]     F(1, 3)   = 55    = 11^1 * 5^1
[ "3", "-2", "1", "0"]     F(3, -2)  = 11    = 11^1 * 5^0
[ "3", "-1", "0", "2"]     F(3, -1)  = 25    = 11^0 * 5^2
["19",  "2", "1", "4"]     F(19, 2)  = 6875  = 11^1 * 5^4
["29","-23", "1", "1"]     F(29, -23)= 55    = 11^1 * 5^1

(These are genuine primitive solutions — each has gcd(X, Y) = 1 and each
quotient is exactly 1 after the two valuations are divided out. The fixture
asserts the adapter's parsing, exponent cross-check, ordering and
verification — it does not assert that this list is the complete primitive
solution set.)

Assertions on solve("x^3 + 2*y^3 = 5^a * 11^b", use_magma=True) with the
fake runner:

  1. result.variables == ('x', 'y', 'a', 'b');
  2. result.solutions == [(-1, 1, 0, 0), (1, 0, 0, 0), (1, 3, 1, 1), (3, -2, 0, 1), (3, -1, 2, 0), (19, 2, 4, 1), (29, -23, 1, 1)] — the
    payload exponents re-derived and re-ordered, sorted; in particular
    (3, -2) gives a = 0, b = 1 because 3^3 + 2·(-2)^3 = 11 = 5^0 · 11^1, which pins the primes ↔ exponents ↔ z_i pairing;
  3. result.kind == "finite-complete", result.completeness == "conditional", result.complete is False;
  4. result.assumptions == ("the external Gherga-Siksek Magma implementation computed correctly",);
  5. "gcd(x, y) = 1" in result.scope, and
    result.scope_info.primitive == "gcd(x, y) = 1";
  6. exactly one Evidence with kind == "external-computation",
    software == "magma", version equal to the fake payload's version, and
    "GhergaSiksek2022" among the evidence references (resolvable in
    bibliography());
  7. result.representation.form == "partial-search";
  8. 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.

The six required package-contract tests

  1. Generated-script snapshot against the pinned package API. The script
    built for the fixture equals the literal rendering above, line for line,
    with the located path substituted. Assert specifically:
    'ChangeDirectory("' in script, 'load "solveThueMahler.m";' in script,
    "alist := [1, 0, 0, 2];" in script, "a := 1;" in script,
    "primes := [11, 5];" in script,
    "solveThueMahler(alist, a, primes : verb := false," in script,
    "coprime := false" in script. Regression guards against the superseded
    draft: "AttachSpec" not in script, and the script contains no unresolved
    <<name>> marker after assembly (assert "<<" not in script), which also
    rules out the superseded draft's unfilled entry-point and flag markers.
    Assert also that load occurs before the try line (it is a top-level
    directive) and that primes is not sorted ("[5, 11]" not in script).
  2. Package-path validation. With DIOPHANTINE_CLASSIFIER_THUEMAHLER
    pointing at a directory that lacks Code/TMSolver/solveThueMahler.m, the
    adapter declines with reason "package-not-found" and starts no process
    (assert with a runner stub that raises if invoked); the PARI result is
    returned. With it pointing at a directory that has the file, the
    ChangeDirectory argument in the generated script is exactly that
    directory's Code/TMSolver path. A path containing a quote, backslash or
    newline is rejected by encode_magma_path and declines.
  3. Payload parsing for the actual return shape. The (2 + v)-element
    arrays above parse into the tuples of assertion 2. An entry of length
    2 (the superseded [X, Y] shape) is malformed → fallback; an entry of
    length 2 + v whose exponents disagree with the recomputed valuations —
    e.g. ["3", "-2", "0", "1"], which asserts 11^0 · 5^1 = 5 instead of
    the true 11 — is a payload inconsistency → MagmaFailure with
    outcome == "cas-failure" and fallback, and the bad pair appears in no
    returned solution set.
  4. Primitive scope. result.scope contains "gcd(x, y) = 1";
    result.scope_info.primitive == "gcd(x, y) = 1"; result.description
    contains the exact two-sentence wording pinned above
    ("This adapter enumerates only the primitive solutions covered by the external package." and "Imprimitive solutions are outside this SolutionSet scope."). Assert also that the description does not
    contain the removed claim — no occurrence of "scaling" together with
    "listed primes", and no assertion that the imprimitive solutions are
    obtained from the returned ones. A payload pair with gcd(X, Y) > 1 (for
    instance ["-5", "5", "0", "3"], a genuine solution with gcd = 5) is a
    payload inconsistency → fallback, never a silently accepted extra tuple.
  5. Failure fallback. Malformed payload → the PARI result is returned,
    with 40 solutions, kind witness, completeness partial, and the reason
    appended to description; timeout (magma_timeout=1, sleeping fake) →
    same PARI result, reason mentions resource-exceeded; status: "error"
    payload (standing in for a violated package assert) → same PARI result;
    a payload preceded by several kilobytes of the package's own banner and
    time output still parses correctly and is not discarded as oversized
    (this pins the note about unconditional progress output above).
  6. Real-package marked test at the pinned commit. A magma-marked test,
    skipped by default, resolves the fixture equation against a real Magma
    (V2.29+) and a real checkout of
    github.com/adelagherga/ThueMahler at commit
    164a4eb9f53c4ccc565379594251c0503952a70b. It asserts that the
    three-argument call with coprime := false runs, that every returned
    entry has length 2 + v, that every returned pair is primitive, that the
    package's exponents agree with the independently recomputed valuations,
    and that every tuple verifies against the equation. It is skipped when the
    environment variable is unset, and is never a substitute for the
    deterministic tests above.

Further failure-path tests (same fixture, fake runner)

  1. DIOPHANTINE_CLASSIFIER_THUEMAHLER unset → adapter declines without
    starting a process; same PARI result.
  2. New guard: an equation whose multiplier shares a prime with the prime
    set — for instance a matched input with m = 5 and prime set containing
    5 — declines before starting a process, with the reason naming the
    p | m guard, and the PARI result is returned. (The package would
    otherwise fail its own assert Valuation(a,p) eq 0.)
  3. Verification firewall. Constructing the adapter's SolutionSet
    directly with an injected point that satisfies the exponent bookkeeping
    but not the equation and passing it through solvers._verified(cls, ...)
    raises InternalConsistencyError (the equation carries no nonvanishing
    conditions, so filtering is not permitted).

Unconditional and repository-wide

  1. With use_magma=False (and with DIOPHANTINE_CLASSIFIER_MAGMA pointing
    at the fake), solve("x^3 + 2*y^3 = 5^a * 11^b") is identical to today's
    result in solutions, kind, completeness, scope and description.
    This runs unconditionally.
  2. 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.

Out of scope

  • Reconstructing the imprimitive solutions into a full solution set for the
    equation as typed. The scope string, Scope.primitive and the
    description record the restriction; enumerating anything outside the
    primitive set is not part of this issue.
  • Calling the package with its default coprime := true (the narrower
    gcd(a_0, Y) = 1 regime), or exposing verb, as user-facing options.
  • Any change to the PARI box search, its bound B = 6, or its capability
    declaration.
  • Vendoring, downloading, installing or redistributing the Gherga–Siksek
    package (no web dependencies; the package is located, never fetched — and
    it carries no license file at the pinned commit).
  • Using anything in the repository outside Code/TMSolver/ — in particular
    Code/Old/, Code/TdWCode/ and the elliptic-curve drivers, which are not
    this adapter's entry point.
  • Thue–Mahler equations over number fields, and forms that are reducible or
    of degree below 3.

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