QUIMAD / simulation.py
metamatematico's picture
Upload simulation.py with huggingface_hub
27ae5d6 verified
"""
QIMAD Visual Simulation
=======================
Canicas cuánticas (esferas de Bloch) rodando sobre una hipersuperficie rugosa.
Paneles:
- Principal 3D : la superficie f: ℝ² → ℝ con las canicas moviéndose
- Convergencia : evolución de f_min a lo largo del tiempo
- Bloch 2D : proyección ecuatorial del estado cuántico de cada canica
Efectos visuales:
- Color de la canica → ángulo α (azul = conservador, rojo = temerario)
- Tamaño de la canica → calidad de la posición (mejor → más grande)
- Líneas doradas → pares entrelazados (F > umbral)
- Flash blanco → evento de túnel cuántico
Uso:
python simulation.py # ventana interactiva
python simulation.py --save # guarda simulation.gif (requiere Pillow)
python simulation.py --agents 12 --iters 150 --seed 7
"""
import argparse
import matplotlib
import matplotlib.gridspec as gridspec
import matplotlib.pyplot as plt
import numpy as np
from matplotlib import cm
from matplotlib.animation import FuncAnimation
# ── Colores del tema ─────────────────────────────────────────────────────────
_BG = '#0b0c10'
_PANEL = '#0f1117'
_GRID = '#1a1d2e'
_TEXT = '#c8d0e0'
_CYAN = '#00e5ff'
_ORANGE = '#ff6b35'
_GOLD = '#ffd060'
_DIM = '#444466'
# ── Superficie f: ℝ² → ℝ ────────────────────────────────────────────────────
BOUNDS = [-6.0, 6.0]
def rugose(x, y):
"""
Superficie rugosa no trivial: combinación de cuenco suave,
crestas periódicas y ruido de alta frecuencia.
Diseñada para tener múltiples mínimos locales y una estructura
de valles que no es trivialmente navegable por gradiente.
"""
bowl = 0.75 * (x**2 + y**2)
ridges = 2.5 * np.tan(2.0 * x) * np.cos(2.0 * y)
fine = 0.7 * np.sin(5.2 * x) * np.tan(5.2 * y)
return bowl + ridges + fine
class Surface2D:
"""Wrapper que expone la superficie como función objetivo para QIMAD."""
dim = 4
bounds = np.array(BOUNDS, dtype=float)
def __call__(self, x):
return float(rugose(x[0], x[1]))
def gradient(self, x, eps=1e-5):
g = np.zeros(2)
for i in range(2):
xp, xm = x.copy(), x.copy()
xp[i] += eps
xm[i] -= eps
g[i] = (self(xp) - self(xm)) / (2 * eps)
return g
# ── Simulador QIMAD que almacena historia completa ───────────────────────────
class QIMADSimulator:
"""
Variante de QIMAD reducida a ℝ² que registra el estado completo
en cada iteración para reproducción en la animación.
Origen del modelo
-----------------
Cada agente es una esfera de Bloch que rueda por la hipersuperficie.
Al rodar, su estado cuántico (α, β) evoluciona. Cuando dos esferas
tienen estados cercanos (alta fidelidad), se entrelazan y abren un
canal de información entre sus posiciones clásicas.
"""
ENTANGLEMENT_THRESHOLD = 0.55 # umbral de fidelidad para entrelazamiento
def __init__(self, obj, num_agents=8, eta=0.05, gamma=0.12,
k=2, alpha_lr=0.06, beta_lr=0.06, seed=42):
import networkx as nx
self.obj = obj
self.N = num_agents
self.eta = eta
self.gamma = gamma
self.k = k
self.alpha_lr = alpha_lr
self.beta_lr = beta_lr
self.rng = np.random.RandomState(seed)
self.bounds = obj.bounds
self.eps = 1e-8
self.G = nx.complete_graph(num_agents)
# Estado inicial de las canicas
self.pos = self.rng.uniform(*self.bounds, (num_agents, 2))
self.alpha = self.rng.uniform(0, np.pi, num_agents)
self.beta = self.rng.uniform(0, 2 * np.pi, num_agents)
self.v_rms = np.zeros((num_agents, 2))
self.stagnation = np.zeros(num_agents, dtype=int)
self.best_pos = self.pos[0].copy()
self.best_val = np.inf
# Historiales (uno por iteración)
self.h_pos = [] # (N, 2) posiciones clásicas
self.h_alpha = [] # (N,) ángulo α de cada esfera
self.h_beta = [] # (N,) ángulo β de cada esfera
self.h_obj = [] # (N,) valor objetivo de cada agente
self.h_best = [] # float mejor global acumulado
self.h_fidelity = [] # (N, N) matriz de fidelidades
self.h_entangled = [] # lista de pares (i, j) entrelazados
self.h_tunneled = [] # índices de agentes que tunelizan en este paso
# ── estado cuántico ──────────────────────────────────────────────────────
def _psi(self, i):
"""Qubit en representación de la esfera de Bloch."""
a, b = self.alpha[i], self.beta[i]
return np.array([
np.cos(a / 2),
np.exp(1j * b) * np.sin(a / 2)
], dtype=complex)
def _fidelity_matrix(self):
psis = [self._psi(i) for i in range(self.N)]
return np.array([
[abs(np.vdot(psis[i], psis[j]))**2 for j in range(self.N)]
for i in range(self.N)
])
# ── un paso de optimización ──────────────────────────────────────────────
def step(self):
objs = np.array([self.obj(self.pos[i]) for i in range(self.N)])
for i, v in enumerate(objs):
if v < self.best_val:
self.best_val = v
self.best_pos = self.pos[i].copy()
self.stagnation[i] = 0
else:
self.stagnation[i] += 1
F = self._fidelity_matrix()
entangled = [
(i, j) for i in range(self.N)
for j in range(i + 1, self.N)
if F[i, j] > self.ENTANGLEMENT_THRESHOLD
]
# Guardar estado ANTES de actualizar posiciones
self.h_pos.append(self.pos.copy())
self.h_alpha.append(self.alpha.copy())
self.h_beta.append(self.beta.copy())
self.h_obj.append(objs.copy())
self.h_best.append(self.best_val)
self.h_fidelity.append(F.copy())
self.h_entangled.append(entangled)
# Actualizar cada canica
tunneled = []
new_pos = self.pos.copy()
for i in range(self.N):
g = self.obj.gradient(self.pos[i])
self.v_rms[i] = 0.99 * self.v_rms[i] + 0.01 * g**2
eta_a = self.eta / (np.sqrt(self.v_rms[i]) + self.eps)
# Atracción hacia el mejor conocido
attract = 0.2 * self.rng.rand() * (self.best_pos - self.pos[i])
# Túnel cuántico: la amplitud |1⟩ dicta la probabilidad de salto
tunnel = np.zeros(2)
if self.stagnation[i] > 3:
prob = float(abs(self._psi(i)[1])**2) # sin²(α/2)
if self.rng.rand() < prob:
span = self.bounds[1] - self.bounds[0]
tunnel = self.rng.uniform(-1, 1, 2) * span * 0.4
tunneled.append(i)
self.stagnation[i] = 0
# Comunicación ponderada por fidelidad (entrelazamiento)
neighbors = list(self.G.neighbors(i))
weights = F[i, neighbors] ** self.k
comm = np.zeros(2)
if weights.sum() > self.eps:
nw = weights / weights.sum()
for w, n in zip(nw, neighbors):
comm += w * (self.pos[n] - self.pos[i])
new_pos[i] = np.clip(
self.pos[i] - eta_a * g + self.gamma * comm + attract + tunnel,
self.bounds[0], self.bounds[1]
)
# La canica rueda → su estado de Bloch evoluciona
self.alpha[i] = np.clip(
self.alpha[i] + self.alpha_lr * self.rng.randn(), 0, np.pi)
self.beta[i] = (
self.beta[i] + self.beta_lr * self.rng.randn()) % (2 * np.pi)
self.h_tunneled.append(tunneled)
self.pos = new_pos
def run(self, n_iters):
for _ in range(n_iters):
self.step()
# ── Animación ────────────────────────────────────────────────────────────────
def _build_mesh(n=100):
x = np.linspace(BOUNDS[0], BOUNDS[1], n)
y = np.linspace(BOUNDS[0], BOUNDS[1], n)
X, Y = np.meshgrid(x, y)
return X, Y, rugose(X, Y)
def run_simulation(num_agents=8, num_iters=120, seed=42, save_gif=False):
# ── Pre-calcular toda la trayectoria ─────────────────────────────────────
print(f"Calculando {num_iters} iteraciones con {num_agents} canicas...")
obj = Surface2D()
sim = QIMADSimulator(obj, num_agents=num_agents, seed=seed)
sim.run(num_iters)
print("Preparando animación...")
X, Y, Z = _build_mesh()
z_lo = Z.min() - 0.3
z_hi = Z.max() + 0.9
cmap_q = matplotlib.colormaps.get_cmap('plasma')
# ── Figura ────────────────────────────────────────────────────────────────
fig = plt.figure(figsize=(17, 9), facecolor=_BG)
fig.suptitle(
'QIMAD · Canicas cuánticas (esferas de Bloch) rodando '
'sobre una hipersuperficie rugosa',
color=_TEXT, fontsize=12, fontweight='bold', y=0.985
)
gs = gridspec.GridSpec(
2, 3, figure=fig,
width_ratios=[3, 1.3, 1.1],
hspace=0.42, wspace=0.32,
left=0.03, right=0.97, top=0.935, bottom=0.07
)
ax3d = fig.add_subplot(gs[:, 0], projection='3d')
ax_cv = fig.add_subplot(gs[0, 1])
ax_bl = fig.add_subplot(gs[1, 1])
ax_lg = fig.add_subplot(gs[:, 2])
for ax in (ax_cv, ax_bl, ax_lg):
ax.set_facecolor(_PANEL)
for sp in ax.spines.values():
sp.set_color(_GRID)
# ── Panel 3D ─────────────────────────────────────────────────────────────
ax3d.set_facecolor(_BG)
for axis in (ax3d.xaxis, ax3d.yaxis, ax3d.zaxis):
axis.pane.fill = False
axis.line.set_color('#222233')
ax3d.tick_params(colors='#444455', labelsize=6)
ax3d.plot_surface(X, Y, Z, cmap='viridis', alpha=0.42,
linewidth=0, antialiased=True, rcount=80, ccount=80)
ax3d.set_xlim(BOUNDS); ax3d.set_ylim(BOUNDS); ax3d.set_zlim(z_lo, z_hi)
ax3d.set_xlabel('x', color='#8888aa', labelpad=1, fontsize=7)
ax3d.set_ylabel('y', color='#8888aa', labelpad=1, fontsize=7)
ax3d.set_zlabel('f(x,y)', color='#8888aa', labelpad=1, fontsize=7)
ax3d.view_init(elev=28, azim=-52)
# Scatter de agentes (inicializado con posiciones reales del primer frame)
pos0 = sim.h_pos[0]
zs0 = np.array([rugose(pos0[i, 0], pos0[i, 1]) for i in range(num_agents)])
colors0 = cmap_q(sim.h_alpha[0] / np.pi)
scat = ax3d.scatter(
pos0[:, 0], pos0[:, 1], zs0 + 0.08,
s=120, c=colors0, depthshade=True, zorder=10
)
iter_txt = ax3d.text2D(0.02, 0.96, '', transform=ax3d.transAxes,
color=_TEXT, fontsize=9, va='top')
best_txt = ax3d.text2D(0.02, 0.89, '', transform=ax3d.transAxes,
color=_CYAN, fontsize=8, va='top')
# Rastros (últimos TRAIL pasos de cada canica)
TRAIL = 8
trail_lines = []
for i in range(num_agents):
ln, = ax3d.plot([], [], [], lw=0.9, alpha=0.35, zorder=4)
trail_lines.append(ln)
# Líneas de entrelazamiento (se recrean cada frame)
ent_objs = []
# ── Panel convergencia ───────────────────────────────────────────────────
ax_cv.set_title('Convergencia', color=_TEXT, fontsize=9, pad=4)
ax_cv.set_xlabel('Iteración', color='#8888aa', fontsize=7)
ax_cv.set_ylabel('f_min', color='#8888aa', fontsize=7)
ax_cv.tick_params(colors='#555566', labelsize=6)
ax_cv.grid(True, color=_GRID, lw=0.5)
bv = sim.h_best
ym = (max(bv) - min(bv)) * 0.1 + 0.1
ax_cv.set_xlim(0, num_iters)
ax_cv.set_ylim(min(bv) - ym, max(bv) + ym)
cv_line, = ax_cv.plot([], [], color=_CYAN, lw=1.5)
cv_dot, = ax_cv.plot([], [], 'o', color=_ORANGE, ms=5, zorder=5)
# ── Panel proyección Bloch ───────────────────────────────────────────────
ax_bl.set_title('Estado cuántico · plano ecuatorial Bloch',
color=_TEXT, fontsize=8, pad=4)
ax_bl.set_xlim(-1.18, 1.18); ax_bl.set_ylim(-1.18, 1.18)
ax_bl.set_aspect('equal')
ax_bl.tick_params(colors='#555566', labelsize=6)
ax_bl.grid(True, color=_GRID, lw=0.5)
# Círculo unitario (ecuador de la esfera)
th = np.linspace(0, 2 * np.pi, 300)
ax_bl.plot(np.cos(th), np.sin(th), color='#2a2a44', lw=1.2)
ax_bl.axhline(0, color='#222233', lw=0.5)
ax_bl.axvline(0, color='#222233', lw=0.5)
ax_bl.text( 0, 1.10, '|+y⟩', color=_DIM, ha='center', va='bottom', fontsize=6)
ax_bl.text( 1.10, 0, '|+x⟩', color=_DIM, ha='left', va='center', fontsize=6)
ax_bl.text( 0, -1.14, 'sin(α)cos(β)', color=_DIM, ha='center', fontsize=5.5)
# Círculo de umbral de entrelazamiento (guía visual)
r_thr = sim.ENTANGLEMENT_THRESHOLD ** 0.5
ax_bl.plot(r_thr * np.cos(th), r_thr * np.sin(th),
color=_GOLD + '55', lw=0.8, ls='--')
ax_bl.text(r_thr * 0.68, r_thr * 0.68, 'umbral F',
color=_GOLD + '88', fontsize=5, ha='center')
bl_scat = ax_bl.scatter([], [], s=65, zorder=5)
# ── Panel leyenda / info ─────────────────────────────────────────────────
ax_lg.axis('off')
ax_lg.set_title('Leyenda', color=_TEXT, fontsize=9, pad=4)
items = [
('●', cmap_q(0.0), 'α ≈ 0 conservador\n P(túnel) ≈ 0'),
('●', cmap_q(0.5), 'α = π/2 indeciso\n P(túnel) = 0.5'),
('●', cmap_q(1.0), 'α ≈ π temerario\n P(túnel) ≈ 1'),
('━', _GOLD, 'Entrelazamiento\n (F > umbral)'),
('●', 'white', 'Evento de túnel\n cuántico'),
]
for k, (sym, col, txt) in enumerate(items):
y = 0.92 - k * 0.18
ax_lg.text(0.04, y, sym, color=col, fontsize=15,
transform=ax_lg.transAxes, va='top')
ax_lg.text(0.18, y, txt, color=_TEXT, fontsize=7.5,
transform=ax_lg.transAxes, va='top',
linespacing=1.4)
ax_lg.text(
0.04, 0.05,
'Canicas = esferas de Bloch\n'
'Al rodar, su estado cuántico\n'
'evoluciona con la curvatura\n'
'de la hipersuperficie',
color='#555577', fontsize=6.5,
transform=ax_lg.transAxes, va='bottom', style='italic'
)
# ── Función de actualización ──────────────────────────────────────────────
def update(frame):
nonlocal ent_objs
pos = sim.h_pos[frame]
alphas = sim.h_alpha[frame]
betas = sim.h_beta[frame]
objs_f = sim.h_obj[frame]
best = sim.h_best[frame]
F = sim.h_fidelity[frame]
entangl = sim.h_entangled[frame]
tunneled = sim.h_tunneled[frame]
zs = np.array([rugose(pos[i, 0], pos[i, 1]) for i in range(num_agents)])
# Color ← estado cuántico α
norm_a = alphas / np.pi
colors = cmap_q(norm_a)
# Tamaño ← calidad de la posición (mejor objetivo = canica más grande)
spread = objs_f.max() - objs_f.min() + 1e-9
norm_obj = (objs_f - objs_f.min()) / spread
sizes = 55 + 160 * (1 - norm_obj)
# Flash blanco en eventos de túnel
for i in tunneled:
colors[i] = np.array([1.0, 1.0, 1.0, 1.0])
sizes[i] = 380
# Actualizar scatter 3D
scat._offsets3d = (pos[:, 0], pos[:, 1], zs + 0.08)
scat.set_color(colors)
scat.set_sizes(sizes)
# Rastros de posiciones anteriores
start = max(0, frame - TRAIL)
for i, ln in enumerate(trail_lines):
tx = [sim.h_pos[t][i, 0] for t in range(start, frame + 1)]
ty = [sim.h_pos[t][i, 1] for t in range(start, frame + 1)]
tz = [rugose(sim.h_pos[t][i, 0], sim.h_pos[t][i, 1]) + 0.04
for t in range(start, frame + 1)]
ln.set_data(tx, ty)
ln.set_3d_properties(tz)
# Color del rastro = mismo hue que la canica pero muy tenue
rc = list(cmap_q(norm_a[i])[:3]) + [0.22]
ln.set_color(rc)
# Eliminar y redibujar líneas de entrelazamiento
for ln in ent_objs:
ln.remove()
ent_objs = []
for (i, j) in entangl:
fid = F[i, j]
vis_alpha = (fid - sim.ENTANGLEMENT_THRESHOLD) / (
1.0 - sim.ENTANGLEMENT_THRESHOLD + 1e-9)
ln, = ax3d.plot(
[pos[i, 0], pos[j, 0]],
[pos[i, 1], pos[j, 1]],
[zs[i] + 0.08, zs[j] + 0.08],
color=_GOLD,
alpha=float(np.clip(vis_alpha * 0.9, 0.1, 0.9)),
lw=0.8 + 2.0 * fid,
zorder=6
)
ent_objs.append(ln)
# Convergencia
cv_line.set_data(range(frame + 1), sim.h_best[:frame + 1])
cv_dot.set_data([frame], [best])
# Proyección esfera de Bloch: (sin α cos β, sin α sin β)
bx = np.sin(alphas) * np.cos(betas)
by = np.sin(alphas) * np.sin(betas)
bl_scat.set_offsets(np.column_stack([bx, by]))
bl_scat.set_color(colors)
# Textos informativos
iter_txt.set_text(f'Iteración {frame + 1} / {num_iters}')
best_txt.set_text(
f'f_min = {best:.4f}\n'
f'enlaces: {len(entangl)} '
f'túneles: {len(tunneled)}'
)
return [scat, cv_line, cv_dot, bl_scat] + ent_objs + trail_lines
# ── Ejecutar ──────────────────────────────────────────────────────────────
anim = FuncAnimation(
fig, update,
frames=num_iters,
interval=90, # ms entre frames
blit=False # blit=True no funciona bien con Axes3D
)
if save_gif:
fname = 'simulation.gif'
print(f"Guardando '{fname}' (puede tardar ~1–2 min)...")
anim.save(fname, writer='pillow', fps=12, dpi=110)
print(f"Listo: {fname}")
else:
plt.show()
return anim
# ── Entry point ───────────────────────────────────────────────────────────────
if __name__ == '__main__':
ap = argparse.ArgumentParser(
description='QIMAD — simulación visual de canicas cuánticas')
ap.add_argument('--save', action='store_true',
help='Guardar como simulation.gif (requiere Pillow)')
ap.add_argument('--agents', type=int, default=8,
help='Número de canicas / agentes (default: 8)')
ap.add_argument('--iters', type=int, default=120,
help='Iteraciones de optimización (default: 120)')
ap.add_argument('--seed', type=int, default=42,
help='Semilla aleatoria (default: 42)')
args = ap.parse_args()
run_simulation(
num_agents=args.agents,
num_iters=args.iters,
seed=args.seed,
save_gif=args.save,
)