-
Notifications
You must be signed in to change notification settings - Fork 0
Benchmarking
The benchmark module orchestrates multi-algorithm comparison on a single problem. It runs each algorithm multiple times with independent seeds, then evaluates a suite of metrics at regularly-sampled time points.
from dfbench.benchmark import Benchmark, AlgorithmConfig
from dfbench.algorithms import AdamGD, EvoxPSO, BotorchBO
from dfbench.problems import VoyagerProblem
problem = VoyagerProblem()
configs = [
AlgorithmConfig(AdamGD(), {"learning_rate": 0.1}, name="Adam_lr0.1"),
AlgorithmConfig(EvoxPSO(variant="PSO"), {"pop_size": 100}, name="PSO_100"),
AlgorithmConfig(BotorchBO(), {"n_initial": 50}, name="BO"),
]
benchmark = Benchmark(
problem=problem,
success_loss=0.1,
configs=configs,
n_runs=100,
max_time=300,
)
results = benchmark.run()
benchmark.print_summary(results)Rather than reporting a single final number, the benchmark evaluates every metric at multiple time slices. This produces curves showing how each metric evolves over wall-clock time, enabling fair comparison between algorithms with very different per-iteration costs.
Given max_time=300 and n_time_samples=100, metrics are computed at [3, 6, 9, …, 300] seconds. At each time point elapsed_time ≤ t, and metrics are computed on that prefix.
Iteration count is a poor comparison axis: one Adam iteration (1 eval) takes ~12 ms, while one BO iteration (1 acquisition + 1 eval) takes ~500 ms after a GP fit. Wall-clock time is the only fair common axis.
A run is "successful" when it achieves a loss below success_loss. This is a problem-specific value chosen by the user — it defines what counts as a "good enough" solution for a given physics problem.
Wraps an algorithm instance with the hyperparameters that should be passed to optimize().
AlgorithmConfig(
algorithm: OptimizationAlgorithm, # algorithm instance
hyperparameters: dict | None = None, # kwargs for optimize()
name: str | None = None, # display name (default: algorithm_str)
)Why a separate config instead of passing kwargs directly?
So the same algorithm instance can appear twice with different hyperparameters (e.g., Adam_lr0.01 vs Adam_lr0.1).
Benchmark(
problem: ContinuousProblem, # the problem to benchmark on
success_loss: float, # loss threshold for "success"
configs: list[AlgorithmConfig], # algorithms to compare
n_runs: int = 100, # independent runs per algorithm
max_time: float = 300.0, # wall-clock budget per run (seconds)
n_time_samples: int = 100, # metric evaluation points
random_baseline_loss: float | None = None, # for normalized AUC
random_seed: int | None = None, # master RNG seed
storage_backend: StorageBackend | None = None, # where run data is stored
)| Parameter | Default | Notes |
|---|---|---|
n_runs |
100 | More runs → more reliable statistics, but linear time cost |
max_time |
300 | Seconds. All algorithms get the same wall-clock budget |
n_time_samples |
100 | Points to sample in [max_time/n, max_time]
|
random_baseline_loss |
None |
Expected loss of random guess. If set, AUC metrics are normalized |
random_seed |
None |
If set, generates deterministic per-run seeds from this master seed |
storage_backend |
None |
Where benchmark run data (NPZ/JSON/CSV) is physically stored. Defaults to a LocalFilesystemBackend (cwd-relative). Swapping this redirects all benchmark artifacts (e.g. to a scratch disk or S3-backed prefix) without code changes. See Storage & Checkpointing. |
results = benchmark.run(
verbose: int = 1,
save_csv: bool = True,
save_run_data: bool = False,
load_from: str | Path | None = None,
output_dir: str | Path = "./data/benchmark_run_data",
) -> list[BenchmarkResult]| Parameter | Description |
|---|---|
save_csv |
Write all time-sampled metrics to a CSV file in ./data/benchmark_results/
|
save_run_data |
Save raw RunData (per-evaluation losses, times, params) to NPZ files |
load_from |
Path to a previously saved run data directory — skips running, re-evaluates metrics only |
output_dir |
Base directory for run data NPZ files |
AlgorithmConfig[] ← user defines
│
▼
Benchmark.run()
│
├── _collect_all_run_data() ← runs algorithms
│ │
│ └── _collect_algorithm_runs() ← n_runs × optimize()
│ │
│ └── RunData.from_objective() ← extracts arrays
│
├── _evaluate_algorithm() ← computes metrics at each time slice
│ │
│ ├── slice_history_at_time()
│ ├── run_min_loss(), run_has_success(), run_auc(), ...
│ ├── agg_mean_std(), agg_fraction_true(), ...
│ └── multi_solution_diversity_*(), compute_performance_profile()
│
└── BenchmarkResult[] ← returned
│
├── print_summary() ← console table
└── _save_results_to_csv() ← CSV file
Serializable data extracted from one Objective after optimization:
| Field | Shape | Description |
|---|---|---|
loss_history |
(n_evals,) |
Loss at each evaluation |
time_steps |
(n_evals,) |
Elapsed wall-clock time at each evaluation |
params_history |
(n_evals, n_params) |
Bounded parameters at each evaluation |
best_loss |
scalar | Global best loss |
best_params |
(n_params,) |
Parameters corresponding to best_loss
|
eval_count |
int |
Total evaluation count |
Created via RunData.from_objective(obj) which reads the Objective's reduced (non-batched) properties.
Groups all runs for one algorithm configuration:
| Field | Type | Description |
|---|---|---|
algorithm_name |
str |
Display name from AlgorithmConfig
|
runs |
list[RunData] |
One per independent run |
hyperparameters |
dict |
Kwargs passed to optimize()
|
Time-sampled metrics for one algorithm. Every metric has shape (n_time_samples,).
Single-value metrics (SingleMetric):
| Metric | Meaning |
|---|---|
fraction_of_success |
Fraction of runs with loss < success_loss
|
min_loss |
Global minimum loss across all runs |
performance_profile_auc |
Normalized AUC of the empirical CDF of final losses |
auc_top_1 |
AUC of the single best run |
Aggregate metrics (AggregateMetric, has .mean and .std):
| Metric | Meaning |
|---|---|
avg_loss |
Per-run minimum loss, averaged |
time_to_success |
Wall-clock time to first success (successful runs only) |
evals_to_success |
Evaluation count to first success (successful runs only) |
solution_diversity_overall |
Mean pairwise distance of successful solutions |
solution_diversity_nn |
Mean nearest-neighbor distance of successful solutions |
auc_top_10 |
AUC statistics of top 10% runs by final loss |
results = benchmark.run(save_run_data=True)This creates a timestamped directory under ./data/benchmark_run_data/:
data/benchmark_run_data/
└── VoyagerProblem_2024-01-15_14-30-00/
├── metadata.json
├── Adam_lr0.1.npz
├── PSO_100.npz
└── BO.npz
metadata.json stores the benchmark configuration (problem name, success threshold, seeds, algorithm list). Each .npz file contains all runs for one algorithm in NumPy's compressed format. All files are written atomically through the configured StorageBackend (temp-in-same-dir + os.replace), so an interrupted benchmark never leaves half-written files. See Storage & Checkpointing.
results = benchmark.run(load_from="./data/benchmark_run_data/VoyagerProblem_2024-01-15_14-30-00")This skips running algorithms entirely and re-evaluates metrics from saved data. Useful for:
- Adjusting
success_lossorn_time_sampleswithout re-running - Computing new metrics on old data
- Moving data between machines
Older data used all_wall_time_indices instead of per-evaluation time_steps. The loader auto-detects the format and converts legacy data via linear interpolation between wall-time checkpoints.
save_csv=True (the default) writes a CSV file to ./data/benchmark_results/:
benchmark_VoyagerProblem_2024-01-15_14-30-00.csv
Each row is one (algorithm, time_sample) pair. Columns include _mean and _std for every metric. This is the primary format for downstream plotting and analysis.
If random_seed is provided, the Benchmark generates deterministic per-run seeds:
rng = np.random.RandomState(self._random_seed)
run_seeds = [int(rng.randint(0, 2**31)) for _ in range(self._n_runs)]Each run gets the same seed regardless of which algorithm is being evaluated. This means run i always starts from the same random state across all algorithms, reducing variance in comparisons.
benchmark.print_summary(results)==========================================================================================
BENCHMARK SUMMARY (at final time)
==========================================================================================
Algorithm Success% Min Loss Avg Loss Time(s)
------------------------------------------------------------------------------------------
Adam_lr0.1 85.0% 0.012345 0.0567±0.0234 45.2±12.3
PSO_100 72.0% 0.023456 0.0890±0.0456 78.9±25.1
BO 68.0% 0.034567 0.1123±0.0678 120.5±42.7
==========================================================================================
Values shown are from the final time sample (i.e., at max_time).
Different algorithms have vastly different per-iteration costs. A BO iteration includes fitting a GP and optimizing an acquisition function; an Adam iteration is a single forward+backward pass. Comparing at "iteration 1000" is meaningless — but comparing at "t = 60 s" is fair.
Metrics evolve as the project matures. Saving raw (loss, time, params) tuples means new metrics can be computed on old data without re-running expensive experiments. The load_from parameter enables this workflow.
NumPy's NPZ format is:
- Zero-dependency (no
h5pyorpyarrowneeded) - Handles ragged arrays via
dtype=object - Fast for small-to-medium datasets (typical benchmark: ~100 runs × ~10k evals ≈ 1 MB)
Artificial Scientist Lab | Website |University of Tübingen
Department of Computer Science
| Read our Documentation | Contact: laurin.sefa@student.uni-tuebingen.de, mario.krenn@uni-tuebingen.de, soham.basu@uni-tuebingen.de
Getting Started
Core API
Benchmarking
Contributing
Reference