Corps finis et la borne de Hasse — distiller un théorème en un calcul
Série Serre 100 (EPIC #16334) — ce notebook distille un fil du A Course in Arithmetic de Jean-Pierre Serre : compter des points sur une courbe dans un corps fini, et vérifier la borne de Hasse-Weil sur des données réelles.
L’idée de la distillation : l’énoncé
Pour une courbe elliptique \(E\) sur \(\mathbb{F}_p\), le nombre de points \(N\) satisfait \(|N - (p+1)| \leq 2\sqrt{p}\)
n’est pas seulement un théorème à admérer — c’est une expérience reproductible : on peut compter chaque point, calculer l’écart, et voir la borne tenir, premier après premier. C’est exactement le geste de Serre : rendre les objets concrets avant de les généraliser.
Plan
Le corps \(\mathbb{F}_p\) — trois opérations suffisent
Courbes elliptiques sur \(\mathbb{F}_p\) : compter les points, naïvement
La trace de Frobenius \(t = p + 1 - N\)
La borne de Hasse mesurée — l’énoncé devient un graphique
La loi de groupe : la structure cachée derrière le comptage
Exercices
# Dependances : stdlib (math) + matplotlib pour la figure finale.import mathimport matplotlib# Backend inline : capture les figures en PNG dans les sorties de cellulematplotlib.use('module://matplotlib_inline.backend_inline')import matplotlib.pyplot as pltprint("imports OK, python", __import__('sys').version.split()[0])
imports OK, python 3.13.3
1. Le corps \(\mathbb{F}_p\)
Un corps fini premier \(\mathbb{F}_p\), c’est \(\{0, 1, \dots, p-1\}\) muni de l’addition et de la multiplication modulo \(p\). Tout le comptage qui suit n’utilise que trois ingrédients : l’addition, la multiplication, et l’inverse — car dans \(\mathbb{F}_p\), tout élément non nul est inversible.
# L'inverse modulaire existe pour tout x != 0 : Python 3.8+ le donne nativement.p =7inverses = {x: pow(x, -1, p) for x inrange(1, p)}for x, inv in inverses.items():assert (x * inv) % p ==1print("Inverses dans F_7 :", inverses)
C’est cette inversibilité qui fait de \(\mathbb{F}_p\) un corps — et qui autorise les formules de la loi de groupe à la fin de ce notebook (diviser, c’est multiplier par l’inverse).
2. Une courbe elliptique sur \(\mathbb{F}_p\) : compter les points
Une courbe elliptique affine est \(y^2 = x^3 + ax + b\) avec \(4a^3 + 27b^2 \neq 0 \pmod p\) (non-singularité — l’Exercice 1 y revient). Ses points \(\mathbb{F}_p\)-rationnels sont les couples \((x, y)\) qui satisfont l’équation, plus le point à l’infini\(\mathcal{O}\).
Compter naïvement : pour chaque \(x\), le membre de droite \(v = x^3+ax+b\) est un carré dans \(\mathbb{F}_p\) pour 0, 1 ou 2 valeurs de \(y\). On précalcule l’ensemble des carrés une fois.
def carres_mod(p):"""Ensemble des residues quadratiques de F_p (avec 0)."""return {(y * y) % p for y inrange(p)}def points_affines(a, b, p):"""Liste des points (x, y) de y^2 = x^3 + ax + b sur F_p (sans O).""" carre = carres_mod(p) pts = []for x inrange(p): v = (x * x * x + a * x + b) % pif v ==0: pts.append((x, 0))elif v in carre:# deux racines y et p - y y =next(yy for yy inrange(1, p) if (yy * yy) % p == v) pts.append((x, y)) pts.append((x, p - y))return ptsdef nombre_points(a, b, p):"""#E(F_p) : points affines + le point a l'infini."""returnlen(points_affines(a, b, p)) +1
a, b =2, 3p =7pts = points_affines(a, b, p)print(f"Courbe y^2 = x^3 + {a}x + {b} sur F_{p} : {len(pts)} points affines")print(pts)print(f"#E(F_{p}) = {nombre_points(a, b, p)} (avec le point a l'infini)")
Courbe y^2 = x^3 + 2x + 3 sur F_7 : 5 points affines
[(2, 1), (2, 6), (3, 1), (3, 6), (6, 0)]
#E(F_7) = 6 (avec le point a l'infini)
3. La trace de Frobenius
L’écart entre \(N\) et l’« attendu » \(p + 1\) a un nom : la trace de Frobenius\(t = p + 1 - N\). Toute l’arithmétique de la courbe se lit dans ce seul entier — la borne de Hasse dit simplement qu’il est de taille \(\sqrt{p}\), pas \(p\).
def trace_frobenius(a, b, p):"""t = p + 1 - #E(F_p)."""return p +1- nombre_points(a, b, p)premiers = [q for q inrange(5, 60) ifall(q % d for d inrange(2, int(math.sqrt(q)) +1))]print(f"{'p':>4}{'N':>4}{'t':>4}{'2*sqrt(p)':>10}")for q in premiers[:8]: N = nombre_points(a, b, q) t = q +1- Nprint(f"{q:>4}{N:>4}{t:>4}{2* math.sqrt(q):>10.2f}")
L’énoncé : \(|t| \leq 2\sqrt{p}\). On le vérifie sur toutes les courbes \(y^2 = x^3 + ax + b\) (lisses) pour une famille de premiers — et on trace \(|t|\) contre \(2\sqrt{p}\). La borne devient un graphique : tous les points sous la parabole \(2\sqrt{p}\), certains s’en approchant dangereusement.
def est_lisse(a, b, p):"""Condition de non-singularite : 4a^3 + 27b^2 != 0 mod p."""return (4* a**3+27* b**2) % p !=0premiers_test = [q for q inrange(11, 3000)ifall(q % d for d inrange(2, int(math.sqrt(q)) +1))]ratios = []donnees = [] # (p, |t|, 2*sqrt(p))for q in premiers_test:for (aa, bb) in ((2, 3), (1, 1), (3, 5), (0, 4)):ifnot est_lisse(aa, bb, q):continue t = trace_frobenius(aa, bb, q) donnees.append((q, abs(t), 2* math.sqrt(q))) ratios.append(abs(t) / (2* math.sqrt(q)))print(f"Courbes mesurees : {len(donnees)}")print(f"Ratio |t| / 2*sqrt(p) : max = {max(ratios):.4f}, moyenne = {sum(ratios)/len(ratios):.4f}")print(f"Toutes sous la borne : {all(r <=1for r in ratios)}")
Courbes mesurees : 1701
Ratio |t| / 2*sqrt(p) : max = 0.9999, moyenne = 0.3934
Toutes sous la borne : True
Lecture : sur les 1701 courbes elliptiques lisses testées, le ratio maximum \(|t| / 2\sqrt{p}\) atteint 0.9999, très proche de la borne théorique de 1. La moyenne de 0.3934 montre que la plupart des courbes sont bien en dessous de cette limite. La vérification Toutes sous la borne : True confirme que le théorème de Hasse tient pour tous les cas mesurés.
fig, ax = plt.subplots(figsize=(7, 4.5))ps = [d[0] for d in donnees]ts = [d[1] for d in donnees]ax.scatter(ps, ts, s=6, alpha=0.35, label="$|t|$ mesuré (4 courbes × premiers < 3000)")borne =sorted(set(ps))ax.plot(borne, [2* math.sqrt(q) for q in borne], "r-", lw=2, label="$2\sqrt{p}$ (borne de Hasse)")ax.set_xlabel("$p$"); ax.set_ylabel("|t|")ax.set_title("La borne de Hasse vérifiée : $|t| \leq 2\sqrt{p}$")ax.legend(); fig.tight_layout()plt.show()
Lecture : le nuage grandit comme \(\sqrt{p}\) et reste sous la courbe rouge — c’est le théorème, vu au lieu d’être énoncé. Pourquoi \(\sqrt{p}\) ? Intuitivement, décider si \(x^3+ax+b\) est un carré dans \(\mathbb{F}_p\) ressemble à une suite de tirages aléatoires ; une somme de \(p\) tels tirages fluctue en \(\sqrt{p}\) (déviations quadratiques). Hasse (et Weil, généralisant) transforme cette intuition en théorème — et Serre, dans A Course in Arithmetic puis ses cours au Collège de France, en a fait l’un des fils directeurs de l’arithmétique des courbes.
5. La loi de groupe : la structure cachée
Ce n’est pas un sac de points : trois points alignés sur la courbe somment à zéro. La règle « corde et tangente » donne à \(E(\mathbb{F}_p)\) une structure de groupe abélien — et c’est elle que la cryptographie sur courbes elliptiques exploite.
O =None# point a l'infini, represente par Nonedef oppose(P, p): x, y = Preturn (x, (-y) % p)def addition(P, Q, a, p):"""Somme de deux points par cordes/tangentes, formules affines mod p."""if P isNone: return Qif Q isNone: return P x1, y1 = P; x2, y2 = Qif x1 == x2 and (y1 + y2) % p ==0:returnNone# P + (-P) = Oif P != Q: m = (y2 - y1) *pow(x2 - x1, -1, p) % pelse: m = (3* x1 * x1 + a) *pow(2* y1, -1, p) % p x3 = (m * m - x1 - x2) % p y3 = (m * (x1 - x3) - y1) % preturn (x3, y3)# Verification sur la courbe y^2 = x^3 + 2x + 3 sur F_7 :# generation du sous-groupe cyclique engendre par un point.E =lambda x: (x*x*x + a*x + b) % pP0 = points_affines(a, b, p)[0]cycle, P = [], P0while P isnotNoneand P notin cycle: cycle.append(P) P = addition(P, P0, a, p)print(f"Sous-groupe engendre par {P0} : ordre {len(cycle)}")print(cycle[:6], "..."iflen(cycle) >6else"")
Et la fonction zêta de la courbe, \(Z(E/\mathbb{F}_p) = \frac{1 - tX + pX^2}{(1-X)(1-pX)}\), condense tout : \(t\) la trace, \(p+1-t\) le nombre de points, et les comptages sur toutes les extensions \(\mathbb{F}_{p^r}\) — le point d’entrée de Weil vers les conjectures qui portent la borne.
# La trace determine les comptages sur les extensions :# #E(F_{p^r}) = p^r + 1 - alpha^r - beta^r avec alpha*beta = p, alpha+beta = t.from decimal import Decimal # tenu simple : racines en complexes via nombres flottantsdef comptage_extension(a, b, p, r):"""#E(F_{p^r}) par la formule du polynome caracteristique de Frobenius.""" t = trace_frobenius(a, b, p) disc = t * t -4* p ar, ai = t /2, math.sqrt(max(-disc, 0) /4)# alpha^r + beta^r = 2 * Re(alpha^r) car beta = conjugue mod = (ar * ar + ai * ai) ** (r /2) arg = math.atan2(ai, ar) s =2* mod * math.cos(r * arg)returnround(p ** r +1- s)print("r :", [comptage_extension(a, b, p, r) for r inrange(1, 5)])print("verification r=1 :", comptage_extension(a, b, p, 1), "== ", nombre_points(a, b, p))
r : [6, 60, 378, 2400]
verification r=1 : 6 == 6
Exercices
Exercice 1 — La non-singularité, testée
La fonction est_lisse ci-dessus encode \(4a^3 + 27b^2 \neq 0\). Écrivez-la vous-même, puis trouvez tous les couples \((a, b)\)singuliers pour \(p = 11\) — il y en a peu, et leur forme est instructive.
# Exercice 1 : non-singularite d'une courbe elliptique sur F_p.def singuliers(p):"""Liste des couples (a, b) de F_p x F_p tels que 4a^3 + 27b^2 = 0 mod p (courbe singuliere)."""# Etape 1 : double boucle sur (a, b) dans range(p)# Indice : la condition est 4*a**3 + 27*b**2, modulo p# Etape 2 : collecter dans une liste et la renvoyerreturnNone# TODO etudiant
Exercice 2 — La somme des caractères
Le lien profond entre carrés et trace : \(\sum_{x \in \mathbb{F}_p} \chi(x^3 + ax + b) = -t\), où \(\chi\) est le symbole de Legendre (\(\chi(v) = 1\) si \(v\) carré non nul, \(-1\) sinon, \(0\) si \(v = 0\)). Implémentez cette somme et vérifiez-la contre trace_frobenius sur quelques courbes.
# Exercice 2 : somme des caracteres de Legendre = -trace de Frobenius.def caractere(v, p):"""Symbole de Legendre chi(v) dans F_p : 1, -1 ou 0."""# Indice : pow(v, (p-1)//2, p) vaut 1, p-1 (c'est-a-dire -1) ou 0returnNone# TODO etudiantdef somme_caracteres(a, b, p):"""Somme sur x dans F_p de chi(x^3 + a*x + b). Doit valoir -t."""# Etape 1 : somme des caracteres(v, p) pour v = x^3+ax+b, x dans range(p)returnNone# TODO etudiant
Exercice 3 — La courbe extrême, par sondage déterminé
La borne \(2\sqrt{p}\) est optimale : pour certains \((p, a, b)\), \(|t|\) l’atteint presque. Balayer tous les \((a, b)\) pour \(p = 10^4 + 7\) ferait \(p^2 \approx 10^8\) courbes — hors de portée. Mais remarquez que points_affines cherche chaque racine carrée par scan linéaire : une table des racines construite une fois pour toutes en \(O(p)\) rend le comptage de chaque courbe \(O(p)\). Avec elle, sondez un échantillon déterminé — les couples \((a, b)\) multiples d’un pas fixe — et cherchez le couple lisse qui maximise le ratio \(|t| / 2\sqrt{p}\), pour regarder comme il est proche de 1.
# Exercice 3 : la courbe la plus proche de la borne de Hasse, par sondage determine.P_EXTREME =10**4+7PAS_SONDAGE =211# budget determine : (p/211)^2 = 48 x 48 = 2304 couples sondesdef racines_table(p):"""Dictionnaire {v: y} ou v = y*y mod p et y est la plus petite racine. Cout O(p) -- a construire UNE fois. Avec elle, le nombre de points d'une courbe se calcule en O(p) au lieu du scan lineaire de points_affines."""# Etape 1 : pour y dans range(p), v = y*y % p est un carre# Etape 2 : ne garder que la plus petite racine de chaque carre (test d'absence)returnNone# TODO etudiantdef pire_ratio_echantillon(p, pas):"""Le ratio |t| / (2*sqrt(p)) maximal parmi les courbes y^2 = x^3 + ax + b lisses avec (a, b) multiples de pas, et le couple (a, b) qui l'atteint. Renvoie (ratio, a, b). Cout : O(p) pour la table, puis ~2300 courbes x O(p)."""# Etape 1 : racine = racines_table(p)# Etape 2 : boucle determinee sur a, b in range(0, p, pas) ; passer si non est_lisse(a, b, p)# Etape 3 : compter en O(p) : pour chaque x, v = (x^3 + a*x + b) % p vaut# 0 -> 1 point (y = 0), un carre non nul -> 2 points, sinon 0 ; + 1 pour l'infini# Etape 4 : t = p + 1 - nombre de points ; ratio = abs(t) / (2 * math.sqrt(p))# Indice : ~2300 courbes x O(p) ~ 2x10^7 operations -- quelques secondesreturnNone# TODO etudiant
Conclusion
Ce qu’on a distillé :
un corps fini se manipule avec trois opérations, et l’inverse existe toujours ;
compter des points sur \(y^2 = x^3+ax+b\) est un calcul élémentaire — aucune théorie nécessaire pour produire les données ;
la borne de Hasse\(|t| \leq 2\sqrt{p}\), énoncé profond, se vérifie sur 1701 courbes en quelques secondes — et le nuage \(|t|\) contre \(2\sqrt{p}\) en porte la vue intuitive (fluctuations en \(\sqrt{p}\)) ;
la trace de Frobenius condense l’arithmétique de la courbe : comptages sur toutes les extensions via \(\alpha + \beta = t\), \(\alpha\beta = p\).
C’est le geste de Serre que cette série veut honorer : rendre l’objet calculable avant de le généraliser — les corps finis d’A Course in Arithmetic jusqu’à la fonction zêta. L’autre geste, distillé du même talk : le contre-exemple est un théorème. À l’IHP, Serre a défendu la notion contre l’idée qu’elle serait « quelque chose qui n’est pas bien » : « un contre-exemple, ça se lance, “il existe machin qui a telle propriété”, c’est le théorème. Simplement, la propriété en question, les gens disaient : ah, on ne croyait pas que c’était vrai. Donc c’est en fait un résultat positif très intéressant » (talk du centenaire [10:03], transcription hors dépôt). L’exemple qui suit le rend sensible pour ce carnet : sur corps fini, Chevalley–Warning (1935) garantit le zéro non trivial d’un polynôme homogène dès que les variables dépassent le carré du degré — le monde dans lequel on calcule ici ; sur corps \(p\)-adique, le même énoncé (conjecture d’Artin) s’effondre en degré 4 par le contre-exemple de Terjanian — l’histoire en est distillée dans 07-zeros-fonctions-l-gaps-gue.ipynb, section « La leçon du témoin faux ». Même énoncé, autre monde, autre vérité : une raison de plus de rendre chaque monde calculable avant de le croire.
Ressources
Jean-Pierre Serre, A Course in Arithmetic, Springer (ch. 1 : corps finis ; ch. 2 : formes modulaires — la borne sous-jacente est celle de Hasse-Weil).