Skip to content
The Lumenary

Math laboratory

The chord-and-tangent group law in exact arithmetic

Implements the group law on y^2 = x^3 + ax + b with exact fractions. Shows that (2, 3) has order 6 on y^2 = x^3 + 1, that (3, 5) on y^2 = x^3 - 2 has multiples with rapidly growing denominators (quadratic growth of height), and checks associativity on all triples of points of a curve over F_13: exact, exhaustive for that one curve only.

Back to the labExact computationExhaustive finite check
The chord-and-tangent group law in exact arithmeticPythonExact computationExhaustive finite checkComputationally tested
"""The chord-and-tangent group law over the rationals, in exact arithmetic.

We implement addition on a short Weierstrass curve y^2 = x^3 + a x + b with
Fraction arithmetic (None is the point at infinity, the identity). Then:

1. On y^2 = x^3 + 1 the point P = (2, 3) has order 6: its multiples are listed
   and every one has integer coordinates, as the Nagell-Lutz theorem demands
   of torsion points.
2. On Bachet's curve y^2 = x^3 - 2 the point P = (3, 5) has infinite order:
   2P is not integral, so (Nagell-Lutz) 2P is not torsion, hence neither is P.
   The number of digits of the denominators of nP grows roughly like n^2, the
   signature of the canonical height.
3. Associativity, the hard axiom, is checked on every triple of points of a
   curve over the finite field F_13 (exhaustive for that curve).

All arithmetic is exact. The order-6 computation is a complete proof for that
point; the associativity check covers one finite curve only.
"""

from fractions import Fraction
import itertools

def on_curve(P, a, b):
    """True if P is the point at infinity or satisfies y^2 = x^3 + a x + b."""
    if P is None:
        return True
    x, y = P
    return y * y == x ** 3 + a * x + b

def neg(P):
    """The negative of a point: reflect in the x-axis."""
    return None if P is None else (P[0], -P[1])

def add(P, Q, a):
    """Chord-and-tangent addition on y^2 = x^3 + a x + b (b is not needed)."""
    if P is None:
        return Q
    if Q is None:
        return P
    (x1, y1), (x2, y2) = P, Q
    if x1 == x2 and y1 == -y2:
        return None  # vertical line: P + (-P) = O
    if x1 == x2:
        slope = (3 * x1 * x1 + a) / (2 * y1)  # tangent
    else:
        slope = (y2 - y1) / (x2 - x1)  # chord
    x3 = slope * slope - x1 - x2
    y3 = slope * (x3 - x1) + y1  # third intersection point
    return (x3, -y3)  # reflect it

def multiple(n, P, a):
    """n * P for n >= 0, by repeated addition."""
    R = None
    for _ in range(n):
        R = add(R, P, a)
    return R

def order(P, a, limit=20):
    """The order of P if it is at most `limit`, otherwise None."""
    R = P
    for n in range(1, limit + 1):
        if R is None:
            return n
        R = add(R, P, a)
    return None

def is_integral(P):
    """True for the point at infinity or a point with integer coordinates."""
    return P is None or (P[0].denominator == 1 and P[1].denominator == 1)

def add_mod(P, Q, a, p):
    """The same chord-and-tangent formulas over the field Z/pZ (p prime > 3)."""
    if P is None:
        return Q
    if Q is None:
        return P
    (x1, y1), (x2, y2) = P, Q
    if x1 == x2 and (y1 + y2) % p == 0:
        return None
    if x1 == x2:
        slope = (3 * x1 * x1 + a) * pow(2 * y1, -1, p) % p
    else:
        slope = (y2 - y1) * pow(x2 - x1, -1, p) % p
    x3 = (slope * slope - x1 - x2) % p
    y3 = (slope * (x3 - x1) + y1) % p
    return (x3, -y3 % p)

def points_mod(a, b, p):
    """All points of y^2 = x^3 + a x + b over Z/pZ, the point at infinity first."""
    return [None] + [(x, y) for x in range(p) for y in range(p)
                     if (y * y - x ** 3 - a * x - b) % p == 0]

def associativity_failures(points, a, p):
    """Triples (P, Q, R) of points over Z/pZ with (P + Q) + R != P + (Q + R)."""
    bad = []
    for P, Q, R in itertools.product(points, repeat=3):
        if add_mod(add_mod(P, Q, a, p), R, a, p) != add_mod(P, add_mod(Q, R, a, p), a, p):
            bad.append((P, Q, R))
    return bad

def fmt(P):
    """Short text for a point."""
    return "O" if P is None else f"({P[0]}, {P[1]})"

def main():
    F = Fraction
    print("1. y^2 = x^3 + 1, P = (2, 3):")
    P = (F(2), F(3))
    for n in range(1, 7):
        print(f"   {n}P = {fmt(multiple(n, P, 0))}")
    print(f"   order of P = {order(P, 0)}; all multiples integral: "
          f"{all(is_integral(multiple(n, P, 0)) for n in range(1, 7))}")
    print()
    print("2. Bachet's curve y^2 = x^3 - 2, P = (3, 5):")
    a, b = F(0), F(-2)
    Q = (F(3), F(5))
    print(f"   2P = {fmt(multiple(2, Q, a))} (not integral, so P has")
    print("   infinite order by Nagell-Lutz)")
    print("    n   digits of denominator of x(nP)   ratio to n^2")
    R = None
    for n in range(1, 11):
        R = add(R, Q, a)
        assert on_curve(R, a, b)
        d = len(str(R[0].denominator))
        print(f"   {n:2d}   {d:30d}   {d / n ** 2:12.2f}")
    print()
    pts = points_mod(2, 3, 13)
    bad = associativity_failures(pts, 2, 13)
    print(f"3. y^2 = x^3 + 2x + 3 over F_13 has {len(pts)} points; "
          f"associativity fails")
    print(f"   for {len(bad)} of all {len(pts) ** 3} triples.")
    print()
    print("Epistemic status: exact arithmetic. Part 1 is a complete")
    print("computation of the order of (2, 3); that (3, 5) has infinite order")
    print("rests on the Nagell-Lutz theorem, quoted, not proved here. Part 3 is")
    print("exhaustive for one curve over one field; associativity in general is")
    print("a theorem (proved in the references, and in Mathlib).")

if __name__ == "__main__":
    main()
Runs entirely in your browser, in an isolated worker. Nothing is sent to a server.

Used in