Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/testing/wrapper' into analysis/w…
Browse files Browse the repository at this point in the history
…orkflow
  • Loading branch information
etpeterson committed Nov 4, 2023
2 parents e40346c + 23c7514 commit a793262
Show file tree
Hide file tree
Showing 5 changed files with 151 additions and 45 deletions.
67 changes: 26 additions & 41 deletions src/original/PV_MUMC/two_step_IVIM_fit.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,36 +7,34 @@
numpy
tqdm
scipy
joblib
"""

# load relevant libraries
from scipy.optimize import curve_fit, nnls
from scipy.optimize import curve_fit
import numpy as np
from joblib import Parallel, delayed
import tqdm




def two_exp_noS0(bvalues, Dpar, Fmv, Dmv):
""" tri-exponential IVIM function, and S0 set to 1"""
""" bi-exponential IVIM function, and S0 set to 1"""
return Fmv * np.exp(-bvalues * Dmv) + (1 - Fmv ) * np.exp(-bvalues * Dpar)

def two_exp(bvalues, S0, Dpar, Fmv, Dmv):
""" tri-exponential IVIM function"""
""" bi-exponential IVIM function"""
return S0 * (Fmv * np.exp(-bvalues * Dmv) + (1 - Fmv ) * np.exp(-bvalues * Dpar))



def fit_least_squares_array(bvalues, dw_data, fitS0=True, bounds=([0.9, 0.0001, 0.0, 0.0025], [1.1, 0.0025, 0.2, 0.2]), cutoff=200):
def fit_least_squares_array(bvalues, dw_data, fitS0=False, bounds=([0.9, 0.0001, 0.0, 0.0025], [1.1, 0.0025, 0.5, 0.2]), cutoff=200):
"""
This is the LSQ implementation, in which we first estimate Dpar using a curve fit to b-values>=cutoff;
Second, we fit the other parameters using all b-values, while fixing Dpar from step 1. This fit
is done on an array.
:param bvalues: 1D Array with the b-values
:param dw_data: 2D Array with diffusion-weighted signal in different voxels at different b-values
:param bounds: Array with fit bounds ([S0min, Dparmin, Fintmin, Dintmin, Fmvmin, Dmvmin],[S0max, Dparmax, Fintmax, Dintmax, Fmvmax, Dmvmax]). default: ([0.9, 0.0001, 0.0, 0.0015, 0.0, 0.004], [1.1, 0.0015, 0.4, 0.004, 0.2, 0.2])
:param bounds: Array with fit bounds ([S0min, Dparmin, Fmvmin, Dmvmin],[S0max, Dparmax, Fmvmax, Dmvmax]).
:param cutoff: cutoff b-value used in step 1
:return Dpar: 1D Array with Dpar in each voxel
:return Fmv: 1D Array with Fmv in each voxel
Expand All @@ -54,69 +52,56 @@ def fit_least_squares_array(bvalues, dw_data, fitS0=True, bounds=([0.9, 0.0001,
return [Dpar, Fmv, Dmv, S0]


def fit_least_squares(bvalues, dw_data, IR=False, S0_output=False, fitS0=True,
bounds=([0.9, 0.0001, 0.0, 0.0025], [1.1, 0.0025, 0.2, 0.2]), cutoff=200):
def fit_least_squares(bvalues, dw_data, S0_output=False, fitS0=False,
bounds=([0.9, 0.0001, 0.0, 0.0025], [1.1, 0.003, 1, 0.2]), cutoff=200):
"""
This is the LSQ implementation, in which we first estimate Dpar using a curve fit to b-values>=cutoff;
Second, we fit the other parameters using all b-values, while fixing Dpar from step 1. This fit
is done on an array. It fits a single curve
:param bvalues: 1D Array with the b-values
:param dw_data: 1D Array with diffusion-weighted signal in different voxels at different b-values
:param IR: Boolean; True will fit the IVIM accounting for inversion recovery, False will fit IVIM without IR; default = True
:param S0_output: Boolean determining whether to output (often a dummy) variable S0; default = False
:param fix_S0: Boolean determining whether to fix S0 to 1; default = True
: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])
:param S0_output: Boolean determining whether to output (often a dummy) variable S0;
:param fitS0: Boolean determining whether to fix S0 to 1;
:param bounds: Array with fit bounds ([S0min, Dparmin, Fmvmin, Dmvmin],[S0max, Dparmax, Fmvmax, Dmvmax]).
:param cutoff: cutoff b-value used in step 1
:return S0: optional 1D Array with S0 in each voxel
:return Dpar: scalar with Dpar of the specific voxel
:return Fint: scalar with Fint of the specific voxel
:return Dint: scalar with Dint of the specific voxel
:return Fmv: scalar with Fmv of the specific voxel
:return Dmv: scalar with Dmv of the specific voxel
"""

#try:
def monofit(bvalues, Dpar):
return np.exp(-bvalues * Dpar)
def monofit(bvalues, Dpar, Fmv):
return (1-Fmv)*np.exp(-bvalues * Dpar)

high_b = bvalues[bvalues >= cutoff]
high_dw_data = dw_data[bvalues >= cutoff]
boundspar = ([bounds[0][1]], [bounds[1][1]])
params, _ = curve_fit(monofit, high_b, high_dw_data, p0=[(bounds[1][1]-bounds[0][1])/2], bounds=boundspar)
Dpar = params[0]

boundsmonoexp = ([bounds[0][1], bounds[0][2]], [bounds[1][1], bounds[1][2]])
params, _ = curve_fit(monofit, high_b, high_dw_data, p0=[(bounds[1][1]-bounds[0][1])/2,(bounds[1][2]-bounds[0][2])/2], bounds=boundsmonoexp)
Dpar1 = params[0]
if not fitS0:
boundsupdated=([Dpar1 , bounds[0][2] , bounds[0][3] ],
[Dpar1 , bounds[1][2] , bounds[1][3] ])
params, _ = curve_fit(two_exp_noS0, bvalues, dw_data, p0=[Dpar1, (bounds[0][2]+bounds[1][2])/2, (bounds[0][3]+bounds[1][3])/2], bounds=boundsupdated)
Dpar1, Fmv, Dmv = params[0], params[1], params[2]
boundsupdated=([bounds[0][2] , bounds[0][3] ],
[bounds[1][2] , bounds[1][3] ])
params, _ = curve_fit(lambda b, Fmv, Dmv: two_exp_noS0(b, Dpar1, Fmv, Dmv), bvalues, dw_data, p0=[(bounds[0][2]+bounds[1][2])/2, (bounds[0][3]+bounds[1][3])/2], bounds=boundsupdated)
Fmv, Dmv = params[0], params[1]
#when the fraction of a compartment equals zero (or very very small), the corresponding diffusivity is non-existing (=NaN)
if Fmv < 1e-4:
Dmv = float("NaN")
#if Fmv < 1e-4:
# Dmv = float("NaN")

else:
#boundsupdated = ([bounds[0][0] , Dpar1 , bounds[0][2] , bounds[0][3] ],
# [bounds[1][0] , Dpar1, bounds[1][2] , bounds[1][3] ])
#params, _ = curve_fit(two_exp, bvalues, dw_data, p0=[1, Dpar1, (bounds[0][2]+bounds[1][2])/2, (bounds[0][3]+bounds[1][3])/2], bounds=boundsupdated)
boundsupdated = ([bounds[0][0] , bounds[0][2] , bounds[0][3] ],
[bounds[1][0] , bounds[1][2] , bounds[1][3] ])
params, _ = curve_fit(lambda bvalues, S0, Fmv, Dmv: two_exp(bvalues, S0, Dpar, Fmv, Dmv), bvalues, dw_data, p0=[1, (bounds[0][2]+bounds[1][2])/2, (bounds[0][3]+bounds[1][3])/2], bounds=boundsupdated)
params, _ = curve_fit(lambda b, S0, Fmv, Dmv: two_exp(b, S0, Dpar1, Fmv, Dmv), bvalues, dw_data, p0=[1, (bounds[0][2]+bounds[1][2])/2, (bounds[0][3]+bounds[1][3])/2], bounds=boundsupdated)
S0 = params[0]
Fmv, Dmv = params[1] , params[2]
#when the fraction of a compartment equals zero (or very very small), the corresponding diffusivity is non-existing (=NaN)
if Fmv < 1e-4:
Dmv = float("NaN")
#if Fmv < 1e-4:
# Dmv = float("NaN")

if S0_output:
return Dpar, Fmv, Dmv, S0
else:
return Dpar, Fmv, Dmv
#except:

if S0_output:
return 0, 0, 0, 0, 0, 0
return Dpar1, Fmv, Dmv, S0
else:
return 0, 0, 0, 0, 0
return Dpar1, Fmv, Dmv



56 changes: 56 additions & 0 deletions src/standardized/PV_MUMC_biexp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import numpy as np
from src.wrappers.OsipiBase import OsipiBase
from src.original.PV_MUMC.two_step_IVIM_fit import fit_least_squares_array, fit_least_squares


class PV_MUMC_biexp(OsipiBase):
"""
Bi-exponential fitting algorithm by Paulien Voorter, Maastricht University
"""

# Some basic stuff that identifies the algorithm
id_author = "Paulien Voorter MUMC"
id_algorithm_type = "Bi-exponential fit"
id_return_parameters = "f, D*, D"
id_units = "seconds per milli metre squared or milliseconds per micro metre squared"

# Algorithm requirements
required_bvalues = 4
required_thresholds = [0,0] # Interval from "at least" to "at most", in case submissions allow a custom number of thresholds
required_bounds = False
required_bounds_optional = True # Bounds may not be required but are optional
required_initial_guess = False
required_initial_guess_optional = True
accepted_dimensions = 1 # Not sure how to define this for the number of accepted dimensions. Perhaps like the thresholds, at least and at most?

def __init__(self, bvalues=None, thresholds=None, bounds=None, initial_guess=None, weighting=None, stats=False):
"""
Everything this algorithm requires should be implemented here.
Number of segmentation thresholds, bounds, etc.
Our OsipiBase object could contain functions that compare the inputs with
the requirements.
"""
super(PV_MUMC_biexp, self).__init__(bvalues, None, bounds, None)
self.PV_algorithm = fit_least_squares


def ivim_fit(self, signals, bvalues=None):
"""Perform the IVIM fit
Args:
signals (array-like)
bvalues (array-like, optional): b-values for the signals. If None, self.bvalues will be used. Default is None.
Returns:
_type_: _description_
"""


fit_results = self.PV_algorithm(bvalues, signals)

f = fit_results[1]
Dstar = fit_results[2]
D = fit_results[0]

return f, Dstar, D
62 changes: 62 additions & 0 deletions src/standardized/PvH_KB_NKI_IVIMfit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
from src.wrappers.OsipiBase import OsipiBase
from src.original.PvH_KB_NKI.DWI_functions_standalone import generate_IVIMmaps_standalone, generate_ADC_standalone
import numpy as np

class PvH_KB_NKI_IVIMfit(OsipiBase):
"""
Bi-exponential fitting algorithm by Petra van Houdt and Koen Baas, NKI
"""

# I'm thinking that we define default attributes for each submission like this
# And in __init__, we can call the OsipiBase control functions to check whether
# the user inputs fulfil the requirements

# Some basic stuff that identifies the algorithm
id_author = "Group Uulke van der Heide, NKI"
id_algorithm_type = "Bi-exponential fit"
id_return_parameters = "f, D*, D"
id_units = "seconds per milli metre squared or milliseconds per micro metre squared"

# Algorithm requirements
required_bvalues = 4
required_thresholds = [0,
0] # Interval from "at least" to "at most", in case submissions allow a custom number of thresholds
required_bounds = False
required_bounds_optional = False # Bounds may not be required but are optional
required_initial_guess = False
required_initial_guess_optional =False
accepted_dimensions = 1 # Not sure how to define this for the number of accepted dimensions. Perhaps like the thresholds, at least and at most?

def __init__(self, bvalues=None, thresholds=None,bounds=None,initial_guess=None):
"""
Everything this algorithm requires should be implemented here.
Number of segmentation thresholds, bounds, etc.
Our OsipiBase object could contain functions that compare the inputs with
the requirements.
"""
super(PvH_KB_NKI_IVIMfit, self).__init__(bvalues, thresholds,bounds,initial_guess)
self.NKI_algorithm = generate_IVIMmaps_standalone


def ivim_fit(self, signals, bvalues=None):
"""Perform the IVIM fit
Args:
signals (array-like)
bvalues (array-like, optional): b-values for the signals. If None, self.bvalues will be used. Default is None.
Returns:
_type_: _description_
"""
#bvalues = np.array(bvalues)
bvalues = bvalues.tolist() #NKI code expects a list instead of nparray
# reshape signal as the NKI code expects a 4D array
signals = np.reshape(signals, (1, 1, 1, len(signals))) # assuming that in this test the signals are always single voxel
fit_results = self.NKI_algorithm(signals,bvalues)

D = fit_results[0][0,0,0]/1000
f = fit_results[1][0,0,0]
Dstar = fit_results[2][0,0,0]/1000

return f, Dstar, D
4 changes: 3 additions & 1 deletion tests/IVIMmodels/unit_tests/algorithms.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
"IAR_LU_subtracted",
"OGC_AmsterdamUMC_Bayesian_biexp",
"OGC_AmsterdamUMC_biexp_segmented",
"OGC_AmsterdamUMC_biexp"
"OGC_AmsterdamUMC_biexp",
"PV_MUMC_biexp",
"PvH_KB_NKI_IVIMfit"
],
"IAR_LU_biexp": {
"tolerances": {
Expand Down
7 changes: 4 additions & 3 deletions tests/IVIMmodels/unit_tests/simple_test_run_of_algorithm.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
from pathlib import Path
#from src.standardized.ETP_SRI_LinearFitting import ETP_SRI_LinearFitting
from src.standardized.IAR_LU_biexp import IAR_LU_biexp
from src.standardized.IAR_LU_segmented_2step import IAR_LU_segmented_2step
#from src.standardized.IAR_LU_segmented_2step import IAR_LU_segmented_2step
from src.standardized.PvH_KB_NKI_IVIMfit import PvH_KB_NKI_IVIMfit
#from src.standardized.PV_MUMC_biexp import PV_MUMC_biexp

## Simple test code...
Expand All @@ -26,7 +27,7 @@ def ivim_model(b, S0=1, f=0.1, Dstar=0.01, D=0.001):

#model1 = ETP_SRI_LinearFitting(thresholds=[200])
#model2 = IAR_LU_biexp()
model2 = IAR_LU_segmented_2step()
model2 = PvH_KB_NKI_IVIMfit()

#dev_test_run(model1, linear_fit_option=True)
dev_test_run(model2, thresholds="Arbitrary kwarg for ivim_fit")
dev_test_run(model2)

0 comments on commit a793262

Please sign in to comment.