| """ |
| 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 |
|
|
| |
|
|
| _BG = '#0b0c10' |
| _PANEL = '#0f1117' |
| _GRID = '#1a1d2e' |
| _TEXT = '#c8d0e0' |
| _CYAN = '#00e5ff' |
| _ORANGE = '#ff6b35' |
| _GOLD = '#ffd060' |
| _DIM = '#444466' |
|
|
| |
|
|
| 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 |
|
|
|
|
| |
|
|
| 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 |
|
|
| 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) |
|
|
| |
| 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 |
|
|
| |
| self.h_pos = [] |
| self.h_alpha = [] |
| self.h_beta = [] |
| self.h_obj = [] |
| self.h_best = [] |
| self.h_fidelity = [] |
| self.h_entangled = [] |
| self.h_tunneled = [] |
|
|
| |
|
|
| 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) |
| ]) |
|
|
| |
|
|
| 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 |
| ] |
|
|
| |
| 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) |
|
|
| |
| 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) |
|
|
| |
| attract = 0.2 * self.rng.rand() * (self.best_pos - self.pos[i]) |
|
|
| |
| tunnel = np.zeros(2) |
| if self.stagnation[i] > 3: |
| prob = float(abs(self._psi(i)[1])**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 |
|
|
| |
| 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] |
| ) |
|
|
| |
| 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() |
|
|
|
|
| |
|
|
| 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): |
| |
| 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') |
|
|
| |
| 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) |
|
|
| |
| 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) |
|
|
| |
| 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') |
|
|
| |
| 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) |
|
|
| |
| ent_objs = [] |
|
|
| |
| 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) |
|
|
| |
| 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) |
|
|
| |
| 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) |
|
|
| |
| 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) |
|
|
| |
| 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' |
| ) |
|
|
| |
|
|
| 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)]) |
|
|
| |
| norm_a = alphas / np.pi |
| colors = cmap_q(norm_a) |
|
|
| |
| spread = objs_f.max() - objs_f.min() + 1e-9 |
| norm_obj = (objs_f - objs_f.min()) / spread |
| sizes = 55 + 160 * (1 - norm_obj) |
|
|
| |
| for i in tunneled: |
| colors[i] = np.array([1.0, 1.0, 1.0, 1.0]) |
| sizes[i] = 380 |
|
|
| |
| scat._offsets3d = (pos[:, 0], pos[:, 1], zs + 0.08) |
| scat.set_color(colors) |
| scat.set_sizes(sizes) |
|
|
| |
| 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) |
| |
| rc = list(cmap_q(norm_a[i])[:3]) + [0.22] |
| ln.set_color(rc) |
|
|
| |
| 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) |
|
|
| |
| cv_line.set_data(range(frame + 1), sim.h_best[:frame + 1]) |
| cv_dot.set_data([frame], [best]) |
|
|
| |
| 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) |
|
|
| |
| 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 |
|
|
| |
|
|
| anim = FuncAnimation( |
| fig, update, |
| frames=num_iters, |
| interval=90, |
| blit=False |
| ) |
|
|
| 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 |
|
|
|
|
| |
|
|
| 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, |
| ) |
|
|