Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions src/standardized/IAR_LU_biexp.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,13 @@ def __init__(self, bvalues=None, thresholds=None, bounds=None, initial_guess=Non
bvec = np.zeros((self.bvalues.size, 3))
bvec[:,2] = 1
gtab = gradient_table(self.bvalues, bvec, b0_threshold=0)

self.IAR_algorithm = IvimModelBiExp(gtab, bounds=self.bounds, initial_guess=self.initial_guess)

# Convert dict bounds/initial_guess to list-of-lists as expected by IvimModelBiExp
bounds_list = [[self.bounds["S0"][0], self.bounds["f"][0], self.bounds["Dp"][0], self.bounds["D"][0]],
[self.bounds["S0"][1], self.bounds["f"][1], self.bounds["Dp"][1], self.bounds["D"][1]]]
initial_guess_list = [self.initial_guess["S0"], self.initial_guess["f"], self.initial_guess["Dp"], self.initial_guess["D"]]

self.IAR_algorithm = IvimModelBiExp(gtab, bounds=bounds_list, initial_guess=initial_guess_list)
else:
self.IAR_algorithm = None

Expand Down Expand Up @@ -104,6 +109,7 @@ def ivim_fit(self, signals, bvalues, **kwargs):

return results


def ivim_fit_full_volume(self, signals, bvalues, **kwargs):
"""Perform the IVIM fit

Expand Down
15 changes: 8 additions & 7 deletions src/standardized/IAR_LU_segmented_3step.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,15 @@ def __init__(self, bvalues=None, thresholds=None, bounds=None, initial_guess=Non
bvec[:,2] = 1
gtab = gradient_table(self.bvalues, bvec, b0_threshold=0)

# Adapt the bounds to the format needed for the algorithm
bounds = [[self.bounds["S0"][0], self.bounds["f"][0], self.bounds["Dp"][0], self.bounds["D"][0]], \
[self.bounds["S0"][1], self.bounds["f"][1], self.bounds["Dp"][1], self.bounds["D"][1]]]
# Adapt the bounds to the format needed for the algorithm (list-of-lists)
bounds = [[self.bounds["S0"][0], self.bounds["f"][0], self.bounds["Dp"][0], self.bounds["D"][0]],
[self.bounds["S0"][1], self.bounds["f"][1], self.bounds["Dp"][1], self.bounds["D"][1]]]

# Adapt the initial guess to the format needed for the algorithm
initial_guess = [self.initial_guess["S0"], self.initial_guess["f"], self.initial_guess["Dp"], self.initial_guess["D"]]

self.IAR_algorithm = IvimModelSegmented3Step(gtab, bounds=self.bounds, initial_guess=self.initial_guess)

# Use the converted list-of-lists bounds and initial_guess, NOT the raw dicts
self.IAR_algorithm = IvimModelSegmented3Step(gtab, bounds=bounds, initial_guess=initial_guess)
else:
self.IAR_algorithm = None

Expand Down Expand Up @@ -115,4 +116,4 @@ def ivim_fit(self, signals, bvalues, **kwargs):
results["Dp"] = fit_results.model_params[2]
results["D"] = fit_results.model_params[3]

return results
return results
49 changes: 35 additions & 14 deletions src/standardized/PV_MUMC_biexp.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def __init__(self, bvalues=None, thresholds=None, bounds=None, initial_guess=Non
self.use_bounds = {"f" : True, "D" : True, "Dp" : True, "S0" : True}
self.use_initial_guess = {"f" : False, "D" : False, "Dp" : False, "S0" : False}


def ivim_fit(self, signals, bvalues=None):
"""Perform the IVIM fit

Expand All @@ -52,30 +52,51 @@ def ivim_fit(self, signals, bvalues=None):
bvalues (array-like, optional): b-values for the signals. If None, self.bvalues will be used. Default is None.

Returns:
_type_: _description_
dict: Fitted IVIM parameters f, Dp (D*), and D.
"""
if self.bounds is None:
self.bounds = ([0.9, 0.0001, 0.0, 0.0025], [1.1, 0.003, 1, 0.2])
# --- bvalues resolution ---
# Edge case: bvalues not passed here → fall back to the ones set at __init__ time
if bvalues is None:
if self.bvalues is None:
raise ValueError(
"PV_MUMC_biexp: bvalues must be provided either at initialization or at fit time."
)
bvalues = self.bvalues
else:
bounds = ([self.bounds["S0"][0], self.bounds["D"][0], self.bounds["f"][0], self.bounds["Dp"][0]],
[self.bounds["S0"][1], self.bounds["D"][1], self.bounds["f"][1], self.bounds["Dp"][1]])

bvalues = np.asarray(bvalues)

# --- Bounds resolution ---
# self.bounds is always a dict (OsipiBase force_default_settings=True).
# The underlying fit_least_squares expects: ([S0min, Dmin, fmin, Dpmin], [S0max, Dmax, fmax, Dpmax])
if isinstance(self.bounds, dict):
bounds = (
[self.bounds["S0"][0], self.bounds["D"][0], self.bounds["f"][0], self.bounds["Dp"][0]],
[self.bounds["S0"][1], self.bounds["D"][1], self.bounds["f"][1], self.bounds["Dp"][1]],
)
else:
# Fallback: already in list/tuple form (legacy)
bounds = self.bounds

if self.thresholds is None:
self.thresholds = 200

DEFAULT_PARAMS = [0.003,0.1,0.05]
# Default fallback parameters (D, f, Dp) used if the optimizer fails
DEFAULT_PARAMS = [0.003, 0.1, 0.05]
Copy link
Contributor

Choose a reason for hiding this comment

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

What happens if we set this to nan? Will the tests fail? Otherwise, I'd suggest 0 so that it's clear that there's something wrong in these voxels.

Copy link
Contributor Author

@Devguru-codes Devguru-codes Mar 12, 2026

Choose a reason for hiding this comment

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

@IvanARashid sir,I think 0 is good as fallback to [0,0,0] will clearly tell that optimizer didnt converge fot the voxel. I think 0 is safer as nan can propagate through downstream calculations and potentially break things. If you prefer nan to be there, I can do that. Please tell me. Thank you.

Copy link
Contributor

Choose a reason for hiding this comment

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

@oliverchampion I have some vague memory that you've done something like this at some point. Did you use nan?


try:
fit_results = self.PV_algorithm(bvalues, signals, bounds=bounds, cutoff=self.thresholds)
except RuntimeError as e:
if "maximum number of function evaluations" in str(e):
fit_results = DEFAULT_PARAMS
else:
raise
# curve_fit raises RuntimeError both for max-evaluations exceeded and other failures
print(f"PV_MUMC_biexp: optimizer failed ({e}). Returning default parameters.")
fit_results = DEFAULT_PARAMS
except Exception as e:
# Catch any other unexpected error (e.g. all-zero signal, NaNs in input)
print(f"PV_MUMC_biexp: unexpected error during fit ({type(e).__name__}: {e}). Returning default parameters.")
fit_results = DEFAULT_PARAMS

results = {}
results = {}
results["f"] = fit_results[1]
results["Dp"] = fit_results[2]
results["D"] = fit_results[0]

return results
2 changes: 0 additions & 2 deletions tests/IVIMmodels/unit_tests/test_ivim_fit.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,6 @@ def test_default_bounds_and_initial_guesses(algorithmlist,eng):
assert 0 <= fit.osipi_initial_guess["f"] <= 0.5, f"For {algorithm}, the default initial guess for f {fit.osipi_initial_guess['f']} is unrealistic"
assert 0.003 <= fit.osipi_initial_guess["Dp"] <= 0.1, f"For {algorithm}, the default initial guess for Dp {fit.osipi_initial_guess['Dp']} is unrealistic"
assert 0.9 <= fit.osipi_initial_guess["S0"] <= 1.1, f"For {algorithm}, the default initial guess for S0 {fit.osipi_initial_guess['S0']} is unrealistic; note signal is normalized"


def test_bounds(bound_input, eng, request):
name, bvals, data, algorithm, xfail, kwargs, tolerances, requires_matlab = bound_input
if xfail["xfail"]:
Expand Down
Loading