Extending The Benchmark#
Plugging in new datasets or surrogates should not require rewriting orchestration code. This guide captures the supported extension points and the conventions that keep everything interoperable.
Add a dataset#
Use datasets/_data_generation/make_new_dataset.py as a working example. It shows how to generate synthetic trajectories, parameters, and timesteps before calling codes.create_dataset.
Shape your arrays
data:(n_samples, n_timesteps, n_quantities)— the raw trajectories.params(optional):(n_samples, n_parameters)— per-trajectory parameters.timesteps:(n_timesteps,)— shared timeline for every trajectory.labels(optional): list of quantity names with lengthn_quantities.
Call
create_datasetfrom codes import create_dataset create_dataset( "my_new_dataset", data=full_dataset, params=full_params, timesteps=timesteps, labels=labels, split=(0.7, 0.1, 0.2), # train/test/val ratios )
create_datasetwritesdatasets/my_new_dataset/data.hdf5with the following groups:train,test,val, optional*_params, andtimesteps. The helper also ensures consistent shuffling and folder creation.Register the download link (optional) in
datasets/data_sources.yamlsoscripts/download_datasets.pyknows where to fetch the data. The docs pull this file automatically, so your dataset will appear in the catalog without extra work.Reference the dataset in configs via
dataset.name. Log transforms / normalization flags in the config can stay unchanged unless your data needs special treatment.
Once the folder exists, all CLI entry points (run_training.py, run_eval.py, run_tuning.py) will automatically pick it up based on dataset.name.
Add a surrogate model#
Every surrogate must inherit from codes.surrogates.AbstractSurrogate.AbstractSurrogateModel. This class wires together data preparation, training, logging, checkpointing, and evaluation.
Implement the class under
codes/surrogates/<YourModel>/<file>.py.forward(self, inputs) -> tuple[Tensor, Tensor]: receives exactly whatprepare_dataemits (for example(branch_input, trunk_input, targets)in MultiONet). Return(predictions, targets)in that order so the shared training/validation utilities can compute metrics without model-specific branching.prepare_data(...): build train, test, and optional validation dataloaders from the benchmark datasets. Pack batches in the form yourforwardexpects.fit(self, train_loader, test_loader, epochs, position, description, multi_objective=False): train the model. This entry point remains required, including for models using the shared loop below.
Reuse shared helpers
self.setup_progress_bar(...)draws status updates without clashing with the concurrent trainer.self.predict(loader, ...)is the canonical way to produce predictions/targets during validation and evaluation—this ensures consistent batching, buffer pre-allocation, and shape handling for every surrogate. Test your model with it to guarantee compatibility.self.validate(...)computes metrics and handles Optuna pruning and checkpointing. A custom loop should call it once per epoch; it runs metrics atself.update_epochsintervals.save,load,denormalize, andpredictare protected methods. Use the optimizer/scheduler factory for the configured training settings.
Register the surrogate by appending
AbstractSurrogateModel.register(MySurrogate)at the end of the file that defines the class. The class name is used to identify the surrogate. Without this hook the CLI cannot instantiate your model.Expose configuration
Create a companion config dataclass (e.g.,
deeponet_config.py’sMultiONetBaseConfig). Inherit fromAbstractSurrogateBaseConfigwhenever possible so shared hyperparameters (learning rate, optimizer, scheduler, loss, activation) remain documented and gain sensible defaults.Keep model-specific knobs inside that dataclass. Dataset-specific defaults can be supplied in
datasets/<name>/surrogates_config.pyas a dataclass named after the surrogate plusConfig; they are loaded whendataset.use_optimal_paramsis enabled.
Checkpointing + evaluation
Checkpoint selection uses the test loader during training. Keep the separate validation split independent for final evaluation. Verify saved models load in
run_eval.py.predictand the evaluation pipeline assume consistent output shapes, so avoid ad-hoc reshaping in downstream scripts—handle it inforwardorprepare_data. This uniform path is what keeps the benchmark fair across models.
Optional: rollout finetuning
To support
run_finetuning.py, implementforward_from_state(self, state, times, params=None) -> Tensor. It receives initial states of shape(batch, n_quantities)in normalised units, time inputs of shape(n_times,)in the dummy time coordinate the model is trained on, and optional parameters, and returns the predicted states of shape(batch, n_times, n_quantities). Pack the inputs exactly asprepare_datadoes and callforward, so the result matchespredict. See the implementations inFullyConnectedandMultiONet.
Standard and custom training loops#
For models whose forward returns predictions and targets, fit can delegate to the optional shared loop. Decorate fit so saved models include the training duration:
from codes.surrogates.training import fit_standard
from codes.utils import time_execution
@time_execution
def fit(
self,
train_loader,
test_loader,
epochs,
position=0,
description="Training MySurrogate",
multi_objective=False,
):
fit_standard(
self,
train_loader,
test_loader,
epochs,
position=position,
description=description,
multi_objective=multi_objective,
)
The helper uses the configured prediction loss, optimizer and scheduler, reports metrics, and handles checkpoints. For a different batch loss, pass a bound loss_fn accepting (batch, criterion) and returning a differentiable scalar tensor. For example, inside a model class:
def training_loss(self, batch, criterion):
predictions, targets = self(batch)
return criterion(predictions, targets) + 0.01 * predictions.square().mean()
# In fit, pass loss_fn=self.training_loss to fit_standard.
The helper applies configured gradient clipping by default. FullyConnected, MultiONet and LatentNeuralODE use it; LatentPoly preserves its existing behavior without clipping. A model may instead implement its own complete fit. It should record train_loss, test_loss and MAE histories at the validation interval, set n_epochs, handle checkpoint restoration, and retain timing metadata via @time_execution.
To profile training, wrap a normal fit call rather than maintaining a separate loop:
import torch
with torch.profiler.profile(activities=[torch.profiler.ProfilerActivity.CPU]) as prof:
model.fit(train_loader, test_loader, epochs=1, position=0, description="Profile")
print(prof.key_averages().table(sort_by="cpu_time_total"))
ChemDataset remains available for existing callers but is deprecated. New sequence loaders can use FlatSeqBatchIterable; it emits batches rather than individual samples, so adapt loader and batch handling accordingly.
Before submitting a PR or relying on the surrogate in large runs, train it with the minimal config and confirm that run_eval.py + predict behave as expected.
Customize benchmark modes#
New benchmark modes (beyond interpolation/extrapolation/sparse/batch-scaling/uncertainty/iterative) follow the same pattern: place an object with enabled: bool and the parameters you want to sweep under the top-level config. Detailed documentation for additional modes is coming soon—refer to Running Benchmarks for the currently supported modalities and CLI flags.