14 - Composition des formes quadratiques binaires et nombre de classes h(D)

A Course in Arithmetic, chapitre 4 (Serre, GTM 7) — Disquisitiones Arithmeticae §§ 222-256 (Gauss, 1801). Le carnet 11 a établi la densité de Chebotarev 1/2 et la liste des 9 corps quadratiques imaginaires de Heegner pour lesquels h(D) = 1. Le carnet 13 a posé le symbole de Jacobi et l’extension Kronecker en O(log n). Ce carnet 14 franchit une étape : comment calculer h(D) ? La réponse canonique pour les discriminants quadratiques imaginaires D < 2 est l’algorithme de réduction de Gauss sur les formes quadratiques binaires — l’ancêtre de l’arithmetique des corps de nombres, antérieur de 100 ans aux idéaux de Dedekind. Outils : Python stdlib pur (math.isqrt). Le carnet 11 a posé la notion de h(D) (citation en fin de parcours) ; ce 14 la calcule et la vérifie numériquement.

Plan

  1. Formes quadratiques binaires (a, b, c) de discriminant D = b² - 4ac
  2. Réduction de Gauss : algorithme des pas b → b ± 2a et swap(a,c)
  3. Nombre de classes h(D) pour D < 0 : énumération des formes réduites
  4. Vérification sur les 9 corps de Heegner (h(D) = 1)
  5. Exercices

1. Formes quadratiques binaires

Une forme quadratique binaire de discriminant D est un triplet (a, b, c) avec a > 0 et \[f(x, y) = ax^2 + bxy + cy^2, \qquad D = b^2 - 4ac.\] Pour un corps quadratique \(K = \mathbb{Q}(\sqrt{D})\) de discriminant quadratique fondamental D (premier ou produit d’impar d ≥ 1), chaque idéal de \(\mathcal{O}_K\) correspond à une classe de formes équivalentes sous l’action de SL₂(Z). Le nombre de classes h(D) est le nombre de classes d’équivalence de formes réduites : - \(|b| \leq a \leq c\) ; - si \(|b| = a\) ou \(a = c\), alors \(b \geq 0\). Pour D < 0 (corps quadratique imaginaire), h(D) est fini. Pour D > 1 (corps quadratique réel), h(D) est infini (les classes sont parametrées par l’unité fondamentale).

from math import isqrt

def gauss_reduce(a, b, c):
    # Précondition (section 1) : a > 0, forme positive définie (D < 0).
    # Pour a <= 0 la descente n'a pas de sens : on refuse l'entrée.
    if a <= 0:
        raise ValueError('a doit être strictement positif (forme positive définie)')
    D = b * b - 4 * a * c
    while True:
        # Translation T^m de SL2(Z) : centrer b dans (-a, a] par pas de 2a.
        # C'est une translation (pas une réflexion) : la classe propre est préservée.
        b = b - ((b + a) // (2 * a)) * (2 * a)
        # c RECALCULÉ à chaque étape : seule façon de garder D = b^2 - 4ac invariant.
        c = (b * b - D) // (4 * a)
        if a > c:
            a, b, c = c, -b, a
            continue
        if (a == c or abs(b) == a) and b < 0:
            b = -b
        return (a, b, c)

print('Réduction de Gauss : invariance du discriminant et forme réduite')
print()
tests = [(1, 1, 1), (2, 1, 3), (5, 13, 9), (2, 5, 9), (9, 25, 21), (3, 3, 5), (1, 0, 5), (2, 2, 3), (7, 13, 9)]
print(f"{'Entrée':>13} | {'Réduite':>13} | {'D entrée':>9} | {'D sortie':>9} | invariant")
print('-' * 66)
for (a, b, c) in tests:
    D_in = b * b - 4 * a * c
    red = gauss_reduce(a, b, c)
    D_out = red[1] * red[1] - 4 * red[0] * red[2]
    ok = 'oui' if D_in == D_out else 'NON'
    print(f'{str((a, b, c)):>13} | {str(red):>13} | {D_in:>9} | {D_out:>9} | {ok}')

print()
print('Rejet de la précondition a > 0 :')
try:
    gauss_reduce(-3, 1, 1)
except ValueError as e:
    print(f'gauss_reduce(-3, 1, 1) -> ValueError : {e}')
Réduction de Gauss : invariance du discriminant et forme réduite

       Entrée |       Réduite |  D entrée |  D sortie | invariant
------------------------------------------------------------------
    (1, 1, 1) |     (1, 1, 1) |        -3 |        -3 | oui
    (2, 1, 3) |     (2, 1, 3) |       -23 |       -23 | oui
   (5, 13, 9) |     (1, 1, 3) |       -11 |       -11 | oui
    (2, 5, 9) |     (2, 1, 6) |       -47 |       -47 | oui
  (9, 25, 21) |     (5, 3, 7) |      -131 |      -131 | oui
    (3, 3, 5) |     (3, 3, 5) |       -51 |       -51 | oui
    (1, 0, 5) |     (1, 0, 5) |       -20 |       -20 | oui
    (2, 2, 3) |     (2, 2, 3) |       -20 |       -20 | oui
   (7, 13, 9) |     (3, 1, 7) |       -83 |       -83 | oui

Rejet de la précondition a > 0 :
gauss_reduce(-3, 1, 1) -> ValueError : a doit être strictement positif (forme positive définie)

Lecture du résultat

Sur les 9 formes de test (toutes à \(a > 0\), conformément à la précondition de la section 1), la sortie vérifie ligne à ligne les deux garanties de l’algorithme : - Invariance du discriminant : la colonne « D sortie » est identique à « D entrée » à chaque ligne — chaque étape est une transformation \(SL_2(\mathbb{Z})\) (translation \(T^m\) pour centrer \(b\) dans \((-a, a]\), puis swap \((a,b,c) \leftrightarrow (c,-b,a)\) tant que \(a > c\)), avec \(c\) recalculé à chaque étape pour maintenir \(D = b^2 - 4ac\). - Bornage de la forme réduite : \(|b| \leq a \leq c\), avec \(b \geq 0\) dans les cas frontière \(|b| = a\) ou \(a = c\) — la sortie \((5, 3, 7)\) pour l’entrée \((9, 25, 21)\) en est l’illustration (trois itérations de swap). La précondition \(a > 0\) est enforced : une entrée à \(a\) négatif est rejetée par un ValueError (dernière ligne de la sortie) — pour \(a < 0\), la « réduction » \(b \to b \pm 2a\) fait croître \(|b|\) au lieu de le réduire, il n’y a pas de forme réduite dans cette direction.

2. Nombre de classes h(D) pour D < 0

Pour \(D < 0\) discriminant quadratique fondamental, h(D) = nombre de formes réduites de discriminant D. C’est un algorithme en \(O(\sqrt{|D|})\) : on itère sur \(b \in [0, \sqrt{|D|/3}]\) (borne due à la condition \(a \leq c \Rightarrow b^2 \leq -3D\)), puis sur les diviseurs \(a\) de \((b^2 - D)/4\), et on garde ceux qui satisfont \(|b| \leq a \leq c\). Pour les 9 corps quadratiques imaginaires de Heegner, l’arithmétique exceptionnelle donne h(D) = 1. Cette liste est célèbre : c’est le théorème de Stark-Heegner (1952/1967), qui résout l’équation $x^2 + Dy^2 = $ premier en généralisant le théorème des deux carrés.

def enumerate_reduced_forms(D):
    forms = []
    b_step = 2
    b_parity = 0 if D % 4 == 0 else 1
    # Borne de réduction : |b| <= a <= sqrt(|D|/3) (cf. section 2) - la même
    # borne pour D = 0 mod 4 et D = 1 mod 4 (une borne -D/4 raterait des formes
    # comme (6, 6, 7) de D = -132).
    b_max = isqrt(-D // 3) if -D >= 3 else 0
    for b in range(b_parity, b_max + 1, b_step):
        if (b * b - D) % 4 != 0:
            continue
        a_c = (b * b - D) // 4
        for a in range(1, isqrt(a_c) + 1):
            if a_c % a != 0:
                continue
            c = a_c // a
            if abs(b) <= a <= c:
                if (a == c or abs(b) == a) and b < 0:
                    continue
                forms.append((a, b, c))
                # Miroir (a, -b, c) : classe PROPRE distincte quand
                # 0 < b < a < c (forme non ambiguë). L'omettre sous-compte h(D)
                # d'un facteur ~2 sur les discriminants génériques.
                if b > 0 and b < a and a < c:
                    forms.append((a, -b, c))
    return forms

HEEGNER_D = [-3, -4, -7, -8, -11, -19, -43, -67, -163]

print('Nombre de classes h(D) pour D < 0 (9 corps de Heegner + 4 controles a h>1)')
print()
print(f"{'D':>6} | {'h(D)':>4} | {'Formes réduites'}")
print('-' * 60)
test_D = HEEGNER_D + [-15, -20, -24, -35]
for D in test_D:
    forms = enumerate_reduced_forms(D)
    h = len(forms)
    if h <= 3:
        forms_str = str(forms)
    else:
        forms_str = f'({h} formes, premiere = {forms[0]})'
    print(f'{D:>5} | {h:>4} | {forms_str}')
Nombre de classes h(D) pour D < 0 (9 corps de Heegner + 4 controles a h>1)

     D | h(D) | Formes réduites
------------------------------------------------------------
   -3 |    1 | [(1, 1, 1)]
   -4 |    1 | [(1, 0, 1)]
   -7 |    1 | [(1, 1, 2)]
   -8 |    1 | [(1, 0, 2)]
  -11 |    1 | [(1, 1, 3)]
  -19 |    1 | [(1, 1, 5)]
  -43 |    1 | [(1, 1, 11)]
  -67 |    1 | [(1, 1, 17)]
 -163 |    1 | [(1, 1, 41)]
  -15 |    2 | [(1, 1, 4), (2, 1, 2)]
  -20 |    2 | [(1, 0, 5), (2, 2, 3)]
  -24 |    2 | [(1, 0, 6), (2, 0, 3)]
  -35 |    2 | [(1, 1, 9), (3, 1, 3)]

Lecture du résultat

Les 9 corps quadratiques de Heegner ont tous h(D) = 1 : - D = -3 : une seule forme \((1, 1, 1)\) (anneau des entiers de \(\mathbb{Q}(\sqrt{-3})\) = \(\mathbb{Z}[(1+\sqrt{-3})/2]\)). - D = -4 : forme \((1, 0, 1)\) (anneau des entiers de Gauss \(\mathbb{Z}[i]\)). - D = -7 : forme \((1, 1, 2)\). - D = -8 : forme \((1, 0, 2)\). - D = -11 : forme \((1, 1, 3)\). - D = -19 : forme \((1, 1, 5)\). - D = -43 : forme \((1, 1, 11)\). - D = -67 : forme \((1, 1, 17)\). - D = -163 : forme \((1, 1, 41)\). En control, les discriminants \(-15, -20, -24, -35\) ont h(D) = 2, 2, 2, 2 respectivement (les plus petits non-Heegner). La liste de Heegner est close : c’est l’invariant h(D) = 1 qui definit le théorème de Stark-Heegner.

3. Vérification empirique du théorème de Stark-Heegner

Le théorème de Stark-Heegner (1952 pour h(D) ≤ 1, 1967 pour h(D) = 1 effectif) dit que la liste \(\{D \in \mathbb{Z} : D \leq -3, h(D) = 1\}\) est exactement les 9 discriminants \(\{-3, -4, -7, -8, -11, -19, -43, -67, -163\}\). C’est l’une des listes les plus célèbres de l’arithmétique. La démonstration originale de Stark (1967) utilise l’analyse complexe (formule de Dirichlet pour h(D) comme residu de la \(L\)-serie \(L(1, \chi_D)\)) ; celle de Heegner (1952) utilise les fonctions modulaires et les valeurs de la fonction \(\eta\) de Dedekind.

print('Verification : h(D) = 1 sur tous les D entre -3 et -200 ?')
print()
HEEGNER_D = [-3, -4, -7, -8, -11, -19, -43, -67, -163]

def is_fundamental_disc(D):
    # Discriminant quadratique fondamental (Python : D % 4 in (0, 1) garde
    # les D négatifs congrus à 1 mod 4, que % == 1 exclusait à tort).
    if D % 4 not in (0, 1):
        return False
    if D % 4 == 1:
        m = D
    else:
        m = D // 4
        if m % 4 not in (2, 3):
            return False
    n = abs(m)
    for d in range(2, isqrt(n) + 1):
        if n % (d * d) == 0:
            return False
    return True

h_eq_1 = []
for D in range(-3, -200, -1):
    if not is_fundamental_disc(D):
        continue
    forms = enumerate_reduced_forms(D)
    if len(forms) == 1:
        h_eq_1.append(D)

print(f'Discriminants quadratiques fondamentaux D entre -3 et -200 avec h(D) = 1 :')
print(h_eq_1)
print()
print(f'Heegner connu : {HEEGNER_D[:9]}')
print()
matches = sorted(set(h_eq_1) & set(HEEGNER_D)) == sorted(h_eq_1)
print(f'Concordance numerique avec la liste de Heegner : {matches}')
Verification : h(D) = 1 sur tous les D entre -3 et -200 ?

Discriminants quadratiques fondamentaux D entre -3 et -200 avec h(D) = 1 :
[-3, -4, -7, -8, -11, -19, -43, -67, -163]

Heegner connu : [-3, -4, -7, -8, -11, -19, -43, -67, -163]

Concordance numerique avec la liste de Heegner : True

Lecture du résultat

Avec le filtre de discriminant fondamental corrigé, l’algorithme de Gauss retrouve la liste de Heegner à partir de la seule condition h(D) = 1 sur l’intervalle D ∈ [-3, -200] : - Pour chaque D discriminant quadratique fondamental (D ≡ 1 mod 4 avec |D| squarefree, ou D = 4m avec m ≡ 2, 3 mod 4 et |m| squarefree — le filtre exclut par exemple -12 = 4 × (-3) où -3 ≡ 1 mod 4), on énumère les formes réduites. - La sortie donne les 9 discriminants h(D) = 1, exactement les Heegner : la concordance est True. - Aucun autre D ∈ [-3, -200] fondamental n’a h(D) = 1. C’est la vérification numérique du théorème de Stark-Heegner — pas la preuve (qui demande l’analyse complexe ou les fonctions modulaires), mais l’observation que la liste est close. Le carnet 07 (zéros de fonctions L) est lié : la formule analytique \(h(D) = \frac{w \sqrt{|D|}}{2\pi} L(1, \chi_D)\) où \(w\) est le nombre de racines de l’unité dans \(\mathcal{O}_K\) (exercice 2 ci-dessous).

4. Exercices

Trois prolongements qui poussent h(D) au-delà de la routine : Exercice 1 : Calculer h(D) pour tous les D entre -3 et -500, et lister les D avec h(D) = 2 ou h(D) = 3. Comparer avec la suite OEIS A014603 (Heegner) et la table des h(D) pour D < 0. Exercice 2 : Pour D = -163 (le plus grand Heegner), vérifier la formule analytique de Dirichlet \(L(1, \chi_{-163}) = \frac{2\pi \, h(D)}{w \sqrt{|D|}}\) où \(w = 2\) (unités ±1), en la confrontant à un calcul indépendant de \(L(1, \chi_D)\) : le produit d’Euler tronqué \(\prod_p (1 - \chi_D(p)/p)^{-1}\). Exercice 3 : Implémenter la composition de Gauss de deux formes réduites : pour (a1, b1, c1) et (a2, b2, c2) de discriminant D, calculer (a3, b3, c3) tel que la classe du composé soit le produit des classes. (Indication : pgcd(a1, a2, (b1+b2)/2), puis algorithme de Lagrange.)

print('Exo 1 : h(D) pour D entre -3 et -500, lister h=2 et h=3')
print()
h1_list = []
h2_list = []
h3_list = []
for D in range(-500, -2):
    if not is_fundamental_disc(D):
        continue
    forms = enumerate_reduced_forms(D)
    h = len(forms)
    if h == 1:
        h1_list.append(D)
    elif h == 2:
        h2_list.append(D)
    elif h == 3:
        h3_list.append(D)

print(f'h(D) = 1 ({len(h1_list)} discriminants) : {sorted(h1_list)}')
print(f'Concordance Heegner : {sorted(h1_list) == sorted(HEEGNER_D)}')
print()
print(f'h(D) = 2 ({len(h2_list)} discriminants) : {sorted(h2_list)}')
print()
print(f'h(D) = 3 ({len(h3_list)} discriminants) : {sorted(h3_list)}')
Exo 1 : h(D) pour D entre -3 et -500, lister h=2 et h=3

h(D) = 1 (9 discriminants) : [-163, -67, -43, -19, -11, -8, -7, -4, -3]
Concordance Heegner : True

h(D) = 2 (18 discriminants) : [-427, -403, -267, -235, -232, -187, -148, -123, -115, -91, -88, -52, -51, -40, -35, -24, -20, -15]

h(D) = 3 (12 discriminants) : [-499, -379, -331, -307, -283, -211, -139, -107, -83, -59, -31, -23]
import math

print('Exo 2 : formule analytique de Dirichlet pour D = -163')
print()
D = -163
h = len(enumerate_reduced_forms(D))
w = 2  # unités ±1 (D < -4)
sqrtD = math.sqrt(-D)

# Côté 1 : formule de classes de Dirichlet, avec h compté par énumération des formes
L1_dirichlet = 2 * math.pi * h / (w * sqrtD)
print(f'h(-163) = {h}, w = {w} : L(1, chi_D) = 2*pi*h/(w*sqrt(|D|)) = {L1_dirichlet:.6f}')
print()

# chi_D(p) = (D/p) : critère d'Euler pour p impair, cas p = 2 à part
# (chi_D(2) = +1 si D = ±1 mod 8, -1 si D = ±3 mod 8).
def euler_product_L1(D, N):
    sieve = bytearray([1]) * (N + 1)
    sieve[0:2] = b'\x00\x00'
    prod = 1.0
    if D % 2 != 0:
        chi2 = 1 if D % 8 in (1, 7) else -1
        prod /= (1 - chi2 / 2)
    for p in range(3, N + 1, 2):
        if not sieve[p]:
            continue
        for m in range(p * p, N + 1, p):
            sieve[m] = 0
        if D % p == 0:
            continue  # premier ramifié : facteur local 1
        chi = pow(D, (p - 1) // 2, p)
        prod /= (1 - (1 if chi == 1 else -1) / p)
    return prod

L1_euler = None
for N in (10**4, 10**5):
    L1_euler = euler_product_L1(D, N)
    écart = abs(L1_euler - L1_dirichlet)
    print(f'N = {N:>6} : L(1, chi_D) par produit d Euler = {L1_euler:.6f}, écart = {écart:.2e}')
print()
print(f'Concordance (écart < 1e-3 à N = 10^5) : {abs(L1_euler - L1_dirichlet) < 1e-3}')
Exo 2 : formule analytique de Dirichlet pour D = -163

h(-163) = 1, w = 2 : L(1, chi_D) = 2*pi*h/(w*sqrt(|D|)) = 0.246069

N =  10000 : L(1, chi_D) par produit d Euler = 0.245458, écart = 6.10e-04
N = 100000 : L(1, chi_D) par produit d Euler = 0.245861, écart = 2.07e-04

Concordance (écart < 1e-3 à N = 10^5) : True
from math import gcd

def gauss_compose_step1(a1, b1, c1, a2, b2, c2):
    if (b1 + b2) % 2 != 0:
        raise ValueError('b1 + b2 doit etre pair')
    D = b1*b1 - 4*a1*c1
    if D != b2*b2 - 4*a2*c2:
        raise ValueError('Discriminants incompatibles')
    g = gcd(gcd(a1, a2), (b1 + b2) // 2)
    return g

print('Exo 3 : pgcd pour composition de Gauss')
print()
tests = [((1, 1, 2), (1, 1, 2), -7), ((1, 1, 3), (1, 1, 3), -11), ((2, 1, 3), (2, 1, 3), -23)]
for (a1, b1, c1), (a2, b2, c2), D in tests:
    g = gauss_compose_step1(a1, b1, c1, a2, b2, c2)
    print(f'gcd pour compose({(a1,b1,c1)}, {(a2,b2,c2)}) avec D={D} : g = {g}')
Exo 3 : pgcd pour composition de Gauss

gcd pour compose((1, 1, 2), (1, 1, 2)) avec D=-7 : g = 1
gcd pour compose((1, 1, 3), (1, 1, 3)) avec D=-11 : g = 1
gcd pour compose((2, 1, 3), (2, 1, 3)) avec D=-23 : g = 1

Conclusion

Le nombre de classes h(D) est l’invariant arithmetique fondamental d’un corps quadratique. Pour D < 0, c’est un entier calculable par énumération des formes réduites (Gauss). Pour D > 0, h(D) est infini (les classes sont parametrées par l’unité fondamentale). La liste des 9 corps de Heegner (h(D) = 1) est close et vérifiable : c’est le théorème de Stark-Heegner, à la croisée de l’arithmétique, de l’analyse et de la théorie des nombres. Suite naturelle : - Carnet 15.1 envisagé : composition de Gauss explicite (algorithme de Lagrange complet), vérification que le groupe de classes est bien abélien. - Pont avec la série GenAI : la formule analytique \(h(D) = \frac{w \sqrt{|D|}}{2\pi} L(1, \chi_D)\) est reliée à l’étude des zéros de fonctions L (carnet 07).

Ressources

  • Serre, A Course in Arithmetic, chapitre 4 (Springer GTM 7) — référence canonique.
  • Cox, Primes of the Form x² + ny² (Wiley) — chapitre 7 sur la composition de Gauss.
  • Gauss, Disquisitiones Arithmeticae §§ 222-256 (1801) — l’original.
  • Stark (1967), On the “gap” in a theorem of Heegner — la preuve d’effectivité.
  • Heegner (1952), Diophantische Analysis und Modulfunktionen — la construction.
  • OEIS A014603 — discriminants quadratiques imaginaires fondamentaux avec h(D) = 1.
Retour au sommet