Skip to content
This repository was archived by the owner on Jan 27, 2025. It is now read-only.

Commit 795a9b7

Browse files
authored
Merge pull request #252 from nipreps/fix/sklearn-module
ENH: Rename ``eddymotion._sklearn`` -> ``eddymotion.gpr``
2 parents 7e5f333 + 44a7bbd commit 795a9b7

File tree

7 files changed

+85
-89
lines changed

7 files changed

+85
-89
lines changed

docs/notebooks/dmri_covariance.ipynb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
"import matplotlib.pyplot as plt\n",
2525
"import numpy as np\n",
2626
"\n",
27-
"from eddymotion.model._sklearn import (\n",
27+
"from eddymotion.model.gpr import (\n",
2828
" compute_pairwise_angles,\n",
2929
" exponential_covariance,\n",
3030
" spherical_covariance,\n",
@@ -345,7 +345,7 @@
345345
}
346346
],
347347
"source": [
348-
"from eddymotion.model._sklearn import EddyMotionGPR, SphericalKriging\n",
348+
"from eddymotion.model.gpr import EddyMotionGPR, SphericalKriging\n",
349349
"\n",
350350
"K = SphericalKriging(beta_a=PARAMETER_SPHERICAL_a, beta_l=PARAMETER_lambda)(X_real)\n",
351351
"K -= K.min()\n",

scripts/dwi_gp_estimation_error_analysis.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
import pandas as pd
3737
from sklearn.model_selection import KFold, RepeatedKFold, cross_val_predict, cross_val_score
3838

39-
from eddymotion.model._sklearn import (
39+
from eddymotion.model.gpr import (
4040
EddyMotionGPR,
4141
SphericalKriging,
4242
)
@@ -63,7 +63,7 @@ def cross_validate(
6363
Number of folds.
6464
n_repeats : :obj:`int`
6565
Number of times the cross-validator needs to be repeated.
66-
gpr : obj:`~eddymotion.model._sklearn.EddyMotionGPR`
66+
gpr : obj:`~eddymotion.model.gpr.EddyMotionGPR`
6767
The eddymotion Gaussian process regressor object.
6868
6969
Returns

scripts/dwi_gp_estimation_simulated_signal.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
import numpy as np
3434
from dipy.core.sphere import Sphere
3535

36-
from eddymotion.model._sklearn import EddyMotionGPR, SphericalKriging
36+
from eddymotion.model.gpr import EddyMotionGPR, SphericalKriging
3737
from eddymotion.testing import simulations as testsims
3838

3939
SAMPLING_DIRECTIONS = 200

src/eddymotion/estimator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030

3131
from eddymotion import utils as eutils
3232
from eddymotion.data.splitting import lovo_split
33-
from eddymotion.model import ModelFactory
33+
from eddymotion.model.base import ModelFactory
3434
from eddymotion.registration.ants import _prepare_registration_data, _run_registration
3535

3636

src/eddymotion/model/_dipy.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
from dipy.reconst.base import ReconstModel
3232
from sklearn.gaussian_process import GaussianProcessRegressor
3333

34-
from eddymotion.model._sklearn import (
34+
from eddymotion.model.gpr import (
3535
EddyMotionGPR,
3636
ExponentialKriging,
3737
SphericalKriging,

src/eddymotion/model/_sklearn.py renamed to src/eddymotion/model/gpr.py

Lines changed: 75 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -20,70 +20,7 @@
2020
#
2121
# https://www.nipreps.org/community/licensing/
2222
#
23-
r"""
24-
Derivations from scikit-learn for Gaussian Processes.
25-
26-
Gaussian Process Model: Pairwise orientation angles
27-
---------------------------------------------------
28-
Squared Exponential covariance kernel
29-
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
30-
Kernel based on a squared exponential function for Gaussian processes on
31-
multi-shell DWI data following to eqs. 14 and 16 in [Andersson15]_.
32-
For a 2-shell case, the :math:`\mathbf{K}` kernel can be written as:
33-
34-
.. math::
35-
\begin{equation}
36-
\mathbf{K} = \left[
37-
\begin{matrix}
38-
\lambda C_{\theta}(\theta (\mathbf{G}_{1}); a) + \sigma_{1}^{2} \mathbf{I} &
39-
\lambda C_{\theta}(\theta (\mathbf{G}_{2}, \mathbf{G}_{1}); a) C_{b}(b_{2}, b_{1}; \ell) \\
40-
\lambda C_{\theta}(\theta (\mathbf{G}_{1}, \mathbf{G}_{2}); a) C_{b}(b_{1}, b_{2}; \ell) &
41-
\lambda C_{\theta}(\theta (\mathbf{G}_{2}); a) + \sigma_{2}^{2} \mathbf{I} \\
42-
\end{matrix}
43-
\right]
44-
\end{equation}
45-
46-
**Squared exponential shell-wise covariance kernel**:
47-
Compute the squared exponential smooth function describing how the
48-
covariance changes along the b direction.
49-
It uses the log of the b-values as the measure of distance along the
50-
b-direction according to eq. 15 in [Andersson15]_.
51-
52-
.. math::
53-
C_{b}(b, b'; \ell) = \exp\left( - \frac{(\log b - \log b')^2}{2 \ell^2} \right).
54-
55-
**Squared exponential covariance kernel**:
56-
Compute the squared exponential covariance matrix following to eq. 14 in [Andersson15]_.
57-
58-
.. math::
59-
k(\textbf{x}, \textbf{x'}) = C_{\theta}(\mathbf{g}, \mathbf{g'}; a) C_{b}(|b - b'|; \ell)
60-
61-
where :math:`C_{\theta}` is given by:
62-
63-
.. math::
64-
\begin{equation}
65-
C(\theta) =
66-
\begin{cases}
67-
1 - \frac{3 \theta}{2 a} + \frac{\theta^3}{2 a^3} & \textnormal{if} \; \theta \leq a \\
68-
0 & \textnormal{if} \; \theta > a
69-
\end{cases}
70-
\end{equation}
71-
72-
:math:`\theta` being computed as:
73-
74-
.. math::
75-
\theta(\mathbf{g}, \mathbf{g'}) = \arccos(|\langle \mathbf{g}, \mathbf{g'} \rangle|)
76-
77-
and :math:`C_{b}` is given by:
78-
79-
.. math::
80-
C_{b}(b, b'; \ell) = \exp\left( - \frac{(\log b - \log b')^2}{2 \ell^2} \right)
81-
82-
:math:`b` and :math:`b'` being the b-values, and :math:`\mathbf{g}` and
83-
:math:`\mathbf{g'}` the unit diffusion-encoding gradient unit vectors of the
84-
shells; and :math:`{a, \ell}` some hyperparameters.
85-
86-
"""
23+
"""Derivations from scikit-learn for Gaussian Processes."""
8724

8825
from __future__ import annotations
8926

@@ -101,10 +38,10 @@
10138
from sklearn.metrics.pairwise import cosine_similarity
10239
from sklearn.utils._param_validation import Interval, StrOptions
10340

104-
BOUNDS_A: tuple[float, float] = (0.1, np.pi)
105-
"""The limits for the parameter *a*."""
41+
BOUNDS_A: tuple[float, float] = (0.1, 2.35)
42+
"""The limits for the parameter *a* (angular distance in rad)."""
10643
BOUNDS_LAMBDA: tuple[float, float] = (1e-3, 1000)
107-
"""The limits for the parameter lambda."""
44+
"""The limits for the parameter λ (signal scaling factor)."""
10845
THETA_EPSILON: float = 1e-5
10946
"""Minimum nonzero angle."""
11047
LBFGS_CONFIGURABLE_OPTIONS = {"disp", "maxiter", "ftol", "gtol"}
@@ -143,8 +80,7 @@ class EddyMotionGPR(GaussianProcessRegressor):
14380
14481
In principle, Scikit-Learn's implementation normalizes the training data
14582
as in [Andersson15]_ (see
146-
`FSL's souce code <https://git.fmrib.ox.ac.uk/fsl/eddy/-/blob/\
147-
2480dda293d4cec83014454db3a193b87921f6b0/DiffusionGP.cpp#L218>`__).
83+
`FSL's souce code <https://git.fmrib.ox.ac.uk/fsl/eddy/-/blob/2480dda293d4cec83014454db3a193b87921f6b0/DiffusionGP.cpp#L218>`__).
14884
From their paper (p. 167, end of first column):
14985
15086
Typically one just substracts the mean (:math:`\bar{\mathbf{f}}`)
@@ -161,7 +97,7 @@ class EddyMotionGPR(GaussianProcessRegressor):
16197
I believe this is overlooked in [Andersson15]_, or they actually did not
16298
use analytical gradient-descent:
16399
164-
_A note on optimisation_
100+
*A note on optimisation*
165101
166102
It is suggested, for example in Rasmussen and Williams (2006), that
167103
an optimisation method that uses derivative information should be
@@ -175,6 +111,44 @@ class EddyMotionGPR(GaussianProcessRegressor):
175111
frequently better at avoiding local maxima.
176112
Hence, that was the method we used for all optimisations in the present
177113
paper.
114+
115+
**Multi-shell regression (TODO).**
116+
For multi-shell modeling, the kernel :math:`k(\textbf{x}, \textbf{x'})`
117+
is updated following Eq. (14) in [Andersson15]_.
118+
119+
.. math::
120+
k(\textbf{x}, \textbf{x'}) = C_{\theta}(\mathbf{g}, \mathbf{g'}; a) C_{b}(|b - b'|; \ell)
121+
122+
and :math:`C_{b}` is based the log of the b-values ratio, a measure of distance along the
123+
b-direction, according to Eq. (15) given by:
124+
125+
.. math::
126+
C_{b}(b, b'; \ell) = \exp\left( - \frac{(\log b - \log b')^2}{2 \ell^2} \right),
127+
128+
:math:`b` and :math:`b'` being the b-values, and :math:`\mathbf{g}` and
129+
:math:`\mathbf{g'}` the unit diffusion-encoding gradient unit vectors of the
130+
shells; and :math:`{a, \ell}` some hyperparameters.
131+
132+
The full GP regression kernel :math:`\mathbf{K}` is then updated for a 2-shell case as
133+
follows (Eq. (16) in [Andersson15]_):
134+
135+
.. math::
136+
\begin{equation}
137+
\mathbf{K} = \left[
138+
\begin{matrix}
139+
\lambda C_{\theta}(\theta (\mathbf{G}_{1}); a) + \sigma_{1}^{2} \mathbf{I} &
140+
\lambda C_{\theta}(\theta (\mathbf{G}_{2}, \mathbf{G}_{1}); a) C_{b}(b_{2}, b_{1}; \ell) \\
141+
\lambda C_{\theta}(\theta (\mathbf{G}_{1}, \mathbf{G}_{2}); a) C_{b}(b_{1}, b_{2}; \ell) &
142+
\lambda C_{\theta}(\theta (\mathbf{G}_{2}); a) + \sigma_{2}^{2} \mathbf{I} \\
143+
\end{matrix}
144+
\right]
145+
\end{equation}
146+
147+
References
148+
----------
149+
.. [Andersson15] J. L. R. Andersson. et al., An integrated approach to
150+
correction for off-resonance effects and subject movement in diffusion MR
151+
imaging, NeuroImage 125 (2016) 1063-11078
178152
179153
"""
180154

@@ -184,7 +158,7 @@ class EddyMotionGPR(GaussianProcessRegressor):
184158
"optimizer": [StrOptions(SUPPORTED_OPTIMIZERS), callable, None],
185159
"n_restarts_optimizer": [Interval(Integral, 0, None, closed="left")],
186160
"copy_X_train": ["boolean"],
187-
"zeromean_y": ["boolean"],
161+
"normalize_y": ["boolean"],
188162
"n_targets": [Interval(Integral, 1, None, closed="left"), None],
189163
"random_state": ["random_state"],
190164
}
@@ -497,9 +471,21 @@ def __repr__(self) -> str:
497471

498472

499473
def exponential_covariance(theta: np.ndarray, a: float) -> np.ndarray:
500-
"""
474+
r"""
501475
Compute the exponential covariance for given distances and scale parameter.
502476
477+
Implements :math:`C_{\theta}`, following Eq. (9) in [Andersson15]_:
478+
479+
.. math::
480+
\begin{equation}
481+
C(\theta) = e^{-\theta/a} \,\, \text{for} \, 0 \leq \theta \leq \pi,
482+
\end{equation}
483+
484+
:math:`\theta` being computed as:
485+
486+
.. math::
487+
\theta(\mathbf{g}, \mathbf{g'}) = \arccos(|\langle \mathbf{g}, \mathbf{g'} \rangle|)
488+
503489
Parameters
504490
----------
505491
theta : :obj:`~numpy.ndarray`
@@ -517,9 +503,25 @@ def exponential_covariance(theta: np.ndarray, a: float) -> np.ndarray:
517503

518504

519505
def spherical_covariance(theta: np.ndarray, a: float) -> np.ndarray:
520-
"""
506+
r"""
521507
Compute the spherical covariance for given distances and scale parameter.
522508
509+
Implements :math:`C_{\theta}`, following Eq. (10) in [Andersson15]_:
510+
511+
.. math::
512+
\begin{equation}
513+
C(\theta) =
514+
\begin{cases}
515+
1 - \frac{3 \theta}{2 a} + \frac{\theta^3}{2 a^3} & \textnormal{if} \; \theta \leq a \\
516+
0 & \textnormal{if} \; \theta > a
517+
\end{cases}
518+
\end{equation}
519+
520+
:math:`\theta` being computed as:
521+
522+
.. math::
523+
\theta(\mathbf{g}, \mathbf{g'}) = \arccos(|\langle \mathbf{g}, \mathbf{g'} \rangle|)
524+
523525
Parameters
524526
----------
525527
theta : :obj:`~numpy.ndarray`
@@ -583,12 +585,6 @@ def compute_pairwise_angles(
583585
>>> compute_pairwise_angles(X)[0, 1]
584586
0.0
585587
586-
References
587-
----------
588-
.. [Andersson15] J. L. R. Andersson. et al., An integrated approach to
589-
correction for off-resonance effects and subject movement in diffusion MR
590-
imaging, NeuroImage 125 (2016) 1063-11078
591-
592588
"""
593589

594590
cosines = np.clip(cosine_similarity(X, Y, dense_output=dense_output), -1.0, 1.0)

test/test_sklearn.py renamed to test/test_gpr.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
import pytest
2727
from dipy.io import read_bvals_bvecs
2828

29-
from eddymotion.model import _sklearn as ems
29+
from eddymotion.model import gpr
3030

3131
GradientTablePatch = namedtuple("gtab", ["bvals", "bvecs"])
3232

@@ -263,7 +263,7 @@ def test_compute_pairwise_angles(bvecs1, bvecs2, closest_polarity, expected):
263263
if bvecs2 is not None:
264264
_bvecs2 = (bvecs2 / np.linalg.norm(bvecs2, axis=0)).T
265265

266-
obtained = ems.compute_pairwise_angles(_bvecs1, _bvecs2, closest_polarity)
266+
obtained = gpr.compute_pairwise_angles(_bvecs1, _bvecs2, closest_polarity)
267267

268268
if _bvecs2 is not None:
269269
assert (_bvecs1.shape[0], _bvecs2.shape[0]) == obtained.shape
@@ -282,7 +282,7 @@ def test_kernel(repodata, covariance):
282282

283283
bvecs = bvecs[bvals > 10]
284284

285-
KernelType = getattr(ems, f"{covariance}Kriging")
285+
KernelType = getattr(gpr, f"{covariance}Kriging")
286286
kernel = KernelType()
287287
K = kernel(bvecs)
288288

0 commit comments

Comments
 (0)