Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions MyIA.AI.Notebooks/IIT/ICT-Series/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ La **strate 5 commence ici** : le substrat mesuré n'est plus un système dynami
| [ICT-22-LLMSubstrat-Python](ICT-22-LLMSubstrat-Python.ipynb) | LLM comme quatrième substrat du banc cross-substrat — on passe des **stratégies** d'Axelrod (ICT-13) aux **représentations internes** (features SAE d'ICT-21) qui les motivent, puis à la jambe argumentation (#7289) qui les met en confrontation explicite. *(GPU)* | [#5102](https://github.com/jsboige/CoursIA/issues/5102) |
| [ICT-22b-CausalInterventionEngine-Python](ICT-22b-CausalInterventionEngine-Python.ipynb) | **Moteur d'intervention causal — opérer, contrôler, mesurer** *(accrétion du palier 22 — LLMSubstrat, moteur transversal [#15479](https://github.com/jsboige/CoursIA/issues/15479))* : les cinq opérations v1 (`ablate`, `clamp`, `patch`, `steer`, `interchange`) sous un **contrat commun** sur un panneau d'activation, avec les **contrôles appariés obligatoires** (cible aléatoire appariée en norme *et* fréquence, sham write-back, doses symétriques), la **séparation des trois canaux** état / readout / comportement en slots jamais sommés, et le format **Gate 24** ([#5635](https://github.com/jsboige/CoursIA/issues/5635)) à trois bras — cible / aléatoire apparié / intact. Le banc est synthétique et sa causalité est connue **par construction** ; le verdict est **falsifiable et fixé avant la lecture** (§8 : test binomial unilatéral, correction de Holm sur 4 seeds — `SUPPORTED` dans les outputs committés), et `selectivity_verdict` **refuse** de qualifier de sélectif un collapse global (double condition : ratio ≥ 5 *et* `off_target` ≤ 0.10). Cœur numpy `ict/causal_engine.py` (tranche 1), confinement torch dans `scripts/causal_hooks.py` (tranche 2) | [#15479](https://github.com/jsboige/CoursIA/issues/15479) |
| [ICT-23-PersonaCatastrophe-Python](ICT-23-PersonaCatastrophe-Python.ipynb) | Désalignement émergent par fronce, énergie libre et MDL (capstone) | [#5104](https://github.com/jsboige/CoursIA/issues/5104) |
| [ICT-23-WolframPrograms-ThermoSignature-Python](ICT-23-WolframPrograms-ThermoSignature-Python.ipynb) | **Programmes simples & signature thermodynamique — automate 1-D de Wolfram** *(accrétion du palier 23 — PersonaCatastrophe, pli 2 Origami #19766)* : `ict.wolfram_step` (extrait d'ICT-18 EG3) en organe de l'automate 1-D canonique, discrimination des 4 classes (I uniforme, II périodique, III chaotique, IV complexe) par densité asymptotique et signature thermodynamique (`σ`, `δ_db`) ; proxy-gliders diagonal pour la discrimination spatiale III/IV *(GPU-free)* | [#19766](https://github.com/jsboige/CoursIA/issues/19766) |
| [ICT-24-WorkspaceIgnition-Python](ICT-24-WorkspaceIgnition-Python.ipynb) | WorkspaceIgnition — l'axe *Global Workspace* (Dehaene, Baars) comme cinquième jambe : module `ict/workspace.py` (série de concentration Gini, événements d'ignition persistants, influence retardée, profil de fan-out, candidats workspace, batterie event-triggered réutilisant `synthesis.emergence_gain`). **Gates 22-23 GPU-free livrés (#5875)** : verdict dissociation (contraste synthétique +0.44, pics emergence_gain et ignitions workspace ne co-localisent pas sur S4) ; Gate 24 (clamp sélectif) en phase 2 GPU *(GPU-free)* | [#5635](https://github.com/jsboige/CoursIA/issues/5635) |
| [ICT-SAE-JLens-TeteATete](ICT-SAE-JLens-TeteATete.ipynb) | **Tête-à-tête SAE↔J-space** (couche 16, 9B-Base) — confronte les deux lentilles du workspace global (SAE Qwen-Scope W64K-L0_50 vs jacobien J-Lens neuronpedia) sur le même modèle et les mêmes prompts, banc numpy-only lisant les 4 fixtures pré-extraites : features différentielles (K_DIFF=64), séries de concentration, matrices de séparation inter-jeux, dégradation top-k *(GPU-free, #5681 Track S)* | [#5681](https://github.com/jsboige/CoursIA/issues/5681) |
| [ICT-21b-SAECalibration-Python](ICT-21b-SAECalibration-Python.ipynb) | **Calibration des SAE par echelle** — verite-terrain de reconstruction (FVU corpus, L0 mesure, features mortes) des SAE officiels Qwen-Scope W32K-L0_50 a profondeur appairee (frac 0.5) sur la batterie d'ICT-21 : 31,4 % (1.7B) vs 71,5 % (2B) de variance expliquee (couverture inversee selon l'echelle), L0 = 50.0 exact (garde release passee), largeur effective ~3 % — prealable Phase 0 a toute comparaison cross-echelle de forme *(GPU-free, traces commitees)* | [#8236](https://github.com/jsboige/CoursIA/issues/8236) |
Expand Down
368 changes: 368 additions & 0 deletions MyIA.AI.Notebooks/IIT/ICT-Series/ict/wolfram_step.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,368 @@
"""Automate cellulaire 1-D de Wolfram -- organe extracté d'ICT-18 (pli 2 EPIC #19766).

Outille la brique « programmes simples » du pli 2 de l'EPIC Origami Wolfram
(#19766). La regle 30 (chaotique, classe III), la regle 110 (complexe, classe
IV) et la regle 0 (triviale, classe I) sont les trois exemples canoniques que
Wolfram 2002 (« A New Kind of Science ») utilise pour definir les 4 classes
d'automates cellulaires 1-D :

* **Classe I** : homogenéité -- la trajectoire converge vers un point fixe
uniforme (ex. regle 0, 32, 160, 232). Densité constante, périodicité 1.
* **Classe II** : cycles simples -- la trajectoire converge vers un cycle
periodique stable de courte periode (ex. regle 4, 8, 13). Densité oscille
entre quelques valeurs.
* **Classe III** : chaotique -- la trajectoire est imprevisible, densité
proche de 0.5, pas de structure persistante (ex. regle 30, 45, 75, 126).
* **Classe IV** : complexe -- la trajectoire porte une structure persistante
(gliders, particules), densité oscille mais reste caracteristique (ex.
regle 110). C'est la classe computationnellement la plus riche.

**Source primaire** : Wolfram, S. (2002) « A New Kind of Science »,
Wolfram Media, chapitres 2-3. L'implémentation ``wolfram_step`` reproduit la
definition standard : voisinage 3-cellules (gauche, centre, droite), regle
sur 8 bits (256 regles au total), bords periodiques (modulo n_cells).

**Provenance dans le depot** : extraite d'ICT-18
(``ICT-18-ArrowOfTimeReversibilization-Python.ipynb``) « Exemple guide 3 »
au cours du cycle c.107, pli 2 de l'EPIC Origami Wolfram. L'exemple guide y
demontre la signature thermodynamique (production d'entropie, detailed
balance) sur les regles 0, 30, 110, 250. Cette extraction transforme
l'exemple inline en un module reutilisable, premier pas du pli 2 carnet
(ICT-19 ou ICT-18b selon le curriculum).

**Cadrage epistemologique** : Wolfram soutient que la regle 110 est
*intrinséquement* computationnellement irreducible -- predire son etat a
l'horizon T demande une simulation T pas. Ce module ne *verifie* pas la these
(elle est empirique et conceptuelle, pas formelle) ; il fournit l'instrument
de mesure. La verification se fait au carnet, par confrontation a
``time_arrow.compare_real_reversed_reversibilized`` (cf. ICT-18).

Usage :

>>> from ict.wolfram_step import wolfram_step, wolfram_trajectory, classify_rule
>>> traj = wolfram_trajectory(rule=30, n_cells=32, n_steps=300, seed=33)
>>> classify_rule(30, n_cells=64, n_steps=500)
{'rule': 30, 'class_label': 'III (chaotic)', 'asymptotic_density': 0.498, ...}
"""

from __future__ import annotations

from typing import Iterable, List, Optional

import numpy as np

__all__ = [
"WOLFRAM_CLASSES",
"CANONICAL_RULES",
"wolfram_step",
"wolfram_trajectory",
"wolfram_density",
"wolfram_class_indicators",
"classify_rule",
]


# ---------------------------------------------------------------------------
# Index de la table des 256 regles de Wolfram.
#
# Wolfram 2002 utilise une convention d'indexation lexicographique sur le
# voisinage (left << 2) | (center << 1) | right : la regle est un entier sur
# 8 bits dont chaque bit est la valeur de sortie pour le voisinage
# correspondant. La regle 30 a pour representation binaire 00011110
# (bit 0 = voisinage 000, bit 1 = voisinage 001, ..., bit 7 = voisinage 111),
# d'ou le nom canonique "regle 30" pour la regle aux sorties
# 0,1,1,1,1,0,0,0 (0, 1, 1, 1, 1, 0, 0, 0 dans l'ordre 7,6,5,4,3,2,1,0).
# ---------------------------------------------------------------------------

WOLFRAM_CLASSES = {
# Classe I : converge vers un point fixe (ex. tout blanc ou tout noir).
0: "I (uniform)",
32: "I (uniform)",
160: "I (uniform)",
232: "I (uniform)",
# Classe II : converge vers un cycle periodique simple.
4: "II (periodic)",
8: "II (periodic)",
13: "II (periodic)",
# Classe III : chaotique, densite proche de 0.5.
30: "III (chaotic)",
45: "III (chaotic)",
75: "III (chaotic)",
126: "III (chaotic)",
# Classe IV : complexe, structure persistante (gliders).
110: "IV (complex)",
}


# Regles canoniques (les 3 exemples d'ICT-18 + une regle complexe).
CANONICAL_RULES = (0, 30, 110, 250)


def wolfram_step(state: np.ndarray, rule: int) -> np.ndarray:
"""Un pas de l'automate cellulaire 1-D de Wolfram, bords periodiques.

Parametres
----------
state : np.ndarray
Vecteur d'etats 0/1 de longueur n_cells. La convention suit
Wolfram 2002 (representation binaire) et ICT-18.
rule : int
Regle sur 8 bits (0 a 255). Le bit d'index ``(left << 2) |
(center << 1) | right`` definit la sortie du voisinage
correspondant.

Retour
------
np.ndarray
Vecteur d'etats apres un pas, meme longueur que ``state``.

Exemple
-------
>>> import numpy as np
>>> state = np.array([0, 0, 0, 1, 0, 0, 0], dtype=int)
>>> wolfram_step(state, 30).tolist()
[0, 0, 1, 1, 1, 0, 0]
"""
state = np.asarray(state, dtype=int)
n = len(state)
if n == 0:
return state.copy()
# Voisinage circulaire : decalage gauche/droite/centre, calcul d'index 3 bits.
left = np.roll(state, 1)
center = state
right = np.roll(state, -1)
idx = (left << 2) | (center << 1) | right
# Table de verite : le bit p de ``rule`` donne la sortie pour le voisinage p.
out = ((rule >> idx) & 1).astype(int)
return out


def wolfram_trajectory(
rule: int,
n_cells: int = 32,
n_steps: int = 300,
seed: int = 33,
record_densities: bool = True,
) -> List[int]:
"""Trajectoire d'un automate de Wolfram ; historique de densites (nb de 1).

Parametres
----------
rule : int
Regle sur 8 bits (0 a 255).
n_cells : int
Taille de la grille (par defaut 32, adapte a ICT-18).
n_steps : int
Nombre de pas a simuler (par defaut 300, adapte a ICT-18).
seed : int
Graine aleatoire de l'etat initial (par defaut 33, adapte a ICT-18).
record_densities : bool
Si ``True``, retourne la sequence de densites (nb de 1) a chaque pas.
Si ``False``, retourne les etats complets (matrices).

Retour
------
List[int] (ou List[np.ndarray])
Historique de la trajectoire.

Notes
-----
La densite (nb de 1) est l'observable privilegiee dans ICT-18
Exemple guide 3 -- elle donne une signature thermodynamique robuste
a la fois sur les regimes homogenes (densite constante), periodiques
(oscillation entre quelques valeurs), chaotiques (densite proche de 0.5)
et complexes (densite caracteristique sans cycle evident).
"""
rng = np.random.default_rng(seed)
state = rng.integers(0, 2, size=n_cells)
history: List[int] = []
states: List[np.ndarray] = []
for _ in range(n_steps):
if record_densities:
history.append(int(state.sum()))
else:
states.append(state.copy())
state = wolfram_step(state, rule)
return history if record_densities else states


def wolfram_density(state: np.ndarray) -> float:
"""Densite d'un etat : proportion de cellules a 1.

Parametres
----------
state : np.ndarray
Vecteur 0/1.

Retour
------
float
Densite dans [0, 1].
"""
state = np.asarray(state, dtype=int)
if len(state) == 0:
return 0.0
return float(state.mean())


def wolfram_class_indicators(
densities: Iterable[float],
tail: int = 100,
n_cells: int = 1,
) -> dict:
"""Indicateurs des 4 classes de Wolfram a partir d'une trajectoire de densites.

Renvoie un dict avec :

* ``asymptotic_density`` : moyenne sur les ``tail`` derniers pas,
normalisee dans [0, 1] par ``n_cells``.
* ``asymptotic_std`` : ecart-type sur les ``tail`` derniers pas
(normalise lui aussi).
* ``unique_densities_tail`` : nombre de densites distinctes en queue.
* ``period_estimate`` : plus petit pas p tel que la sequence de
densites est periodique de periode p (renvoie 1 si constante,
0 si pas de periode detectee sur la queue).
* ``class_heuristic`` : etiquette sommaire heuristique parmi
``{'I-uniform', 'II-periodic', 'III-chaotic', 'IV-complex',
'undecided'}``.

Parametres
----------
densities : iterable de float
Historique des densites brutes (nb de cellules a 1 par pas).
Sortie de `` wolfram_trajectory(..., record_densities=True)``.
tail : int
Fenetre d'analyse en queue de trajectoire (par defaut 100).
n_cells : int
Taille de la grille (par defaut 1 : pas de normalisation, les
densites sont prises telles quelles). En pratique, passer la
valeur utilisee pour `` wolfram_trajectory`` pour obtenir des
indicateurs normalises.
"""
densities = np.asarray(list(densities), dtype=float)
if len(densities) < tail:
tail = len(densities)
if tail == 0:
return {
"asymptotic_density": 0.0,
"asymptotic_std": 0.0,
"unique_densities_tail": 0,
"period_estimate": 0,
"class_heuristic": "undecided",
}
tail_d = densities[-tail:]
# Normalisation : densites par cellule, dans [0, 1].
if n_cells > 1:
tail_d = tail_d / n_cells
asym_d = float(tail_d.mean())
asym_std = float(tail_d.std())
unique = int(len(np.unique(np.round(tail_d, 6))))
# Detection de periodicite : plus petit p >= 1 tel que la sequence est
# periodique de periode p sur la queue.
period = 0
max_p = min(20, tail // 2)
for p in range(1, max_p + 1):
# Comparaison par blocs successifs de taille p.
n_blocks = tail // p
if n_blocks < 2:
continue
ok_periodic = True
ref = tail_d[:p]
for k in range(1, n_blocks):
if not np.allclose(tail_d[k * p:(k + 1) * p], ref, atol=1e-9):
ok_periodic = False
break
if ok_periodic:
period = p
break
# Heuristique 4 classes (empirique, non theoremique).
# Seuils sur la densite normalisee. La discrimination fine III (chaotique)
# vs IV (complexe) necessite l'analyse des motifs spatiaux (gliders) -- hors
# du scope de ce module, qui ne mesure que la trajectoire de densite. On
# utilise donc l'etiquette "III-or-IV" pour les regimes a forte variance
# non periodique ; la discrimination fine se fait dans le carnet par
# capture du pattern spatial.
if asym_std < 1e-3:
heuristic = "I-uniform"
elif period > 0 and period <= 8:
heuristic = "II-periodic"
elif asym_std > 0.03:
heuristic = "III-or-IV"
else:
heuristic = "undecided"
return {
"asymptotic_density": asym_d,
"asymptotic_std": asym_std,
"unique_densities_tail": unique,
"period_estimate": period,
"class_heuristic": heuristic,
}


def classify_rule(
rule: int,
n_cells: int = 64,
n_steps: int = 500,
seed: int = 33,
tail: int = 100,
) -> dict:
"""Classification heuristique d'une regle par sa trajectoire de densite.

Combine l'indexation de Wolframe (connue pour les regles canoniques)
et les indicateurs mesures empiriquement (``wolfram_class_indicators``).
Renvoie un dict avec :

* ``rule`` : la regle demandee.
* ``known_class`` : etiquette de Wolfram si la regle est dans
``WOLFRAM_CLASSES``, sinon ``None``.
* ``indicators`` : resultat de ``wolfram_class_indicators``.
* ``class_label`` : heuristique finale, avec mention si l'heuristique
et l'indexation Wolframe divergent.

Parametres
----------
rule : int
Regle de Wolfram (0 a 255).
n_cells : int
Taille de la grille (par defaut 64, plus long que ICT-18 pour
ameliorer la stabilite des indicateurs).
n_steps : int
Pas de simulation (par defaut 500, plus long que ICT-18).
seed : int
Graine aleatoire (par defaut 33).
tail : int
Fenetre d'analyse en queue (par defaut 100).
"""
densities = wolfram_trajectory(
rule=rule, n_cells=n_cells, n_steps=n_steps, seed=seed, record_densities=True
)
indicators = wolfram_class_indicators(densities, tail=tail, n_cells=n_cells)
known = WOLFRAM_CLASSES.get(rule)
heuristic = indicators["class_heuristic"]
# Heuristique -> etiquette courte (I/II/III-or-IV/undecided).
# La discrimination III vs IV necessite l'analyse spatiale (gliders),
# qui n'est pas dans le scope de la mesure de densite seule.
if heuristic.startswith("I-"):
final = "I"
elif heuristic.startswith("II-"):
final = "II"
elif heuristic.startswith("III-or-IV"):
# Si la regle est indexee, on garde la discrimination connue.
final = None # a fixer ci-dessous si known
else:
final = "undecided"
if known is not None:
known_short = known.split(" ")[0]
measured_short = "III-or-IV" if final is None else final
if known_short == measured_short or known_short in ("III", "IV") and measured_short == "III-or-IV":
label = f"{known_short} (known+measured)"
else:
label = f"{known_short} (known) | {measured_short} (measured)"
else:
label = f"{final or 'III-or-IV'} (measured only)"
return {
"rule": rule,
"known_class": known,
"indicators": indicators,
"class_label": label,
}
Loading