Skip to content

Wrapper dev - attribute harmonization and signal normalization #106

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jun 20, 2025

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions ivim_simulation.bval
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0 50 100 500 1000
3 changes: 3 additions & 0 deletions ivim_simulation.bvec
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
1 0 0
0 1 0
0 0 1
3 changes: 2 additions & 1 deletion src/standardized/ETP_SRI_LinearFitting.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,14 @@ class ETP_SRI_LinearFitting(OsipiBase):
required_bounds_optional = True # 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?

# Supported inputs in the standardized class
supported_bounds = False
supported_initial_guess = False
supported_thresholds = True
supported_dimensions = 1
supported_priors = False

def __init__(self, bvalues=None, thresholds=None, bounds=None, initial_guess=None, weighting=None, stats=False):
"""
Expand Down
3 changes: 2 additions & 1 deletion src/standardized/IAR_LU_biexp.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,13 @@ class IAR_LU_biexp(OsipiBase):
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?

# Supported inputs in the standardized class
supported_bounds = True
supported_initial_guess = True
supported_thresholds = False
supported_dimensions = 1
supported_priors = False

def __init__(self, bvalues=None, thresholds=None, bounds=None, initial_guess=None, weighting=None, stats=False):
"""
Expand Down
3 changes: 2 additions & 1 deletion src/standardized/IAR_LU_modified_mix.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,13 @@ class IAR_LU_modified_mix(OsipiBase):
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?

# Supported inputs in the standardized class
supported_bounds = True
supported_initial_guess = False
supported_thresholds = False
supported_dimensions = 1
supported_priors = False

def __init__(self, bvalues=None, thresholds=None, bounds=None, initial_guess=None, weighting=None, stats=False):
"""
Expand Down
3 changes: 2 additions & 1 deletion src/standardized/IAR_LU_modified_topopro.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,13 @@ class IAR_LU_modified_topopro(OsipiBase):
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?

# Supported inputs in the standardized class
supported_bounds = True
supported_initial_guess = False
supported_thresholds = False
supported_dimensions = 1
supported_priors = False

def __init__(self, bvalues=None, thresholds=None, bounds=None, initial_guess=None, weighting=None, stats=False):
"""
Expand Down
3 changes: 2 additions & 1 deletion src/standardized/IAR_LU_segmented_2step.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,13 @@ class IAR_LU_segmented_2step(OsipiBase):
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?

# Supported inputs in the standardized class
supported_bounds = True
supported_initial_guess = True
supported_thresholds = True
supported_dimensions = 1
supported_priors = False

def __init__(self, bvalues=None, thresholds=None, bounds=None, initial_guess=None):
"""
Expand Down
3 changes: 2 additions & 1 deletion src/standardized/IAR_LU_segmented_3step.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,13 @@ class IAR_LU_segmented_3step(OsipiBase):
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?

# Supported inputs in the standardized class
supported_bounds = True
supported_initial_guess = True
supported_thresholds = False
supported_dimensions = 1
supported_priors = False

def __init__(self, bvalues=None, thresholds=None, bounds=None, initial_guess=None, weighting=None, stats=False):
"""
Expand Down
3 changes: 2 additions & 1 deletion src/standardized/IAR_LU_subtracted.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,13 @@ class IAR_LU_subtracted(OsipiBase):
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?

# Supported inputs in the standardized class
supported_bounds = True
supported_initial_guess = True
supported_thresholds = False
supported_dimensions = 1
supported_priors = False

def __init__(self, bvalues=None, thresholds=None, bounds=None, initial_guess=None):
"""
Expand Down
4 changes: 2 additions & 2 deletions src/standardized/OGC_AmsterdamUMC_Bayesian_biexp.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,14 @@ class OGC_AmsterdamUMC_Bayesian_biexp(OsipiBase):
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?
accepts_priors = True


# Supported inputs in the standardized class
supported_bounds = True
supported_initial_guess = True
supported_thresholds = True
supported_dimensions = 1
supported_priors = True

def __init__(self, bvalues=None, thresholds=None, bounds=None, initial_guess=None, fitS0=True, prior_in=None):

Expand Down
3 changes: 2 additions & 1 deletion src/standardized/OGC_AmsterdamUMC_biexp.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,14 @@ class OGC_AmsterdamUMC_biexp(OsipiBase):
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?


# Supported inputs in the standardized class
supported_bounds = True
supported_initial_guess = True
supported_thresholds = False
supported_dimensions = 1
supported_priors = False

def __init__(self, bvalues=None, thresholds=None, bounds=None, initial_guess=None, fitS0=True):
"""
Expand Down
3 changes: 2 additions & 1 deletion src/standardized/OGC_AmsterdamUMC_biexp_segmented.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,14 @@ class OGC_AmsterdamUMC_biexp_segmented(OsipiBase):
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?


# Supported inputs in the standardized class
supported_bounds = True
supported_initial_guess = True
supported_thresholds = True
supported_dimensions = 1
supported_priors = False

def __init__(self, bvalues=None, thresholds=150, bounds=None, initial_guess=None):
"""
Expand Down
3 changes: 2 additions & 1 deletion src/standardized/OJ_GU_seg.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,13 @@ class OJ_GU_seg(OsipiBase):
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?

# Supported inputs in the standardized class
supported_bounds = False
supported_initial_guess = False
supported_thresholds = True
supported_dimensions = 1
supported_priors = False

def __init__(self, bvalues=None, thresholds=None, bounds=None, initial_guess=None, weighting=None, stats=False):
"""
Expand Down
3 changes: 2 additions & 1 deletion src/standardized/PV_MUMC_biexp.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,13 @@ class PV_MUMC_biexp(OsipiBase):
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?

# Supported inputs in the standardized class
supported_bounds = True
supported_initial_guess = False
supported_thresholds = True
supported_dimensions = 1
supported_priors = False

def __init__(self, bvalues=None, thresholds=None, bounds=None, initial_guess=None, weighting=None, stats=False):
"""
Expand Down
3 changes: 2 additions & 1 deletion src/standardized/PvH_KB_NKI_IVIMfit.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,13 @@ class PvH_KB_NKI_IVIMfit(OsipiBase):
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?

# Supported inputs in the standardized class
supported_bounds = False
supported_initial_guess = False
supported_thresholds = False
supported_dimensions = 1
supported_priors = False

def __init__(self, bvalues=None, thresholds=None,bounds=None,initial_guess=None):
"""
Expand Down
13 changes: 11 additions & 2 deletions src/wrappers/OsipiBase.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,12 @@ def osipi_fit(self, data, bvalues=None, **kwargs):
#results[ijk] = fit

for ijk in tqdm(np.ndindex(data.shape[:-1]), total=np.prod(data.shape[:-1])):
args = [data[ijk], use_bvalues]
# Normalize array
single_voxel_data = data[ijk]
single_voxel_data_s0 = single_voxel_data[0]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is assuming that the first index will be the b=0 data. Probably valid but is that an requirement we want to make? Seems like something someone is going to get caught on at some point.
I actually have an ancient PR that should find the b=0 automatically here: https://github.com/OSIPI/TF2.4_IVIM-MRI_CodeCollection/pull/80/files#diff-3e73b3e25749003f8536e5b52b164fa715d358ea4237220c3a1e32f445be03a7
Would dusting that off be good to include here?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with having something more sophisticated than this. I remember raising this and it sort of barely landed on us just requiring it to be first. But I like the solution of taking the mean of all b0's in this example, and shouldn't be hard to implement as we have all the b-values in an array. So I'll adjust the code to include this functionality.

single_voxel_data_normalized = single_voxel_data/single_voxel_data_s0

args = [single_voxel_data_normalized, use_bvalues]
fit = self.ivim_fit(*args, **kwargs) # For single voxel fits, we assume this is a dict with a float value per key.
for key in list(fit.keys()):
results[key][ijk] = fit[key]
Expand Down Expand Up @@ -141,7 +146,11 @@ def osipi_fit_full_volume(self, data, bvalues=None, **kwargs):
for key in self.result_keys:
results[key] = np.empty(list(data.shape[:-1]))

args = [data, use_bvalues]
normalization_factors = np.array([data[..., 0] for i in range(data.shape[-1])])
normalization_factors = np.moveaxis(normalization_factors, 0, -1)
data_normalized = data/normalization_factors

args = [data_normalized, use_bvalues]
fit = self.ivim_fit_full_volume(*args, **kwargs) # Assume this is a dict with an array per key representing the parametric maps
for key in list(fit.keys()):
results[key] = fit[key]
Expand Down
10 changes: 5 additions & 5 deletions tests/IVIMmodels/unit_tests/simple_test_run_of_algorithm.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,22 @@ def ivim_model(b, S0=1, f=0.1, Dstar=0.01, D=0.001):
return S0*(f*np.exp(-b*Dstar) + (1-f)*np.exp(-b*D))

signals = ivim_model(bvalues)
data = np.array([signals, signals, signals])
data = np.array([[signals, signals, signals], [signals, signals, signals]])
#print(data)
signals = data

#model = ETP_SRI_LinearFitting(thresholds=[200])
if kwargs:
results = model.osipi_fit(signals, bvalues, **kwargs)
results = model.osipi_fit_full_volume(signals, bvalues, **kwargs)
else:
results = model.osipi_fit(signals, bvalues)
results = model.osipi_fit_full_volume(signals, bvalues)
print(results)
#test = model.osipi_simple_bias_and_RMSE_test(SNR=20, bvalues=bvalues, f=0.1, Dstar=0.03, D=0.001, noise_realizations=10)

#model1 = ETP_SRI_LinearFitting(thresholds=[200])
#model2 = IAR_LU_biexp(bounds=([0,0,0,0], [1,1,1,1]))
model2 = IAR_LU_biexp(bounds=([0,0,0,0], [1,1,1,1]))
#model2 = IAR_LU_modified_mix()
model2 = OGC_AmsterdamUMC_biexp()
#model2 = OGC_AmsterdamUMC_biexp()

#dev_test_run(model1)
dev_test_run(model2)
2 changes: 1 addition & 1 deletion tests/IVIMmodels/unit_tests/test_ivim_synthetic.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def test_generated(ivim_algorithm, ivim_data, SNR, rtol, atol, fit_count, rician
Dp = data["Dp"]
fit = OsipiBase(algorithm=ivim_algorithm)
# here is a prior
if use_prior and hasattr(fit, "accepts_priors") and fit.accepts_priors:
if use_prior and hasattr(fit, "supported_priors") and fit.supported_priors:
prior = [rng.normal(D, D/3, 10), rng.normal(f, f/3, 10), rng.normal(Dp, Dp/3, 10), rng.normal(1, 1/3, 10)]
# prior = [np.repeat(D, 10)+np.random.normal(0,D/3,np.shape(np.repeat(D, 10))), np.repeat(f, 10)+np.random.normal(0,f/3,np.shape(np.repeat(D, 10))), np.repeat(Dp, 10)+np.random.normal(0,Dp/3,np.shape(np.repeat(D, 10))),np.repeat(np.ones_like(Dp), 10)+np.random.normal(0,1/3,np.shape(np.repeat(D, 10)))] # D, f, D*
fit.initialize(prior_in=prior)
Expand Down