Skip to content

Commit b8a769a

Browse files
author
IvanARashid
authored
Merge pull request #23 from OSIPI/testing/wrapper
PV and PvH submissions
2 parents 973bf64 + e3c24ec commit b8a769a

File tree

5 files changed

+151
-45
lines changed

5 files changed

+151
-45
lines changed

src/original/PV_MUMC/two_step_IVIM_fit.py

Lines changed: 26 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -7,36 +7,34 @@
77
numpy
88
tqdm
99
scipy
10-
joblib
1110
"""
1211

1312
# load relevant libraries
14-
from scipy.optimize import curve_fit, nnls
13+
from scipy.optimize import curve_fit
1514
import numpy as np
16-
from joblib import Parallel, delayed
1715
import tqdm
1816

1917

2018

2119

2220
def two_exp_noS0(bvalues, Dpar, Fmv, Dmv):
23-
""" tri-exponential IVIM function, and S0 set to 1"""
21+
""" bi-exponential IVIM function, and S0 set to 1"""
2422
return Fmv * np.exp(-bvalues * Dmv) + (1 - Fmv ) * np.exp(-bvalues * Dpar)
2523

2624
def two_exp(bvalues, S0, Dpar, Fmv, Dmv):
27-
""" tri-exponential IVIM function"""
25+
""" bi-exponential IVIM function"""
2826
return S0 * (Fmv * np.exp(-bvalues * Dmv) + (1 - Fmv ) * np.exp(-bvalues * Dpar))
2927

3028

3129

32-
def fit_least_squares_array(bvalues, dw_data, fitS0=True, bounds=([0.9, 0.0001, 0.0, 0.0025], [1.1, 0.0025, 0.2, 0.2]), cutoff=200):
30+
def fit_least_squares_array(bvalues, dw_data, fitS0=False, bounds=([0.9, 0.0001, 0.0, 0.0025], [1.1, 0.0025, 0.5, 0.2]), cutoff=200):
3331
"""
3432
This is the LSQ implementation, in which we first estimate Dpar using a curve fit to b-values>=cutoff;
3533
Second, we fit the other parameters using all b-values, while fixing Dpar from step 1. This fit
3634
is done on an array.
3735
:param bvalues: 1D Array with the b-values
3836
:param dw_data: 2D Array with diffusion-weighted signal in different voxels at different b-values
39-
:param bounds: Array with fit bounds ([S0min, Dparmin, Fintmin, Dintmin, Fmvmin, Dmvmin],[S0max, Dparmax, Fintmax, Dintmax, Fmvmax, Dmvmax]). default: ([0.9, 0.0001, 0.0, 0.0015, 0.0, 0.004], [1.1, 0.0015, 0.4, 0.004, 0.2, 0.2])
37+
:param bounds: Array with fit bounds ([S0min, Dparmin, Fmvmin, Dmvmin],[S0max, Dparmax, Fmvmax, Dmvmax]).
4038
:param cutoff: cutoff b-value used in step 1
4139
:return Dpar: 1D Array with Dpar in each voxel
4240
:return Fmv: 1D Array with Fmv in each voxel
@@ -54,69 +52,56 @@ def fit_least_squares_array(bvalues, dw_data, fitS0=True, bounds=([0.9, 0.0001,
5452
return [Dpar, Fmv, Dmv, S0]
5553

5654

57-
def fit_least_squares(bvalues, dw_data, IR=False, S0_output=False, fitS0=True,
58-
bounds=([0.9, 0.0001, 0.0, 0.0025], [1.1, 0.0025, 0.2, 0.2]), cutoff=200):
55+
def fit_least_squares(bvalues, dw_data, S0_output=False, fitS0=False,
56+
bounds=([0.9, 0.0001, 0.0, 0.0025], [1.1, 0.003, 1, 0.2]), cutoff=200):
5957
"""
6058
This is the LSQ implementation, in which we first estimate Dpar using a curve fit to b-values>=cutoff;
6159
Second, we fit the other parameters using all b-values, while fixing Dpar from step 1. This fit
6260
is done on an array. It fits a single curve
6361
:param bvalues: 1D Array with the b-values
6462
:param dw_data: 1D Array with diffusion-weighted signal in different voxels at different b-values
65-
:param IR: Boolean; True will fit the IVIM accounting for inversion recovery, False will fit IVIM without IR; default = True
66-
:param S0_output: Boolean determining whether to output (often a dummy) variable S0; default = False
67-
:param fix_S0: Boolean determining whether to fix S0 to 1; default = True
68-
:param bounds: Array with fit bounds ([S0min, Dparmin, Fintmin, Dintmin, Fmvmin, Dmvmin],[S0max, Dparmax, Fintmax, Dintmax, Fmvmax, Dmvmax]). Default: ([0, 0, 0, 0.005, 0, 0.06], [2.5, 0.005, 1, 0.06, 1, 0.5])
63+
:param S0_output: Boolean determining whether to output (often a dummy) variable S0;
64+
:param fitS0: Boolean determining whether to fix S0 to 1;
65+
:param bounds: Array with fit bounds ([S0min, Dparmin, Fmvmin, Dmvmin],[S0max, Dparmax, Fmvmax, Dmvmax]).
6966
:param cutoff: cutoff b-value used in step 1
7067
:return S0: optional 1D Array with S0 in each voxel
7168
:return Dpar: scalar with Dpar of the specific voxel
72-
:return Fint: scalar with Fint of the specific voxel
73-
:return Dint: scalar with Dint of the specific voxel
7469
:return Fmv: scalar with Fmv of the specific voxel
7570
:return Dmv: scalar with Dmv of the specific voxel
7671
"""
7772

7873
#try:
79-
def monofit(bvalues, Dpar):
80-
return np.exp(-bvalues * Dpar)
74+
def monofit(bvalues, Dpar, Fmv):
75+
return (1-Fmv)*np.exp(-bvalues * Dpar)
8176

8277
high_b = bvalues[bvalues >= cutoff]
8378
high_dw_data = dw_data[bvalues >= cutoff]
84-
boundspar = ([bounds[0][1]], [bounds[1][1]])
85-
params, _ = curve_fit(monofit, high_b, high_dw_data, p0=[(bounds[1][1]-bounds[0][1])/2], bounds=boundspar)
86-
Dpar = params[0]
87-
79+
boundsmonoexp = ([bounds[0][1], bounds[0][2]], [bounds[1][1], bounds[1][2]])
80+
params, _ = curve_fit(monofit, high_b, high_dw_data, p0=[(bounds[1][1]-bounds[0][1])/2,(bounds[1][2]-bounds[0][2])/2], bounds=boundsmonoexp)
81+
Dpar1 = params[0]
8882
if not fitS0:
89-
boundsupdated=([Dpar1 , bounds[0][2] , bounds[0][3] ],
90-
[Dpar1 , bounds[1][2] , bounds[1][3] ])
91-
params, _ = curve_fit(two_exp_noS0, bvalues, dw_data, p0=[Dpar1, (bounds[0][2]+bounds[1][2])/2, (bounds[0][3]+bounds[1][3])/2], bounds=boundsupdated)
92-
Dpar1, Fmv, Dmv = params[0], params[1], params[2]
83+
boundsupdated=([bounds[0][2] , bounds[0][3] ],
84+
[bounds[1][2] , bounds[1][3] ])
85+
params, _ = curve_fit(lambda b, Fmv, Dmv: two_exp_noS0(b, Dpar1, Fmv, Dmv), bvalues, dw_data, p0=[(bounds[0][2]+bounds[1][2])/2, (bounds[0][3]+bounds[1][3])/2], bounds=boundsupdated)
86+
Fmv, Dmv = params[0], params[1]
9387
#when the fraction of a compartment equals zero (or very very small), the corresponding diffusivity is non-existing (=NaN)
94-
if Fmv < 1e-4:
95-
Dmv = float("NaN")
88+
#if Fmv < 1e-4:
89+
# Dmv = float("NaN")
9690

9791
else:
98-
#boundsupdated = ([bounds[0][0] , Dpar1 , bounds[0][2] , bounds[0][3] ],
99-
# [bounds[1][0] , Dpar1, bounds[1][2] , bounds[1][3] ])
100-
#params, _ = curve_fit(two_exp, bvalues, dw_data, p0=[1, Dpar1, (bounds[0][2]+bounds[1][2])/2, (bounds[0][3]+bounds[1][3])/2], bounds=boundsupdated)
10192
boundsupdated = ([bounds[0][0] , bounds[0][2] , bounds[0][3] ],
10293
[bounds[1][0] , bounds[1][2] , bounds[1][3] ])
103-
params, _ = curve_fit(lambda bvalues, S0, Fmv, Dmv: two_exp(bvalues, S0, Dpar, Fmv, Dmv), bvalues, dw_data, p0=[1, (bounds[0][2]+bounds[1][2])/2, (bounds[0][3]+bounds[1][3])/2], bounds=boundsupdated)
94+
params, _ = curve_fit(lambda b, S0, Fmv, Dmv: two_exp(b, S0, Dpar1, Fmv, Dmv), bvalues, dw_data, p0=[1, (bounds[0][2]+bounds[1][2])/2, (bounds[0][3]+bounds[1][3])/2], bounds=boundsupdated)
10495
S0 = params[0]
10596
Fmv, Dmv = params[1] , params[2]
10697
#when the fraction of a compartment equals zero (or very very small), the corresponding diffusivity is non-existing (=NaN)
107-
if Fmv < 1e-4:
108-
Dmv = float("NaN")
98+
#if Fmv < 1e-4:
99+
# Dmv = float("NaN")
109100

110101
if S0_output:
111-
return Dpar, Fmv, Dmv, S0
112-
else:
113-
return Dpar, Fmv, Dmv
114-
#except:
115-
116-
if S0_output:
117-
return 0, 0, 0, 0, 0, 0
102+
return Dpar1, Fmv, Dmv, S0
118103
else:
119-
return 0, 0, 0, 0, 0
104+
return Dpar1, Fmv, Dmv
120105

121106

122107

src/standardized/PV_MUMC_biexp.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import numpy as np
2+
from src.wrappers.OsipiBase import OsipiBase
3+
from src.original.PV_MUMC.two_step_IVIM_fit import fit_least_squares_array, fit_least_squares
4+
5+
6+
class PV_MUMC_biexp(OsipiBase):
7+
"""
8+
Bi-exponential fitting algorithm by Paulien Voorter, Maastricht University
9+
"""
10+
11+
# Some basic stuff that identifies the algorithm
12+
id_author = "Paulien Voorter MUMC"
13+
id_algorithm_type = "Bi-exponential fit"
14+
id_return_parameters = "f, D*, D"
15+
id_units = "seconds per milli metre squared or milliseconds per micro metre squared"
16+
17+
# Algorithm requirements
18+
required_bvalues = 4
19+
required_thresholds = [0,0] # Interval from "at least" to "at most", in case submissions allow a custom number of thresholds
20+
required_bounds = False
21+
required_bounds_optional = True # Bounds may not be required but are optional
22+
required_initial_guess = False
23+
required_initial_guess_optional = True
24+
accepted_dimensions = 1 # Not sure how to define this for the number of accepted dimensions. Perhaps like the thresholds, at least and at most?
25+
26+
def __init__(self, bvalues=None, thresholds=None, bounds=None, initial_guess=None, weighting=None, stats=False):
27+
"""
28+
Everything this algorithm requires should be implemented here.
29+
Number of segmentation thresholds, bounds, etc.
30+
31+
Our OsipiBase object could contain functions that compare the inputs with
32+
the requirements.
33+
"""
34+
super(PV_MUMC_biexp, self).__init__(bvalues, None, bounds, None)
35+
self.PV_algorithm = fit_least_squares
36+
37+
38+
def ivim_fit(self, signals, bvalues=None):
39+
"""Perform the IVIM fit
40+
41+
Args:
42+
signals (array-like)
43+
bvalues (array-like, optional): b-values for the signals. If None, self.bvalues will be used. Default is None.
44+
45+
Returns:
46+
_type_: _description_
47+
"""
48+
49+
50+
fit_results = self.PV_algorithm(bvalues, signals)
51+
52+
f = fit_results[1]
53+
Dstar = fit_results[2]
54+
D = fit_results[0]
55+
56+
return f, Dstar, D
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
from src.wrappers.OsipiBase import OsipiBase
2+
from src.original.PvH_KB_NKI.DWI_functions_standalone import generate_IVIMmaps_standalone, generate_ADC_standalone
3+
import numpy as np
4+
5+
class PvH_KB_NKI_IVIMfit(OsipiBase):
6+
"""
7+
Bi-exponential fitting algorithm by Petra van Houdt and Koen Baas, NKI
8+
"""
9+
10+
# I'm thinking that we define default attributes for each submission like this
11+
# And in __init__, we can call the OsipiBase control functions to check whether
12+
# the user inputs fulfil the requirements
13+
14+
# Some basic stuff that identifies the algorithm
15+
id_author = "Group Uulke van der Heide, NKI"
16+
id_algorithm_type = "Bi-exponential fit"
17+
id_return_parameters = "f, D*, D"
18+
id_units = "seconds per milli metre squared or milliseconds per micro metre squared"
19+
20+
# Algorithm requirements
21+
required_bvalues = 4
22+
required_thresholds = [0,
23+
0] # Interval from "at least" to "at most", in case submissions allow a custom number of thresholds
24+
required_bounds = False
25+
required_bounds_optional = False # Bounds may not be required but are optional
26+
required_initial_guess = False
27+
required_initial_guess_optional =False
28+
accepted_dimensions = 1 # Not sure how to define this for the number of accepted dimensions. Perhaps like the thresholds, at least and at most?
29+
30+
def __init__(self, bvalues=None, thresholds=None,bounds=None,initial_guess=None):
31+
"""
32+
Everything this algorithm requires should be implemented here.
33+
Number of segmentation thresholds, bounds, etc.
34+
35+
Our OsipiBase object could contain functions that compare the inputs with
36+
the requirements.
37+
"""
38+
super(PvH_KB_NKI_IVIMfit, self).__init__(bvalues, thresholds,bounds,initial_guess)
39+
self.NKI_algorithm = generate_IVIMmaps_standalone
40+
41+
42+
def ivim_fit(self, signals, bvalues=None):
43+
"""Perform the IVIM fit
44+
45+
Args:
46+
signals (array-like)
47+
bvalues (array-like, optional): b-values for the signals. If None, self.bvalues will be used. Default is None.
48+
49+
Returns:
50+
_type_: _description_
51+
"""
52+
#bvalues = np.array(bvalues)
53+
bvalues = bvalues.tolist() #NKI code expects a list instead of nparray
54+
# reshape signal as the NKI code expects a 4D array
55+
signals = np.reshape(signals, (1, 1, 1, len(signals))) # assuming that in this test the signals are always single voxel
56+
fit_results = self.NKI_algorithm(signals,bvalues)
57+
58+
D = fit_results[0][0,0,0]/1000
59+
f = fit_results[1][0,0,0]
60+
Dstar = fit_results[2][0,0,0]/1000
61+
62+
return f, Dstar, D

tests/IVIMmodels/unit_tests/algorithms.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
"IAR_LU_subtracted",
1010
"OGC_AmsterdamUMC_Bayesian_biexp",
1111
"OGC_AmsterdamUMC_biexp_segmented",
12-
"OGC_AmsterdamUMC_biexp"
12+
"OGC_AmsterdamUMC_biexp",
13+
"PV_MUMC_biexp",
14+
"PvH_KB_NKI_IVIMfit"
1315
],
1416
"IAR_LU_biexp": {
1517
"tolerances": {

tests/IVIMmodels/unit_tests/simple_test_run_of_algorithm.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
from pathlib import Path
44
#from src.standardized.ETP_SRI_LinearFitting import ETP_SRI_LinearFitting
55
from src.standardized.IAR_LU_biexp import IAR_LU_biexp
6-
from src.standardized.IAR_LU_segmented_2step import IAR_LU_segmented_2step
6+
#from src.standardized.IAR_LU_segmented_2step import IAR_LU_segmented_2step
7+
from src.standardized.PvH_KB_NKI_IVIMfit import PvH_KB_NKI_IVIMfit
78
#from src.standardized.PV_MUMC_biexp import PV_MUMC_biexp
89

910
## Simple test code...
@@ -26,7 +27,7 @@ def ivim_model(b, S0=1, f=0.1, Dstar=0.01, D=0.001):
2627

2728
#model1 = ETP_SRI_LinearFitting(thresholds=[200])
2829
#model2 = IAR_LU_biexp()
29-
model2 = IAR_LU_segmented_2step()
30+
model2 = PvH_KB_NKI_IVIMfit()
3031

3132
#dev_test_run(model1, linear_fit_option=True)
32-
dev_test_run(model2, thresholds="Arbitrary kwarg for ivim_fit")
33+
dev_test_run(model2)

0 commit comments

Comments
 (0)