Skip to content

Commit b5c6720

Browse files
jhlegarretaoesteban
andcommittedJan 20, 2025·
ENH: Add benchmarking files
Add benchmarking files so that `nifreeze` can be benchmarked using `asv`: - Add the actual files that allow to benchmark different `nifreeze` capabilities. - Add a new `benchmark` optional dependencies section to `pyproject.toml`. - Add the `asv` configuration file. - Add a `README.rst` file to explain how to run the benchmarking. - Add a GitHub Actions workflow file to run the benchmarks for every PR. Co-authored-by: Oscar Esteban <code@oscaresteban.es>
1 parent 0236045 commit b5c6720

File tree

6 files changed

+306
-0
lines changed

6 files changed

+306
-0
lines changed
 

‎.github/workflows/benchmark.yml

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
name: Benchmark
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
- maint/*
8+
pull_request:
9+
branches:
10+
- main
11+
- maint/*
12+
# Allow job to be triggered manually from GitHub interface
13+
workflow_dispatch:
14+
15+
defaults:
16+
run:
17+
shell: bash
18+
19+
concurrency:
20+
group: ${{ github.workflow }}-${{ github.ref }}
21+
cancel-in-progress: true
22+
23+
permissions:
24+
contents: read
25+
26+
jobs:
27+
benchmark:
28+
name: Linux
29+
runs-on: ubuntu-latest
30+
defaults:
31+
run:
32+
shell: bash
33+
strategy:
34+
fail-fast: false
35+
matrix:
36+
python-version: [ '3.11' ]
37+
38+
steps:
39+
- name: Set up system
40+
uses: actions/checkout@v4
41+
with:
42+
fetch-depth: 0
43+
- name: Set up Python ${{ matrix.python-version }}
44+
uses: actions/setup-python@v5
45+
with:
46+
python-version: ${{ matrix.python-version }}
47+
- name: Install dependencies
48+
run: |
49+
python -m pip install --upgrade pip
50+
pip install .[antsopt,benchmark]
51+
- name: Set threading parameters for reliable benchmarking
52+
run: |
53+
export OPENBLAS_NUM_THREADS=1
54+
export MKL_NUM_THREADS=1
55+
export OMP_NUM_THREADS=1
56+
- name: Run benchmarks
57+
run: |
58+
asv machine --yes --config benchmarks/asv.conf.json
59+
asv run --config benchmarks/asv.conf.json --show-stderr

‎benchmarks/README.rst

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
.. -*- rst -*-
2+
3+
===================
4+
NiFreeze benchmarks
5+
===================
6+
Benchmarking NiFreeze with Airspeed Velocity.
7+
8+
Usage
9+
-----
10+
Airspeed Velocity manages building and Python environments by itself,
11+
unless told otherwise.
12+
To run the benchmarks, you do not need to install
13+
a development version of *NiFreeze* on your current
14+
*Python* environment.
15+
16+
To run all benchmarks for the latest commit, navigate to *NiFreeze*'s root
17+
``benchmarks`` directory and execute::
18+
19+
asv run
20+
21+
For testing benchmarks locally, it may be better to run these without
22+
replications::
23+
24+
export REGEXP="bench.*Ufunc"
25+
asv run --dry-run --show-stderr --python=same --quick -b $REGEXP
26+
27+
All of the commands above display the results in plain text in the console,
28+
and the results are not saved for comparison with future commits.
29+
For greater control, a graphical view, and to have results saved for future
30+
comparisons, you can run ASV as follows to record results and generate
31+
the HTML reports::
32+
33+
asv run --skip-existing-commits --steps 10 ALL
34+
asv publish
35+
asv preview
36+
37+
More on how to use ``asv`` can be found in the `ASV documentation`_.
38+
Command-line help is available as usual via ``asv --help`` and
39+
``asv run --help``.
40+
41+
.. _ASV documentation: https://asv.readthedocs.io/

‎benchmarks/asv.conf.json

+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
{
2+
// The version of the config file format. Do not change, unless
3+
// you know what you are doing.
4+
"version": 1,
5+
6+
// The name of the project being benchmarked
7+
"project": "nifreeze",
8+
9+
// The project's homepage
10+
"project_url": "https://www.nipreps.org/nifreeze/",
11+
12+
// The URL or local path of the source code repository for the
13+
// project being benchmarked
14+
"repo": "..",
15+
16+
// List of branches to benchmark. If not provided, defaults to "master"
17+
// (for git) or "tip" (for mercurial).
18+
"branches": ["HEAD"],
19+
20+
"build_command": [
21+
"python -m build --wheel -o {build_cache_dir} {build_dir}"
22+
],
23+
24+
// The DVCS being used. If not set, it will be automatically
25+
// determined from "repo" by looking at the protocol in the URL
26+
// (if remote), or by looking for special directories, such as
27+
// ".git" (if local).
28+
"dvcs": "git",
29+
30+
// The tool to use to create environments. May be "conda",
31+
// "virtualenv" or other value depending on the plugins in use.
32+
// If missing or the empty string, the tool will be automatically
33+
// determined by looking for tools on the PATH environment
34+
// variable.
35+
"environment_type": "virtualenv",
36+
37+
// the base URL to show a commit for the project.
38+
"show_commit_url": "https://github.com/nipreps/nifreeze/commit/",
39+
40+
// The Pythons you'd like to test against. If not provided, defaults
41+
// to the current version of Python used to run `asv`.
42+
// "pythons": ["3.12"],
43+
44+
// The matrix of dependencies to test. Each key is the name of a
45+
// package (in PyPI) and the values are version numbers. An empty
46+
// list indicates to just test against the default (latest)
47+
// version.
48+
"matrix": {
49+
"dipy": [],
50+
"nipype": [],
51+
"nest-asyncio": [],
52+
"nitransforms": [],
53+
"numpy": [],
54+
"scikit_learn": [],
55+
"scipy": []
56+
},
57+
58+
// The directory (relative to the current directory) that benchmarks are
59+
// stored in. If not provided, defaults to "benchmarks"
60+
"benchmark_dir": "benchmarks",
61+
62+
// The directory (relative to the current directory) to cache the Python
63+
// environments in. If not provided, defaults to "env"
64+
"env_dir": "env",
65+
66+
67+
// The directory (relative to the current directory) that raw benchmark
68+
// results are stored in. If not provided, defaults to "results".
69+
"results_dir": "results",
70+
71+
// The directory (relative to the current directory) that the html tree
72+
// should be written to. If not provided, defaults to "html".
73+
"html_dir": "html",
74+
75+
// The number of characters to retain in the commit hashes.
76+
// "hash_length": 8,
77+
78+
// `asv` will cache wheels of the recent builds in each
79+
// environment, making them faster to install next time. This is
80+
// number of builds to keep, per environment.
81+
"build_cache_size": 8,
82+
83+
// The commits after which the regression search in `asv publish`
84+
// should start looking for regressions. Dictionary whose keys are
85+
// regexps matching to benchmark names, and values corresponding to
86+
// the commit (exclusive) after which to start looking for
87+
// regressions. The default is to start from the first commit
88+
// with results. If the commit is `null`, regression detection is
89+
// skipped for the matching benchmark.
90+
//
91+
// "regressions_first_commits": {
92+
// "some_benchmark": "352cdf", // Consider regressions only after this commit
93+
// "another_benchmark": null, // Skip regression detection altogether
94+
// }
95+
96+
// Maximum time in seconds that a benchmark is allowed to run before it is terminated.
97+
"default_benchmark_timeout": 240
98+
}

‎benchmarks/benchmarks/__init__.py

Whitespace-only changes.

‎benchmarks/benchmarks/bench_model.py

+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
2+
# vi: set ft=python sts=4 ts=4 sw=4 et:
3+
#
4+
# Copyright The NiPreps Developers <nipreps@gmail.com>
5+
#
6+
# Licensed under the Apache License, Version 2.0 (the "License");
7+
# you may not use this file except in compliance with the License.
8+
# You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
#
18+
# We support and encourage derived works from this project, please read
19+
# about our expectations at
20+
#
21+
# https://www.nipreps.org/community/licensing/
22+
#
23+
"""Benchmarking for nifreeze's models."""
24+
25+
from abc import ABC
26+
27+
import dipy.data as dpd
28+
import nibabel as nb
29+
import numpy as np
30+
from dipy.core.gradients import get_bval_indices
31+
from dipy.io import read_bvals_bvecs
32+
from dipy.segment.mask import median_otsu
33+
from scipy.ndimage import binary_dilation
34+
from skimage.morphology import ball
35+
36+
from nifreeze.model.gpr import DiffusionGPR, SphericalKriging
37+
38+
39+
class DiffusionGPRBenchmark(ABC):
40+
def __init__(self):
41+
self._estimator = None
42+
self._X_train = None
43+
self._y_train = None
44+
self._X_test = None
45+
self._y_test = None
46+
47+
def setup(self, *args, **kwargs):
48+
beta_a = 1.38
49+
beta_l = 1 / 2.1
50+
alpha = 0.1
51+
disp = True
52+
optimizer = None
53+
self.make_estimator((beta_a, beta_l, alpha, disp, optimizer))
54+
self.make_data()
55+
56+
def make_estimator(self, params):
57+
beta_a, beta_l, alpha, disp, optimizer = params
58+
kernel = SphericalKriging(beta_a=beta_a, beta_l=beta_l)
59+
self._estimator = DiffusionGPR(
60+
kernel=kernel,
61+
alpha=alpha,
62+
disp=disp,
63+
optimizer=optimizer,
64+
)
65+
66+
def make_data(self):
67+
name = "sherbrooke_3shell"
68+
69+
dwi_fname, bval_fname, bvec_fname = dpd.get_fnames(name=name)
70+
dwi_data = nb.load(dwi_fname).get_fdata()
71+
bvals, bvecs = read_bvals_bvecs(bval_fname, bvec_fname)
72+
73+
_, brain_mask = median_otsu(dwi_data, vol_idx=[0])
74+
brain_mask = binary_dilation(brain_mask, ball(8))
75+
76+
bval = 1000
77+
indices = get_bval_indices(bvals, bval, tol=20)
78+
79+
bvecs_shell = bvecs[indices]
80+
shell_data = dwi_data[..., indices]
81+
dwi_vol_idx = len(indices) // 2
82+
83+
# Prepare a train/test mask (False for all directions except the left-out where it's true)
84+
train_test_mask = np.zeros(bvecs_shell.shape[0], dtype=bool)
85+
train_test_mask[dwi_vol_idx] = True
86+
87+
# Generate train/test bvecs
88+
self._X_train = bvecs_shell[~train_test_mask, :]
89+
self._X_test = bvecs_shell[train_test_mask, :]
90+
91+
# Select voxels within brain mask
92+
y = shell_data[brain_mask]
93+
94+
# Generate train/test data
95+
self._y_train = y[:, ~train_test_mask]
96+
self._y_test = y[:, train_test_mask]
97+
98+
def time_fit(self, *args):
99+
self._estimator = self._estimator.fit(self._X_train, self._y_train.T)
100+
101+
def time_predict(self):
102+
self._estimator.predict(self._X_test)

‎pyproject.toml

+6
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,12 @@ antsopt = [
8383
"smac",
8484
]
8585

86+
benchmark = [
87+
"asv",
88+
"pyperf",
89+
"virtualenv",
90+
]
91+
8692
# Aliases
8793
docs = ["nifreeze[doc]"]
8894
tests = ["nifreeze[test]"]

0 commit comments

Comments
 (0)
Please sign in to comment.