Skip to content

Commit 6e6a9a0

Browse files
authored
Merge pull request #79 from OSIPI/Unit_Testing_Tolerances
Unit testing tolerances + more consistent definition of SNR
2 parents c9af8ef + 01d51aa commit 6e6a9a0

26 files changed

+571
-515
lines changed

WrapImage/nifti_wrapper.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ def loop_over_first_n_minus_1_dimensions(arr):
111111
for idx, view in tqdm(loop_over_first_n_minus_1_dimensions(data), desc=f"{args.algorithm} is fitting", dynamic_ncols=True, total=total_iteration):
112112
fit_result = fit.osipi_fit(view, bvals)
113113
f_image.append(fit_result["f"])
114-
Dp_image.append(fit_result["D*"])
114+
Dp_image.append(fit_result["Dp"])
115115
D_image.append(fit_result["D"])
116116

117117
# Convert lists to NumPy arrays

doc/Introduction_to_TF24_IVIM-MRI_CodeCollection_github_and_IVIM_Analysis_using_Python.ipynb

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -532,7 +532,7 @@
532532
{
533533
"data": {
534534
"text/plain": [
535-
"{'f': array(0.04609779), 'D*': array(0.01136011), 'D': array(0.00071134)}"
535+
"{'f': array(0.04609779), 'Dp': array(0.01136011), 'D': array(0.00071134)}"
536536
]
537537
},
538538
"execution_count": 15,
@@ -569,7 +569,7 @@
569569
{
570570
"data": {
571571
"text/plain": [
572-
"{'f': array(0.04611801), 'D*': array(0.0113541), 'D': array(0.0007113)}"
572+
"{'f': array(0.04611801), 'Dp': array(0.0113541), 'D': array(0.0007113)}"
573573
]
574574
},
575575
"execution_count": 16,
@@ -636,10 +636,10 @@
636636
"#plot the results of algorithm 1\n",
637637
"plt.subplot(121)\n",
638638
"plt.plot(np.unique(bval),signal_1dir,'x')\n",
639-
"plt.plot(np.unique(bval),fit['f']*np.exp(-np.unique(bval)*fit['D*'])+(1-fit['f'])*np.exp(-np.unique(bval)*fit['D']))\n",
640-
"plt.plot(np.unique(bval),fit['f']*np.exp(-np.unique(bval)*fit['D*']))\n",
639+
"plt.plot(np.unique(bval),fit['f']*np.exp(-np.unique(bval)*fit['Dp'])+(1-fit['f'])*np.exp(-np.unique(bval)*fit['D']))\n",
640+
"plt.plot(np.unique(bval),fit['f']*np.exp(-np.unique(bval)*fit['Dp']))\n",
641641
"plt.plot(np.unique(bval),(1-fit['f'])*np.exp(-np.unique(bval)*fit['D']))\n",
642-
"plt.legend(['measured data','model fit','D*','D'])\n",
642+
"plt.legend(['measured data','model fit','Dp','D'])\n",
643643
"plt.ylabel('S/S0')\n",
644644
"plt.xlabel('b-value [s/mm^2]')\n",
645645
"plt.title('algorithm 1')\n",
@@ -650,10 +650,10 @@
650650
"#plot the results of algorithm 2\n",
651651
"plt.subplot(122)\n",
652652
"plt.plot(np.unique(bval),signal_1dir,'x')\n",
653-
"plt.plot(np.unique(bval),fit['f']*np.exp(-np.unique(bval)*fit['D*'])+(1-fit['f'])*np.exp(-np.unique(bval)*fit['D']))\n",
654-
"plt.plot(np.unique(bval),fit['f']*np.exp(-np.unique(bval)*fit['D*']))\n",
653+
"plt.plot(np.unique(bval),fit['f']*np.exp(-np.unique(bval)*fit['Dp'])+(1-fit['f'])*np.exp(-np.unique(bval)*fit['D']))\n",
654+
"plt.plot(np.unique(bval),fit['f']*np.exp(-np.unique(bval)*fit['Dp']))\n",
655655
"plt.plot(np.unique(bval),(1-fit['f'])*np.exp(-np.unique(bval)*fit['D']))\n",
656-
"plt.legend(['measured data','model fit','D*','D'])\n",
656+
"plt.legend(['measured data','model fit','Dp','D'])\n",
657657
"plt.ylabel('S/S0')\n",
658658
"plt.xlabel('b-value [s/mm^2]')\n",
659659
"plt.title('algorithm 2')\n"
@@ -818,7 +818,7 @@
818818
"data": {
819819
"text/plain": [
820820
"{'f': array([0., 0., 0., ..., 0., 0., 0.]),\n",
821-
" 'D*': array([0., 0., 0., ..., 0., 0., 0.]),\n",
821+
" 'Dp': array([0., 0., 0., ..., 0., 0., 0.]),\n",
822822
" 'D': array([0., 0., 0., ..., 0., 0., 0.])}"
823823
]
824824
},

phantoms/MR_XCAT_qMRI/sim_ivim_sig.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -459,9 +459,9 @@ def parse_bvalues_file(file_path):
459459
signals = np.squeeze(voxels[int(voxels.shape[0] * voxel_selector_fraction)]).tolist()
460460
generic_data[name] = {
461461
'noise': noise,
462-
'D': np.mean(Dim[selector], axis=0),
463-
'f': np.mean(fim[selector], axis=0),
464-
'Dp': np.mean(Dpim[selector], axis=0),
462+
'D': np.median(Dim[selector], axis=0),
463+
'f': np.median(fim[selector], axis=0),
464+
'Dp': np.median(Dpim[selector], axis=0),
465465
'data': signals
466466
}
467467
generic_data['config'] = {

src/original/OGC_AmsterdamUMC/LSQ_fitting.py

Lines changed: 26 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -124,29 +124,32 @@ def fit_segmented(bvalues, dw_data, bounds=([0, 0, 0.005],[0.005, 0.7, 0.2]), cu
124124
:return Dp: Fitted Dp
125125
:return S0: Fitted S0
126126
"""
127-
p0 = [p0[0] * 1000, p0[1] * 10, p0[2] * 10, p0[3]]
128127
try:
129128
# determine high b-values and data for D
129+
dw_data=dw_data/np.mean(dw_data[bvalues==0])
130130
high_b = bvalues[bvalues >= cutoff]
131131
high_dw_data = dw_data[bvalues >= cutoff]
132132
# correct the bounds. Note that S0 bounds determine the max and min of f
133-
bounds1 = ([bounds[0][0] * 1000., 0.7 - bounds[1][1]], [bounds[1][0] * 1000., 1.3 - bounds[0][
134-
1]]) # By bounding S0 like this, we effectively insert the boundaries of f
135-
# fit for S0' and D
136-
params, _ = curve_fit(lambda b, Dt, int: int * np.exp(-b * Dt / 1000), high_b, high_dw_data,
137-
p0=(p0[0], p0[3]-p0[1]/10),
133+
assert bounds[0][1] < p0[1]
134+
assert bounds[1][1] > p0[1]
135+
bounds1 = ([bounds[0][0], 0], [bounds[1][0], 10000000000])
136+
params, _ = curve_fit(lambda b, Dt, int: int * np.exp(-b * Dt ), high_b, high_dw_data,
137+
p0=(p0[0], p0[3]-p0[1]),
138138
bounds=bounds1)
139-
Dt, Fp = params[0] / 1000, 1 - params[1]
139+
Dt, Fp = params[0], 1 - params[1]
140+
if Fp < bounds[0][1] : Fp = np.float64(bounds[0][1])
141+
if Fp > bounds[1][1] : Fp = np.float64(bounds[1][1])
142+
140143
# remove the diffusion part to only keep the pseudo-diffusion
141144
dw_data_remaining = dw_data - (1 - Fp) * np.exp(-bvalues * Dt)
142-
bounds2 = (bounds[0][2]*10, bounds[1][2]*10)
145+
bounds2 = (bounds[0][2], bounds[1][2])
143146
# fit for D*
144147
params, _ = curve_fit(lambda b, Dp: Fp * np.exp(-b * Dp), bvalues, dw_data_remaining, p0=(p0[2]), bounds=bounds2)
145148
Dp = params[0]
146149
return Dt, Fp, Dp
147150
except:
148151
# if fit fails, return zeros
149-
# print('segnetned fit failed')
152+
# print('segmented fit failed')
150153
return 0., 0., 0.
151154

152155

@@ -235,17 +238,17 @@ def fit_least_squares(bvalues, dw_data, S0_output=False, fitS0=True,
235238
try:
236239
if not fitS0:
237240
# bounds are rescaled such that each parameter changes at roughly the same rate to help fitting.
238-
bounds = ([bounds[0][0] * 1000, bounds[0][1] * 10, bounds[0][2] * 10],
241+
bounds2 = ([bounds[0][0] * 1000, bounds[0][1] * 10, bounds[0][2] * 10],
239242
[bounds[1][0] * 1000, bounds[1][1] * 10, bounds[1][2] * 10])
240-
p0=[p0[0]*1000,p0[1]*10,p0[2]*10]
241-
params, _ = curve_fit(ivimN_noS0, bvalues, dw_data, p0=p0, bounds=bounds)
243+
p1=[p0[0]*1000,p0[1]*10,p0[2]*10]
244+
params, _ = curve_fit(ivimN_noS0, bvalues, dw_data, p0=p1, bounds=bounds2)
242245
S0 = 1
243246
else:
244247
# bounds are rescaled such that each parameter changes at roughly the same rate to help fitting.
245-
bounds = ([bounds[0][0] * 1000, bounds[0][1] * 10, bounds[0][2] * 10, bounds[0][3]],
248+
bounds2 = ([bounds[0][0] * 1000, bounds[0][1] * 10, bounds[0][2] * 10, bounds[0][3]],
246249
[bounds[1][0] * 1000, bounds[1][1] * 10, bounds[1][2] * 10, bounds[1][3]])
247-
p0=[p0[0]*1000,p0[1]*10,p0[2]*10,p0[3]]
248-
params, _ = curve_fit(ivimN, bvalues, dw_data, p0=p0, bounds=bounds)
250+
p1=[p0[0]*1000,p0[1]*10,p0[2]*10,p0[3]]
251+
params, _ = curve_fit(ivimN, bvalues, dw_data, p0=p1, bounds=bounds2)
249252
S0 = params[3]
250253
# correct for the rescaling of parameters
251254
Dt, Fp, Dp = params[0] / 1000, params[1] / 10, params[2] / 10
@@ -258,10 +261,10 @@ def fit_least_squares(bvalues, dw_data, S0_output=False, fitS0=True,
258261
# if fit fails, then do a segmented fit instead
259262
# print('lsq fit failed, trying segmented')
260263
if S0_output:
261-
Dt, Fp, Dp = fit_segmented(bvalues, dw_data, bounds=bounds)
264+
Dt, Fp, Dp = fit_segmented(bvalues, dw_data, bounds=bounds,p0=p0)
262265
return Dt, Fp, Dp, 1
263266
else:
264-
return fit_segmented(bvalues, dw_data)
267+
return fit_segmented(bvalues, dw_data, bounds=bounds,p0=p0)
265268

266269

267270
def fit_least_squares_array_tri_exp(bvalues, dw_data, S0_output=True, fitS0=True, njobs=4,
@@ -561,19 +564,19 @@ def neg_log_prior(p):
561564
Dt, Fp, Dp = p[0], p[1], p[2]
562565
# make D*<D very unlikely
563566
if (Dp < Dt):
564-
return 1e3
567+
return 1e10
565568
else:
566569
# determine and return the prior for D, f and D* (and S0)
567570
if len(p) == 4:
568-
if Dt_range[0] < Dt < Dt_range[1] and Fp_range[0] < Fp < Fp_range[1] and Dp_range[0] < Dp < Dp_range[1]: # and S0_range[0] < S0 < S0_range[1]: << not sure whether this helps. Technically it should be here
571+
if Dt_range[0] < Dt < Dt_range[1] and Fp_range[0] < Fp < Fp_range[1] and Dp_range[0] < Dp < Dp_range[1] and S0_range[0] < S0 < S0_range[1]: #<< not sure whether this helps. Technically it should be here
569572
return 0
570573
else:
571-
return 1e3
574+
return 1e10
572575
else:
573576
if Dt_range[0] < Dt < Dt_range[1] and Fp_range[0] < Fp < Fp_range[1] and Dp_range[0] < Dp < Dp_range[1]:
574577
return 0
575578
else:
576-
return 1e3
579+
return 1e10
577580

578581
return neg_log_prior
579582

@@ -638,7 +641,7 @@ def parfun(i):
638641
return Dt_pred, Fp_pred, Dp_pred, S0_pred
639642

640643

641-
def fit_bayesian(bvalues, dw_data, neg_log_prior, x0=[0.001, 0.2, 0.05, 1], fitS0=True):
644+
def fit_bayesian(bvalues, dw_data, neg_log_prior, x0=[0.001, 0.2, 0.05, 1], fitS0=True, bounds=([0,0,0,0],[0.005,1.5,2,2.5])):
642645
'''
643646
This is an implementation of the Bayesian IVIM fit. It returns the Maximum a posterior probability.
644647
The fit is taken from Barbieri et al. which was initially introduced in http://arxiv.org/10.1002/mrm.25765 and
@@ -655,7 +658,7 @@ def fit_bayesian(bvalues, dw_data, neg_log_prior, x0=[0.001, 0.2, 0.05, 1], fitS
655658
'''
656659
try:
657660
# define fit bounds
658-
bounds = [(0, 0.005), (0, 1.5), (0, 2), (0, 2.5)]
661+
bounds = [(bounds[0][0], bounds[1][0]), (bounds[0][1], bounds[1][1]), (bounds[0][2], bounds[1][2]), (bounds[0][3], bounds[1][3])]
659662
# Find the Maximum a posterior probability (MAP) by minimising the negative log of the posterior
660663
if fitS0:
661664
params = minimize(neg_log_posterior, x0=x0, args=(bvalues, dw_data, neg_log_prior), bounds=bounds)

src/standardized/ETP_SRI_LinearFitting.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,13 @@ def __init__(self, bvalues=None, thresholds=None, bounds=None, initial_guess=Non
4141
Our OsipiBase object could contain functions that compare the inputs with
4242
the requirements.
4343
"""
44-
super(ETP_SRI_LinearFitting, self).__init__(bvalues=bvalues, thresholds=thresholds, bounds=bounds, initial_guess=initial_guess)
45-
44+
45+
super(ETP_SRI_LinearFitting, self).__init__(bvalues, thresholds, bounds, initial_guess)
46+
if bounds is not None:
47+
print('warning, bounds from wrapper are not (yet) used in this algorithm')
48+
self.use_bounds = False
49+
self.use_initial_guess = False
50+
4651
# Could be a good idea to have all the submission-specfic variable be
4752
# defined with initials?
4853
self.ETP_weighting = weighting
@@ -62,6 +67,7 @@ def ivim_fit(self, signals, bvalues=None, linear_fit_option=False, **kwargs):
6267
Returns:
6368
_type_: _description_
6469
"""
70+
signals[signals<0.0000001]=0.0000001
6571
if bvalues is None:
6672
bvalues = self.bvalues
6773

@@ -75,14 +81,14 @@ def ivim_fit(self, signals, bvalues=None, linear_fit_option=False, **kwargs):
7581
f, Dstar = ETP_object.linear_fit(bvalues, signals, self.ETP_weighting, self.ETP_stats)
7682

7783
results["f"] = f
78-
results["D*"] = Dstar
84+
results["Dp"] = Dstar
7985

8086
return results
8187
else:
8288
f, D, Dstar = ETP_object.ivim_fit(bvalues, signals)
8389

8490
results["f"] = f
85-
results["D*"] = Dstar
91+
results["Dp"] = Dstar
8692
results["D"] = D
8793

8894
return results

src/standardized/IAR_LU_biexp.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,10 @@ def __init__(self, bvalues=None, thresholds=None, bounds=None, initial_guess=Non
4242
the requirements.
4343
"""
4444
super(IAR_LU_biexp, self).__init__(bvalues, thresholds, bounds, initial_guess)
45-
45+
if bounds is not None:
46+
print('warning, bounds from wrapper are not (yet) used in this algorithm')
47+
self.use_bounds = False
48+
self.use_initial_guess = False
4649
# Check the inputs
4750

4851
# Initialize the algorithm
@@ -83,7 +86,7 @@ def ivim_fit(self, signals, bvalues, **kwargs):
8386

8487
results = {}
8588
results["f"] = fit_results.model_params[1]
86-
results["D*"] = fit_results.model_params[2]
89+
results["Dp"] = fit_results.model_params[2]
8790
results["D"] = fit_results.model_params[3]
8891

8992
return results
@@ -115,7 +118,7 @@ def ivim_fit_full_volume(self, signals, bvalues, **kwargs):
115118

116119
results = {}
117120
results["f"] = fit_results.model_params[..., 1]
118-
results["D*"] = fit_results.model_params[..., 2]
121+
results["Dp"] = fit_results.model_params[..., 2]
119122
results["D"] = fit_results.model_params[..., 3]
120123

121124
return results

src/standardized/IAR_LU_modified_mix.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,10 @@ def __init__(self, bvalues=None, thresholds=None, bounds=None, initial_guess=Non
4242
the requirements.
4343
"""
4444
super(IAR_LU_modified_mix, self).__init__(bvalues, thresholds, bounds, initial_guess)
45-
45+
if bounds is not None:
46+
print('warning, bounds from wrapper are not (yet) used in this algorithm')
47+
self.use_bounds = False
48+
self.use_initial_guess = False
4649
# Check the inputs
4750

4851
# Initialize the algorithm
@@ -86,7 +89,7 @@ def ivim_fit(self, signals, bvalues, **kwargs):
8689
#D = fit_results.model_params[3]
8790
results = {}
8891
results["f"] = fit_results.model_params[1]
89-
results["D*"] = fit_results.model_params[2]
92+
results["Dp"] = fit_results.model_params[2]
9093
results["D"] = fit_results.model_params[3]
9194

9295
return results

src/standardized/IAR_LU_modified_topopro.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,10 @@ def __init__(self, bvalues=None, thresholds=None, bounds=None, initial_guess=Non
4242
the requirements.
4343
"""
4444
super(IAR_LU_modified_topopro, self).__init__(bvalues, thresholds, bounds, initial_guess)
45-
45+
if bounds is not None:
46+
print('warning, bounds from wrapper are not (yet) used in this algorithm')
47+
self.use_bounds = False
48+
self.use_initial_guess = False
4649
# Check the inputs
4750

4851
# Initialize the algorithm
@@ -88,7 +91,7 @@ def ivim_fit(self, signals, bvalues, **kwargs):
8891
#return f, Dstar, D
8992
results = {}
9093
results["f"] = fit_results.model_params[1]
91-
results["D*"] = fit_results.model_params[2]
94+
results["Dp"] = fit_results.model_params[2]
9295
results["D"] = fit_results.model_params[3]
9396

9497
return results

src/standardized/IAR_LU_segmented_2step.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,10 @@ def __init__(self, bvalues=None, thresholds=None, bounds=None, initial_guess=Non
4242
the requirements.
4343
"""
4444
super(IAR_LU_segmented_2step, self).__init__(bvalues, thresholds, bounds, initial_guess)
45-
45+
if bounds is not None:
46+
print('warning, bounds from wrapper are not (yet) used in this algorithm')
47+
self.use_bounds = False
48+
self.use_initial_guess = False
4649
# Check the inputs
4750

4851
# Initialize the algorithm
@@ -92,7 +95,7 @@ def ivim_fit(self, signals, bvalues, thresholds=None, **kwargs):
9295
#return f, Dstar, D
9396
results = {}
9497
results["f"] = fit_results.model_params[1]
95-
results["D*"] = fit_results.model_params[2]
98+
results["Dp"] = fit_results.model_params[2]
9699
results["D"] = fit_results.model_params[3]
97100

98101
return results

src/standardized/IAR_LU_segmented_3step.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,10 @@ def __init__(self, bvalues=None, thresholds=None, bounds=None, initial_guess=Non
4242
the requirements.
4343
"""
4444
super(IAR_LU_segmented_3step, self).__init__(bvalues, thresholds, bounds, initial_guess)
45-
45+
if bounds is not None:
46+
print('warning, bounds from wrapper are not (yet) used in this algorithm')
47+
self.use_bounds = False
48+
self.use_initial_guess = False
4649
# Check the inputs
4750

4851
# Initialize the algorithm
@@ -88,7 +91,7 @@ def ivim_fit(self, signals, bvalues, **kwargs):
8891
#return f, Dstar, D
8992
results = {}
9093
results["f"] = fit_results.model_params[1]
91-
results["D*"] = fit_results.model_params[2]
94+
results["Dp"] = fit_results.model_params[2]
9295
results["D"] = fit_results.model_params[3]
9396

9497
return results

src/standardized/IAR_LU_subtracted.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,10 @@ def __init__(self, bvalues=None, thresholds=None, bounds=None, initial_guess=Non
4242
the requirements.
4343
"""
4444
super(IAR_LU_subtracted, self).__init__(bvalues, thresholds, bounds, initial_guess)
45-
45+
if bounds is not None:
46+
print('warning, bounds from wrapper are not (yet) used in this algorithm')
47+
self.use_bounds = False
48+
self.use_initial_guess = False
4649
# Check the inputs
4750

4851
# Initialize the algorithm
@@ -88,7 +91,7 @@ def ivim_fit(self, signals, bvalues, **kwargs):
8891
#return f, Dstar, D
8992
results = {}
9093
results["f"] = fit_results.model_params[1]
91-
results["D*"] = fit_results.model_params[2]
94+
results["Dp"] = fit_results.model_params[2]
9295
results["D"] = fit_results.model_params[3]
9396

9497
return results

0 commit comments

Comments
 (0)