Skip to content
Merged
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
760 changes: 519 additions & 241 deletions MyIA.AI.Notebooks/IIT/ICT-Series/ICT-37-FLens-BeliefState.ipynb

Large diffs are not rendered by default.

434 changes: 322 additions & 112 deletions MyIA.AI.Notebooks/IIT/ICT-Series/ICT-40a-TriangulationCausale.ipynb

Large diffs are not rendered by default.

129 changes: 109 additions & 20 deletions MyIA.AI.Notebooks/IIT/ICT-Series/ict/bench_factorise.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,14 +119,6 @@ def beliefs(self, obs: Array) -> Array:
return out


# Alias historique : l'ancien nom ``Mess3`` est preserve pour ne pas casser
# les imports existants, mais il designe maintenant le banc DEPRECIE
# (gaussien, observation couplee). Le nouveau banc canonique s'appelle
# ``Mess3`` aussi mais precede l'ancien dans ce fichier ; voir :class:`Mess3`
# ci-dessous.
Mess3 = Mess3_ObsCoupled # noqa: F811 — alias de compatibilite, voir NOTE ci-dessus


@dataclass(frozen=True)
class Mess3Canonical:
"""Mess3 canonique (Marzen & Crutchfield 2017) : POMDP a 3 etats
Expand Down Expand Up @@ -231,18 +223,119 @@ def beliefs(self, obs: Array) -> Array:
return out


# Alias canonique (#16225) : ``Mess3`` designe le generateur CONFORME a la
# litterature -- alphabet discret ternaire qui ne revele pas l'etat cache.
# Le banc gaussien historique reste disponible sous son nom explicite
# ``Mess3_ObsCoupled`` pour les comparaisons de probe sur signaux continus.
Mess3 = Mess3Canonical # noqa: F811 — un seul generateur Mess3 par defaut


@dataclass(frozen=True)
class RRXOR:
"""XOR recursif : bits iid ``b_t``, observation ``y_t = b_{t-1} XOR b_t``.
"""RRXOR (Riechers & Crutchfield 2018, arXiv:1706.00883v1, Fig. 4).

Le processus repete trois etapes : (i) un 0 ou 1 equiprobable ``r1``,
(ii) un autre 0 ou 1 equiprobable ``r2``, (iii) le XOR des deux derniers
symboles ``r1 XOR r2``. Correlations par paires nulles, spectre plat :
toute la structure vit dans la contrainte de triplet.

L'epsilon-machine compte **5 etats causaux** et est **Mealy** : les
emissions vivent sur les aretes, pas dans les etats. Etats ordonnes :

- ``0`` = G (phase de reset, va emettre ``r1``),
- ``1`` = A0, ``2`` = A1 (memorise ``r1``),
- ``3`` = X0, ``4`` = X1 (memorise ``r1 XOR r2``, va emettre le XOR).

Aretes : ``G -(r1, 1/2)-> A_{r1}`` ; ``A_{r1} -(r2, 1/2)-> X_{r1 XOR r2}`` ;
``X_v -(v, 1)-> G``. La MSP depuis le prior stationnaire compte 36
croyances distinctes (31 transitoires + 5 recurrentes, cf. p. 17 de
l'article) : le regime transitoire resout l'ambiguite de phase du
processus periodise d'ordre 3.

Note : la version anterieure de cette classe modelisait
``y_t = b_{t-1} XOR b_t`` sur bits iid -- un processus **iid** (les XOR
adjacents de bits iid sont independants), sans aucune structure. Le banc
ne meritait pas son nom ; cette version est conforme a la litterature.
"""

n_states: int = 5
name: str = "rrxor"

def edge_tensor(self) -> Array:
"""Tenseur W[s, s', y] = P(transiter s -> s' en emettant y) (Mealy)."""
w = np.zeros((5, 5, 2))
w[0, 1, 0] = 0.5; w[0, 2, 1] = 0.5 # G -> A_{r1}
w[1, 3, 0] = 0.5; w[1, 4, 1] = 0.5 # A0 -> X_{0 XOR r2}
w[2, 4, 0] = 0.5; w[2, 3, 1] = 0.5 # A1 -> X_{1 XOR r2}
w[3, 0, 0] = 1.0 # X0 emet 0 -> G
w[4, 0, 1] = 1.0 # X1 emet 1 -> G
return w

def transition_matrix(self) -> Array:
"""T[s, s'] = somme des emissions de l'arete (machine agregnee)."""
return self.edge_tensor().sum(axis=2)

def stationary(self) -> Array:
"""Distribution stationnaire : (1/3 sur G, 1/6 sur chaque autre etat)."""
out = np.full(5, 1.0 / 6.0)
out[0] = 1.0 / 3.0
return out

def sample(self, n: int, seed: int) -> Tuple[Array, Array]:
"""Echantillonne n symboles ; retourne (etats d'arrivee par pas, observations)."""
if n < 1:
raise ProcessError("RRXOR.sample attend n >= 1")
rng = np.random.default_rng(seed)
states = np.empty(n, dtype=np.int64)
obs = np.empty(n, dtype=np.int64)
s = 0 # G
for k in range(n):
if s == 0: # emet r1
y = int(rng.integers(0, 2))
s = 1 + y # A_{r1}
elif s in (1, 2): # emet r2
y = int(rng.integers(0, 2))
s = 3 + ((1 if s == 2 else 0) ^ y) # X_{r1 XOR r2}
else: # X : emet le XOR memorise
y = s - 3
s = 0
obs[k] = y
states[k] = s
return states, obs

def beliefs(self, obs: Array) -> Array:
"""Filtration forward exacte sur les aretes (Mealy).

``out[k] = P(s_k | y_0..y_k)`` ou ``s_k`` est l'etat d'arrivee du
symbole ``y_k`` ; mise a jour ``b <- normaliser(b @ W[:, :, y])``
depuis le prior stationnaire sur l'etat emetteur initial.
"""
if obs.ndim != 1 or not np.all(np.isin(obs, (0, 1))):
raise ProcessError("RRXOR.beliefs attend une serie binaire 1D")
w = self.edge_tensor()
b = self.stationary()
out = np.empty((len(obs), 5))
for k in range(len(obs)):
v = b @ w[:, :, int(obs[k])]
b = v / v.sum()
out[k] = b
return out

Etat cache au pas t : la paire ``(b_{t-1}, b_t)``, 4 etats ordonnes
``00, 01, 10, 11``. L'observation etant deterministe dans l'etat, le
belief exact apres observation vit sur les 2 etats coherents avec
``y_t``, uniformes (les entrees sont iid uniformes).

@dataclass(frozen=True)
class RRXOR_Iid:
"""RRXOR legacy : bits iid ``b_t``, observation ``y_t = b_{t-1} XOR b_t``.

DEPRECIE : les XOR adjacents de bits iid sont eux-memes iid -- ce banc
ne portait AUCUNE structure et ne meritait pas le nom RRXOR (cf.
:class:`RRXOR`, conforme a Riechers & Crutchfield 2018). Conserve pour
la REPRODUCTIBILITE de la batterie d'intervention (#15480/#16230) et du
pilote ICT-40, calibres sur ce banc ; toute nouvelle etude doit utiliser
:class:`RRXOR`.
"""

n_states: int = 4
name: str = "rrxor"
name: str = "rrxor_iid"

def transition_matrix(self) -> Array:
"""T[(a,b) -> (b,c)] = 1/2 pour c dans {0,1} : le bit frais est iid uniforme."""
Expand Down Expand Up @@ -273,13 +366,9 @@ def sample(self, n: int, seed: int) -> Tuple[Array, Array]:
return states, obs

def beliefs(self, obs: Array) -> Array:
"""Filtration forward exacte sur les 4 etats ; observation binaire deterministe.

Le premier pas n'a pas d'observation antecedente : prior stationnaire
(l'etat (b_{-1}, b_0) n'est jamais observable via y_0 seul).
"""
"""Filtration forward exacte sur les 4 etats ; observation binaire deterministe."""
if obs.ndim != 1 or not np.all(np.isin(obs, (0, 1))):
raise ProcessError("RRXOR.beliefs attend une serie binaire 1D")
raise ProcessError("RRXOR_Iid.beliefs attend une serie binaire 1D")
t = self.transition_matrix()
e = self.emission_matrix()
prior = self.stationary()
Expand Down
152 changes: 94 additions & 58 deletions MyIA.AI.Notebooks/IIT/ICT-Series/ict/mixed_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,21 @@ class MixedStatePresentation:
"""

nodes: Tuple[Tuple[Array, ...], ...] # nodes[depth] = tuple de Array
edges: Tuple[Tuple[Tuple[int, ...], ...], ...] # edges[depth][i] = indices des filles de nodes[depth][i]
edges: Tuple[Tuple[Tuple[int, ...], ...], ...] # edges[depth][i] = indices des filles de nodes[depth][i], dans l'ordre de l'alphabet
alphabet: Tuple[int, ...] = () # alphabet des observations, dans l'ordre utilise par le BFS

@property
def depth(self) -> int:
return len(self.nodes)

def n_distinct_total(self) -> int:
"""Cardinal de l'union des croyances distinctes sur toutes profondeurs."""
seen = set()
for level in self.nodes:
for b in level:
seen.add(_round_belief(b))
return len(seen)

def n_distinct(self, depth: int) -> int:
if depth < 0 or depth >= self.depth:
raise IndexError(
Expand Down Expand Up @@ -99,32 +108,30 @@ def verify_invariants(self, forward_beliefs_factory) -> Tuple[bool, List[str]]:
# de la trajectoire forward.
if self.depth >= 2:
try:
# Reconstruction du premier chemin : on suit edges[d][0][*]
obs_seq: List[int] = []
d = 0
current_idx = 0
while d < self.depth - 1 and self.edges[d] and self.edges[d][current_idx]:
next_idx = self.edges[d][current_idx][0]
obs_seq.append(next_idx)
# Avancer vers la fille
d += 1
current_idx = next_idx # la fille devient parent
obs_array = np.asarray(obs_seq, dtype=np.int64)
fb = forward_beliefs_factory(obs_array)
if fb.ndim == 2 and fb.shape[0] >= 1:
last_fb = fb[-1]
if last_fb.shape == self.nodes[self.depth - 1][0].shape:
key = _round_belief(last_fb)
keys = {_round_belief(b) for b in self.nodes[self.depth - 1]}
if key not in keys:
# Tolerance : au cas ou l'arrondi differe d'une ULP
similar = any(
np.allclose(last_fb, b, atol=1e-5)
for b in self.nodes[self.depth - 1]
)
if not similar:
# Reconstruction d'un chemin : on suit la premiere fille
# (position 0 = premier symbole de l'alphabet) et le symbole
# correspondant -- edges[d][i] est ordonne par l'alphabet.
if self.alphabet:
obs_seq: List[int] = []
d = 0
current_idx = 0
while d < self.depth - 1 and self.edges[d] and self.edges[d][current_idx]:
next_idx = self.edges[d][current_idx][0]
if next_idx < 0:
break # arete impossible depuis cette croyance
obs_seq.append(int(self.alphabet[0]))
d += 1
current_idx = next_idx
obs_array = np.asarray(obs_seq, dtype=np.int64)
fb = forward_beliefs_factory(obs_array)
if fb.ndim == 2 and fb.shape[0] == len(obs_seq) and len(obs_seq) >= 1:
last_fb = fb[-1]
target = self.nodes[len(obs_seq)][current_idx]
if last_fb.shape == target.shape:
if not np.allclose(last_fb, target, atol=1e-7):
failures.append(
f"forward last belief at depth {self.depth-1} absent de la MSP"
f"forward belief at depth {len(obs_seq)} "
f"diverge du node du chemin reconstruit"
)
except Exception as exc:
failures.append(f"forward factory leve {type(exc).__name__}: {exc}")
Expand All @@ -144,31 +151,61 @@ def build_msp(
max_depth: int,
obs_alphabet: Iterable[int],
prior: Array,
transition: Array,
emission: Array,
transition: Array = None,
emission: Array = None,
edge_tensor: Array = None,
) -> MixedStatePresentation:
"""Construit la MSP par BFS sur l'arbre des sequences d'observations.

Deux conventions d'emission, mutuellement exclusives :

- **Moore** (``transition`` + ``emission``) : l'etat emet, puis transite.
Mise a jour : ``b' = normaliser((b @ T) * E[:, y])``. Convient a
:class:`Mess3Canonical`.
- **Mealy** (``edge_tensor`` seul) : les emissions vivent sur les aretes,
``W[s, s', y] = P(s' et y | s)``. Mise a jour :
``b' = normaliser(b @ W[:, :, y])``. Convient a :class:`RRXOR`
(Riechers & Crutchfield 2018, arXiv:1706.00883v1 : l'epsilon-machine
du RRXOR est Mealy -- l'emission revele l'arete, pas l'etat).

Parametres :
- ``generator`` : instance conforme (Mess3Canonical, RRXOR, ...)
utilisee seulement pour les metadonnees (profondeur max fixee par
l'usage, pas par le generateur).
- ``generator`` : instance conforme (Mess3Canonical, RRXOR, ...).
- ``max_depth`` : profondeur maximale de l'arbre.
- ``obs_alphabet`` : iterable des valeurs d'observation possibles
(par exemple ``range(n_states)`` pour Mess3 ou ``(0, 1)`` pour RRXOR).
- ``obs_alphabet`` : iterable des valeurs d'observation possibles.
- ``prior`` : distribution a priori sur les etats caches (np.ndarray).
- ``transition`` : matrice de transition T (np.ndarray).
- ``emission`` : matrice d'emission E (np.ndarray, shape ``(n_states, n_obs)``).
- ``transition``/``emission`` : convention Moore.
- ``edge_tensor`` : convention Mealy (priorise sur Moore si fourni).

Retourne : :class:`MixedStatePresentation`.
Retourne : :class:`MixedStatePresentation` (avec ``alphabet`` stocke pour
permettre la reconstruction de chemins dans ``verify_invariants``).
"""
if max_depth < 1:
raise ProcessError("max_depth doit etre >= 1")
if edge_tensor is None and (transition is None or emission is None):
raise ProcessError(
"build_msp exige soit edge_tensor (Mealy), soit transition + emission (Moore)"
)
obs_alphabet = tuple(obs_alphabet)
n_states = prior.shape[0]
nodes: List[List[Array]] = []
edges: List[List[Tuple[int, ...]]] = []

def _child(parent_b: Array, o: int):
"""Croyance fille apres l'observation ``o``, ou ``None`` si impossible.

Une croyance Dirac sur un etat a emission deterministe (cas Mealy :
les X du RRXOR) peut rendre un symbole de probabilite nulle ;
l'arete correspondante n'existe pas dans la MSP.
"""
if edge_tensor is not None:
w = parent_b @ edge_tensor[:, :, int(o)]
else:
pred = parent_b @ transition
w = pred * emission[:, int(o)]
z = w.sum()
if z <= 0.0:
return None
return w / z

# Niveau 0 : prior unique (avant toute observation)
nodes.append([prior.copy()])
# edges[0] sera peuple quand on developpe la profondeur 0 vers 1.
Expand All @@ -183,14 +220,10 @@ def build_msp(
for parent_b in nodes[d]:
child_indices: List[int] = []
for o in obs_alphabet:
pred = parent_b @ transition
w = pred * emission[:, int(o)]
z = w.sum()
if z <= 0.0:
raise ProcessError(
f"vraisemblance nulle a depth {d}, obs {o}"
)
child_b = w / z
child_b = _child(parent_b, o)
if child_b is None:
child_indices.append(-1) # arete impossible
continue
key = _round_belief(child_b)
if key in next_seen:
child_idx = next_seen[key]
Expand All @@ -199,6 +232,11 @@ def build_msp(
next_nodes.append(child_b)
child_idx = next_seen[key]
child_indices.append(child_idx)
if all(ci < 0 for ci in child_indices):
raise ProcessError(
f"generateur degenerate : croyance sans aucune observation "
f"possible a depth {d} (toutes les emissions sont nulles)"
)
current_edges.append(tuple(child_indices))
# Pas de nouvelle node : on s'arrete
if not next_nodes:
Expand All @@ -211,6 +249,7 @@ def build_msp(
return MixedStatePresentation(
nodes=tuple(tuple(arr for arr in lvl) for lvl in nodes),
edges=tuple(tuple(e for e in lvl) for lvl in edges),
alphabet=obs_alphabet,
)


Expand All @@ -233,24 +272,21 @@ def msp_mess3(max_depth: int = 6) -> MixedStatePresentation:
)


def msp_rrxor(max_depth: int = 6) -> MixedStatePresentation:
"""MSP du RRXOR : alphabet binaire, 4 etats caches.
def msp_rrxor(max_depth: int = 8) -> MixedStatePresentation:
"""MSP du RRXOR (Riechers & Crutchfield 2018) : alphabet binaire, 5 etats causaux Mealy.

Note : avec prior stationnaire uniforme, la MSP au pas 0 contient 1
croyance ; au pas 1 (apres 1 observation), elle contient 2 croyances
distinctes (deux valeurs possibles de y determinent le sous-ensemble
d'etats coherents) ; au pas k, le cardinal de la MSP suit la
dynamique de l'arbre binaire.
Depuis le prior stationnaire, l'union des croyances distinctes vaut
**36** : 31 transitoires + 5 recurrentes (les Diracs sur les etats
causaux, atteints en profondeur <= 7). C'est la valeur de la
litterature (arXiv:1706.00883v1, p. 17, Fig. 7) : le regime
transitoire resout l'ambiguite de phase de la modulation periodique
d'ordre 3 -- c'est précisément ce que le notebook ICT-37 montre.
"""
r = RRXOR()
prior = r.stationary()
T = r.transition_matrix()
E = r.emission_matrix()
return build_msp(
generator=r,
max_depth=max_depth,
obs_alphabet=(0, 1),
prior=prior,
transition=T,
emission=E,
prior=r.stationary(),
edge_tensor=r.edge_tensor(),
)
Loading
Loading