import matplotlib.pyplot as plt
import matplotlib.dates as mdates
from datetime import datetime, timedelta
# --- Estilo académico, paleta del sitio ---------------------------------------
paper, ink, accent, grey = "#fcfcfa", "#22262b", "#3a5a7a", "#8a8f96"
colors = {
"model": "#2a6f68", # teal
"symbolic": "#b45f06", # amber
"qft": "#6b4c7a", # muted purple
"analysis": "#9e4a4a", # muted red
"roadmap": accent, # slate
}
plt.rcParams.update({
"font.family": "serif",
"font.size": 10,
"axes.edgecolor": ink,
"axes.linewidth": 0.8,
"figure.facecolor": paper,
"axes.facecolor": paper,
"savefig.facecolor": paper,
})
# --- Datos --------------------------------------------------------------------
llm_events = [
(datetime(2020, 6, 11), "GPT-3"),
(datetime(2022, 11, 30), "ChatGPT"),
(datetime(2023, 3, 14), "GPT-4"),
(datetime(2023, 7, 19), "Llama 2"),
(datetime(2024, 3, 14), "Claude 3"),
(datetime(2024, 12, 6), "Gemini 2 / o1"),
(datetime(2025, 9, 17), ""), # etiqueta manual más abajo
]
phys_events = [
(datetime(2025, 1, 9), "Barman (LPMs)", "roadmap"),
(datetime(2025, 2, 7), "v. Hippel (IBP)", "symbolic"),
(datetime(2026, 3, 30), "Albert", "model"),
(datetime(2026, 4, 1), "Yu (QFT/strings)", "qft"),
(datetime(2026, 4, 20), "Woodward (QFT)", "qft"),
(datetime(2026, 5, 6), "Hell & Thiele\n(LLM+CAS)", "symbolic"),
(datetime(2026, 6, 9), "DarkAgents", "model"),
(datetime(2026, 6, 11), "AgentRivet", "analysis"),
(datetime(2026, 6, 17), "Bom (DE)", "model"),
(datetime(2026, 6, 19), "bsm_agent", "model"),
]
events_2025 = [e for e in phys_events if e[0].year == 2025]
events_2026 = [e for e in phys_events if e[0].year == 2026]
# --- Figura: eje quiebrado en el panel inferior -------------------------------
fig = plt.figure(figsize=(7.2, 5.4))
gs = fig.add_gridspec(2, 2, height_ratios=[0.7, 1.3], width_ratios=[0.30, 0.70])
# Panel superior: hitos de LLMs
ax_top = fig.add_subplot(gs[0, :])
ax_top.axhline(0, color=grey, lw=0.8, alpha=0.5, zorder=1)
for i, (date, label) in enumerate(llm_events):
ax_top.vlines(date, -0.08, 0.08, color=grey, lw=1.2, zorder=2)
ax_top.plot(date, 0.08, "o", color=ink, markersize=4, zorder=3)
if i % 2 == 0:
ax_top.text(date, 0.12, label, rotation=25, ha="left", va="bottom",
fontsize=8.5, color=ink, zorder=3)
else:
ax_top.text(date, -0.12, label, rotation=25, ha="left", va="top",
fontsize=8.5, color=ink, zorder=3)
# Etiqueta del último hito arriba, alineada a la derecha para no salirse
ax_top.text(datetime(2025, 9, 17), 0.12, "Modelos de\nrazonamiento",
rotation=0, ha="right", va="bottom", fontsize=8.5, color=ink, zorder=3)
ax_top.text(datetime(2019, 9, 1), 0, "LLMs", ha="right", va="center",
fontsize=10, color=ink, weight="bold")
ax_top.set_ylim(-0.38, 0.38)
ax_top.set_yticks([])
ax_top.xaxis.set_major_locator(mdates.YearLocator())
ax_top.xaxis.set_major_formatter(mdates.DateFormatter("%Y"))
ax_top.xaxis.set_minor_locator(mdates.MonthLocator(interval=6))
plt.setp(ax_top.get_xticklabels(), rotation=0, ha="center", fontsize=9)
ax_top.tick_params(axis="x", colors=ink, length=4)
for s in ["left", "right", "top"]:
ax_top.spines[s].set_visible(False)
ax_top.spines["bottom"].set_color(ink)
ax_top.set_xlim(datetime(2019, 8, 1), datetime(2026, 9, 1))
# Panel inferior izquierdo: 2025
ax_l = fig.add_subplot(gs[1, 0])
ax_l.axhline(0, color=accent, lw=0.8, alpha=0.5, zorder=1)
ax_l.axvspan(datetime(2025, 1, 1), datetime(2025, 3, 1),
color=accent, alpha=0.06, zorder=0)
for i, (date, label, cat) in enumerate(events_2025):
c = colors[cat]
side = 1 if i % 2 == 0 else -1
y_dot = side * 0.15
y_text = side * 0.23
ax_l.vlines(date, 0, y_dot, color=c, lw=1.2, zorder=2)
ax_l.plot(date, y_dot, "o", color=c, markersize=4, zorder=3)
va = "bottom" if side == 1 else "top"
ax_l.text(date + timedelta(days=3), y_text, label, rotation=0, ha="left", va=va,
fontsize=7.5, color=c, zorder=3)
ax_l.set_ylim(-0.30, 0.30)
ax_l.set_yticks([])
ax_l.xaxis.set_major_locator(mdates.MonthLocator())
ax_l.xaxis.set_major_formatter(mdates.DateFormatter("%b"))
plt.setp(ax_l.get_xticklabels(), rotation=0, ha="center", fontsize=8)
ax_l.tick_params(axis="x", colors=ink, length=3)
ax_l.set_xlim(datetime(2024, 11, 1), datetime(2025, 4, 1))
for s in ["left", "right", "top"]:
ax_l.spines[s].set_visible(False)
ax_l.spines["bottom"].set_color(ink)
# Panel inferior derecho: 2026 ampliado (con etiquetas)
ax_r = fig.add_subplot(gs[1, 1])
ax_r.axhline(0, color=accent, lw=0.8, alpha=0.5, zorder=1)
ax_r.axvspan(datetime(2026, 3, 15), datetime(2026, 6, 30),
color=accent, alpha=0.06, zorder=0)
zoom_levels = [
(0.20, 1, 0), # Albert
(0.20, -1, 0), # Yu
(0.38, 1, 0), # Woodward
(0.38, -1, 0), # Hell
(0.56, 1, 8), # DarkAgents
(0.56, -1, -8), # AgentRivet
(0.74, 1, -8), # Bom
(0.74, -1, 8), # bsm_agent
]
for (date, label, cat), (lvl, side, xoff) in zip(events_2026, zoom_levels):
c = colors[cat]
y_dot = side * lvl
y_text = side * (lvl + 0.08)
x_text = date + timedelta(days=xoff)
ax_r.vlines(date, 0, y_dot, color=c, lw=1.2, zorder=2)
ax_r.plot(date, y_dot, "o", color=c, markersize=3.5, zorder=3)
va = "bottom" if side == 1 else "top"
ha = "left" if xoff >= 0 else "right"
ax_r.text(x_text, y_text, label, rotation=0, ha=ha, va=va,
fontsize=7.0, color=c, zorder=3)
ax_r.set_ylim(-0.95, 0.95)
ax_r.set_yticks([])
ax_r.xaxis.set_major_locator(mdates.MonthLocator())
ax_r.xaxis.set_major_formatter(mdates.DateFormatter("%b"))
plt.setp(ax_r.get_xticklabels(), rotation=0, ha="center", fontsize=8)
ax_r.tick_params(axis="x", colors=ink, length=3)
ax_r.set_xlim(datetime(2026, 3, 10), datetime(2026, 8, 5))
for s in ["left", "right", "top"]:
ax_r.spines[s].set_visible(False)
ax_r.spines["bottom"].set_color(ink)
# Marcas de quiebre en la unión de los dos ejes inferiores
kw = dict(transform=ax_l.transAxes, color=ink, clip_on=False, lw=0.8)
ax_l.plot((0.96, 0.99), (-0.018, 0.012), **kw)
ax_l.plot((0.96, 0.99), (0.012, -0.018), **kw)
kw = dict(transform=ax_r.transAxes, color=ink, clip_on=False, lw=0.8)
ax_r.plot((0.01, 0.04), (-0.018, 0.012), **kw)
ax_r.plot((0.01, 0.04), (0.012, -0.018), **kw)
# Etiqueta del eje inferior
fig.text(0.02, 0.30, "Física teórica", ha="left", va="center",
fontsize=10, color=accent, weight="bold", rotation=90)
# Leyenda de categorías (a nivel de figura, centrada entre paneles inferiores)
from matplotlib.lines import Line2D
legend_elements = [
Line2D([0], [0], marker="o", color="w", markerfacecolor=colors["model"],
markersize=6, label="Model building"),
Line2D([0], [0], marker="o", color="w", markerfacecolor=colors["symbolic"],
markersize=6, label="Simbólico/ CAS"),
Line2D([0], [0], marker="o", color="w", markerfacecolor=colors["qft"],
markersize=6, label="QFT"),
Line2D([0], [0], marker="o", color="w", markerfacecolor=colors["analysis"],
markersize=6, label="Análisis experimental"),
Line2D([0], [0], marker="o", color="w", markerfacecolor=colors["roadmap"],
markersize=6, label="Roadmap"),
]
ax_top.legend(handles=legend_elements, loc="lower left", fontsize=6.8,
frameon=True, facecolor=paper, edgecolor=grey,
ncol=5, bbox_to_anchor=(0.0, -0.31))
fig.tight_layout(rect=[0.04, 0, 1, 1])
plt.show()