from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import numpy as np
IMG_DIR = Path(__file__).resolve().parent.parent / "docs" / "orgmode" / "img"
IMG_DIR.mkdir(parents=True, exist_ok=True)
FONT = "Jost"
plt.rcParams.update({
"font.family": "sans-serif",
"font.sans-serif": [FONT, "DejaVu Sans"],
"font.size": 11,
"axes.titlesize": 12,
"axes.labelsize": 11,
"xtick.labelsize": 10,
"ytick.labelsize": 10,
"figure.facecolor": "white",
"axes.facecolor": "#FAFAFA",
"axes.edgecolor": "#CCCCCC",
"axes.grid": True,
"grid.alpha": 0.3,
})
TEAL = "#004D40"
CORAL = "#FF655D"
YELLOW = "#F1DB4B"
LIGHT_TEAL = "#4DB6AC"
GRAY = "#9E9E9E"
def plot_throughput():
datasets = ["218x100\n(1.6 MiB)", "218x1000\n(9.7 MiB)", "10kx100\n(46.9 MiB)", "10kx10\n(4.7 MiB)"]
c_times = [10.6, 73, 361, 36]
ase_times = [36.1, 286, 956, 94]
readcon_times = [4.4, 55, 185, 13]
x = np.arange(len(datasets))
w = 0.25
fig, ax = plt.subplots(figsize=(10, 5))
bars_ase = ax.bar(x - w, ase_times, w, label="ASE (Python)", color=CORAL, edgecolor="white", linewidth=0.5)
bars_c = ax.bar(x, c_times, w, label="C sscanf (eOn-style)", color=YELLOW, edgecolor="white", linewidth=0.5)
bars_rc = ax.bar(x + w, readcon_times, w, label="readcon-core (Rust)", color=TEAL, edgecolor="white", linewidth=0.5)
ax.set_ylabel("Parse time (ms, lower is better)")
ax.set_title("Parsing throughput across trajectory sizes")
ax.set_xticks(x)
ax.set_xticklabels(datasets)
ax.legend(loc="upper left")
ax.set_yscale("log")
ax.set_ylim(1, 2000)
for i, (rc, ase) in enumerate(zip(readcon_times, ase_times)):
ax.annotate(f"{ase/rc:.1f}x", xy=(x[i] + w, rc), xytext=(0, 5),
textcoords="offset points", ha="center", fontsize=9, color=TEAL, fontweight="bold")
plt.tight_layout()
out = IMG_DIR / "parsing_throughput.svg"
plt.savefig(out, dpi=150)
plt.close()
print(f" {out}")
def plot_memory():
datasets = ["218x1000\n(9.7 MiB)", "10kx100\n(46.9 MiB)", "10kx10\n(4.7 MiB)"]
readcon_rss = [70, 263, 263]
ase_rss = [268, 270, 270]
x = np.arange(len(datasets))
w = 0.3
fig, ax = plt.subplots(figsize=(8, 4.5))
ax.bar(x - w/2, ase_rss, w, label="ASE", color=CORAL, edgecolor="white")
ax.bar(x + w/2, readcon_rss, w, label="readcon-core", color=TEAL, edgecolor="white")
ax.set_ylabel("Peak RSS (MiB, lower is better)")
ax.set_title("Memory usage (all frames loaded)")
ax.set_xticks(x)
ax.set_xticklabels(datasets)
ax.legend()
for i, (rc, ase) in enumerate(zip(readcon_rss, ase_rss)):
if ase > rc * 1.5:
ax.annotate(f"{ase/rc:.1f}x less", xy=(x[i] + w/2, rc), xytext=(0, 5),
textcoords="offset points", ha="center", fontsize=9, color=TEAL, fontweight="bold")
plt.tight_layout()
out = IMG_DIR / "memory_usage.svg"
plt.savefig(out, dpi=150)
plt.close()
print(f" {out}")
def plot_feature_matrix():
formats = [
"CON v2",
"XYZ",
"POSCAR",
"extxyz",
"CIF",
"GRO",
"LAMMPS",
"PDB",
"DCD",
"TRR",
]
features = [
"Positions", "Velocities", "Forces", "Unit cell",
"Per-dir\nconstraints", "Atom ID\n(round-trip)", "Metadata\n(JSON)",
"Compression", "Multi-frame", "Streaming\niterator",
]
data = np.array([
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 0.5, 0, 0.5, 0, 0, 0, 0, 1, 1],
[1, 0, 0, 1, 0.5, 0, 0, 0, 0, 0],
[1, 1, 1, 1, 0, 0, 0.5, 0, 1, 1],
[1, 0, 0, 1, 0, 0, 0.5, 0, 0, 0],
[1, 1, 0, 1, 0, 0, 0, 0, 1, 1],
[1, 1, 1, 1, 0, 0, 0.5, 0, 1, 1],
[1, 0, 0, 1, 0, 0, 0.5, 0, 1, 0],
[1, 0, 0, 1, 0, 0, 0, 0, 1, 1],
[1, 1, 0, 1, 0, 0, 0, 0, 1, 1],
])
fig, ax = plt.subplots(figsize=(12, 5.5))
cmap = matplotlib.colors.ListedColormap(["#FFEBEE", "#FFF9C4", "#E0F2F1"])
bounds = [-0.25, 0.25, 0.75, 1.25]
norm = matplotlib.colors.BoundaryNorm(bounds, cmap.N)
im = ax.imshow(data, cmap=cmap, norm=norm, aspect="auto")
ax.set_xticks(range(len(features)))
ax.set_xticklabels(features, rotation=45, ha="right", fontsize=9)
ax.set_yticks(range(len(formats)))
ax.set_yticklabels(formats, fontsize=10)
for i in range(len(formats)):
for j in range(len(features)):
v = data[i, j]
text = {1.0: "Y", 0.5: "~", 0.0: "N"}[v]
color = {1.0: TEAL, 0.5: "#F57F17", 0.0: CORAL}[v]
ax.text(j, i, text, ha="center", va="center", fontsize=12,
fontweight="bold", color=color)
ax.add_patch(plt.Rectangle((-0.5, -0.5), len(features), 1,
fill=False, edgecolor=TEAL, linewidth=2.5))
ax.set_title("Feature comparison: CON v2 vs common atomic structure formats")
patches = [
mpatches.Patch(facecolor="#E0F2F1", edgecolor=TEAL, label="Supported"),
mpatches.Patch(facecolor="#FFF9C4", edgecolor="#F57F17", label="Partial"),
mpatches.Patch(facecolor="#FFEBEE", edgecolor=CORAL, label="Not supported"),
]
ax.legend(handles=patches, loc="lower right", fontsize=9)
plt.tight_layout()
out = IMG_DIR / "feature_comparison.svg"
plt.savefig(out, dpi=150)
plt.close()
print(f" {out}")
def plot_pareto():
points = [
("CON v2\n(readcon-core)", 10, 4.4, TEAL),
("CON v2\n(C sscanf)", 10, 10.6, LIGHT_TEAL),
("extxyz\n(ASE)", 6, 36.1, CORAL), ("XYZ\n(chemfiles)", 3, 8.0, YELLOW), ("POSCAR\n(ASE)", 3, 15.0, CORAL), ("GRO\n(chemfiles)", 5, 10.0, YELLOW), ("LAMMPS\n(chemfiles)", 7, 12.0, YELLOW), ("DCD\n(MDAnalysis)", 4, 3.0, GRAY), ("TRR\n(chemfiles)", 5, 5.0, YELLOW), ]
fig, ax = plt.subplots(figsize=(10, 6))
for name, feat, ms, color in points:
ax.scatter(feat, ms, s=120, c=color, edgecolors="white", linewidth=1.5, zorder=3)
offset = (8, -12) if "readcon" in name else (8, 5)
ax.annotate(name, (feat, ms), textcoords="offset points",
xytext=offset, fontsize=8, ha="left")
ax.plot([10, 10], [0, 4.4], '--', color=TEAL, alpha=0.3, linewidth=1)
ax.plot([0, 10], [4.4, 4.4], '--', color=TEAL, alpha=0.3, linewidth=1)
ax.set_xlabel("Features supported (out of 10)")
ax.set_ylabel("Parse time for 100 frames x 218 atoms (ms, lower is better)")
ax.set_title("Feature coverage vs parsing performance")
ax.set_xlim(1, 11.5)
ax.set_ylim(0, 42)
ax.invert_yaxis()
ax.text(9, 38, "Ideal:\nfast + feature-rich", ha="center", fontsize=9,
color=TEAL, fontstyle="italic", alpha=0.7)
ax.text(2, 3, "Fast but\nfeature-poor", ha="center", fontsize=9,
color=GRAY, fontstyle="italic", alpha=0.7)
plt.tight_layout()
out = IMG_DIR / "pareto_features_vs_speed.svg"
plt.savefig(out, dpi=150)
plt.close()
print(f" {out}")
if __name__ == "__main__":
print("Generating plots...")
plot_throughput()
plot_memory()
plot_feature_matrix()
plot_pareto()
print("Done.")