Skip to content

Commit 82642fd

Browse files
Added the fittings algorithms to fit a tri-exponential model
1 parent 43a94a6 commit 82642fd

File tree

1 file changed

+251
-0
lines changed

1 file changed

+251
-0
lines changed
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
"""
2+
January 2022 by Paulien Voorter
3+
p.voorter@maastrichtuniversity.nl
4+
https://www.github.com/paulienvoorter
5+
6+
Code is uploaded as part of the publication: Voorter et al. MRM DOI:10.1002/mrm.29753 (2023)
7+
8+
requirements:
9+
numpy
10+
tqdm
11+
scipy
12+
joblib
13+
"""
14+
15+
# load relevant libraries
16+
from scipy.optimize import curve_fit, nnls
17+
import numpy as np
18+
from joblib import Parallel, delayed
19+
import tqdm
20+
import warnings
21+
22+
#constants
23+
#relaxation times and acquisition parameters, which are required when accounting for inversion recovery
24+
bloodT2 = 275 #ms [Wong et al. JMRI (2020)]
25+
tissueT2 = 95 #ms [Wong et al. JMRI (2020)]
26+
isfT2 = 503 # ms [Rydhog et al Magn.Res.Im. (2014)]
27+
bloodT1 = 1624 #ms [Wong et al. JMRI (2020)]
28+
tissueT1 = 1081 #ms [Wong et al. JMRI (2020)]
29+
isfT1 = 1250 # ms [Wong et al. JMRI (2020)]
30+
echotime = 84 # ms
31+
repetitiontime = 6800 # ms
32+
inversiontime = 2230 # ms
33+
34+
35+
def tri_expN_noS0_IR(bvalues, Dpar, Fint, Dint, Fmv, Dmv):
36+
""" tri-exponential IVIM function accounted for inversion recovery (IR), and S0 set to 1"""
37+
return (( (1 - Fmv - Fint ) * (1 - 2*np.exp(-inversiontime/tissueT1) + np.exp(-repetitiontime/tissueT1) ) * (np.exp(-echotime/tissueT2-bvalues * Dpar ))
38+
+ Fint * (1 - 2*np.exp(-inversiontime/isfT1) + np.exp(-repetitiontime/isfT1) ) * (np.exp(-echotime/isfT2-bvalues * Dint ))
39+
+ Fmv * ( (1 - np.exp(-repetitiontime/bloodT1)) * (np.exp(-echotime/bloodT2 -bvalues * (Dmv ) )) ))
40+
/ ( (1 - Fmv - Fint ) * (1 - 2*np.exp(-inversiontime/tissueT1) + np.exp(-repetitiontime/tissueT1) ) * np.exp(-echotime/tissueT2)
41+
+ Fint * (1 - 2*np.exp(-inversiontime/isfT1) + np.exp(-repetitiontime/isfT1) ) * (np.exp(-echotime/isfT2))
42+
+ Fmv * (1 - np.exp(-repetitiontime/bloodT1)) * np.exp(-echotime/bloodT2 )))
43+
44+
def tri_expN_IR(bvalues, S0, Dpar, Fint, Dint, Fmv, Dmv):
45+
""" tri-exponential IVIM function accounted for inversion recovery (IR)"""
46+
return (S0 * (( (1 - Fmv - Fint) * (1 - 2*np.exp(-inversiontime/tissueT1) + np.exp(-repetitiontime/tissueT1) ) * (np.exp(-echotime/tissueT2-bvalues * Dpar))
47+
+ Fint * (1 - 2*np.exp(-inversiontime/isfT1) + np.exp(-repetitiontime/isfT1) ) * (np.exp(-echotime/isfT2-bvalues * Dint))
48+
+ Fmv * ( (1 - np.exp(-repetitiontime/bloodT1)) * (np.exp(-echotime/bloodT2 -bvalues * (Dmv) )) ))
49+
/ ( (1 - Fmv - Fint) * (1 - 2*np.exp(-inversiontime/tissueT1) + np.exp(-repetitiontime/tissueT1) ) * np.exp(-echotime/tissueT2)
50+
+ Fint * (1 - 2*np.exp(-inversiontime/isfT1) + np.exp(-repetitiontime/isfT1) ) * (np.exp(-echotime/isfT2))
51+
+ Fmv * (1 - np.exp(-repetitiontime/bloodT1)) * np.exp(-echotime/bloodT2 ))))
52+
53+
def tri_expN_noS0(bvalues, Dpar, Fint, Dint, Fmv, Dmv):
54+
""" tri-exponential IVIM function, and S0 set to 1"""
55+
return Fmv * np.exp(-bvalues * Dmv) + Fint * np.exp(-bvalues * Dint) + (1 - Fmv - Fint) * np.exp(-bvalues * Dpar)
56+
57+
def tri_expN(bvalues, S0, Dpar, Fint, Dint, Fmv, Dmv):
58+
""" tri-exponential IVIM function"""
59+
return S0 * (Fmv * np.exp(-bvalues * Dmv) + Fint * np.exp(-bvalues * Dint) + (1 - Fmv - Fint) * np.exp(-bvalues * Dpar))
60+
61+
def fit_least_squares_tri_exp(bvalues, dw_data, IR=False, S0_output=False, fitS0=False,
62+
bounds=([0.9, 0.0001, 0.0, 0.0015, 0.0, 0.004], [1.1, 0.0015, 0.4, 0.004, 0.2, 0.2]), cutoff=200):
63+
"""
64+
This is the LSQ implementation for a tri-exp model, in which we first estimate Dpar using a curve fit to b-values>=cutoff;
65+
Second, we fit the other parameters using all b-values, while setting Dpar from step 1 as upper bound and starting value.
66+
It fits a single curve
67+
:param bvalues: 1D Array with the b-values
68+
:param dw_data: 1D Array with diffusion-weighted signal in different voxels at different b-values
69+
:param IR: Boolean; True will fit the IVIM accounting for inversion recovery, False will fit IVIM without IR; default = True
70+
:param S0_output: Boolean determining whether to output (often a dummy) variable S0; default = False
71+
:param fix_S0: Boolean determining whether to fix S0 to 1; default = True
72+
: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])
73+
:param cutoff: cutoff b-value used in step 1
74+
:return S0: optional 1D Array with S0 in each voxel
75+
:return Dpar: scalar with Dpar of the specific voxel
76+
:return Fint: scalar with Fint of the specific voxel
77+
:return Dint: scalar with Dint of the specific voxel
78+
:return Fmv: scalar with Fmv of the specific voxel
79+
:return Dmv: scalar with Dmv of the specific voxel
80+
"""
81+
def monofit(bvalues, Dpar, Fp):
82+
return (1-Fp)*np.exp(-bvalues * Dpar)
83+
84+
high_b = bvalues[bvalues >= cutoff]
85+
high_dw_data = dw_data[bvalues >= cutoff]
86+
boundspar = ([0, 0], [bounds[1][1], bounds[1][5]])
87+
params, _ = curve_fit(monofit, high_b, high_dw_data, p0=[(bounds[1][1]-bounds[0][1])/2, 0.05], bounds=boundspar)
88+
Dpar1 = params[0]
89+
if IR:
90+
if not fitS0:
91+
bounds = ([bounds[0][1] , bounds[0][2] , bounds[0][3] , bounds[0][4] , bounds[0][5] ],
92+
[Dpar1 , bounds[1][2] , bounds[1][3] , bounds[1][4] , bounds[1][5] ])
93+
params, _ = curve_fit(tri_expN_noS0_IR, bvalues, dw_data, p0=[Dpar1, 0.0, (bounds[0][3]+bounds[1][3])/2, 0.05, (bounds[0][5]+bounds[1][5])/2], bounds=bounds)
94+
Dpar, Fint, Dint, Fmv, Dmv = params[0], params[1], params[2], params[3], params[4]
95+
#when the fraction of a compartment equals zero (or very very small), the corresponding diffusivity is non-existing (=NaN)
96+
97+
else:
98+
boundsupdated = ([bounds[0][0] , bounds[0][1] , bounds[0][2] , bounds[0][3] , bounds[0][4] , bounds[0][5] ],
99+
[bounds[1][0] , Dpar1 , bounds[1][2] , bounds[1][3] , bounds[1][4] , bounds[1][5] ])
100+
params, _ = curve_fit(tri_expN_IR, bvalues, dw_data, p0=[1, Dpar1, 0.0, (bounds[0][3]+bounds[1][3])/2, 0.05, (bounds[0][5]+bounds[1][5])/2], bounds=boundsupdated)
101+
S0 = params[0]
102+
Dpar, Fint, Dint, Fmv, Dmv = params[1] , params[2] , params[3] , params[4] , params[5]
103+
#when the fraction of a compartment equals zero (or very very small), the corresponding diffusivity is non-existing (=NaN)
104+
105+
else:
106+
if not fitS0:
107+
bounds = ([bounds[0][1] , bounds[0][2] , bounds[0][3] , bounds[0][4] , bounds[0][5] ],
108+
[Dpar1 , bounds[1][2] , bounds[1][3] , bounds[1][4] , bounds[1][5] ])
109+
params, _ = curve_fit(tri_expN_noS0, bvalues, dw_data, p0=[Dpar1, 0.0, (bounds[0][3]+bounds[1][3])/2, 0.05, (bounds[0][5]+bounds[1][5])/2], bounds=bounds)
110+
Dpar, Fint, Dint, Fmv, Dmv = params[0], params[1], params[2], params[3], params[4]
111+
#when the fraction of a compartment equals zero (or very very small), the corresponding diffusivity is non-existing (=NaN)
112+
113+
else:
114+
boundsupdated = ([bounds[0][0] , bounds[0][1] , bounds[0][2] , bounds[0][3] , bounds[0][4] , bounds[0][5] ],
115+
[bounds[1][0] , Dpar1 , bounds[1][2] , bounds[1][3] , bounds[1][4] , bounds[1][5] ])
116+
params, _ = curve_fit(tri_expN, bvalues, dw_data, p0=[1, Dpar1, 0.0, (bounds[0][3]+bounds[1][3])/2, 0.05, (bounds[0][5]+bounds[1][5])/2], bounds=boundsupdated)
117+
S0 = params[0]
118+
Dpar, Fint, Dint, Fmv, Dmv = params[1] , params[2] , params[3] , params[4] , params[5]
119+
#when the fraction of a compartment equals zero (or very very small), the corresponding diffusivity is non-existing (=NaN)
120+
121+
if S0_output:
122+
return Dpar, Fint, Dint, Fmv, Dmv, S0
123+
else:
124+
return Dpar, Fint, Dint, Fmv, Dmv
125+
126+
127+
128+
def fit_NNLS(bvalues, dw_data, IR=False,
129+
bounds=([0.9, 0.0001, 0.0, 0.0015, 0.0, 0.004], [1.1, 0.0015, 0.4, 0.004, 0.2, 0.2])):
130+
"""
131+
This is an implementation of the tri-exponential fit. It fits a single curve with the non-negative least squares (NNLS) fitting approach, see 10.1002/jmri.26920.
132+
:param bvalues: 1D Array with the b-values
133+
:param dw_data: 2D Array with diffusion-weighted signal in different voxels at different b-values
134+
:param IR: Boolean; True will fit the IVIM accounting for inversion recovery, False will fit IVIM without IR correction. default = True
135+
:param bounds: Array with fit bounds ([fp0min, Dparmin, Fintmin, Dintmin, Fmvmin, Dmvmin],[fp0max, Dparmax, Fintmax, Dintmax, Fmvmax, Dmvmax]). Default: ([0, 0, 0, 0.005, 0, 0.06], [2.5, 0.005, 1, 0.06, 1, 0.5])
136+
:return Fp0: optional 1D Array with f0 in each voxel
137+
:return Dpar: 1D Array with Dpar in each voxel
138+
:return Fint: 1D Array with Fint in each voxel
139+
:return Dint: 1D Array with Dint in each voxel
140+
:return Fmv: 1D Array with the fraciton of signal for Dmv in each voxel
141+
:return Dmv: 1D Array with Dmv in each voxel
142+
"""
143+
144+
145+
try:
146+
Dspace = np.logspace(np.log10(bounds[0][1]), np.log10(bounds[1][5]), num=200)
147+
Dbasis = np.exp(-np.kron(np.reshape(bvalues,[len(bvalues),1]), np.reshape(Dspace,[1,len(Dspace)])))
148+
149+
Dint = np.zeros(len(dw_data))
150+
Dpar = np.zeros(len(dw_data))
151+
Fint = np.zeros(len(dw_data))
152+
Fmv = np.zeros(len(dw_data))
153+
Dmv = np.zeros(len(dw_data))
154+
S0 = np.zeros(len(dw_data))
155+
156+
def find_idx_nearest(array, value):
157+
array = np.asarray(array)
158+
idx = (np.abs(array - value)).argmin()
159+
return idx
160+
161+
idx_parint = find_idx_nearest(Dspace,bounds[1][1])
162+
idx_intmv = find_idx_nearest(Dspace,bounds[1][3])
163+
164+
for i in tqdm.tqdm(range(len(dw_data)), position=0, leave=True):
165+
# fill arrays with fit results on a per voxel base:
166+
x,rnorm = nnls(Dbasis,dw_data[i,:]) # x contains the diffusion spectrum
167+
ampl_Dpar = np.sum(x[:idx_parint]) #summing the amplitudes within the Dpar range
168+
ampl_Dint = np.sum(x[idx_parint:idx_intmv]) #summing the amplitudes within the Dint range
169+
ampl_Dmv = np.sum(x[idx_intmv:]) #summing the amplitudes within the Dmv range
170+
if len(np.nonzero(x[:idx_parint])[0])>0: # Dpar exists
171+
avg_Dpar = np.sum(Dspace[np.nonzero(x[:idx_parint])] * x[np.nonzero(x[:idx_parint])])/ampl_Dpar;
172+
else:
173+
avg_Dpar = 0
174+
if len(np.nonzero(x[idx_parint:idx_intmv])[0])>0: # Dint exists
175+
avg_Dint = np.sum(Dspace[idx_parint:][np.nonzero(x[idx_parint:idx_intmv])] * x[idx_parint:][np.nonzero(x[idx_parint:idx_intmv])])/ampl_Dint;
176+
else:
177+
avg_Dint = 0
178+
if len(np.nonzero(x[idx_intmv:])[0])>0: # Dmv exists
179+
avg_Dmv = np.sum(Dspace[idx_intmv:][np.nonzero(x[idx_intmv:])] * x[idx_intmv:][np.nonzero(x[idx_intmv:])])/ampl_Dmv;
180+
else:
181+
avg_Dmv = 0
182+
183+
if IR:
184+
corr_Fpar, corr_Fint, corr_Fmv = correct_for_IR(ampl_Dpar, ampl_Dint, ampl_Dmv)
185+
Fint[i] = corr_Fint
186+
Fmv[i] = corr_Fmv
187+
else:
188+
Fint[i] = ampl_Dint
189+
Fmv[i] = ampl_Dmv
190+
191+
Dpar[i] = avg_Dpar
192+
Dint[i] = avg_Dint
193+
Dmv[i] = avg_Dmv
194+
S0[i] = ampl_Dpar+ampl_Dint+ampl_Dmv # This is the sum before correction
195+
#note that after correction for IR, the sum of fractions always equals 1
196+
197+
return Dpar, Fmv, Dmv, Dint, Fint, S0
198+
199+
except:
200+
return 0, 0, 0, 0, 0, 0
201+
202+
203+
def correct_for_IR(ampl_Dpar, ampl_Dint, ampl_Dmv):
204+
"""
205+
This function corrects for the inversion recovery in the IVIM sequence, as described in Wong et al. (2019) Spectral Diffusion analysis of intravoxel incoherent motion MRI in cerebral small vessel disease
206+
:param ampl_Dpar: Scalar, the sum of amplitudes within the Dpar range
207+
:param ampl_Dint: Scalar, the sum of amplitudes within the Dint range
208+
:param ampl_Dmv: Scalar, the sum of amplitudes within the Dmv range
209+
:return corr_Fpar: Scalar, the fraction of Dpar, corrected for inversion recovery
210+
:return corr_Fint: Scalar, the fraction of Dint, corrected for inversion recovery
211+
:return corr_Fmv: Scalar, the fraction of Dmv, corrected for inversion recovery
212+
213+
"""
214+
TtLt = np.exp(-echotime/tissueT2)*(1-2*np.exp(-inversiontime/tissueT1) + np.exp(-repetitiontime/tissueT1));
215+
TbLb = np.exp(-echotime/bloodT2)*(1-np.exp(-repetitiontime/bloodT1));
216+
TpLp = np.exp(-echotime/isfT2)*(1-2*np.exp(-inversiontime/isfT1) + np.exp(-repetitiontime/isfT1));
217+
218+
#if all three components are present:
219+
if ampl_Dpar>0 and ampl_Dint>0 and ampl_Dmv>0:
220+
#Calculate corrected fractions
221+
n1 = ((TbLb*ampl_Dpar)/(ampl_Dmv*TtLt))+1;
222+
n2 = (TtLt*TbLb*ampl_Dpar*ampl_Dint)/(ampl_Dpar*ampl_Dmv*TtLt*TpLp);
223+
denom = n1 + n2;
224+
z = 1/denom; # z is the microvascular fraction
225+
x = ((TbLb*ampl_Dpar)/(ampl_Dmv*TtLt))*z; # x is the parenchymal fraction
226+
y = 1-x-z; # y is the interstitial fluid fraction
227+
corr_Fpar = x;
228+
corr_Fint = y;
229+
corr_Fmv = z;
230+
231+
#if two components are present:
232+
elif ampl_Dpar>0 and ampl_Dint>0 and ampl_Dmv==0:
233+
corr_Fint = 1/(((ampl_Dpar/ampl_Dint)*(TpLp/TtLt))+1);
234+
corr_Fpar = 1-corr_Fint;
235+
corr_Fmv = ampl_Dmv;
236+
elif ampl_Dpar>0 and ampl_Dint==0 and ampl_Dmv>0:
237+
corr_Fmv = 1/(((ampl_Dpar/ampl_Dmv)*(TbLb/TtLt))+1);
238+
corr_Fpar = 1-corr_Fmv;
239+
corr_Fint = ampl_Dint;
240+
elif ampl_Dpar==0 and ampl_Dint>0 and ampl_Dmv>0:
241+
corr_Fmv = 1/(((ampl_Dint/ampl_Dmv)*(TbLb/TpLp))+1);
242+
corr_Fint = 1-corr_Fmv;
243+
corr_Fpar = ampl_Dpar;
244+
245+
#if one component is present:
246+
else:
247+
corr_Fmv = ampl_Dmv;
248+
corr_Fint = ampl_Dint;
249+
corr_Fpar = ampl_Dpar;
250+
251+
return corr_Fpar, corr_Fint, corr_Fmv

0 commit comments

Comments
 (0)