blob: 72a03c1d009939599177d1519a862adb52105936 [file]
#!/usr/bin/env python3
"""
Visualization script for adversarial experiment results.
Generates publication-quality figures for BFT protocol comparison papers.
Usage:
python3 plot_results.py --results-dir adversarial_results/ --output figures/
Requires: matplotlib, numpy (install with: pip3 install matplotlib numpy)
"""
import argparse
import csv
import json
import os
import sys
try:
import matplotlib
matplotlib.use('Agg') # Non-interactive backend
import matplotlib.pyplot as plt
import numpy as np
except ImportError:
print("ERROR: matplotlib and numpy required. Install with: pip3 install matplotlib numpy")
sys.exit(1)
# Paper-friendly style
plt.rcParams.update({
'font.size': 12,
'font.family': 'serif',
'axes.labelsize': 14,
'axes.titlesize': 14,
'xtick.labelsize': 11,
'ytick.labelsize': 11,
'legend.fontsize': 11,
'figure.figsize': (8, 5),
'figure.dpi': 300,
'savefig.bbox': 'tight',
'savefig.pad_inches': 0.1,
})
PROTOCOL_COLORS = {
'fides': '#2196F3',
'damysus': '#FF5722',
'achilles': '#4CAF50',
'hotstuff': '#9C27B0',
'pbft': '#FF9800',
}
PROTOCOL_MARKERS = {
'fides': 'o',
'damysus': 's',
'achilles': '^',
'hotstuff': 'D',
'pbft': 'v',
}
def load_timeseries_csv(filepath):
"""Load a timeseries CSV file."""
data = []
with open(filepath) as f:
reader = csv.DictReader(f)
for row in reader:
entry = {}
for k, v in row.items():
try:
entry[k] = float(v) if v else 0
except ValueError:
entry[k] = v
data.append(entry)
return data
def find_result_dirs(base_dir, pattern=""):
"""Find result directories matching a pattern."""
dirs = []
if not os.path.isdir(base_dir):
return dirs
for name in sorted(os.listdir(base_dir)):
path = os.path.join(base_dir, name)
if os.path.isdir(path) and (not pattern or pattern in name):
dirs.append(path)
return dirs
def plot_throughput_timeseries(csv_files, labels, output_path, title="Throughput Over Time"):
"""Plot throughput time-series for multiple experiments/protocols."""
fig, ax = plt.subplots()
for csv_file, label in zip(csv_files, labels):
data = load_timeseries_csv(csv_file)
times = [d.get('time_s', 0) for d in data]
tps = [d.get('tps', 0) for d in data]
protocol = label.split('_')[0].lower() if '_' in label else label.lower()
color = PROTOCOL_COLORS.get(protocol, '#333333')
marker = PROTOCOL_MARKERS.get(protocol, 'o')
ax.plot(times, tps, label=label, color=color, marker=marker,
markersize=4, linewidth=1.5)
ax.set_xlabel('Time (seconds)')
ax.set_ylabel('Throughput (txn/s)')
ax.set_title(title)
ax.legend()
ax.grid(True, alpha=0.3)
ax.set_ylim(bottom=0)
plt.savefig(output_path)
plt.close()
print(f" Saved: {output_path}")
def plot_throughput_vs_crashes(crash_data, output_path):
"""Plot throughput degradation vs number of crash faults.
crash_data: dict of {protocol: [(num_crashes, avg_tps), ...]}
"""
fig, ax = plt.subplots()
for protocol, points in crash_data.items():
points.sort(key=lambda x: x[0])
crashes = [p[0] for p in points]
tps = [p[1] for p in points]
color = PROTOCOL_COLORS.get(protocol, '#333333')
marker = PROTOCOL_MARKERS.get(protocol, 'o')
ax.plot(crashes, tps, label=protocol.capitalize(), color=color,
marker=marker, markersize=6, linewidth=2)
ax.set_xlabel('Number of Crash Faults')
ax.set_ylabel('Average Throughput (txn/s)')
ax.set_title('Throughput vs. Crash Faults')
ax.legend()
ax.grid(True, alpha=0.3)
ax.set_ylim(bottom=0)
plt.savefig(output_path)
plt.close()
print(f" Saved: {output_path}")
def plot_throughput_vs_latency(latency_data, output_path):
"""Plot throughput under different network delays.
latency_data: dict of {protocol: [(delay_ms, avg_tps), ...]}
"""
fig, ax = plt.subplots()
for protocol, points in latency_data.items():
points.sort(key=lambda x: x[0])
delays = [p[0] for p in points]
tps = [p[1] for p in points]
color = PROTOCOL_COLORS.get(protocol, '#333333')
marker = PROTOCOL_MARKERS.get(protocol, 'o')
ax.plot(delays, tps, label=protocol.capitalize(), color=color,
marker=marker, markersize=6, linewidth=2)
ax.set_xlabel('Network Delay (ms)')
ax.set_ylabel('Average Throughput (txn/s)')
ax.set_title('Throughput vs. Network Delay')
ax.legend()
ax.grid(True, alpha=0.3)
ax.set_xscale('log')
ax.set_ylim(bottom=0)
plt.savefig(output_path)
plt.close()
print(f" Saved: {output_path}")
def plot_ablation(ablation_data, output_path):
"""Plot TEE component ablation study.
ablation_data: dict of {config_name: avg_tps}
"""
fig, ax = plt.subplots(figsize=(8, 5))
configs = list(ablation_data.keys())
tps_values = [ablation_data[c] for c in configs]
colors = ['#2196F3', '#64B5F6', '#90CAF9', '#BBDEFB'][:len(configs)]
bars = ax.bar(configs, tps_values, color=colors, edgecolor='black', linewidth=0.5)
# Add value labels on bars
for bar, val in zip(bars, tps_values):
ax.text(bar.get_x() + bar.get_width() / 2, bar.get_height() + max(tps_values) * 0.02,
f'{val:.0f}', ha='center', va='bottom', fontsize=10)
ax.set_ylabel('Average Throughput (txn/s)')
ax.set_title('TEE Component Ablation Study')
ax.set_ylim(0, max(tps_values) * 1.15)
ax.grid(True, alpha=0.3, axis='y')
plt.savefig(output_path)
plt.close()
print(f" Saved: {output_path}")
def plot_phase_comparison(phase_data, output_path):
"""Plot bar chart comparing phases across protocols.
phase_data: dict of {protocol: {phase_name: avg_tps}}
"""
fig, ax = plt.subplots(figsize=(10, 5))
protocols = list(phase_data.keys())
if not protocols:
return
phases = list(phase_data[protocols[0]].keys())
n_protocols = len(protocols)
n_phases = len(phases)
x = np.arange(n_phases)
width = 0.8 / n_protocols
for i, protocol in enumerate(protocols):
values = [phase_data[protocol].get(phase, 0) for phase in phases]
color = PROTOCOL_COLORS.get(protocol, '#333333')
offset = (i - n_protocols / 2 + 0.5) * width
ax.bar(x + offset, values, width, label=protocol.capitalize(),
color=color, edgecolor='black', linewidth=0.5)
ax.set_xlabel('Experiment Phase')
ax.set_ylabel('Average Throughput (txn/s)')
ax.set_title('Per-Phase Throughput Comparison')
ax.set_xticks(x)
ax.set_xticklabels(phases, rotation=15)
ax.legend()
ax.grid(True, alpha=0.3, axis='y')
ax.set_ylim(bottom=0)
plt.savefig(output_path)
plt.close()
print(f" Saved: {output_path}")
def auto_plot(results_dir, output_dir):
"""Auto-detect result directories and generate appropriate plots."""
os.makedirs(output_dir, exist_ok=True)
# Find all timeseries CSVs
csv_files = []
labels = []
for root, dirs, files in os.walk(results_dir):
for f in files:
if f == 'timeseries.csv':
csv_path = os.path.join(root, f)
label = os.path.basename(root)
csv_files.append(csv_path)
labels.append(label)
if csv_files:
print(f"\nFound {len(csv_files)} timeseries CSV files")
# Group by scenario type
scenario_groups = {}
for csv_file, label in zip(csv_files, labels):
# Extract scenario name (e.g., "gst_ramp" from "gst_ramp_20260327_120000")
parts = label.rsplit('_', 2)
scenario_name = parts[0] if len(parts) >= 3 else label
if scenario_name not in scenario_groups:
scenario_groups[scenario_name] = ([], [])
scenario_groups[scenario_name][0].append(csv_file)
scenario_groups[scenario_name][1].append(label)
for scenario_name, (files, lbls) in scenario_groups.items():
output_path = os.path.join(output_dir, f"timeseries_{scenario_name}.png")
plot_throughput_timeseries(files, lbls, output_path,
title=f"Throughput: {scenario_name}")
else:
print("No timeseries CSV files found. Run experiments first.")
print(f"Looked in: {results_dir}")
# Generate example figure templates with placeholder data
print("\nGenerating example figure templates...")
# Example: Throughput vs crashes
example_crash_data = {
'fides': [(0, 50000), (1, 45000), (2, 38000), (3, 30000), (4, 22000), (5, 15000)],
'damysus': [(0, 48000), (1, 42000), (2, 5000), (3, 3000), (4, 1000), (5, 500)],
}
plot_throughput_vs_crashes(example_crash_data,
os.path.join(output_dir, "example_crash_faults.png"))
# Example: Throughput vs latency
example_latency_data = {
'fides': [(40, 50000), (100, 35000), (200, 20000), (500, 8000), (1000, 3000), (2000, 1200)],
'damysus': [(40, 48000), (100, 30000), (200, 15000), (500, 2000), (1000, 500), (2000, 0)],
}
plot_throughput_vs_latency(example_latency_data,
os.path.join(output_dir, "example_throughput_vs_latency.png"))
# Example: Ablation
example_ablation = {
'Fides(full)': 50000,
'Fides(-MC)': 42000,
'Fides(-RAC)': 46000,
'Fides(-RNG)': 44000,
}
plot_ablation(example_ablation, os.path.join(output_dir, "example_ablation.png"))
print(f"\nAll figures saved to: {output_dir}")
def main():
parser = argparse.ArgumentParser(description="Plot adversarial experiment results")
parser.add_argument("--results-dir", default="adversarial_results",
help="Directory containing experiment results")
parser.add_argument("--output", default="figures",
help="Output directory for figures")
args = parser.parse_args()
auto_plot(args.results_dir, args.output)
if __name__ == "__main__":
main()