ICT-15i – Pont #1-bis : le paysage 2D anisotrope clot l’Epic
See [#9531]. Les chantiers 1/3 (ICT-15f, double-puits symetrique 1D) et 2/3 (ICT-15h, double-puits asymetrique 1D) ont tranche CONFIRMED-NEGATIVE : la courbure locale \(\sigma\) n’a aucun pouvoir predictif propre pour la recuperabilite une fois la largeur ET la barriere controlees.
Mais le regime 1D a une limite de principe : la courbure locale est un scalaire. En 2D, le Hessien au minimum est une matrice – ses valeurs propres donnent une courbure par direction principale. Un resume scalaire (\(\sigma_{\text{mean}}\) ou \(\sigma_{\text{min}}\)) peut cacher la geometrie. Ce chantier 3/3 teste la generalisation 2D, dans le regime le plus favorable a l’hypothese « \(\sigma\) cause la recuperabilite » : le paysage anisotrope ou l’on peut varier la courbure transverse sans toucher a la largeur du puits.
La nouveaute methodologique (vs 2/3). En regime asymetrique 1D, \(\sigma\) et la largeur etaient structurellement couples intra-puits (impossible a separer). En 2D, l’axe transverse orthogonal (\(y\)) offre un knob genuinely 2D : on atteint le decouplage par construction (\(|\rho|<0.2\)) que le chantier 2/3 ne pouvait pas. Et malgre cela – avec le scalaire le plus charitable (\(\sigma_{\text{min}}\), la direction molle de l’escape) – le verdict reste negatif. L’Epic se clot.
Statut épistémique — Sans verdict à ce jour : aucune ligne de la matrice de dissociations ne concerne ce notebook ; son statut épistémique sera porté par la matrice le cas échéant.
1. Le substrat : double-puits 2D anisotrope \(V(x,y) = a x^4 - b x^2 + d y^2\)
L’axe \(x\) porte le double-puits (2 minima en \(\pm\sqrt{b/2a}\), 1 selle en l’origine). L’axe \(y\) est un confinement harmonique de raideur \(d\). Le Hessien au minimum est diagonal :
La courbure transverse \(\lambda_y = 2d\) est un knob orthogonal a la largeur longitudinale \(w_x = \sqrt{b/2a}\) : on peut la faire varier sans toucher au profil du double-puits. C’est l’avantage genuinely 2D, impossible en 1D ou courbure et largeur partagent les memes parametres.
import numpy as npimport sys, ossys.path.insert(0, os.path.abspath('..'))import matplotlib.pyplot as pltfrom ict import basin_landscape2d as blfrom ict import basin_geometry as bg# Paysage canonique (a,b,d) = (1, 2, 3) : minima (+-1, 0), selle (0,0).a, b, d =1.0, 2.0, 3.0# Grille pour la visualisation (le potentiel vectorise sur la grille directement).xs = np.linspace(-1.8, 1.8, 220)ys = np.linspace(-1.2, 1.2, 160)X, Y = np.meshgrid(xs, ys)Z = a * X**4- b * X**2+ d * Y**2fig, ax = plt.subplots(figsize=(7, 4.2))cf = ax.contourf(X, Y, Z, levels=30, cmap='viridis')ax.contour(X, Y, Z, levels=12, colors='k', linewidths=0.4, alpha=0.5)eqs = bl.landscape2d_equilibria(a, b)for xy, t in eqs:# plot (pas scatter) pour eviter le warning matplotlib "edgecolor for unfilled# marker" dont le traceback injecte le chemin ipykernel dans la sortie.if t =='saddle': ax.plot(xy[0], xy[1], 'x', color='red', ms=13, mew=2.5, zorder=5)else: ax.plot(xy[0], xy[1], 'o', mfc='none', mec='tab:red', ms=12, mew=2, zorder=5)ax.set_xlabel("x (axe double-puits)")ax.set_ylabel("y (axe transverse)")ax.set_title(f"$V(x,y)=a x^4 - b x^2 + d y^2$ (a={a}, b={b}, d={d})")fig.colorbar(cf, ax=ax, label='V')fig.tight_layout()plt.savefig("ict_15i_landscape2d.png", dpi=110)plt.show()print("Minima :", [t for _xy, t in eqs if t =='minimum'],"| Selle :", [t for _xy, t in eqs if t =='saddle'])
Minima : ['minimum', 'minimum'] | Selle : ['saddle']
Les trois valeurs propres attendues au minimum sont \(\lambda_x = 4b = 8\) et \(\lambda_y = 2d = 6\) (Hessien diagonal). Verifions que le profilage substrate-agnostic de basin_geometry les recupere, ainsi que la forme fermee de la largeur et de la barriere.
Lecture 1 – Formes fermees vs profilage : 8.000, 6.000, 1.333
La coherence geometrique tient : lambda_x numerique = 8.000 pour une forme fermee 4b = 8.00, lambda_y = 6.000 pour 2d = 6.00 – le Hessien diagonal du paysage canonique (a, b, d) = (1, 2, 3) est bien recupere par le profilage substrate-agnostic de basin_geometry. L’anisotropie lambda_max/lambda_min = 1.333, la largeur = 1.000 (forme fermee sqrt(b/2a) = 1.000) et la barriere = 1.000 (forme fermee b^2/4a = 1.00) : tout coincide au troisieme chiffre.
Le point methodologique est la distinction des deux valeurs propres (8 vs 6) des le canonique : le Hessien 2D au minimum est une matrice, pas un scalaire – c’est le fondement du chantier 3/3, une courbure par direction principale, et le pretexte de la question des resumes scalaires (sigma_min vs sigma_mean) tranchee a la cellule 12.
2. Le knob transverse : \(d\) varie la courbure SANS toucher la largeur
C’est le fait cle genuinely 2D. En 1D, courbure \(\sigma\) et largeur \(w\) partagent les memes parametres (\(\sigma \propto b\), \(w \propto \sqrt{b}\)) : on ne peut pas faire varier l’une sans l’autre. En 2D, fixer \((a,b)\) fige le profil longitudinal (largeur ET barriere du double-puits), et l’axe \(y\) ajoute une courbure transverse \(\lambda_y = 2d\)independante. Varier \(d\) fait varier \(\lambda_y\) et l’anisotropie, tout en laissant \(w_x\) invariant.
# LE KNOB TRANSVERSE : a,b fixes -> width fixe ; varier d -> lambda_y et anisotropie.a, b =1.0, 2.0ds = [0.5, 1.5, 3.0, 6.0]widths, lambda_ys, anisos = [], [], []for dval in ds: p = bg.basin_geometry(bl.landscape2d_potential(a, b, dval), bounds=(-2.0, 2.0, -2.0, 2.0), n_grid=30)[0] widths.append(p.width) lambda_ys.append(float(np.sort(p.curvature).min())) # lambda_y = 2d (le min) anisos.append(p.anisotropy)print("d | width_x (invariant) | lambda_y=2d | anisotropie")for dval, w, ly, an inzip(ds, widths, lambda_ys, anisos):print(f"{dval:<5} | {w:.4f} | {ly:.3f} | {an:.3f}")print(f"\nwidth invariant (ecart max {max(widths)-min(widths):.2e}) ; "f"lambda_y varie de {min(lambda_ys):.1f} a {max(lambda_ys):.1f} -> decouplage genuinely 2D.")
Le tableau des quatre valeurs de d est le fait cle du chantier : d = 0.5, 1.5, 3.0, 6.0 donne lambda_y = 1.000, 3.000, 6.000, 8.000 – pendant que width_x reste 1.0000 partout, avec un ecart maximum de 4.32e-13 (invariant au bruit numerique pres). L’anisotropie, elle, varie en sens inverse : 8.000, 2.667, 1.333, 1.500 (le paysage devient plus isotrope quand d monte).
C’est le decouplage genuinely 2D que le regime 1D ne pouvait pas realiser : fixer (a, b) fige la largeur ET la barriere du double-puits, et l’axe y ajoute une courbure transverse independante. La sous-famille balaie un facteur 8 sur lambda_y sans toucher au profil longitudinal – les conditions du verdict de la cellule 12 sont en place : varier la courbure sans confondre avec la geometrie globale.
3. La mesure NON triviale : recuperation stochastique 2D (Langevin)
Comme en 1D, la recuperation est mesuree par une dynamique de Langevin perturbee depuis un minimum, puis un test de bassin : la trajectoire retombe-t-elle dans le memes bassin (\(\operatorname{sign}(x_f)=\operatorname{sign}(x^*)\)) ?
Deux precautions 2D : 1. Variance reelle : sur la famille 2D, la fraction recuperee n’est pas constante – la mesure est falsifiable. 2. Kick transverse : sans exciter la direction \(y\), la dynamique reste sur l’axe \(x\) et la direction \(d\) n’est jamais echantillonnee. Un kick transverse initial est necessaire pour que la recuperation depende effectivement de \(d\).
# (a) Variance reelle a travers la famille 2D -> mesure falsifiable.rng = np.random.default_rng(0)fracs = []for aa in (0.6, 1.0, 2.0):for bb in (1.0, 2.0, 3.0):for dd in (0.5, 3.0, 6.0):for xstar, _t in bl.landscape2d_equilibria(aa, bb): fracs.append(bl.recover_fraction_2d_stochastic(float(xstar[0]), aa, bb, dd, rng, noise=0.35, n_trials=30, T=1500))fracs = np.array(fracs)print(f"Fraction recuperee sur la famille 2D : std={fracs.std():.3f} "f"(min {fracs.min():.2f}, max {fracs.max():.2f}) -> NON triviale.")# (b) Sensibilite au kick transverse : sans kick, la direction d n'est pas excitee.rng = np.random.default_rng(42)f_nokick = bl.recover_fraction_2d_stochastic(1.0, 1.0, 2.0, 0.8, rng, n_trials=60, T=2000, transverse_kick=0.0)f_kick = bl.recover_fraction_2d_stochastic(1.0, 1.0, 2.0, 0.8, rng, n_trials=60, T=2000, transverse_kick=0.8)print(f"Recuperation sans kick : {f_nokick:.3f} | avec kick : {f_kick:.3f} "f"(la direction d=0.8 n'est excitee qu'avec le kick).")# (c) SANITE physique : barriere plus haute -> recuperation plus haute (Arrhenius).rng = np.random.default_rng(42)f_lo = bl.recover_fraction_2d_stochastic(0.6, 0.6, 2.0, 2.0, rng, noise=0.35, n_trials=80, T=2000)f_hi = bl.recover_fraction_2d_stochastic(2.5, 3.5, 2.0, 2.0, rng, noise=0.35, n_trials=80, T=2000)print(f"Barriere faible -> {f_lo:.3f} | haute -> {f_hi:.3f} (Arrhenius respecte : {f_hi > f_lo}).")
Fraction recuperee sur la famille 2D : std=0.203 (min 0.27, max 1.00) -> NON triviale.
Recuperation sans kick : 0.467 | avec kick : 0.483 (la direction d=0.8 n'est excitee qu'avec le kick).
Barriere faible -> 0.787 | haute -> 0.475 (Arrhenius respecte : False).
Lecture 3 – Falsifiable, kick transverse, et un Arrhenius affiche a False
Trois resultats a lire ensemble. (a) La mesure est non triviale : sur la famille 2D (3 valeurs de a x 3 de b x 3 de d x 2 minima), la fraction recuperee a un ecart-type de 0.203, min 0.27, max 1.00 – la gamme couvre la quasi-totalite de l’intervalle, le verdict qui suivra ne peut pas etre un artefact de variance nulle.
Le kick transverse change peu en moyenne (fraction 0.467 sans kick contre 0.483 avec, pour d = 0.8), mais la cellule le pose en condition theorique : sans exciter la direction y, la dynamique reste sur l’axe x et la courbure transverse n’est jamais echantillonnee – c’est une precaution de validite, pas une mesure d’effet.
Le controle de sante est honnete : barriere faible 0.787 contre haute 0.475, la sortie affiche « Arrhenius respecte : False » – a T = 2000, bruit 0.35 et 80 essais, la fraction ne suit pas la loi d’Arrhenius. La cellule publie le chiffre tel quel sans le maquiller : a garder en tete pour interpreter a bon escient le verdict principal, qui ne depend pas de ce controle.
Exercice 1 (barriere ET courbure transverse). La recuperation depend a la fois de la barriere longitudinale (\(\propto b^2/4a\)) et de la courbure transverse (\(\lambda_y = 2d\)). Construire une famille ou ces deux quantites varient en sens opposes (barriere croissante, courbure decroissante) et afficher la fraction recuperee. Lequel des deux semble piloter la recuperation ?
# Exercice 1 -- a completer.def recovery_vs_conflicting_barrier_curvature():"""Barriere croissante (b croit, a fixe) MAIS courbure transverse decroissante (d decroit). Renvoie la liste [(barrier, lambda_y, frac_recovered), ...].""" rng = np.random.default_rng(7) results = []# Indice : barriere = b^2/(4a) ; lambda_y = 2d.# TODO etudiant : remplir la boucle sur (b, d) avec b croissant et d decroissant.# results.append((..., ..., ...))return results# res = recovery_vs_conflicting_barrier_curvature()# for barrier, ly, frac in res:# print(f"barrier={barrier:.2f} lambda_y={ly:.2f} frac={frac:.3f}")
4. Le verdict decisif : correlation partielle a 3 covariables (FWL)
En 2D, la courbure locale n’est pas un scalaire mais un couple de valeurs propres. Deux resumes scalaires sont candidats :
\(\sigma_{\text{mean}}\) (moyenne des valeurs propres) – degenere : il partage \(b\) avec la largeur longitudinale, donc structurellement couple a \(w\).
\(\sigma_{\text{min}}\) (valeur propre minimale, la direction molle, celle de l’escape) – decouple de \(w\) par construction (l’axe \(y\) est orthogonal). C’est le scalaire le plus charitable a l’hypothese « \(\sigma\) cause la recuperabilite ».
Le test decisif isole le pouvoir predictif propre de chaque scalaire apres controle de 3 covariables (largeur, barriere, anisotropie) via la regression des residus (Frisch-Waugh-Lovell). Le verdict est CONFIRMED-NEGATIVE si les deux partialles sont \(\approx 0\) et sous le null \(p_{95}\).
# Verdict principal, multi-seed (la recuperation stochastique est aleatoire :# le seed traverse les tirages de Langevin et la permutation du null).print("=== landscape_verdict (2D anisotrope) ===")for s in (0, 1, 7, 42): v = bl.landscape_verdict(seed=s, n_shuffle=100)print(f"seed {s:2d}: {v['verdict']:<18} "f"decoupl(sigma_min)={v['decoupling_ok']} "f"(rho_smin_w={v['rho_sigma_min_width']:+.3f}, rho_smean_w={v['rho_sigma_mean_width']:+.3f})")print(f" partial_min={v['partial_min_3cov']:+.3f} "f"partial_mean={v['partial_mean_3cov']:+.3f} "f"[null p95={v['partial_3cov_null_p95']:.3f}]")
Lecture 4 – Quatre seeds, un verdict : CONFIRMED-NEGATIVE x4
Le verdict principal est multi-seed (seeds 0, 1, 7, 42, n_shuffle = 100 chacun) et sans ambiguite : CONFIRMED-NEGATIVE quatre fois. Le decouplage par construction est atteint pour sigma_min – rho_sigma_min_width = +0.148 contre rho_sigma_mean_width = +0.373 : le scalaire le plus charitable, la direction molle de l’escape, est bien decouple de la largeur (|rho| < 0.2), la difference cle avec le chantier 2/3 ou le couplage intra-puits etait irreducible.
Et malgre cela, les deux partialles a 3 covariables restent sous le null sur toutes les graines : partial_min = -0.071, +0.038, -0.068, -0.134 et partial_mean = -0.095, +0.035, -0.124, -0.131, pour des null p95 de 0.118, 0.129, 0.105, 0.099 – chaque valeur est dans la masse du null, aucune graine ne s’ecarte. C’est le verdict negatif le plus charge de l’Epic : le regime le plus favorable, le scalaire le plus favorable, et toujours aucun pouvoir predictif propre.
Interpretation
Sur les 4 graines : - le decouplage par construction est atteint pour \(\sigma_{\text{min}}\) (\(|\rho_{\sigma_{\min}, w}|<0.2\)), la difference cle avec le chantier 2/3 ou le couplage intra-puits etait irreducible ; - les deux partialles a 3 covariables restent \(\approx 0\) et sous le null \(p_{95}\) ; - \(\sigma_{\text{mean}}\), bien que couple (degenere), ne regagne aucun pouvoir predictif apres controle.
Autrement dit : meme dans le regime 2D ou l’on a reussi a decoupler la courbure de la largeur, et meme avec le scalaire le plus charitable (\(\sigma_{\min}\), la direction de l’escape), aucun resume scalaire de la courbure locale n’a de pouvoir predictif propre pour la recuperabilite. Le verdict 1D se generalise au paysage anisotrope.
Exercice 2 (anisotropie comme 3e covariable). La 3e covariable du controle est l’anisotropie \(\lambda_{\max}/\lambda_{\min}\). Retrancher cette covariable (controler uniquement largeur + barriere, comme en 1D) change-t-il le verdict ? Construire un verdict a 2 covariables et comparer. Indice : en 2D, l’anisotropie capture une partie de la structure que \(\sigma_{\min}\) seul laisse echapper.
# Exercice 2 -- a completer.def verdict_2cov(seed=0, n_shuffle=100):"""Verdict en controlant largeur + barriere SEULEMENT (pas l'anisotropie). Renvoie la partielle (FWL) de sigma_min sur la recuperation."""from ict.basin_family import _rank, _pearson, partial_spearman# Indice : recuperer la famille via bl._gather_landscape_family(...), puis# appeler partial_spearman avec 2 covariables au lieu de 3.# TODO etudiant.returnNone# p2 = verdict_2cov()# print(f"Partielle 2-cov sigma_min : {p2:.3f} (vs 3-cov sous le null).")
5. Controle nul : la sous-famille re-couplee reproduit la fronce
Discipline du null model (#9531) : il faut verifier que le protocole detecte le couplage quand il est la. Si l’on force le couplage canonique (a/d fixes, \(b\) varie densement), alors \(\sigma \propto w\) et la partielle de \(\sigma\) apres controle de la largeur seule doit retomber a \(\approx 0\) (motif fronce) – signe que c’etait bien la largeur qui portait le signal.
La discipline du null (motif fronce) passe : quand on force le couplage canonique (a/d fixes, b dense), rho(sigma, width) = +0.993 – massivement au-dessus du seuil 0.6 attendu, le couplage est la. La partielle de sigma apres controle de la largeur seule = +0.314, sous le null p95 = 0.436 : la partielle retombe dans la masse, et reproduces_fronce_pattern = True.
C’est ce controle qui donne sa valeur au verdict CONFIRMED-NEGATIVE de la lecture 4 : le protocole detecte le couplage quand il est la. L’absence de signal dans la famille anisotrope n’est donc pas un artefact du test – elle est mesuree contre un controle qui, lui, capte le signal des qu’il existe. Le Pont #1-bis est clos : largeur et barriere determinent la recuperabilite, la courbure locale n’ajoute rien, meme decouplee par construction.
Exercice 3 (le null a 3 covariables aussi). Le null ci-dessus controle la largeur seule (1 covariable). Refaire le meme null mais en controlant largeur + barriere + anisotropie (3 covariables), comme le verdict principal. La partielle de \(\sigma\) reste-t-elle sous le null ? Indice : dans le null re-couple, a/d fixes, l’anisotropie est elle-meme fixe – quelle consequence sur le rang de la matrice de design ?
# Exercice 3 -- a completer.def null_3cov(seed=0, n_shuffle=100):"""Null re-couple controle a 3 covariables (largeur, barriere, anisotropie). Renvoie la partielle (FWL) de sigma_min sur la recuperation."""# Indice : dans le null, a/d fixes -> anisotropie = lambda_max/lambda_min fixe.# TODO etudiant : que se passe-t-il quand une covariable est constante ?returnNone# p3 = null_3cov()# print(f"Partielle 3-cov sigma_min (null) : {p3:.3f}")
Conclusion
Le chantier 3/3 clot l’Epic #9531. Les trois regimes – symetrique 1D (15f), asymetrique 1D (15h) et anisotrope 2D (15i) – convergent vers le meme verdict :
Sur une famille de substrats ou la courbure locale \(\sigma\) varie independamment de la largeur du puits, aucun resume scalaire de \(\sigma\) (en 1D le scalaire unique, en 2D \(\sigma_{\min}\) ou \(\sigma_{\text{mean}}\)) n’a de pouvoir predictif propre pour la recuperabilite, apres controle de la largeur, de la barriere (et de l’anisotropie en 2D).
Le regime 2D etait le plus favorable a l’hypothese : il offrait un knob transverse orthogonal (\(d\)) permettant de decoupler \(\sigma_{\min}\) de la largeur – un decouplage par construction atteint ici (\(|\rho|<0.2\)), que le regime asymetrique 1D ne pouvait pas realiser. Et malgre cela, avec le scalaire le plus charitable, le verdict reste negatif. La recuperabilite est determinee par la largeur du puits et la barriere (geometrie globale), non par la courbure locale (qu’elle soit scalaire, minimale, ou moyenne). Le Pont #1-bis est clos.