Skip to content

Commit f7c9114

Browse files
Merge pull request #101 from OSIPI/reference_fitting_algorithm
Added a segmented-fitting algorithm based on the recommendation paper
2 parents 129f58f + 42c8c2d commit f7c9114

6 files changed

Lines changed: 170 additions & 1 deletion

File tree

phantoms/MR_XCAT_qMRI/sim_ivim_sig.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -429,7 +429,7 @@ def parse_bvalues_file(file_path):
429429
elif args.bvalue:
430430
bvalues = {"cmd": args.bvalue}
431431
else:
432-
bvalues = parse_bvalues_file("b_values.json")
432+
bvalues = parse_bvalues_file("b_values.json")
433433

434434

435435
noise = args.noise

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,5 @@ pandas
1616
sphinx
1717
sphinx_rtd_theme
1818
pytest-json-report
19+
statsmodels
1920
ivimnet
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
"""
2+
Code to calculate IVIM maps
3+
4+
Segmented fitting approach
5+
First fit D using a WLLS
6+
7+
"""
8+
9+
import numpy as np
10+
import statsmodels.api as sm
11+
import scipy
12+
13+
def ivim_biexp(bvalues, D, f, Dp, S0=1):
14+
return (S0 * (f * np.exp(-bvalues * Dp) + (1 - f) * np.exp(-bvalues * D)))
15+
16+
17+
def segmented_IVIM_fit(bvalues, dw_data, b_cutoff = 200, bounds=([0.0001, 0.0, 0.001], [0.004, 0.7, 0.01])):
18+
"""
19+
A segmented fitting implementation for a bi-exponential model.
20+
First D is fitted using a mono exponential model on all signal above the bvalue cutoff using an iterative WLLSVertrekpassage, Schiphol
21+
Then f is fitted by using the b=0 intercept from the mono expontential fit and substracting this from the measured signal at b=0
22+
Then D* is fitted using a bi-exponential model with fixed D and f
23+
24+
"""
25+
bvalues_D = bvalues[bvalues >= b_cutoff]
26+
dw_data_D = dw_data[bvalues >= b_cutoff]
27+
log_data_D = np.log(dw_data_D)
28+
29+
D, b0_intercept = d_fit_iterative_wls(bvalues_D, log_data_D)
30+
31+
D = np.clip(D, bounds[0][0], bounds[1][0])
32+
S0 = dw_data[bvalues == 0].item()
33+
f = (S0 - b0_intercept) / S0
34+
f = np.clip(f, bounds[0][1], bounds[1][1])
35+
36+
Dp = scipy.optimize.least_squares(
37+
fun=lambda Dp: ivim_biexp(bvalues, D, f, Dp[0]) - dw_data,
38+
x0=np.clip(D * 10, bounds[0][2], bounds[1][2]), # Initial guess for D*
39+
bounds=(bounds[0][2], bounds[1][2]))
40+
41+
Dp = Dp.x[0]
42+
43+
return D, f, Dp
44+
45+
46+
def d_fit_iterative_wls(bvalues_D, log_signal, max_iter=50):
47+
"""
48+
Function to calculate D using an iterative wlls on the log(signal)
49+
weights for the wlls are initialized from the predicted signal of a lls as described in
50+
http://dx.doi.org/10.1016/j.neuroimage.2013.05.028 equation (7)
51+
52+
Parameters:
53+
log_signal: the log() of the signal above the threshold for segmented fitting
54+
55+
bvalues_D: all bvalues above the threshold for fitting D
56+
57+
max_iter: the maximum number of iterations for WLS
58+
59+
"""
60+
61+
bvals = sm.add_constant(-bvalues_D)
62+
# First do a LLS to initialize the weights
63+
beta_lls = np.linalg.lstsq(bvals, log_signal, rcond=None)[0]
64+
# initialize the weights based on the predicted signals W=diag(exp(2*X*Beta_lls))
65+
init_weights = np.exp(2*bvals@beta_lls)
66+
weights = init_weights
67+
68+
for i in range(max_iter):
69+
# Weighted linear regression
70+
model = sm.WLS(log_signal, bvals, weights=weights)
71+
results = model.fit()
72+
# The weights for the next iteration are based on the predicted signal of this iteration
73+
new_weights = np.exp(2*bvals@results.params)
74+
weights = new_weights
75+
76+
ln_b0_intercept, D = results.params
77+
b0_intercept = np.exp(ln_b0_intercept)
78+
return D, b0_intercept
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
from src.wrappers.OsipiBase import OsipiBase
2+
from src.original.TF_reference.segmented_IVIMfit import segmented_IVIM_fit
3+
import numpy as np
4+
5+
class TF_reference_IVIMfit(OsipiBase):
6+
"""
7+
Bi-exponential fitting algorithm by IVIM Task force
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 = "OSIPI IVIM TF"
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+
1] # Interval from "at least" to "at most", in case submissions allow a custom number of thresholds
24+
required_bounds = False
25+
required_bounds_optional = True # 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+
# Supported inputs in the standardized class
31+
supported_bounds = True
32+
supported_initial_guess = False
33+
supported_thresholds = True
34+
35+
def __init__(self, bvalues=None, thresholds=200, bounds=None, initial_guess=None):
36+
"""
37+
Everything this algorithm requires should be implemented here.
38+
Number of segmentation thresholds, bounds, etc.
39+
40+
Our OsipiBase object could contain functions that compare the inputs with
41+
the requirements.
42+
"""
43+
super(TF_reference_IVIMfit, self).__init__(bvalues=bvalues, thresholds=thresholds,bounds=bounds,initial_guess=initial_guess)
44+
self.TF_reference_algorithm = segmented_IVIM_fit
45+
self.initialize(bounds, thresholds)
46+
self.use_initial_guess = False
47+
48+
49+
def initialize(self, bounds, thresholds):
50+
if bounds is None:
51+
print('warning, no bounds were defined, so default bounds are used of [0, 0, 0.005],[0.005, 1.0, 0.2]')
52+
self.bounds=([0, 0, 0.005, 0.8],[0.005, 1.0, 0.2, 1.2])
53+
else:
54+
self.bounds=bounds
55+
self.use_bounds = True
56+
if thresholds is None:
57+
self.thresholds = 200
58+
print('warning, no thresholds were defined, so default threshold are used of 200')
59+
else:
60+
self.thresholds = thresholds
61+
62+
63+
def ivim_fit(self, signals, bvalues=None):
64+
"""Perform the IVIM fit
65+
66+
Args:
67+
signals (array-like)
68+
bvalues (array-like, optional): b-values for the signals. If None, self.bvalues will be used. Default is None.
69+
70+
Returns:
71+
_type_: _description_
72+
"""
73+
#bvalues = np.array(bvalues)
74+
bvalues = np.array(bvalues)
75+
76+
if np.any(signals < 0):
77+
signals = np.clip(signals,0.01, None)
78+
print('warning, negative values in signal: values clipped to 0.01')
79+
80+
81+
fit_results = self.TF_reference_algorithm(bvalues,signals,b_cutoff=self.thresholds, bounds=self.bounds)
82+
83+
results = {}
84+
results["D"] = fit_results[0]
85+
results["f"] = fit_results[1]
86+
results["Dp"] = fit_results[2]
87+
88+
return results

tests/IVIMmodels/unit_tests/algorithms.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"PV_MUMC_biexp",
1515
"PvH_KB_NKI_IVIMfit",
1616
"OJ_GU_seg",
17+
"TF_reference_IVIMfit",
1718
"IVIM_NEToptim"
1819
],
1920
"IVIM_NEToptim": {

tests/IVIMmodels/unit_tests/simple_test_run_of_algorithm.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#from src.standardized.PvH_KB_NKI_IVIMfit import PvH_KB_NKI_IVIMfit
1313
#from src.standardized.PV_MUMC_biexp import PV_MUMC_biexp
1414
from src.standardized.OGC_AmsterdamUMC_biexp import OGC_AmsterdamUMC_biexp
15+
from src.standardized.TF_reference_IVIMfit import TF_reference_IVIMfit
1516

1617
## Simple test code...
1718
# Used to just do a test run of an algorithm during development

0 commit comments

Comments
 (0)