Skip to content

Commit 254701e

Browse files
Merge pull request #142 from Devguru-codes/fix-issue-86
Fix #86: Correct bounds/initial_guess passing in 5 wrapped algorithms
2 parents b613122 + 8cd8942 commit 254701e

5 files changed

Lines changed: 53 additions & 27 deletions

File tree

src/standardized/IAR_LU_biexp.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,13 @@ def __init__(self, bvalues=None, thresholds=None, bounds=None, initial_guess=Non
6060
bvec = np.zeros((self.bvalues.size, 3))
6161
bvec[:,2] = 1
6262
gtab = gradient_table(self.bvalues, bvecs=bvec, b0_threshold=0)
63-
64-
self.IAR_algorithm = IvimModelBiExp(gtab, bounds=self.bounds, initial_guess=self.initial_guess)
63+
64+
# Convert dict bounds/initial_guess to list-of-lists as expected by IvimModelBiExp
65+
bounds_list = [[self.bounds["S0"][0], self.bounds["f"][0], self.bounds["Dp"][0], self.bounds["D"][0]],
66+
[self.bounds["S0"][1], self.bounds["f"][1], self.bounds["Dp"][1], self.bounds["D"][1]]]
67+
initial_guess_list = [self.initial_guess["S0"], self.initial_guess["f"], self.initial_guess["Dp"], self.initial_guess["D"]]
68+
69+
self.IAR_algorithm = IvimModelBiExp(gtab, bounds=bounds_list, initial_guess=initial_guess_list)
6570
else:
6671
self.IAR_algorithm = None
6772

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

105110
return results
106111

112+
107113
def ivim_fit_full_volume(self, signals, bvalues, **kwargs):
108114
"""Perform the IVIM fit
109115

src/standardized/IAR_LU_segmented_3step.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -62,14 +62,15 @@ def __init__(self, bvalues=None, thresholds=None, bounds=None, initial_guess=Non
6262
bvec[:,2] = 1
6363
gtab = gradient_table(self.bvalues, bvecs=bvec, b0_threshold=0)
6464

65-
# Adapt the bounds to the format needed for the algorithm
66-
bounds = [[self.bounds["S0"][0], self.bounds["f"][0], self.bounds["Dp"][0], self.bounds["D"][0]], \
67-
[self.bounds["S0"][1], self.bounds["f"][1], self.bounds["Dp"][1], self.bounds["D"][1]]]
68-
65+
# Adapt the bounds to the format needed for the algorithm (list-of-lists)
66+
bounds = [[self.bounds["S0"][0], self.bounds["f"][0], self.bounds["Dp"][0], self.bounds["D"][0]],
67+
[self.bounds["S0"][1], self.bounds["f"][1], self.bounds["Dp"][1], self.bounds["D"][1]]]
68+
6969
# Adapt the initial guess to the format needed for the algorithm
7070
initial_guess = [self.initial_guess["S0"], self.initial_guess["f"], self.initial_guess["Dp"], self.initial_guess["D"]]
71-
72-
self.IAR_algorithm = IvimModelSegmented3Step(gtab, bounds=self.bounds, initial_guess=self.initial_guess)
71+
72+
# Use the converted list-of-lists bounds and initial_guess, NOT the raw dicts
73+
self.IAR_algorithm = IvimModelSegmented3Step(gtab, bounds=bounds, initial_guess=initial_guess)
7374
else:
7475
self.IAR_algorithm = None
7576

@@ -100,7 +101,7 @@ def ivim_fit(self, signals, bvalues, **kwargs):
100101
bvec = np.zeros((bvalues.size, 3))
101102
bvec[:,2] = 1
102103
gtab = gradient_table(bvalues, bvecs=bvec, b0_threshold=0)
103-
104+
104105
self.IAR_algorithm = IvimModelSegmented3Step(gtab, bounds=bounds, initial_guess=initial_guess)
105106

106107
fit_results = self.IAR_algorithm.fit(signals)
@@ -115,4 +116,4 @@ def ivim_fit(self, signals, bvalues, **kwargs):
115116
results["Dp"] = fit_results.model_params[2]
116117
results["D"] = fit_results.model_params[3]
117118

118-
return results
119+
return results

src/standardized/IAR_LU_subtracted.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ def ivim_fit(self, signals, bvalues, **kwargs):
9898
bvec = np.zeros((bvalues.size, 3))
9999
bvec[:,2] = 1
100100
gtab = gradient_table(bvalues, bvecs=bvec, b0_threshold=0)
101-
101+
102102
self.IAR_algorithm = IvimModelSubtracted(gtab, bounds=bounds, initial_guess=initial_guess)
103103

104104
fit_results = self.IAR_algorithm.fit(signals)

src/standardized/PV_MUMC_biexp.py

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def __init__(self, bvalues=None, thresholds=None, bounds=None, initial_guess=Non
4343
self.use_bounds = {"f" : True, "D" : True, "Dp" : True, "S0" : True}
4444
self.use_initial_guess = {"f" : False, "D" : False, "Dp" : False, "S0" : False}
4545

46-
46+
4747
def ivim_fit(self, signals, bvalues=None):
4848
"""Perform the IVIM fit
4949
@@ -52,30 +52,51 @@ def ivim_fit(self, signals, bvalues=None):
5252
bvalues (array-like, optional): b-values for the signals. If None, self.bvalues will be used. Default is None.
5353
5454
Returns:
55-
_type_: _description_
55+
dict: Fitted IVIM parameters f, Dp (D*), and D.
5656
"""
57-
if self.bounds is None:
58-
self.bounds = ([0.9, 0.0001, 0.0, 0.0025], [1.1, 0.003, 1, 0.2])
57+
# --- bvalues resolution ---
58+
# Edge case: bvalues not passed here → fall back to the ones set at __init__ time
59+
if bvalues is None:
60+
if self.bvalues is None:
61+
raise ValueError(
62+
"PV_MUMC_biexp: bvalues must be provided either at initialization or at fit time."
63+
)
64+
bvalues = self.bvalues
5965
else:
60-
bounds = ([self.bounds["S0"][0], self.bounds["D"][0], self.bounds["f"][0], self.bounds["Dp"][0]],
61-
[self.bounds["S0"][1], self.bounds["D"][1], self.bounds["f"][1], self.bounds["Dp"][1]])
62-
66+
bvalues = np.asarray(bvalues)
67+
68+
# --- Bounds resolution ---
69+
# self.bounds is always a dict (OsipiBase force_default_settings=True).
70+
# The underlying fit_least_squares expects: ([S0min, Dmin, fmin, Dpmin], [S0max, Dmax, fmax, Dpmax])
71+
if isinstance(self.bounds, dict):
72+
bounds = (
73+
[self.bounds["S0"][0], self.bounds["D"][0], self.bounds["f"][0], self.bounds["Dp"][0]],
74+
[self.bounds["S0"][1], self.bounds["D"][1], self.bounds["f"][1], self.bounds["Dp"][1]],
75+
)
76+
else:
77+
# Fallback: already in list/tuple form (legacy)
78+
bounds = self.bounds
79+
6380
if self.thresholds is None:
6481
self.thresholds = 200
6582

66-
DEFAULT_PARAMS = [0.003,0.1,0.05]
83+
# Default fallback parameters (D, f, Dp) used if the optimizer fails
84+
DEFAULT_PARAMS = [0, 0, 0]
6785

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

76-
results = {}
97+
results = {}
7798
results["f"] = fit_results[1]
7899
results["Dp"] = fit_results[2]
79100
results["D"] = fit_results[0]
80-
101+
81102
return results

tests/IVIMmodels/unit_tests/test_ivim_fit.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,6 @@ def test_default_bounds_and_initial_guesses(algorithmlist,eng):
110110
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"
111111
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"
112112
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"
113-
114-
115113
def test_bounds(bound_input, eng, request):
116114
name, bvals, data, algorithm, xfail, kwargs, tolerances, requires_matlab = bound_input
117115
if xfail["xfail"]:

0 commit comments

Comments
 (0)