ANALYSE-04 : Trois primitives de PFR, et l’endroit exact où elles cessent de valoir

Série : SymbolicAI / Lean — Digestions de résultats profonds, companion du capstone Lean-20 — Digestions Tao Source : ANALYSE-03-PFR-Lean.ipynb (digestion PFR par Gowers-Green-Manners-Tao, 2023) Lac source : https://github.com/teorth/pfr (formalisation collaborative Lean 4) Kernel : python3 — le notebook conceptuel Python illustre les primitives, les preuves formelles restent dans ANALYSE-03

Pourquoi ce notebook

La digestion ANALYSE-03 a établi l’essentiel : PFR est un théorème de « compression ⇒ structure », et l’entropie y est monnaie commune entre combinatoire additive, machines causales et ICT. Ce qu’ANALYSE-04 ajoute n’est pas une reprise de la preuve — c’est l’extraction de primitives transportables et, indissociablement, le test de leurs limites.

Trois primitives, et une seule est inconditionnellement transportable :

# Primitive Transport
1 Distance entropique de Ruzsa — d[X;Y] = H(X′−Y′) − ½H(X′) − ½H(Y′) conditionnel : exige une structure de groupe additif
2 Décomposition projection / fibres — X ⟶ π(X) + X\|π(X) large : règle de chaîne + information mutuelle conditionnelle
3 La fonctionnelle τ — descente strictement monotone ; quand elle ne peut plus descendre, la structure est forcée patron, sous hypothèses à expliciter

Le test de limite est le livrable, autant que l’extraction

Une primitive qui ne transfère pas est un résultat.

Le garde-fou est explicite et il est le cœur du grain : coller Ruzsa sur des proxys sans structure de groupe, ce serait le nouveau mapping affine — exactement l’erreur que le dépôt est en train de réparer. Le notebook doit donc chercher où ça casse, pas seulement où ça marche.

Honnêteté de portée

PFR ne « prouve » évidemment rien pour ICT — son résultat est spécifique à la combinatoire additive, avec des propriétés particulières de F₂ⁿ jusque dans son endgame. Mais il fournit un exemple CERTIFIÉ où une quantité informationnelle FORCE une structure algébrique. C’est beaucoup plus précieux qu’une analogie.

Et sur τ_strictly_decreases : c’est un analogue de fonction de Lyapunov pour la preuve, pas une Lyapunov dynamique au sens physique. La qualification doit figurer dans le notebook.

Plan

  1. Primitive 1 — Distance entropique de Ruzsa : énoncé, calcul, test de limite.
  2. Primitive 2 — Décomposition projection / fibres : règle de chaîne, test de limite.
  3. Primitive 3 — La fonctionnelle τ : patron de descente, test de limite.
  4. Conclusion — lien vers ANALYSE-03 et le lac pfr.

1. Primitive 1 — Distance entropique de Ruzsa

1.1 Énoncé

Soient X, Y deux variables aléatoires sur un groupe abélien (G, +). On note X′ une copie indépendante de X et Y′ une copie indépendante de Y. La distance de Ruzsa est définie par :

d[X;Y] = H(X′ − Y′) − ½ H(X′) − ½ H(Y′)

où H est l’entropie de Shannon en bits.

Invariants (sur groupe abélien) : - d[X;Y] ≥ 0 toujours (inégalité de conditional mutual information / chaîne). - d[X;Y] = d[Y;X] (symétrique). - d[X;X] = 0. - Triangle : d[X;Z] ≤ d[X;Y] + d[Y;Z].

Sémantique : d[X;Y] mesure combien « les distributions sont loin d’être des translatées l’une de l’autre ».

1.2 Calcul sur son terrain — un groupe additif fini

On prend G = ℤ/nℤ et on compare deux variables X, Y à distributions connues.

import math
from collections import Counter
def entropy_bits(dist):
    """Entropie de Shannon en bits à partir d'un dict {symbol: proba}."""
    return -sum(p * math.log2(p) for p in dist.values() if p > 0)
def d_ruzsa(X_dist, Y_dist, n):
    """Distance de Ruzsa sur ℤ/nℤ entre deux distributions.
    Suppose X_dist et Y_dist sont des dicts {k: proba} sur 0..n-1.
    """
    # X' - Y' : convolution signée modulo n
    XY_minus = Counter()
    for x, px in X_dist.items():
        for y, py in Y_dist.items():
            XY_minus[(x - y) % n] += px * py
    H_XY_minus = entropy_bits(XY_minus)
    H_X = entropy_bits(X_dist)
    H_Y = entropy_bits(Y_dist)
    return H_XY_minus - 0.5 * H_X - 0.5 * H_Y
# Test 1 : X et Y uniformes indépendants sur ℤ/8ℤ
n = 8
uniform = {k: 1/n for k in range(n)}
print(f"Test 1 : X = Y uniformes i.i.d. sur ℤ/{n}ℤ")
print(f"  d[X;Y] = {d_ruzsa(uniform, uniform, n):.4f}  (attendu ~ 0)")
# Test 2 : Y = X + 1 (translation)
shifted = {k: uniform[(k - 1) % n] for k in range(n)}
print(f"Test 2 : Y = X + 1 (translation)")
print(f"  d[X;Y] = {d_ruzsa(uniform, shifted, n):.4f}  (attendu 0)")
# Test 3 : X = Y même variable (couplage parfait)
# Si X et Y ont la même distribution, d[X;Y] ≈ 0 si indépendants,
# ou peut être < 0 si corrélés (en théorie >= 0 mais bruit numérique).
print(f"Test 3 : X uniforme, Y concentré en 0")
Y_degen = {0: 1.0, **{k: 0.0 for k in range(1, n)}}
print(f"  d[X;Y] = {d_ruzsa(uniform, Y_degen, n):.4f}  (H(Y)=0, formule dégénère)")
Test 1 : X = Y uniformes i.i.d. sur ℤ/8ℤ
  d[X;Y] = 0.0000  (attendu ~ 0)
Test 2 : Y = X + 1 (translation)
  d[X;Y] = 0.0000  (attendu 0)
Test 3 : X uniforme, Y concentré en 0
  d[X;Y] = 1.5000  (H(Y)=0, formule dégénère)

Lecture du résultat

Les trois tests illustrent les invariants sur groupe additif : - Test 1 : variables i.i.d. ⇒ d[X;Y] ≈ 0 (distributions identiques modulo permutation). - Test 2 : translation ⇒ d[X;Y] = 0 exactement (la distance de Ruzsa est invariante par translation, c’est sa propriété fondamentale). - Test 3 : dégénérescence — la formule reste définie mais perd sa sémantique.

Point clé : la définition exige la convolution X′ − Y′, qui n’a de sens que sur groupe additif. C’est ce que « Ruzsa exige une structure de groupe additif » veut dire.

1.3 Test de limite — quand la primitive ne transporte plus

On garde la formule, les distributions et l’entropie inchangées, et on ne fait varier que l’opération x − y. Deux exhibitions :

  • (a) le même ensemble {0,1,2,3} muni de deux lois de groupe différentes (ℤ/4ℤ et le groupe de Klein) donne deux valeurs différentes de d pour les mêmes distributions — la valeur de d n’est donc pas une fonction des seules distributions, mais du couple (distributions, loi de groupe) ;
  • (b) en retirant l’inversibilité de x ↦ x − y, d devient négatif — et une distance négative n’en est plus une.
# 1.3 — Test de limite : ce que la formule cesse de mesurer sans loi de groupe.
# On ne change QUE l'opération « x − y ». Le reste — les distributions, la
# formule, l'entropie — est identique à la cellule précédente.
def d_ruzsa_op(X_dist, Y_dist, sub):
    """d[X;Y] = H(X−Y) − H(X)/2 − H(Y)/2, la soustraction étant fournie."""
    XY = Counter()
    for x, px in X_dist.items():
        for y, py in Y_dist.items():
            XY[sub(x, y)] += px * py
    return entropy_bits(XY) - 0.5 * entropy_bits(X_dist) - 0.5 * entropy_bits(Y_dist)

# (a) Même ensemble, mêmes distributions, deux lois de groupe différentes.
#     X = Y = uniforme sur {0, 1}, plongées dans un ensemble à 4 éléments.
X = Y = {0: 0.5, 1: 0.5}
z4    = lambda a, b: (a - b) % 4        # ℤ/4ℤ : groupe cyclique
klein = lambda a, b: a ^ b              # (ℤ/2ℤ)² : groupe de Klein

print("(a) mêmes distributions, deux lois de groupe sur le même ensemble {0,1,2,3}")
print(f"  ℤ/4ℤ  : H(X−Y) = {entropy_bits(Counter({z4(x, y): 0.25 for x in X for y in Y})):.4f} bits"
      f"   →  d[X;Y] = {d_ruzsa_op(X, Y, z4):.4f}")
print(f"  Klein : d[X;Y] = {d_ruzsa_op(X, Y, klein):.4f}")
print("  La valeur de d n'est donc PAS une fonction des seules distributions :")
print("  c'est une fonction du couple (distributions, loi de groupe).")

# (b) On retire l'inversibilité : « x − y » cesse d'être une bijection en x.
#     Si d devient négatif, ce n'est plus une distance.
import random
rng = random.Random(7)

def dirichlet(k):
    g = [rng.gammavariate(1.0, 1.0) for _ in range(k)]
    s = sum(g)
    return {i: v / s for i, v in enumerate(g)}

lois = [
    ("(a − b) mod 4", "groupe cyclique", z4),
    ("a XOR b",       "groupe de Klein", klein),
    ("max(a − b, 0)", "non inversible",  lambda a, b: max(a - b, 0)),
    ("(a × b) mod 4", "non inversible",  lambda a, b: (a * b) % 4),
]
print("\n(b) 40 000 couples de distributions tirés au hasard sur {0,1,2,3} :")
for nom, nature, sub in lois:
    d_min = min(d_ruzsa_op(dirichlet(4), dirichlet(4), sub) for _ in range(40_000))
    verdict = "d ≥ 0 : distance" if d_min > -1e-9 else "d < 0 : PLUS une distance"
    print(f"  {nom:<14} {nature:<16} →  min d = {d_min:+.4f}   {verdict}")
print("\n  Les deux opérations non inversibles rendent d négatif. Une « distance »")
print("  négative n'en est pas une : la primitive a cessé de valoir, et on voit où.")
(a) mêmes distributions, deux lois de groupe sur le même ensemble {0,1,2,3}
  ℤ/4ℤ  : H(X−Y) = 1.5000 bits   →  d[X;Y] = 0.5000
  Klein : d[X;Y] = 0.0000
  La valeur de d n'est donc PAS une fonction des seules distributions :
  c'est une fonction du couple (distributions, loi de groupe).

(b) 40 000 couples de distributions tirés au hasard sur {0,1,2,3} :
  (a − b) mod 4  groupe cyclique  →  min d = +0.0128   d ≥ 0 : distance
  a XOR b        groupe de Klein  →  min d = +0.0170   d ≥ 0 : distance
  max(a − b, 0)  non inversible   →  min d = -1.2699   d < 0 : PLUS une distance
  (a × b) mod 4  non inversible   →  min d = -0.9756   d < 0 : PLUS une distance

  Les deux opérations non inversibles rendent d négatif. Une « distance »
  négative n'en est pas une : la primitive a cessé de valoir, et on voit où.

Verdict Primitive 1

Transportable sous condition : d_ruzsa[X;Y] exige une structure de groupe additif (G, +). La cellule précédente le montre en deux temps — la valeur de d change quand on change la loi de groupe à distributions fixées, et sans inversibilité de x ↦ x − y elle passe sous zéro, donc cesse d’être une distance. C’est exactement la classe « conditionnelle » du tableau d’introduction.

Leçon : la primitive a une puissance incomparable sur son terrain (groupes abéliens, F₂ⁿ, entropie appliquée à la combinatoire additive), mais l’utiliser hors de ce terrain serait du « mapping affine » — c’est-à-dire plaquer une structure sans la mériter. Le carnet ANALYSE-03 lui-même s’en garde : la preuve PFR ne s’applique qu’à F₂ⁿ, pas à un objet métrique général.

Exercice 1 — Ruzsa sur deux groupes de même ordre : Z/4Z contre (Z/2Z)²

La cellule du cours calcule d[X;Y] sur Z/8Z (n = 8). Le verdict ci-dessus dit que la valeur de d change quand on change la loi de groupe, à distributions fixées — vérifiez-le sur deux groupes d’ordre 4 construits sur les mêmes étiquettes {0, 1, 2, 3} :

  • Z/4Z (cyclique) : add_z4(x, y) = (x + y) % 4 ;
  • (Z/2Z)² (groupe de Klein) : add_klein(x, y) = x ^ y (ou exclusif bit à bit sur les étiquettes codées en 2 bits).

Pour les mêmes distributions — X uniforme sur {0, 1} et Y uniforme sur {2, 3} — calculez d[X;Y] = H(X−Y) − ½H(X) − ½H(Y) sous chacune des deux lois, et comparez les deux valeurs.

Indice : dans (Z/2Z)² chaque élément est son propre inverse, donc X−Y = X+Y ; côté Z/4Z, l’inverse de y est (4−y) % 4. Écrivez la convolution une seule fois, paramétrée par la loi add. - Etape 1 : écrire convolution(px, py, add) — le produit de convolution sous une loi de groupe quelconque. - Etape 2 : composer d en réutilisant entropy_bits de la cellule du cours. - Etape 3 : afficher les deux valeurs et conclure : la distance dépend de la loi de groupe, pas seulement des distributions.

# Exercice 1 -- d[X;Y] sur Z/4Z puis (Z/2Z)^2, memes etiquettes {0,1,2,3} (a completer)
def convolution(px, py, add):
    """Produit de convolution de deux lois sous une loi de groupe add(x, y)."""
    pass  # TODO etudiant (Etape 1)


def d_ruzsa_groupe(px, py, add, inverse):
    """d[X;Y] = H(X-Y) - 0.5*H(X) - 0.5*H(Y) ; X-Y se calcule en convoluant X et -Y = inverse(Y)."""
    pass  # TODO etudiant (Etape 2)


# Etape 3 : X uniforme sur {0,1}, Y uniforme sur {2,3}, deux lois de groupe -- comparer.
print("Exercice a completer : d[X;Y] sous Z/4Z puis sous (Z/2Z)^2, memes X et Y.")
print("Attendu : deux valeurs distinctes -- la distance depend de la loi de groupe.")
Exercice a completer : d[X;Y] sous Z/4Z puis sous (Z/2Z)^2, memes X et Y.
Attendu : deux valeurs distinctes -- la distance depend de la loi de groupe.

2. Primitive 2 — Décomposition projection / fibres

2.1 Énoncé

Soient X une variable aléatoire et Y une variable auxiliaire. On note π(X) := 𝔼[X | Y] (espérance conditionnelle) ou plus généralement une projection sur une σ-algèbre. La décomposition est :

X = π(X) + (X | π(X))

où X | π(X) désigne la composante résiduelle (information mutuelle I(X ; π(X)) = 0).

Invariants (règle de chaîne) : - H(X) = H(π(X)) + H(X | π(X)). - I(X ; Y) = H(X) − H(X | Y).

2.2 Application : décomposer un dataset ML

# Primitive 2 — Décomposition projection / fibres sur un cas concret
# On prend un dataset de notes d'étudiants (X) et on projette sur le cours (Y).
# H(X) = entropie totale des notes, H(X|Y) = entropie intra-cours.
import math
from collections import Counter, defaultdict
def normalize(counts):
    total = sum(counts.values())
    return {k: v / total for k, v in counts.items()}
# Notes de 100 étudiants sur 5 cours
notes_brutes = [
    ('Math', 14), ('Math', 16), ('Math', 12), ('Math', 18), ('Math', 15),
    ('Phys', 11), ('Phys', 13), ('Phys', 9),  ('Phys', 12), ('Phys', 14),
    ('Info', 17), ('Info', 19), ('Info', 16), ('Info', 18), ('Info', 20),
    ('Bio',  10), ('Bio',  12), ('Bio',  11), ('Bio',  13), ('Bio',  9),
    ('Chim', 13), ('Chim', 15), ('Chim', 14), ('Chim', 16), ('Chim', 12),
]
# H(X) sur les notes
X_dist = normalize(Counter(n for _, n in notes_brutes))
H_X = -sum(p * math.log2(p) for p in X_dist.values() if p > 0)
print(f"H(X) = entropie sur les notes = {H_X:.3f} bits")
# H(X | Y) par cours
H_X_given_Y = 0
n_total = len(notes_brutes)
for cours, ns in defaultdict(list, {c: [] for c, _ in notes_brutes}).items():
    pass  # syntax workaround
by_cours = defaultdict(list)
for c, n in notes_brutes:
    by_cours[c].append(n)
H_X_given_Y = 0
for cours, ns in by_cours.items():
    p_y = len(ns) / n_total
    dist_ns = normalize(Counter(ns))
    H_n_given_c = -sum(p * math.log2(p) for p in dist_ns.values() if p > 0)
    H_X_given_Y += p_y * H_n_given_c
print(f"H(X | Y) = entropie intra-cours = {H_X_given_Y:.3f} bits")
# H(Y) sur les cours
Y_dist = normalize(Counter(c for c, _ in notes_brutes))
H_Y = -sum(p * math.log2(p) for p in Y_dist.values() if p > 0)
print(f"H(Y) = entropie sur les cours = {H_Y:.3f} bits")
# Vérification règle de chaîne : H(X) = I(X;Y) + H(X|Y)
I_XY = H_X - H_X_given_Y
print(f"I(X;Y) = H(X) - H(X|Y) = {I_XY:.3f} bits")
print(f"H(Y) - I(X;Y) = {H_Y - I_XY:.3f} bits (≠ H(Y), pas d'égalité directe)")
print(f"Règle de chaîne vérifiée : H(X) = {H_X:.3f} ≈ H(X|Y) + I(X;Y) = {H_X_given_Y + I_XY:.3f}")
H(X) = entropie sur les notes = 3.433 bits
H(X | Y) = entropie intra-cours = 2.322 bits
H(Y) = entropie sur les cours = 2.322 bits
I(X;Y) = H(X) - H(X|Y) = 1.111 bits
H(Y) - I(X;Y) = 1.211 bits (≠ H(Y), pas d'égalité directe)
Règle de chaîne vérifiée : H(X) = 3.433 ≈ H(X|Y) + I(X;Y) = 3.433

Lecture du résultat

La règle de chaîne H(X) = H(X|Y) + I(X;Y) est vérifiée numériquement. L’information mutuelle I(X;Y) capture combien le cours (Y) explique la note (X) ; la résiduelle H(X|Y) capture la variation inexpliquée par le cours.

Sémantique opérationnelle : - H(π(X)) ≈ « ce que la projection capte ». - H(X | π(X)) ≈ « ce qui reste dans les fibres ».

2.3 Test de limite — quand la primitive ne transporte plus

La règle de chaîne H(X) = H(X|Y) + I(X;Y) est-elle valide pour n’importe quelle « projection » Y ?

import math
from collections import Counter
# Test de limite : projection arbitraire sur un objet non-probabiliste
# On prend un texte brut et on essaie de définir H(X) comme nombre de mots uniques.
# Mais sans distribution de probabilité, ce n'est PAS de l'entropie de Shannon.
texte = "le chat mange le poisson le chien mange la viande"
mots = texte.split()
print(f"Texte : {texte!r}")
print(f"Mots uniques : {len(set(mots))}, Mots totaux : {len(mots)}")
# H_naive = -sum(1/N * log2(1/N)) sur N = nb total mots ? NON : ce n'est pas une distribution.
# La primitive EXIGE une distribution de probabilité sur X et Y.
# Sur du texte brut, on a un COMPTAGE, pas une distribution : il faut normaliser.
# Construire une distribution : P(mot) = count / total
dist = Counter(mots)
N = len(mots)
probas = {m: c / N for m, c in dist.items()}
H_text = -sum(p * math.log2(p) for p in probas.values() if p > 0)
print(f"Distribution normalisée → H(text) = {H_text:.3f} bits")
print("Une fois normalisée, l'entropie a un sens. Mais :")
print("  - Pas de σ-algèbre canonique sur 'mots'")
print("  - 'Y' = 'premier mot de la phrase' ? — Y est-il une v.a. légitime ?")
print("  - Sans mesure de probabilité, 'projection' n'a pas de sens formel")
print()
print("Verdict Primitive 2 :")
print("  Décomposition X = π(X) + (X|π(X)) exige DISTRIBUTION DE PROBABILITÉ.")
print("  Transportable LARGE (la règle de chaîne est universelle en théorie de l'information).")
print("  Mais 'projection' et 'fibres' doivent être formalisées en proba —")
print("  sinon la décomposition est un formalisme vide.")
Texte : 'le chat mange le poisson le chien mange la viande'
Mots uniques : 7, Mots totaux : 10
Distribution normalisée → H(text) = 2.646 bits
Une fois normalisée, l'entropie a un sens. Mais :
  - Pas de σ-algèbre canonique sur 'mots'
  - 'Y' = 'premier mot de la phrase' ? — Y est-il une v.a. légitime ?
  - Sans mesure de probabilité, 'projection' n'a pas de sens formel

Verdict Primitive 2 :
  Décomposition X = π(X) + (X|π(X)) exige DISTRIBUTION DE PROBABILITÉ.
  Transportable LARGE (la règle de chaîne est universelle en théorie de l'information).
  Mais 'projection' et 'fibres' doivent être formalisées en proba —
  sinon la décomposition est un formalisme vide.

Verdict Primitive 2

Transportable large : la règle de chaîne H(X) = H(X|Y) + I(X;Y) est universelle en théorie de l’information — elle n’exige qu’un espace probabilisé (Ω, ℱ, ℙ) et deux v.a. X, Y. C’est la primitive la plus transportable des trois.

Mais : pour qu’elle ait une sémantique opérationnelle, il faut que la projection π(X) := 𝔼[X | Y] soit bien définie — c’est-à-dire que Y soit une variable aléatoire légitime (mesurable, à valeurs dans un espace convenable). Sur du texte brut, du graphe, ou tout objet sans mesure canonique, la décomposition est un formalisme vide.

Leçon : la primitive est plus transportable que Ruzsa, mais elle exige quand même un cadre probabiliste. Le mapping « projection/fibres » vers ICT (où « fibres » = classes d’équivalence, « projection » = quotient) est légitime SI on construit explicitement la mesure.

Exercice 2 — Décomposer un autre dataset en projection / fibres

Le cours décompose un dataset ML par projection sur une coordonnée puis étudie les fibres. Refaites l’exercice sur le dataset jouet des notes : 8 étudiants (maths, physique), projection sur maths >= 10, fibres = les deux groupes. Mesurez pour chaque fibre : effectif, moyenne physique, écart-type.

Indice : une fibre est simplement la sous-liste des points partageant la même valeur de projection. - Etape 1 : écrire projeter(points, seuil) qui renvoie (admis, refuses). - Etape 2 : écrire stats(fibre) -> (effectif, moyenne, ecart-type).

# Exercice 2 — decomposition projection / fibres sur les notes (a completer)
def projeter(points, seuil=10):
    """Separe les points selon la projection booleenne maths >= seuil."""
    pass  # stub pedagogique (regle C.1)


def stats_fibre(fibre):
    """(effectif, moyenne physique, ecart-type physique) d'une fibre."""
    pass  # stub pedagogique (regle C.1)


print("Exercice a completer : decomposer [(12, 14), (8, 6), (15, 11), (9, 13), (11, 10), (6, 5), (14, 16), (10, 9)].")
print("Attendu : comparer la dispersion par fibre a la dispersion globale.")
Exercice a completer : decomposer [(12, 14), (8, 6), (15, 11), (9, 13), (11, 10), (6, 5), (14, 16), (10, 9)].
Attendu : comparer la dispersion par fibre a la dispersion globale.

3. Primitive 3 — La fonctionnelle τ

3.1 Énoncé

Soit S une structure combinatoire munie d’une fonctionnelle τ : S → ℝ⁺ qui décroît strictement à chaque étape d’une procédure de raffinement :

S₀ ⊃ S₁ ⊃ S₂ ⊃ ...

avec τ(Si₊₁) < τ(Si). Quand τ ne peut plus descendre (τ(Si₊₁) = τ(Si)), la procédure s’arrête et la structure est forcée.

Sémantique : τ est un analogue de fonction de Lyapunov pour la preuve. C’est l’invariant qui garantit la terminaison.

3.2 Patron : descente monotone sur un problème jouet

On prend le problème « factoriser un entier N » avec τ(N) = nombre de diviseurs premiers distincts. La procédure : tester la divisibilité par 2, 3, 5, 7, 11, … jusqu’à √N.

# Primitive 3 — τ = nombre de diviseurs premiers distincts (omega(N)), sur N = 60.
# Procédure : à chaque étape, retirer toute la valuation du plus petit facteur premier.
def omega(N):
    """Nombre de diviseurs premiers distincts de N."""
    n, count, p = N, 0, 2
    while p * p <= n:
        if n % p == 0:
            count += 1
            while n % p == 0:
                n //= p
        p += 1
    if n > 1:
        count += 1
    return count

# Trace : N₀ = 60, on retire 2² → 15, puis 3 → 5, puis 5 → 1.
# Chaque étape élimine un facteur premier distinct :
# ω(60)=3, ω(15)=2, ω(5)=1, ω(1)=0.
N = 60
print(
    f"ω({N}) = {omega(N)} "
    "(2×2×3×5 → facteurs premiers distincts 2, 3 et 5)"
)

# Re-vérification : 60 = 2² × 3 × 5 → 3 facteurs premiers distincts.
expected = len({2, 3, 5})
print(f"Vérif manuelle : 60 = 2² × 3 × 5 → ω = {expected}")

# La procédure de raffinement s'arrête à N=1, où ω=0.
N = 60
steps = [N]
while N > 1:
    for p in range(2, int(N**0.5) + 1):
        if N % p == 0:
            while N % p == 0:
                N //= p
            steps.append(N)
            break
    else:
        # N est premier : retirer son unique facteur premier restant.
        N = 1
        steps.append(N)

omega_steps = [omega(step) for step in steps]
strict_decrease = [
    omega_steps[i] < omega_steps[i - 1]
    for i in range(1, len(omega_steps))
]
print(f"Séquence de raffinement : {steps}")
print(f"ω à chaque étape : {omega_steps}")
print(f"ω décroît-elle strictement ? {strict_decrease}")
ω(60) = 3 (2×2×3×5 → facteurs premiers distincts 2, 3 et 5)
Vérif manuelle : 60 = 2² × 3 × 5 → ω = 3
Séquence de raffinement : [60, 15, 5, 1]
ω à chaque étape : [3, 2, 1, 0]
ω décroît-elle strictement ? [True, True, True]

Lecture du résultat

La séquence 60 → 15 → 5 → 1 montre ω strictement décroissant jusqu’à 0. La procédure termine parce que ω est un entier positif et ne peut pas descendre en dessous de 0.

Point clé : c’est exactement le rôle de τ_strictly_decreases dans ANALYSE-03 (lac pfr) — c’est l’invariant qui garantit la terminaison de la procédure de preuve. Sans cet invariant strictement décroissant, on ne pourrait pas conclure que la procédure s’arrête.

Exercice 3 — Trouver le τ d’Euclide

Le patron du cours exige une fonctionnelle τ qui décroît strictement à chaque étape d’une procédure, sur un ordre bien fondé. Le cas positif le plus ancien : l’algorithme d’Euclide (a, b) ↦ (b, a mod b). Le τ qui prouve sa terminaison est le second argument : τ(a, b) = b.

Vérifiez-le : pour plusieurs couples (a, b), construisez la trajectoire des paires et la suite des valeurs de τ ; constatez qu’elle est strictement décroissante et atteint 0 — l’ordre sur ℕ est bien fondé, la procédure termine donc forcément.

Indice : a mod b < b est l’inégalité qui fait tout le travail ; la suite des seconds arguments est exactement la trajectoire de τ. - Etape 1 : écrire trajectoire_euclide(a, b) qui renvoie la liste des paires successives jusqu’à (g, 0). - Etape 2 : extraire la suite des τ et vérifier la décroissance stricte jusqu’à 0.

# Exercice 3 -- trouver le tau d'Euclide : tau(a, b) = b (a completer)
def trajectoire_euclide(a, b):
    """Liste des paires (a, b) -> (b, a % b) -> ... jusqu'à (g, 0)."""
    pass  # TODO etudiant (Etape 1)


def suite_tau_euclide(a, b):
    """Suite des tau (seconds arguments) le long de la trajectoire."""
    pass  # TODO etudiant (Etape 2)


print("Exercice a completer : trajectoire d'Euclide et suite des tau (ex. (252, 105), (1071, 462)).")
print("Attendu : suite strictement decroissante jusqu'a 0 -- terminaison forcee par l'ordre sur N.")
Exercice a completer : trajectoire d'Euclide et suite des tau (ex. (252, 105), (1071, 462)).
Attendu : suite strictement decroissante jusqu'a 0 -- terminaison forcee par l'ordre sur N.

Exercice 4 — Collatz, le cas limite : mesurer l’absence de τ

La conjecture de Collatz (n pair ↦ n/2 ; n impair ↦ 3n+1) résiste précisément parce que personne ne connaît de τ qui décroisse strictement le long d’une trajectoire — sa terminaison est un problème ouvert. Mesurez cette absence : pour la trajectoire de n = 27 (une des longues), suivez les trois candidats naturels — la valeur n, le nombre de bits n.bit_length(), et ω(n) (diviseurs premiers distincts, comme au §3.2) — et comptez les pas où chacun augmente strictement.

Indice : les valeurs restent < 10⁴, l’essai de division jusqu’à √v suffit pour ω ; un compte d’augmentations > 0 est la preuve mesurée qu’un candidat n’est pas un τ — l’exact contraire de la cellule du cours, où ω descendait de 3 à 0 sans jamais remonter. - Etape 1 : écrire trajectoire_collatz(n) et la parcourir pour n = 27. - Etape 2 : pour chaque candidat, compter les pas strictement croissants et afficher les trois comptes.

# Exercice 4 -- Collatz n=27 : valeur, bits et omega(n) ne decroissent pas strictement (a completer)
def trajectoire_collatz(n):
    """Liste des valeurs successives jusqu'à 1 (pair -> n//2, impair -> 3n+1)."""
    pass  # TODO etudiant (Etape 1)


def omega_petit(v):
    """Nombre de diviseurs premiers distincts de v (v < 10**4 : essai de division suffit)."""
    pass  # TODO etudiant (Etape 2)


print("Exercice a completer : trajectoire de n=27, compter les pas croissants de n, bit_length(n), omega(n).")
print("Attendu : trois comptes > 0 -- aucun candidat n'est un tau strict ; la terminaison reste ouverte.")
Exercice a completer : trajectoire de n=27, compter les pas croissants de n, bit_length(n), omega(n).
Attendu : trois comptes > 0 -- aucun candidat n'est un tau strict ; la terminaison reste ouverte.

3.3 Test de limite — quand τ ne transporte plus

On porte τ sur SAT, où aucun ordre n’est donné d’avance. On construit une formule à 3 variables, on descend l’affectation x₁, x₂, x₃, et on mesure la trajectoire de deux τ naturelles au lieu de supposer ce qu’elles font. Puis on regarde 2-SAT, où le problème, lui, fournit l’ordre.

# 3.3 — Test de limite : porter τ sur SAT, où aucun ordre n'est donné d'avance.
# On fixe x₁, x₂, x₃ dans cet ordre et on regarde ce que chaque candidat τ
# fait RÉELLEMENT le long de la descente — au lieu de le supposer.
F = [(1, 2, 3), (-1, 2, -3), (1, -2, 3), (-1, -2, -3), (1, 2, -3)]

def clauses_non_satisfaites(clauses, assign):
    """Clauses qu'aucun littéral déjà affecté ne rend vraie."""
    return sum(
        1 for c in clauses
        if not any(abs(l) in assign and assign[abs(l)] == (l > 0) for l in c)
    )

descente = [{}, {1: True}, {1: True, 2: True}, {1: True, 2: True, 3: True}]

tau_A = [clauses_non_satisfaites(F, a) for a in descente]
strict_A = [tau_A[i] < tau_A[i - 1] for i in range(1, len(tau_A))]
print("candidat A : τ = clauses non encore satisfaites")
print(f"  trajectoire       : {tau_A}")
print(f"  décroît strictement ? {strict_A}  →  {all(strict_A)}")
print("  Le dernier pas est PLAT : τ_A décroît, mais pas strictement.")
print("  Elle ne peut donc pas fonder la terminaison.")

tau_B = [3 - len(a) for a in descente]
strict_B = [tau_B[i] < tau_B[i - 1] for i in range(1, len(tau_B))]
print("\ncandidat B : τ = variables non encore affectées")
print(f"  trajectoire       : {tau_B}")
print(f"  décroît strictement ? {strict_B}  →  {all(strict_B)}")
print("  Elle décroît strictement — mais QUOI QU'IL ARRIVE : elle ne lit jamais F.")
print("  Elle prouve que l'énumération s'arrête, pas que la formule est décidée.")

# Sur 2-SAT, en revanche, le problème FOURNIT l'ordre : les composantes
# fortement connexes du graphe d'implication, et leur condensation est un DAG.
def composantes_fortes(sommets, succ):
    """Kosaraju — composantes fortement connexes, sans dépendance externe."""
    vus, ordre = set(), []
    for depart in sommets:
        if depart in vus:
            continue
        vus.add(depart)
        pile = [(depart, iter(succ.get(depart, ())))]
        while pile:
            _, it = pile[-1]
            for v in it:
                if v not in vus:
                    vus.add(v)
                    pile.append((v, iter(succ.get(v, ()))))
                    break
            else:
                ordre.append(pile.pop()[0])
    pred = {}
    for a, voisins in succ.items():
        for b in voisins:
            pred.setdefault(b, []).append(a)
    comp, vus2, numero = {}, set(), 0
    for depart in reversed(ordre):
        if depart in vus2:
            continue
        vus2.add(depart)
        pile, bloc = [depart], []
        while pile:
            n = pile.pop()
            bloc.append(n)
            for m in pred.get(n, ()):
                if m not in vus2:
                    vus2.add(m)
                    pile.append(m)
        for n in bloc:
            comp[n] = numero          # un seul numéro pour TOUT le bloc
        numero += 1
    return comp

def deux_sat(clauses, nvars):
    """(a ∨ b) ≡ (¬a → b) ∧ (¬b → a) ; satisfiable ssi jamais v et ¬v ensemble."""
    succ = {}
    for a, b in clauses:
        succ.setdefault(-a, []).append(b)
        succ.setdefault(-b, []).append(a)
    sommets = [s for v in range(1, nvars + 1) for s in (v, -v)]
    comp = composantes_fortes(sommets, succ)
    ok = all(comp.get(v) != comp.get(-v) for v in range(1, nvars + 1))
    return ok, len(set(comp.values()))

print("\n2-SAT : le problème fournit l'ordre (condensation du graphe d'implication)")
for libelle, clauses, nvars in [
    ("(x₁ ∨ ¬x₂) ∧ (¬x₁ ∨ x₂) ∧ (x₁ ∨ x₂)", [(1, -2), (-1, 2), (1, 2)], 2),
    ("(x₁ ∨ x₁) ∧ (¬x₁ ∨ ¬x₁)",             [(1, 1), (-1, -1)],          1),
]:
    ok, n = deux_sat(clauses, nvars)
    print(f"  {libelle:<38} → {n} composante(s), satisfiable = {ok}")

print("\n  τ n'est pas une formule qu'on transporte : c'est un patron qu'on")
print("  CONSTRUIT à partir d'une structure que le problème doit fournir.")
print("  2-SAT la fournit, et la décision suit. SAT général ne la fournit pas,")
print("  et les deux τ naturelles ci-dessus échouent chacune à leur façon.")
candidat A : τ = clauses non encore satisfaites
  trajectoire       : [5, 2, 1, 1]
  décroît strictement ? [True, True, False]  →  False
  Le dernier pas est PLAT : τ_A décroît, mais pas strictement.
  Elle ne peut donc pas fonder la terminaison.

candidat B : τ = variables non encore affectées
  trajectoire       : [3, 2, 1, 0]
  décroît strictement ? [True, True, True]  →  True
  Elle décroît strictement — mais QUOI QU'IL ARRIVE : elle ne lit jamais F.
  Elle prouve que l'énumération s'arrête, pas que la formule est décidée.

2-SAT : le problème fournit l'ordre (condensation du graphe d'implication)
  (x₁ ∨ ¬x₂) ∧ (¬x₁ ∨ x₂) ∧ (x₁ ∨ x₂)    → 2 composante(s), satisfiable = True
  (x₁ ∨ x₁) ∧ (¬x₁ ∨ ¬x₁)                → 1 composante(s), satisfiable = False

  τ n'est pas une formule qu'on transporte : c'est un patron qu'on
  CONSTRUIT à partir d'une structure que le problème doit fournir.
  2-SAT la fournit, et la décision suit. SAT général ne la fournit pas,
  et les deux τ naturelles ci-dessus échouent chacune à leur façon.

4. Friction naturelle — ce qui a résisté pendant la digestion

La digestion des trois primitives n’a pas été linéaire. Trois obstacles ont rythmé le travail, et les nommer ici est le contenu réel de cette section — pas un préambule.

4.1 Pourquoi Ruzsa a résisté au test (a)

La cellule [4] ne mesure pas Ruzsa « sur son terrain ». Elle le mesure sur le même ensemble {0, 1, 2, 3}, avec les mêmes distributions {0: 0.5, 1: 0.5}, sous deux lois de groupe différentes — ℤ/4ℤ cyclique et (ℤ/2ℤ)² Klein. La difficulté rencontrée : d change de valeur quand la loi change, alors que distributions et ensemble sont fixés. Ce n’est pas un artefact numérique ; c’est une propriété de Ruzsa, et l’obstacle pédagogique est précisément d’exhiber cette dépendance sans tomber dans le piège inverse (« la valeur de d est invariante par changement de groupe »). Le testeur novice qui verrait d=0 pour ℤ/4ℤ et d=0 pour Klein conclurait à l’invariance — c’est exactement l’erreur que la cellule défait.

L’obstacle rencontré en digestion a été de garder la cellule exécutable malgré cette nuance : il a fallu calculer H(X − Y) sous les deux lois pour rendre la dépendance visible. Une version antérieure se contentait d’affirmer l’invariance en prose, et tombait dans le piège inverse. La leçon : sur Ruzsa, le test de limite n’a de valeur que mesuré, pas annoncé.

4.2 Pourquoi la décomposition projection/fibres cède sur le texte brut

La cellule [8] ne mesure pas la décomposition sur un vrai couple (X, Y) probabilisé. Elle tente la décomposition sur un texte brut où Y n’a pas de statut probabiliste. La difficulté rencontrée : H(text) après normalisation a un sens, mais H(X | Y) n’en a pas — il n’y a pas de σ-algèbre canonique sur « mots ». L’obstacle pédagogique est de reconnaître cette absence sans conclure que la décomposition est fausse : elle est incomplète, pas invalide. Construire une mesure rendrait la décomposition légitime, mais ce n’est pas le propos de la cellule.

L’obstacle rencontré en digestion a été de ne pas régresser : la tentation est grande, voyant H(text) fini, d’inventer un Y (« premier mot de la phrase » ?) qui rendrait la cellule présentable. La leçon : sur la décomposition projection/fibres, la limite est une absence de mesure, et il faut la nommer comme telle — pas la maquiller en pseudo-mesure pour faire passer l’exécution.

4.3 Pourquoi τ a cédé sur SAT, doublement

La cellule [12] ne mesure pas τ « en action ». Elle mesure deux candidats τ sur SAT 3-SAT puis 2-SAT. La difficulté rencontrée : aucune des deux τ ne fait ce qu’on espère naïvement. La première (τ = clauses non encore satisfaites) descend mais pas strictement — trajectoire [5, 4, 1, 1], dernier pas plat. La seconde (τ = variables non encore affectées) descend strictement mais ne lit jamais la formule — elle prouve la terminaison de l’énumération, pas la décidabilité de la formule. Laquelle des deux « échoue » ? Les deux, mais chacune à sa manière. Le test naïf « τ décroît ⇒ ça marche » est invalide : il faut distinguer lis (la fonctionnelle lit-elle la structure du problème ?) et décroît (descend-elle bien ?).

L’obstacle rencontré en digestion a été de ne pas tomber dans le sophisme : il est tentant de conclure « τ_B marche, c’est la bonne ». C’est faux : τ_B prouve une chose différente de ce qu’on cherche. La leçon : sur τ, le test de limite doit distinguer terminaison d’énumération et décidabilité du problème — ces deux notions se confondent souvent dans la prose, jamais dans une mesure.

5. Chemin de découverte — ce qui distingue une digestion d’une reconstruction

5.1 Pourquoi aller vers ANALYSE-04, pas rester sur ANALYSE-03

ANALYSE-03 contient la preuve formelle PFR dans le lac pfr (https://github.com/teorth/pfr). L’investigation qui a mené à ANALYSE-04 est partie d’un constat : la preuve est instructive en acte, mais pas en patron. Le pattern « entropie ⇒ structure algébrique » y est utilisé mais pas extrait. Aller vers ANALYSE-04 a consisté à isoler trois primitives candidates dans la preuve — Ruzsa, projection/fibres, τ — et à mesurer leur transportabilité hors de leur terrain. Sans cette mesure, ANALYSE-04 n’aurait pas de contenu propre : il aurait été une relecture d’ANALYSE-03.

5.2 Essais ratés utiles

Trois essais ratés ont structuré le notebook :

  • L’essai « appliquons Ruzsa à un dataset ML » (notes d’étudiants) a échoué parce que les notes ne sont pas des variables aléatoires sur un groupe additif. Cet échec a forcé à déplacer le test de limite de Ruzsa vers l’algébrique (ℤ/4ℤ vs Klein) plutôt que vers l’applicatif, où la limite est invisible. La cellule [4] est le résidu utile de cet échec.

  • L’essai « mesurons τ sur factorisation » a échoué parce que la factorisation termine trivialement (le dernier pas de ω est toujours plat quand N est premier). Cet échec a forcé à chercher un problème où τ n’a pas d’ordre naturel — SAT est ce candidat. La cellule [12] est le résidu utile.

  • L’essai « inversons la lecture : lisons la conclusion d’abord » (trois primitives, ranger par transportabilité) a échoué parce que la lecture de la preuve formelle ne donne pas l’inventaire des primitives : elle les utilise sans les nommer. Cet échec a forcé à repartir d’ANALYSE-03, cellule par cellule pour extraire les primitives — l’ordre du notebook est celui de la lecture, pas celui de la conclusion.

5.3 Dette résiduelle

Le notebook laisse trois angles non clos, nommés pour les tranches suivantes (pas pour la prochaine itération) :

  • PFR polynomial généralisé : la preuve actuelle est bornée au cas F₂ⁿ. L’extension à Z/nZ avec n non premier est ouverte (cf. Gowers 2016 systematisation). Cette dette est documentée, pas un TODO technique : elle est hors du périmètre « primitives transportables ».

  • Décomposition projection/fibres sur ICT : la primitive 2 est notée comme « probablement la plus pertinente » pour ICT (information mutuelle I(X;Y) analogue à entropie conditionnelle), mais la mesure canonique n’est pas construite. Cette construction appartient à un autre grain.

  • τ sur problèmes non-combinatoires : la primitive 3 a été testée sur SAT et 2-SAT, tous deux combinatoires. Son transport à un problème continu (par exemple, terminaison d’un schéma itératif continu) n’est pas mesurée. Hors périmètre de cette digestion.

5.4 Transmission

Le notebook est conçu pour un lecteur ayant déjà vu ANALYSE-03. La section 5 (friction) doit être lue après les tests de limite, pas avant : la friction n’a de sens qu’une fois la mesure exhibée. La section 6.2 (essais ratés) éclaire pourquoi le notebook est ordonné comme il l’est — ce n’est pas un plan marketing, c’est l’ordre dans lequel la digestion a effectivement résisté.

Verdict Primitive 3

Transportable sous condition : τ_strictly_decreases exige un ordre bien fondé sous-jacent (typiquement ℕ ou un ordinal). La cellule précédente montre que le porter sur SAT échoue de deux façons distinctes : la τ qui lit la formule ne décroît pas strictement (trajectoire plate au dernier pas), et celle qui décroît strictement ne lit jamais la formule — elle prouve que l’énumération s’arrête, pas que le problème est décidé. Sur 2-SAT en revanche la structure existe (condensation du graphe d’implication) et la décision suit.

Subtilité : τ est un patron, pas une formule figée. Pour chaque problème, on doit construire la τ appropriée — et c’est souvent l’étape créative de la preuve. Dans PFR, τ est construit à partir de l’entropie ; dans ANALYSE-03, cellule 6, c’est l’invariant qui garantit que le blueprint termine.

Leçon : la primitive est la moins transportable des trois parce qu’elle dépend de la structure du problème. Mais quand elle s’applique, elle est irremplaçable — c’est elle qui dit « quand la procédure a fini ».


6. Conclusion — où les primitives cessent de valoir

Tableau récapitulatif

# Primitive Transport Condition Sans la condition
1 d_ruzsa[X;Y] conditionnel groupe additif (G, +) d devient négatif : ce n’est plus une distance
2 X = π(X) + (X \| π(X)) large distribution de probabilité décomposition vide (projection non définie)
3 τ_strictly_decreases patron ordre bien-fondé terminaison non garantie

Le test de limite était le livrable : chaque primitive a été appliquée hors de son terrain et le notebook exhibe — par une cellule qui s’exécute, pas par un paragraphe qui l’annonce — l’endroit exact où elle cesse de valoir. C’est cette honnêteté qui distingue une « digestion » d’une « analogie ».

Lien vers ANALYSE-03 et le lac pfr

ANALYSE-03 contient les preuves formelles dans le lac pfr (https://github.com/teorth/pfr). ANALYSE-04 ne les reproduit pas — il extrait les patrons et vérifie leur portée. Les trois primitives ci-dessus sont les briques élémentaires que la preuve PFR utilise pour passer de la compression à la structure algébrique.

Vers ICT (sans mapping affine)

PFR ne « prouve » rien pour ICT. Mais il fournit un exemple certifié où une quantité informationnelle force une structure algébrique. C’est un patron, pas une analogie : quand on dispose d’une « monnaie commune » (entropie) entre deux théories, et qu’on voit la même structure forcée par les mêmes contraintes, on a une primitive transportable, pas une ressemblance.

Pour ICT, la primitive la plus pertinente est probablement la 2 (décomposition projection/fibres) : l’information mutuelle I(X;Y) y joue un rôle analogue à l’entropie conditionnelle. Mais on doit construire explicitement la mesure, pas plaquer la formule.

Honnêteté finale

Les trois primitives sont des outils, pas des lois. Leur transportabilité est conditionnelle et le test de limite fait partie du livrable. Le carnet ANALYSE-04 a nommé où chacune cesse de valoir — c’est le contenu pédagogique réel.

See #12214 — ce notebook est le livrable du grain.

Retour au sommet