Skip to content

Commit d2d775e

Browse files
update after review Ivan
- Now use ivim_fit_full_volume instead - Skipped
1 parent 8f70b7f commit d2d775e

File tree

8 files changed

+173
-38
lines changed

8 files changed

+173
-38
lines changed

src/standardized/IVIM_NEToptim.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def initialize(self, bounds, initial_guess, fitS0, traindata, SNR):
6060
if SNR is None:
6161
warnings.warn('No SNR indicated. Data simulated with SNR = (5-1000)')
6262
SNR = (5, 1000)
63-
self.training_data(self.bvalues,n=1000000,SNR=SNR)
63+
self.osipi_training_data(self.bvalues,n=1000000,SNR=SNR)
6464
self.arg=Arg()
6565
if bounds is not None:
6666
self.arg.net_pars.cons_min = bounds[0] # Dt, Fp, Ds, S0
@@ -77,7 +77,7 @@ def ivim_fit(self, signals, bvalues, **kwargs):
7777
7878
Args:
7979
signals (array-like)
80-
bvalues (array-like, optional): b-values for the signals. If None, self.bvalues will be used. Default is None.
80+
bvalues (array-like): b-values for the signals. If None, self.bvalues will be used. Default is None.
8181
8282
Returns:
8383
_type_: _description_
@@ -94,6 +94,29 @@ def ivim_fit(self, signals, bvalues, **kwargs):
9494

9595
return results
9696

97+
98+
def ivim_fit_full_volume(self, signals, bvalues, **kwargs):
99+
"""Perform the IVIM fit
100+
101+
Args:
102+
signals (array-like)
103+
bvalues (array-like): b-values for the signals. If None, self.bvalues will be used. Default is None.
104+
105+
Returns:
106+
_type_: _description_
107+
"""
108+
if not np.array_equal(bvalues, self.bvalues):
109+
raise ValueError("bvalue list at fitting must be identical as the one at initiation, otherwise it will not run")
110+
signals = self.osipi_reshape_to_voxelwise(signals)
111+
paramsNN = deep.predict_IVIM(signals, self.bvalues, self.net, self.arg)
112+
113+
results = {}
114+
results["D"] = paramsNN[0]
115+
results["f"] = paramsNN[1]
116+
results["Dp"] = paramsNN[2]
117+
118+
return results
119+
97120
class NetArgs:
98121
def __init__(self):
99122
self.optim = 'adam' # these are the optimisers implementd. Choices are: 'sgd'; 'sgdr'; 'adagrad' adam

src/standardized/OGC_AmsterdamUMC_Bayesian_biexp.py

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from src.wrappers.OsipiBase import OsipiBase
2-
from src.original.OGC_AmsterdamUMC.LSQ_fitting import flat_neg_log_prior, fit_bayesian, empirical_neg_log_prior, fit_segmented
2+
from src.original.OGC_AmsterdamUMC.LSQ_fitting import flat_neg_log_prior, fit_bayesian, empirical_neg_log_prior, fit_segmented, fit_bayesian_array, fit_segmented_array
33
import numpy as np
44

55
class OGC_AmsterdamUMC_Bayesian_biexp(OsipiBase):
@@ -34,7 +34,7 @@ class OGC_AmsterdamUMC_Bayesian_biexp(OsipiBase):
3434
supported_dimensions = 1
3535
supported_priors = True
3636

37-
def __init__(self, bvalues=None, thresholds=None, bounds=None, initial_guess=None, fitS0=True, prior_in=None):
37+
def __init__(self, bvalues=None, thresholds=150, bounds=None, initial_guess=None, fitS0=True, prior_in=None):
3838

3939
"""
4040
Everything this algorithm requires should be implemented here.
@@ -44,14 +44,21 @@ def __init__(self, bvalues=None, thresholds=None, bounds=None, initial_guess=Non
4444
the requirements.
4545
4646
Args:
47-
datain is a 2D array with values of D, f, D* (and S0) that will form the prior.
47+
datain (Array): is a 2D array with values of D, f, D* (and S
48+
) that will form the prior.
49+
thresholds (Bolean, optional): a bolean indicating what threshold is used
50+
prior_in (array, optional): 2D array of D, f, D* and (optionally) S0 values which form the prior
51+
4852
"""
4953
super(OGC_AmsterdamUMC_Bayesian_biexp, self).__init__(bvalues=bvalues, bounds=bounds, thresholds=thresholds, initial_guess=initial_guess) #, fitS0, prior_in)
5054
self.OGC_algorithm = fit_bayesian
51-
self.initialize(bounds, initial_guess, fitS0, prior_in)
55+
self.OGC_algorithm_array = fit_bayesian_array
56+
self.initialize(bounds, initial_guess, fitS0, prior_in, thresholds)
5257
self.fit_segmented=fit_segmented
5358

54-
def initialize(self, bounds=None, initial_guess=None, fitS0=True, prior_in=None):
59+
def initialize(self, bounds=None, initial_guess=None, fitS0=True, prior_in=None, thresholds=None):
60+
61+
5562
if bounds is None:
5663
print('warning, no bounds were defined, so default bounds are used of [0, 0, 0.005, 0.7],[0.005, 1.0, 0.2, 1.3]')
5764
self.bounds=([0, 0, 0.005, 0.7],[0.005, 1.0, 0.2, 1.3])
@@ -64,7 +71,7 @@ def initialize(self, bounds=None, initial_guess=None, fitS0=True, prior_in=None)
6471
self.initial_guess = initial_guess
6572
self.use_initial_guess = True
6673
self.use_bounds = True
67-
self.thresholds = 150
74+
self.thresholds = thresholds
6875
if prior_in is None:
6976
print('using a flat prior between bounds')
7077
self.neg_log_prior=flat_neg_log_prior([self.bounds[0][0],self.bounds[1][0]],[self.bounds[0][1],self.bounds[1][1]],[self.bounds[0][2],self.bounds[1][2]],[self.bounds[0][3],self.bounds[1][3]])
@@ -101,4 +108,31 @@ def ivim_fit(self, signals, bvalues, initial_guess=None, **kwargs):
101108
results["f"] = fit_results[1]
102109
results["Dp"] = fit_results[2]
103110

111+
return results
112+
113+
def ivim_fit_full_volume(self, signals, bvalues, initial_guess=None, **kwargs):
114+
"""Perform the IVIM fit
115+
116+
Args:
117+
signals (array-like)
118+
bvalues (array-like, optional): b-values for the signals. If None, self.bvalues will be used. Default is None.
119+
120+
Returns:
121+
_type_: _description_
122+
"""
123+
bvalues=np.array(bvalues)
124+
125+
epsilon = 0.000001
126+
fit_results = fit_segmented_array(bvalues, signals, bounds=self.bounds, cutoff=self.thresholds, p0=self.initial_guess)
127+
fit_results=np.array(fit_results+(1,))
128+
for i in range(4):
129+
if fit_results[i] < self.bounds[0][i] : fit_results[0] = self.bounds[0][i]+epsilon
130+
if fit_results[i] > self.bounds[1][i] : fit_results[0] = self.bounds[1][i]-epsilon
131+
fit_results = self.OGC_algorithm_array(bvalues, signals, self.neg_log_prior, x0=fit_results, fitS0=self.fitS0, bounds=self.bounds)
132+
133+
results = {}
134+
results["D"] = fit_results[0]
135+
results["f"] = fit_results[1]
136+
results["Dp"] = fit_results[2]
137+
104138
return results

src/standardized/OGC_AmsterdamUMC_biexp.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from src.wrappers.OsipiBase import OsipiBase
2-
from src.original.OGC_AmsterdamUMC.LSQ_fitting import fit_least_squares
2+
from src.original.OGC_AmsterdamUMC.LSQ_fitting import fit_least_squares, fit_least_squares_array
33
import numpy as np
44

55
class OGC_AmsterdamUMC_biexp(OsipiBase):
@@ -45,6 +45,7 @@ def __init__(self, bvalues=None, thresholds=None, bounds=None, initial_guess=Non
4545
#super(OGC_AmsterdamUMC_biexp, self).__init__(bvalues, bounds, initial_guess, fitS0)
4646
super(OGC_AmsterdamUMC_biexp, self).__init__(bvalues=bvalues, bounds=bounds, initial_guess=initial_guess)
4747
self.OGC_algorithm = fit_least_squares
48+
self.OGC_algorithm_array = fit_least_squares_array
4849
self.fitS0=fitS0
4950
self.initialize(bounds, initial_guess, fitS0)
5051

@@ -83,4 +84,25 @@ def ivim_fit(self, signals, bvalues, **kwargs):
8384
results["f"] = fit_results[1]
8485
results["Dp"] = fit_results[2]
8586

87+
return results
88+
89+
def ivim_fit_full_volume(self, signals, bvalues, **kwargs):
90+
"""Perform the IVIM fit
91+
92+
Args:
93+
signals (array-like)
94+
bvalues (array-like, optional): b-values for the signals. If None, self.bvalues will be used. Default is None.
95+
96+
Returns:
97+
_type_: _description_
98+
"""
99+
100+
bvalues=np.array(bvalues)
101+
fit_results = self.OGC_algorithm_array(bvalues, signals, p0=self.initial_guess, bounds=self.bounds, fitS0=self.fitS0)
102+
103+
results = {}
104+
results["D"] = fit_results[0]
105+
results["f"] = fit_results[1]
106+
results["Dp"] = fit_results[2]
107+
86108
return results

src/standardized/OGC_AmsterdamUMC_biexp_segmented.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from src.wrappers.OsipiBase import OsipiBase
2-
from src.original.OGC_AmsterdamUMC.LSQ_fitting import fit_segmented
2+
from src.original.OGC_AmsterdamUMC.LSQ_fitting import fit_segmented, fit_segmented_array
33
import numpy as np
44

55
class OGC_AmsterdamUMC_biexp_segmented(OsipiBase):
@@ -44,6 +44,7 @@ def __init__(self, bvalues=None, thresholds=150, bounds=None, initial_guess=None
4444
"""
4545
super(OGC_AmsterdamUMC_biexp_segmented, self).__init__(bvalues, thresholds, bounds, initial_guess)
4646
self.OGC_algorithm = fit_segmented
47+
self.OGC_algorithm_array = fit_segmented_array
4748
self.initialize(bounds, initial_guess, thresholds)
4849

4950

@@ -65,6 +66,7 @@ def initialize(self, bounds, initial_guess, thresholds):
6566
print('warning, no thresholds were defined, so default bounds are used of 150')
6667
else:
6768
self.thresholds = thresholds
69+
6870
def ivim_fit(self, signals, bvalues, **kwargs):
6971
"""Perform the IVIM fit
7072
@@ -84,4 +86,27 @@ def ivim_fit(self, signals, bvalues, **kwargs):
8486
results["f"] = fit_results[1]
8587
results["Dp"] = fit_results[2]
8688

89+
return results
90+
91+
92+
def ivim_fit_full_volume(self, signals, bvalues, **kwargs):
93+
"""Perform the IVIM fit
94+
95+
Args:
96+
signals (array-like)
97+
bvalues (array-like, optional): b-values for the signals. If None, self.bvalues will be used. Default is None.
98+
99+
Returns:
100+
_type_: _description_
101+
"""
102+
signals = self.osipi_reshape_to_voxelwise(signals)
103+
104+
bvalues=np.array(bvalues)
105+
fit_results = self.OGC_algorithm_array(bvalues, signals, bounds=self.bounds, cutoff=self.thresholds, p0=self.initial_guess)
106+
107+
results = {}
108+
results["D"] = fit_results[0]
109+
results["f"] = fit_results[1]
110+
results["Dp"] = fit_results[2]
111+
87112
return results

src/standardized/Super_IVIM_DC.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,3 +108,30 @@ def ivim_fit(self, signals, bvalues, **kwargs):
108108
results["Dp"] = Dp
109109

110110
return results
111+
112+
113+
def test_deep_learning_algorithms(self, signals, bvalues, **kwargs):
114+
"""Perform the IVIM fit
115+
116+
Args:
117+
signals (array-like)
118+
bvalues (array-like, optional): b-values for the signals. If None, self.bvalues will be used. Default is None.
119+
120+
Returns:
121+
results: a dictionary containing "d", "f", and "Dp".
122+
"""
123+
if not np.array_equal(bvalues, self.bvalues):
124+
raise ValueError("bvalue list at fitting must be identical as the one at initiation, otherwise it will not run")
125+
126+
Dp, Dt, f, S0_superivimdc = infer_from_signal(
127+
signal=signals,
128+
bvalues=self.bvalues,
129+
model_path=f"{self.working_dir}/{self.super_ivim_dc_filename}.pt",
130+
)
131+
132+
results = {}
133+
results["D"] = Dt
134+
results["f"] = f
135+
results["Dp"] = Dp
136+
137+
return results

src/wrappers/OsipiBase.py

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -110,31 +110,26 @@ def osipi_fit(self, data, bvalues=None, **kwargs):
110110
minimum_bvalue = np.min(use_bvalues) # We normalize the signal to the minimum bvalue. Should be 0 or very close to 0.
111111
b0_indices = np.where(use_bvalues == minimum_bvalue)[0]
112112

113-
if not self.deep_learning:
114-
for ijk in tqdm(np.ndindex(data.shape[:-1]), total=np.prod(data.shape[:-1])):
115-
# Normalize array
116-
single_voxel_data = data[ijk]
117-
single_voxel_data_normalization_factor = np.mean(single_voxel_data[b0_indices])
118-
single_voxel_data_normalized = single_voxel_data/single_voxel_data_normalization_factor
119-
120-
args = [single_voxel_data_normalized, use_bvalues]
121-
fit = self.ivim_fit(*args, **kwargs) # For single voxel fits, we assume this is a dict with a float value per key.
122-
for key in list(fit.keys()):
123-
results[key][ijk] = fit[key]
124-
else:
125-
# Note that I am probably high-jacking the wrong part of the code as DL works best for 2D arrays (b-values vs signal decays).
126-
# normalizing of signal still needs implementing for DL.
127-
args = [data, use_bvalues]
128-
results = self.ivim_fit(*args,**kwargs) # For single voxel fits, we assume this is a dict with a float value per key.
113+
for ijk in tqdm(np.ndindex(data.shape[:-1]), total=np.prod(data.shape[:-1])):
114+
# Normalize array
115+
single_voxel_data = data[ijk]
116+
single_voxel_data_normalization_factor = np.mean(single_voxel_data[b0_indices])
117+
single_voxel_data_normalized = single_voxel_data/single_voxel_data_normalization_factor
118+
119+
args = [single_voxel_data_normalized, use_bvalues]
120+
fit = self.ivim_fit(*args, **kwargs) # For single voxel fits, we assume this is a dict with a float value per key.
121+
for key in list(fit.keys()):
122+
results[key][ijk] = fit[key]
129123

130124
#self.parameter_estimates = self.ivim_fit(data, bvalues)
131125
return results
132-
126+
127+
133128
def osipi_fit_full_volume(self, data, bvalues=None, **kwargs):
134129
"""Sends a full volume in one go to the fitting algorithm. The osipi_fit method only sends one voxel at a time.
135130
136131
Args:
137-
data (array): 3D (single slice) or 4D (multi slice) DWI data.
132+
data (array): 2D (data x b-values), 3D (single slice) or 4D (multi slice) DWI data, with last dimension the b-value dimension.
138133
bvalues (array, optional): The b-values of the DWI data. Defaults to None.
139134
140135
Returns:
@@ -179,7 +174,20 @@ def osipi_fit_full_volume(self, data, bvalues=None, **kwargs):
179174
print("Full volume fitting not supported for this algorithm")
180175

181176
return False
182-
177+
178+
179+
def osipi_reshape_to_voxelwise(self, data):
180+
"""
181+
reshapes multi-D input (spatial dims, bvvalue) data to 2D voxel-wise array
182+
Args:
183+
data (array): mulit-D array (data x b-values)
184+
Returns:
185+
out (array): 2D array (voxel x b-value)
186+
"""
187+
B = data.shape[-1]
188+
voxels = int(np.prod(data.shape[:-1])) # e.g., X*Y*Z
189+
return data.reshape(voxels, B)
190+
183191
def osipi_print_requirements(self):
184192
"""
185193
Prints the requirements of the algorithm.
@@ -285,11 +293,11 @@ def osipi_check_required_initial_guess(self):
285293
return True
286294

287295

288-
def osipi_check_required_bvalues():
296+
def osipi_check_required_bvalues(self):
289297
"""Minimum number of b-values required"""
290298
pass
291299

292-
def osipi_author():
300+
def osipi_author(self):
293301
"""Author identification"""
294302
return ''
295303

@@ -323,7 +331,7 @@ def osipi_simple_bias_and_RMSE_test(self, SNR, bvalues, f, Dstar, D, noise_reali
323331
print(f"Dstar bias:\t{Dstar_bias}\nDstar RMSE:\t{Dstar_RMSE}")
324332
print(f"D bias:\t{D_bias}\nD RMSE:\t{D_RMSE}")
325333

326-
def training_data(self, bvalues, data=None, SNR=(5,1000), n=1000000,Drange=(0.0005,0.0034),frange=(0,1),Dprange=(0.005,0.1),rician_noise=False):
334+
def osipi_training_data(self, bvalues, data=None, SNR=(5,1000), n=1000000,Drange=(0.0005,0.0034),frange=(0,1),Dprange=(0.005,0.1),rician_noise=False):
327335
rng = np.random.RandomState(42)
328336
if data is None:
329337
gen = GenerateData(rng=rng)

tests/IVIMmodels/unit_tests/algorithms.json

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,11 @@
1414
"PV_MUMC_biexp",
1515
"PvH_KB_NKI_IVIMfit",
1616
"OJ_GU_seg",
17-
"IVIM_NEToptim",
18-
"Super_IVIM_DC"
17+
"IVIM_NEToptim"
1918
],
2019
"IVIM_NEToptim": {
2120
"deep_learning": true
2221
},
23-
"Super_IVIM_DC": {
24-
"deep_learning": true
25-
},
2622
"ASD_MemorialSloanKettering_QAMPER_IVIM": {
2723
"requires_matlab": true
2824
},

tests/IVIMmodels/unit_tests/test_ivim_fit.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ def test_deep_learning_algorithms(deep_learning_algorithms, record_property):
163163

164164
array_2d = np.array([dat["data"] for _, dat in data.items()])
165165
start_time = time.time()
166-
fit_result = fit.osipi_fit(array_2d, bvals)
166+
fit_result = fit.osipi_fit_full_volume(array_2d, bvals)
167167
elapsed_time = time.time() - start_time
168168

169169
errors = [] # Collect all assertion errors

0 commit comments

Comments
 (0)