Skip to main content

Saving, loading, and caching results

neuroboros provides three functions for saving, loading, and reusing results:

Saving files with nb.save

nb.save(fn, data) selects a writer from the filename extension and data type.

import numpy as np
import neuroboros as nb

corr = np.array([0.2, 0.5, 0.8])
nb.save('results/isc/corr.npy', corr)

This creates results/isc/ if needed and saves corr as a NumPy array.

Supported output formats

Extension data Written with
.npy array numpy.save
.npz SciPy sparse matrix scipy.sparse.save_npz
.npz dict of arrays numpy.savez
.pkl picklable Python object pickle.dump
.png array or PIL image Pillow
.shape.gii 1-D array GIFTI shape file, float32
.func.gii 1-D or 2-D array GIFTI time-series file, one data array per column, float32
.json JSON-compatible object json.dump

Unsupported combinations of filename extension and data type raise TypeError.

  • Use .npz to save a SciPy sparse matrix with scipy.sparse.save_npz. Other extensions select their usual writers; for example, .pkl pickles the sparse matrix.
  • For .npy files, non-array input is passed to NumPy for conversion, with a warning.

Loading files with nb.load

nb.load(fn, **kwargs) selects a reader from the filename extension.

corr = nb.load('results/isc/corr.npy')
events = nb.load('sub-01_task-movie_events.tsv')

Supported input formats

Extension Read with Returns
.npy numpy.load array
.npz scipy.sparse.load_npz or numpy.load sparse matrix, or an NpzFile for dict saves
.tsv pandas.read_csv with tab delimiter DataFrame
.csv pandas.read_csv DataFrame
.pkl pickle.load the pickled object
.json, .json.gz json.load the decoded object

Passing options to the reader

nb.load forwards extra keyword arguments to the selected reader. For example, you can memory-map a NumPy array, select columns from a table, or customize how JSON numbers are decoded:

# numpy.load: memory-map a large array instead of loading it all into memory.
data = nb.load('large_array.npy', mmap_mode='r')

# pandas.read_csv: read selected columns from a TSV file.
events = nb.load(
    'sub-01_task-movie_events.tsv',
    usecols=['onset', 'duration', 'trial_type'],
)

# pandas.read_csv: use a CSV column as the DataFrame index.
participants = nb.load('participants.csv', index_col='participant_id')

# json.load: preserve decimal precision when decoding JSON numbers.
from decimal import Decimal

metadata = nb.load('metadata.json', parse_float=Decimal)

The keyword arguments must be supported by the selected reader.

Recording function results with nb.record

Long analyses often consist of independent jobs, such as one per subject or searchlight. nb.record saves each job’s results so that later calls can reuse them:

def compute_isc(dset, sid, task):
    ...
    return isc  # an array

out_fn = f'results/isc/{task}/{sid}.npy'
nb.record(out_fn, compute_isc)(dset, sid, task)

Skipping completed work

The call nb.record(out_fn, func) returns a wrapped version of func. Unless rerun=True, the wrapper follows these steps:

  1. Checks .completed. If it exists, reuses the completed result and attempts to remove any leftover .started marker.
  2. Checks .started. If another job started less than rerun_hours ago, returns without computing, or waits if return_results=True.
  3. Checks the output files. If all expected files exist, skips computation even without a .completed marker.
  4. Claims the job and checks again for completed results. If computation is still needed, runs func, saves its results using nb.save, and writes .completed after all outputs are saved.

Returning results

By default, the wrapper returns None. With return_results=True, it returns the computed result or loads the saved result with nb.load:

isc = nb.record(out_fn, compute_isc, return_results=True)(dset, sid, task)

nb.record and nb.save_results are aliases for the same function.

Multiple outputs

To save multiple outputs, pass a list or tuple of filenames. The function must return the same number of results, in the corresponding order:

out_fns = [f'results/glm/{sid}_betas.npy', f'results/glm/{sid}_tvalues.npy']
nb.record(out_fns, run_glm)(dset, sid)

Lock and log files

nb.record uses two small text files to track each computation. Their names are based on the first output filename by default:

  • <out_fn>.started is created when the computation starts and removed after the outputs and completion marker are written. It holds the start time.
  • <out_fn>.completed is published after all outputs have been saved successfully. It holds the CPU time, wall time, host name, and the number of CPUs.

Saving outputs safely

Each output is first saved to a temporary location on the same filesystem, then moved into place with an atomic replacement. Readers see either the previous file or the complete new file; a failed save does not leave a partial file at the output path. The .completed marker is written the same way, after all outputs have been saved. For multiple outputs, replacement is atomic per file: if a later save fails, earlier outputs may already have been replaced, but no new completion marker is written.

If both markers exist, .completed takes precedence and nb.record attempts to remove .started. This lets a successful result be reused even if a competing job crashed and left a start marker behind.

Jobs on the same node or different nodes can compete for an output path. To claim the job, each writes a timestamp to a unique temporary file, then attempts to create .started as a hard link to that file. Only one job can create the shared name, even if other nodes have stale directory listings. The temporary name is then removed, leaving .started in place for the job that acquired it. This requires a filesystem that supports hard links, such as NFS, and all competing jobs must use the same protocol.

Without a .completed marker, a recent .started marker blocks duplicate computation for rerun_hours (48 hours by default). The wrapper returns None, or waits with return_results=True, checking every ten minutes. After the timeout, another job may start even if the original job is still running. rerun=True also allows a new run: it overrides .started and bypasses the completion checks.

Timeout recovery is best-effort and assumes sufficiently synchronized node clocks. Simultaneous attempts to replace an expired marker do not have the same guarantee as claiming an absent marker. Cleanup checks the file’s identity before removal to reduce the risk of deleting another job’s replacement marker.

If a job fails and no other job completes the work, .started remains until timeout recovery or manual removal. After confirming that the original job has stopped, delete the marker or shorten rerun_hours to retry sooner.

A .completed marker skips computation even if the output files have since been moved or deleted. With return_results=True, those files must still be available to load. Set log_fn to use a different path prefix for both markers.