|
| 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