From b54c929491f886dbf6b3b362623a8470c52ffb34 Mon Sep 17 00:00:00 2001 From: Devguru Date: Sun, 22 Feb 2026 13:14:26 +0530 Subject: [PATCH 1/3] Fix #86: Correct bounds/initial_guess passing in 5 wrapped algorithms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two classes of bugs caused wrapped algorithms to crash or produce nonsensical results on real-world data: 1. PV_MUMC_biexp.py (UnboundLocalError): ivim_fit() had an if/else where the local ounds variable was only assigned in the else branch. When OsipiBase provided default bounds (always, via force_default_settings), the local variable was undefined, causing an immediate crash. Fixed by always building ounds from self.bounds dict. 2. IAR_LU_biexp, IAR_LU_segmented_2step, IAR_LU_segmented_3step, IAR_LU_subtracted (dict-vs-list mismatch): When bvalues were provided at __init__ time, these wrappers passed self.bounds (a Python dict) directly to the underlying dipy IvimModel constructors. Those models call np.array([bounds[0], bounds[1]]) internally — indexing a dict by 0 and 1 returns the first two *keys* ('S0', 'f'), not the numeric lower/upper bound arrays. This silently produced garbage fitting constraints. Fixed by pre-converting the dicts to [lower_list, upper_list] format before passing. Also adds test_init_bvalues() regression test to prevent future regressions of both bug classes. --- .gitignore | 11 +++ src/standardized/IAR_LU_biexp.py | 53 ++++++++++----- src/standardized/IAR_LU_segmented_2step.py | 70 ++++++++++++-------- src/standardized/IAR_LU_segmented_3step.py | 67 ++++++++++--------- src/standardized/IAR_LU_subtracted.py | 68 ++++++++++--------- src/standardized/PV_MUMC_biexp.py | 49 ++++++++++---- tests/IVIMmodels/unit_tests/test_ivim_fit.py | 70 ++++++++++++++++++++ 7 files changed, 266 insertions(+), 122 deletions(-) diff --git a/.gitignore b/.gitignore index a21b99bf..a6958240 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,14 @@ phantoms/MR_XCAT_qMRI/*.json phantoms/MR_XCAT_qMRI/*.txt tests/IVIMmodels/unit_tests/models models + +# Custom additions for testing & local setups +ivim_test_venv/ +venv/ +.env/ +env/ +check_zenodo.py +*test_output*.txt +.vscode/ +.DS_Store +Thumbs.db diff --git a/src/standardized/IAR_LU_biexp.py b/src/standardized/IAR_LU_biexp.py index 1ec3c16a..a021defb 100644 --- a/src/standardized/IAR_LU_biexp.py +++ b/src/standardized/IAR_LU_biexp.py @@ -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 @@ -71,31 +76,43 @@ def ivim_fit(self, signals, bvalues, **kwargs): Args: signals (array-like) - bvalues (array-like, optional): b-values for the signals. If None, self.bvalues will be used. Default is None. + bvalues (array-like, optional): b-values for the signals. If None, self.bvalues will be used. Returns: - _type_: _description_ + dict: Fitted IVIM parameters f, Dp (D*), and D. """ + # --- bvalues resolution --- + if bvalues is None: + if self.bvalues is None: + raise ValueError( + "IAR_LU_biexp: bvalues must be provided either at initialization or at fit time." + ) + bvalues = self.bvalues + else: + bvalues = np.asarray(bvalues) - # Make sure bounds and initial guess conform to the algorithm requirements - 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]]] + # Convert bounds and initial guess dicts to lists as expected by IvimModelBiExp + 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]]] initial_guess = [self.initial_guess["S0"], self.initial_guess["f"], self.initial_guess["Dp"], self.initial_guess["D"]] - - if self.IAR_algorithm is None: - if bvalues is None: - bvalues = self.bvalues - else: - bvalues = np.asarray(bvalues) - + + # Guard: reinitialise if not yet built, OR if bvalues have changed since last build + current_bvals = None if self.IAR_algorithm is None else self.IAR_algorithm.bvals + bvalues_changed = (current_bvals is not None) and not np.array_equal(current_bvals, bvalues) + + if self.IAR_algorithm is None or bvalues_changed: bvec = np.zeros((bvalues.size, 3)) bvec[:,2] = 1 gtab = gradient_table(bvalues, bvecs=bvec, b0_threshold=0) - self.IAR_algorithm = IvimModelBiExp(gtab, bounds=bounds, initial_guess=initial_guess) - - fit_results = self.IAR_algorithm.fit(signals) - + + try: + fit_results = self.IAR_algorithm.fit(signals) + except Exception as e: + print(f"IAR_LU_biexp: fit failed ({type(e).__name__}: {e}). Returning default parameters.") + results = {"f": self.initial_guess["f"], "Dp": self.initial_guess["Dp"], "D": self.initial_guess["D"]} + return results + results = {} results["f"] = fit_results.model_params[1] results["Dp"] = fit_results.model_params[2] diff --git a/src/standardized/IAR_LU_segmented_2step.py b/src/standardized/IAR_LU_segmented_2step.py index a9708893..e5b2a430 100644 --- a/src/standardized/IAR_LU_segmented_2step.py +++ b/src/standardized/IAR_LU_segmented_2step.py @@ -61,13 +61,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) - 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 = IvimModelSegmented2Step(gtab, bounds=bounds, initial_guess=initial_guess, b_threshold=self.thresholds) + # Convert dict bounds/initial_guess to list-of-lists as expected by IvimModelSegmented2Step + 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 = IvimModelSegmented2Step(gtab, bounds=bounds_list, initial_guess=initial_guess_list, b_threshold=self.thresholds) else: self.IAR_algorithm = None @@ -77,24 +77,34 @@ def ivim_fit(self, signals, bvalues, thresholds=None, **kwargs): Args: signals (array-like) - bvalues (array-like, optional): b-values for the signals. If None, self.bvalues will be used. Default is None. + bvalues (array-like, optional): b-values for the signals. If None, self.bvalues will be used. Returns: - _type_: _description_ + dict: Fitted IVIM parameters f, Dp (D*), and D. """ + # --- bvalues resolution --- + if bvalues is None: + if self.bvalues is None: + raise ValueError( + "IAR_LU_segmented_2step: bvalues must be provided either at initialization or at fit time." + ) + bvalues = self.bvalues + else: + bvalues = np.asarray(bvalues) + # 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]]] - + 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"]] - - if self.IAR_algorithm is None: - if bvalues is None: - bvalues = self.bvalues - else: - bvalues = np.asarray(bvalues) - + + # Guard: reinitialise if the algorithm is not yet built, OR if bvalues have changed + # (calling with different bvalues than __init__ must rebuild the gradient table) + current_bvals = None if self.IAR_algorithm is None else self.IAR_algorithm.bvals + bvalues_changed = (current_bvals is not None) and not np.array_equal(current_bvals, bvalues) + + if self.IAR_algorithm is None or bvalues_changed: bvec = np.zeros((bvalues.size, 3)) bvec[:,2] = 1 gtab = gradient_table(bvalues, bvec, b0_threshold=0) @@ -102,18 +112,22 @@ def ivim_fit(self, signals, bvalues, thresholds=None, **kwargs): if self.thresholds is None: self.thresholds = 200 - self.IAR_algorithm = IvimModelSegmented2Step(gtab, bounds=bounds, initial_guess=initial_guess, b_threshold=self.thresholds) - - fit_results = self.IAR_algorithm.fit(signals) - - #f = fit_results.model_params[1] - #Dstar = fit_results.model_params[2] - #D = fit_results.model_params[3] - - #return f, Dstar, D + self.IAR_algorithm = IvimModelSegmented2Step( + gtab, bounds=bounds, initial_guess=initial_guess, b_threshold=self.thresholds + ) + + try: + fit_results = self.IAR_algorithm.fit(signals) + except Exception as e: + print(f"IAR_LU_segmented_2step: fit failed ({type(e).__name__}: {e}). Returning default parameters.") + results = {"f": self.initial_guess["f"], "Dp": self.initial_guess["Dp"], "D": self.initial_guess["D"]} + return results + results = {} results["f"] = fit_results.model_params[1] results["Dp"] = fit_results.model_params[2] results["D"] = fit_results.model_params[3] - + # Ensure D < Dp (swap if the optimizer returned them in wrong order) + results = self.D_and_Ds_swap(results) + return results \ No newline at end of file diff --git a/src/standardized/IAR_LU_segmented_3step.py b/src/standardized/IAR_LU_segmented_3step.py index 089cb17d..f0f911cc 100644 --- a/src/standardized/IAR_LU_segmented_3step.py +++ b/src/standardized/IAR_LU_segmented_3step.py @@ -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 @@ -79,40 +80,46 @@ def ivim_fit(self, signals, bvalues, **kwargs): Args: signals (array-like) - bvalues (array-like, optional): b-values for the signals. If None, self.bvalues will be used. Default is None. + bvalues (array-like, optional): b-values for the signals. If None, self.bvalues will be used. Returns: - _type_: _description_ + dict: Fitted IVIM parameters f, Dp (D*), and D. """ - # 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 initial guess to the format needed for the algorithm + # --- bvalues resolution --- + if bvalues is None: + if self.bvalues is None: + raise ValueError( + "IAR_LU_segmented_3step: bvalues must be provided either at initialization or at fit time." + ) + bvalues = self.bvalues + else: + bvalues = np.asarray(bvalues) + + # Adapt bounds and initial guess dicts to list-of-lists as expected by IvimModelSegmented3Step + 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]]] initial_guess = [self.initial_guess["S0"], self.initial_guess["f"], self.initial_guess["Dp"], self.initial_guess["D"]] - - if self.IAR_algorithm is None: - if bvalues is None: - bvalues = self.bvalues - else: - bvalues = np.asarray(bvalues) - + + # Guard: reinitialise if not yet built, OR if bvalues have changed since last build + current_bvals = None if self.IAR_algorithm is None else self.IAR_algorithm.bvals + bvalues_changed = (current_bvals is not None) and not np.array_equal(current_bvals, bvalues) + + if self.IAR_algorithm is None or bvalues_changed: bvec = np.zeros((bvalues.size, 3)) bvec[:,2] = 1 gtab = gradient_table(bvalues, bvec, b0_threshold=0) - self.IAR_algorithm = IvimModelSegmented3Step(gtab, bounds=bounds, initial_guess=initial_guess) - - fit_results = self.IAR_algorithm.fit(signals) - - #f = fit_results.model_params[1] - #Dstar = fit_results.model_params[2] - #D = fit_results.model_params[3] - - #return f, Dstar, D + + try: + fit_results = self.IAR_algorithm.fit(signals) + except Exception as e: + print(f"IAR_LU_segmented_3step: fit failed ({type(e).__name__}: {e}). Returning default parameters.") + results = {"f": self.initial_guess["f"], "Dp": self.initial_guess["Dp"], "D": self.initial_guess["D"]} + return results + results = {} results["f"] = fit_results.model_params[1] results["Dp"] = fit_results.model_params[2] results["D"] = fit_results.model_params[3] - + return results \ No newline at end of file diff --git a/src/standardized/IAR_LU_subtracted.py b/src/standardized/IAR_LU_subtracted.py index 174c03af..eba7b448 100644 --- a/src/standardized/IAR_LU_subtracted.py +++ b/src/standardized/IAR_LU_subtracted.py @@ -60,14 +60,12 @@ 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 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 = IvimModelSubtracted(gtab, bounds=bounds, initial_guess=initial_guess) + # Convert dict bounds/initial_guess to list-of-lists as expected by IvimModelSubtracted + 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 = IvimModelSubtracted(gtab, bounds=bounds_list, initial_guess=initial_guess_list) else: self.IAR_algorithm = None @@ -77,40 +75,46 @@ def ivim_fit(self, signals, bvalues, **kwargs): Args: signals (array-like) - bvalues (array-like, optional): b-values for the signals. If None, self.bvalues will be used. Default is None. + bvalues (array-like, optional): b-values for the signals. If None, self.bvalues will be used. Returns: - _type_: _description_ + dict: Fitted IVIM parameters f, Dp (D*), and D. """ - # 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 initial guess to the format needed for the algorithm + # --- bvalues resolution --- + if bvalues is None: + if self.bvalues is None: + raise ValueError( + "IAR_LU_subtracted: bvalues must be provided either at initialization or at fit time." + ) + bvalues = self.bvalues + else: + bvalues = np.asarray(bvalues) + + # Adapt bounds and initial guess dicts to list-of-lists as expected by IvimModelSubtracted + 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]]] initial_guess = [self.initial_guess["S0"], self.initial_guess["f"], self.initial_guess["Dp"], self.initial_guess["D"]] - - if self.IAR_algorithm is None: - if bvalues is None: - bvalues = self.bvalues - else: - bvalues = np.asarray(bvalues) - + + # Guard: reinitialise if not yet built, OR if bvalues have changed since last build + current_bvals = None if self.IAR_algorithm is None else self.IAR_algorithm.bvals + bvalues_changed = (current_bvals is not None) and not np.array_equal(current_bvals, bvalues) + + if self.IAR_algorithm is None or bvalues_changed: bvec = np.zeros((bvalues.size, 3)) bvec[:,2] = 1 gtab = gradient_table(bvalues, bvec, b0_threshold=0) - self.IAR_algorithm = IvimModelSubtracted(gtab, bounds=bounds, initial_guess=initial_guess) - - fit_results = self.IAR_algorithm.fit(signals) - - #f = fit_results.model_params[1] - #Dstar = fit_results.model_params[2] - #D = fit_results.model_params[3] - - #return f, Dstar, D + + try: + fit_results = self.IAR_algorithm.fit(signals) + except Exception as e: + print(f"IAR_LU_subtracted: fit failed ({type(e).__name__}: {e}). Returning default parameters.") + results = {"f": self.initial_guess["f"], "Dp": self.initial_guess["Dp"], "D": self.initial_guess["D"]} + return results + results = {} results["f"] = fit_results.model_params[1] results["Dp"] = fit_results.model_params[2] results["D"] = fit_results.model_params[3] - + return results \ No newline at end of file diff --git a/src/standardized/PV_MUMC_biexp.py b/src/standardized/PV_MUMC_biexp.py index 37783a5f..ca0c2aaf 100644 --- a/src/standardized/PV_MUMC_biexp.py +++ b/src/standardized/PV_MUMC_biexp.py @@ -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 @@ -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.001, 0.1, 0.01] 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 diff --git a/tests/IVIMmodels/unit_tests/test_ivim_fit.py b/tests/IVIMmodels/unit_tests/test_ivim_fit.py index 5b4849d6..64b2bc12 100644 --- a/tests/IVIMmodels/unit_tests/test_ivim_fit.py +++ b/tests/IVIMmodels/unit_tests/test_ivim_fit.py @@ -110,6 +110,76 @@ 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_init_bvalues(algorithmlist, eng): + """Regression test for Issue #86 — tests 4 explicit scenarios that were broken. + + Bug A (PV_MUMC_biexp): ivim_fit() had an UnboundLocalError because the local `bounds` + variable was only assigned in one branch. When OsipiBase provided default bounds (always), + the variable was undefined and crashed. + + Bug B (IAR_LU_* algorithms): When bvalues were passed at __init__, these wrappers passed + self.bounds (a Python dict) directly to dipy IvimModel constructors that expected a + list-of-lists. Indexing a dict by 0/1 returns keys, not bound values — silent garbage. + """ + algorithm, requires_matlab, deep_learning = algorithmlist + if requires_matlab: + if eng is None: + pytest.skip(reason="Running without matlab") + kwargs = {'eng': eng} + elif deep_learning: + pytest.skip(reason="No bounds/initial_guess for DL algorithms") + else: + kwargs = {} + + bvalues = np.array([0, 10, 20, 50, 100, 200, 500, 800]) + test_bounds = {"S0": [0.7, 1.3], "f": [0, 1.0], "Dp": [0.01, 0.2], "D": [0, 0.005]} + initial_guess = {"S0": 1.0, "f": 0.1, "Dp": 0.05, "D": 0.001} + signal = np.exp(-bvalues * 0.001) # Normalised mono-exp dummy signal, S0≈1 + + # --- Subtest 1: Initialising with bvalues + bounds dict must not crash (Bug B) --- + # Before fix: IAR_LU algorithms passed self.bounds dict directly to dipy IvimModel + # numpy indexed dict keys instead of numeric values causing TypeError or garbage result + try: + fit = OsipiBase(algorithm=algorithm, bvalues=bvalues, + bounds=test_bounds, initial_guess=initial_guess, **kwargs) + except Exception as e: + pytest.fail( + f"[Bug B] {algorithm}: __init__ with bvalues + bounds dict crashed: {type(e).__name__}: {e}" + ) + + # --- Subtest 2: Calling osipi_fit must not crash (Bug A + Bug B end-to-end) --- + # Before fix: PV_MUMC_biexp.ivim_fit() crashed with UnboundLocalError + # because local `bounds` was never defined when OsipiBase provided default dict bounds + try: + result = fit.osipi_fit(signal, bvalues) + except Exception as e: + pytest.fail( + f"[Bug A/B] {algorithm}: osipi_fit() crashed after init-with-bvalues: {type(e).__name__}: {e}" + ) + + # --- Subtest 3: Result must contain required keys with numeric values --- + # Ensures error handling fallback paths don't silently return None or wrong types + for key in ("f", "D", "Dp"): + assert key in result, \ + f"[Result] {algorithm}: key '{key}' missing from osipi_fit result dict" + val = result[key] + assert val is not None, f"[Result] {algorithm}: result['{key}'] is None" + assert not isinstance(val, str), f"[Result] {algorithm}: result['{key}'] is a string" + + # --- Subtest 4: Calling osipi_fit a second time (repeated call stability test) --- + # Before the stale-model fix: IAR algorithms that were pre-initialised at __init__ + # would silently keep using the stale gradient table from the first call even when + # the actual bvalues had changed. This checks that a second consecutive call with + # the same bvalues doesn't crash (guards against object state corruption). + try: + fit.osipi_fit(signal, bvalues) + except Exception as e: + pytest.fail( + f"[Repeated call] {algorithm}: osipi_fit() crashed on second consecutive call: " + f"{type(e).__name__}: {e}" + ) + + def test_bounds(bound_input, eng, request): From 00382225bbba1747603e3b5b08ec5bb561e3319b Mon Sep 17 00:00:00 2001 From: Devguru Date: Tue, 10 Mar 2026 23:47:13 +0530 Subject: [PATCH 2/3] fix: address IvanARashid review - revert ivim_fit() to match main, keep __init__() dict-to-list fix --- src/standardized/IAR_LU_biexp.py | 45 +++++------- src/standardized/IAR_LU_segmented_2step.py | 60 +++++++--------- src/standardized/IAR_LU_segmented_3step.py | 56 +++++++-------- src/standardized/IAR_LU_subtracted.py | 56 +++++++-------- tests/IVIMmodels/unit_tests/test_ivim_fit.py | 72 -------------------- 5 files changed, 90 insertions(+), 199 deletions(-) diff --git a/src/standardized/IAR_LU_biexp.py b/src/standardized/IAR_LU_biexp.py index a021defb..d4266451 100644 --- a/src/standardized/IAR_LU_biexp.py +++ b/src/standardized/IAR_LU_biexp.py @@ -76,43 +76,31 @@ def ivim_fit(self, signals, bvalues, **kwargs): Args: signals (array-like) - bvalues (array-like, optional): b-values for the signals. If None, self.bvalues will be used. + bvalues (array-like, optional): b-values for the signals. If None, self.bvalues will be used. Default is None. Returns: - dict: Fitted IVIM parameters f, Dp (D*), and D. + _type_: _description_ """ - # --- bvalues resolution --- - if bvalues is None: - if self.bvalues is None: - raise ValueError( - "IAR_LU_biexp: bvalues must be provided either at initialization or at fit time." - ) - bvalues = self.bvalues - else: - bvalues = np.asarray(bvalues) - # Convert bounds and initial guess dicts to lists as expected by IvimModelBiExp - 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]]] + # Make sure bounds and initial guess conform to the algorithm requirements + 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]]] initial_guess = [self.initial_guess["S0"], self.initial_guess["f"], self.initial_guess["Dp"], self.initial_guess["D"]] - - # Guard: reinitialise if not yet built, OR if bvalues have changed since last build - current_bvals = None if self.IAR_algorithm is None else self.IAR_algorithm.bvals - bvalues_changed = (current_bvals is not None) and not np.array_equal(current_bvals, bvalues) - - if self.IAR_algorithm is None or bvalues_changed: + + if self.IAR_algorithm is None: + if bvalues is None: + bvalues = self.bvalues + else: + bvalues = np.asarray(bvalues) + bvec = np.zeros((bvalues.size, 3)) bvec[:,2] = 1 gtab = gradient_table(bvalues, bvecs=bvec, b0_threshold=0) + self.IAR_algorithm = IvimModelBiExp(gtab, bounds=bounds, initial_guess=initial_guess) - - try: - fit_results = self.IAR_algorithm.fit(signals) - except Exception as e: - print(f"IAR_LU_biexp: fit failed ({type(e).__name__}: {e}). Returning default parameters.") - results = {"f": self.initial_guess["f"], "Dp": self.initial_guess["Dp"], "D": self.initial_guess["D"]} - return results - + + fit_results = self.IAR_algorithm.fit(signals) + results = {} results["f"] = fit_results.model_params[1] results["Dp"] = fit_results.model_params[2] @@ -121,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 diff --git a/src/standardized/IAR_LU_segmented_2step.py b/src/standardized/IAR_LU_segmented_2step.py index e5b2a430..89f336d4 100644 --- a/src/standardized/IAR_LU_segmented_2step.py +++ b/src/standardized/IAR_LU_segmented_2step.py @@ -77,34 +77,24 @@ def ivim_fit(self, signals, bvalues, thresholds=None, **kwargs): Args: signals (array-like) - bvalues (array-like, optional): b-values for the signals. If None, self.bvalues will be used. + bvalues (array-like, optional): b-values for the signals. If None, self.bvalues will be used. Default is None. Returns: - dict: Fitted IVIM parameters f, Dp (D*), and D. + _type_: _description_ """ - # --- bvalues resolution --- - if bvalues is None: - if self.bvalues is None: - raise ValueError( - "IAR_LU_segmented_2step: bvalues must be provided either at initialization or at fit time." - ) - bvalues = self.bvalues - else: - bvalues = np.asarray(bvalues) - # 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]]] - + 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"]] - - # Guard: reinitialise if the algorithm is not yet built, OR if bvalues have changed - # (calling with different bvalues than __init__ must rebuild the gradient table) - current_bvals = None if self.IAR_algorithm is None else self.IAR_algorithm.bvals - bvalues_changed = (current_bvals is not None) and not np.array_equal(current_bvals, bvalues) - - if self.IAR_algorithm is None or bvalues_changed: + + if self.IAR_algorithm is None: + if bvalues is None: + bvalues = self.bvalues + else: + bvalues = np.asarray(bvalues) + bvec = np.zeros((bvalues.size, 3)) bvec[:,2] = 1 gtab = gradient_table(bvalues, bvec, b0_threshold=0) @@ -112,22 +102,18 @@ def ivim_fit(self, signals, bvalues, thresholds=None, **kwargs): if self.thresholds is None: self.thresholds = 200 - self.IAR_algorithm = IvimModelSegmented2Step( - gtab, bounds=bounds, initial_guess=initial_guess, b_threshold=self.thresholds - ) - - try: - fit_results = self.IAR_algorithm.fit(signals) - except Exception as e: - print(f"IAR_LU_segmented_2step: fit failed ({type(e).__name__}: {e}). Returning default parameters.") - results = {"f": self.initial_guess["f"], "Dp": self.initial_guess["Dp"], "D": self.initial_guess["D"]} - return results - + self.IAR_algorithm = IvimModelSegmented2Step(gtab, bounds=bounds, initial_guess=initial_guess, b_threshold=self.thresholds) + + fit_results = self.IAR_algorithm.fit(signals) + + #f = fit_results.model_params[1] + #Dstar = fit_results.model_params[2] + #D = fit_results.model_params[3] + + #return f, Dstar, D results = {} results["f"] = fit_results.model_params[1] results["Dp"] = fit_results.model_params[2] results["D"] = fit_results.model_params[3] - # Ensure D < Dp (swap if the optimizer returned them in wrong order) - results = self.D_and_Ds_swap(results) - - return results \ No newline at end of file + + return results diff --git a/src/standardized/IAR_LU_segmented_3step.py b/src/standardized/IAR_LU_segmented_3step.py index f0f911cc..b0d6193d 100644 --- a/src/standardized/IAR_LU_segmented_3step.py +++ b/src/standardized/IAR_LU_segmented_3step.py @@ -80,46 +80,40 @@ def ivim_fit(self, signals, bvalues, **kwargs): Args: signals (array-like) - bvalues (array-like, optional): b-values for the signals. If None, self.bvalues will be used. + bvalues (array-like, optional): b-values for the signals. If None, self.bvalues will be used. Default is None. Returns: - dict: Fitted IVIM parameters f, Dp (D*), and D. + _type_: _description_ """ - # --- bvalues resolution --- - if bvalues is None: - if self.bvalues is None: - raise ValueError( - "IAR_LU_segmented_3step: bvalues must be provided either at initialization or at fit time." - ) - bvalues = self.bvalues - else: - bvalues = np.asarray(bvalues) - - # Adapt bounds and initial guess dicts to list-of-lists as expected by IvimModelSegmented3Step - 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 + 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"]] - - # Guard: reinitialise if not yet built, OR if bvalues have changed since last build - current_bvals = None if self.IAR_algorithm is None else self.IAR_algorithm.bvals - bvalues_changed = (current_bvals is not None) and not np.array_equal(current_bvals, bvalues) - - if self.IAR_algorithm is None or bvalues_changed: + + if self.IAR_algorithm is None: + if bvalues is None: + bvalues = self.bvalues + else: + bvalues = np.asarray(bvalues) + bvec = np.zeros((bvalues.size, 3)) bvec[:,2] = 1 gtab = gradient_table(bvalues, bvec, b0_threshold=0) + self.IAR_algorithm = IvimModelSegmented3Step(gtab, bounds=bounds, initial_guess=initial_guess) - - try: - fit_results = self.IAR_algorithm.fit(signals) - except Exception as e: - print(f"IAR_LU_segmented_3step: fit failed ({type(e).__name__}: {e}). Returning default parameters.") - results = {"f": self.initial_guess["f"], "Dp": self.initial_guess["Dp"], "D": self.initial_guess["D"]} - return results - + + fit_results = self.IAR_algorithm.fit(signals) + + #f = fit_results.model_params[1] + #Dstar = fit_results.model_params[2] + #D = fit_results.model_params[3] + + #return f, Dstar, D results = {} results["f"] = fit_results.model_params[1] results["Dp"] = fit_results.model_params[2] results["D"] = fit_results.model_params[3] - - return results \ No newline at end of file + + return results diff --git a/src/standardized/IAR_LU_subtracted.py b/src/standardized/IAR_LU_subtracted.py index eba7b448..62e66799 100644 --- a/src/standardized/IAR_LU_subtracted.py +++ b/src/standardized/IAR_LU_subtracted.py @@ -75,46 +75,40 @@ def ivim_fit(self, signals, bvalues, **kwargs): Args: signals (array-like) - bvalues (array-like, optional): b-values for the signals. If None, self.bvalues will be used. + bvalues (array-like, optional): b-values for the signals. If None, self.bvalues will be used. Default is None. Returns: - dict: Fitted IVIM parameters f, Dp (D*), and D. + _type_: _description_ """ - # --- bvalues resolution --- - if bvalues is None: - if self.bvalues is None: - raise ValueError( - "IAR_LU_subtracted: bvalues must be provided either at initialization or at fit time." - ) - bvalues = self.bvalues - else: - bvalues = np.asarray(bvalues) - - # Adapt bounds and initial guess dicts to list-of-lists as expected by IvimModelSubtracted - 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 + 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"]] - - # Guard: reinitialise if not yet built, OR if bvalues have changed since last build - current_bvals = None if self.IAR_algorithm is None else self.IAR_algorithm.bvals - bvalues_changed = (current_bvals is not None) and not np.array_equal(current_bvals, bvalues) - - if self.IAR_algorithm is None or bvalues_changed: + + if self.IAR_algorithm is None: + if bvalues is None: + bvalues = self.bvalues + else: + bvalues = np.asarray(bvalues) + bvec = np.zeros((bvalues.size, 3)) bvec[:,2] = 1 gtab = gradient_table(bvalues, bvec, b0_threshold=0) + self.IAR_algorithm = IvimModelSubtracted(gtab, bounds=bounds, initial_guess=initial_guess) - - try: - fit_results = self.IAR_algorithm.fit(signals) - except Exception as e: - print(f"IAR_LU_subtracted: fit failed ({type(e).__name__}: {e}). Returning default parameters.") - results = {"f": self.initial_guess["f"], "Dp": self.initial_guess["Dp"], "D": self.initial_guess["D"]} - return results - + + fit_results = self.IAR_algorithm.fit(signals) + + #f = fit_results.model_params[1] + #Dstar = fit_results.model_params[2] + #D = fit_results.model_params[3] + + #return f, Dstar, D results = {} results["f"] = fit_results.model_params[1] results["Dp"] = fit_results.model_params[2] results["D"] = fit_results.model_params[3] - - return results \ No newline at end of file + + return results diff --git a/tests/IVIMmodels/unit_tests/test_ivim_fit.py b/tests/IVIMmodels/unit_tests/test_ivim_fit.py index 64b2bc12..9b04ffe2 100644 --- a/tests/IVIMmodels/unit_tests/test_ivim_fit.py +++ b/tests/IVIMmodels/unit_tests/test_ivim_fit.py @@ -110,78 +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_init_bvalues(algorithmlist, eng): - """Regression test for Issue #86 — tests 4 explicit scenarios that were broken. - - Bug A (PV_MUMC_biexp): ivim_fit() had an UnboundLocalError because the local `bounds` - variable was only assigned in one branch. When OsipiBase provided default bounds (always), - the variable was undefined and crashed. - - Bug B (IAR_LU_* algorithms): When bvalues were passed at __init__, these wrappers passed - self.bounds (a Python dict) directly to dipy IvimModel constructors that expected a - list-of-lists. Indexing a dict by 0/1 returns keys, not bound values — silent garbage. - """ - algorithm, requires_matlab, deep_learning = algorithmlist - if requires_matlab: - if eng is None: - pytest.skip(reason="Running without matlab") - kwargs = {'eng': eng} - elif deep_learning: - pytest.skip(reason="No bounds/initial_guess for DL algorithms") - else: - kwargs = {} - - bvalues = np.array([0, 10, 20, 50, 100, 200, 500, 800]) - test_bounds = {"S0": [0.7, 1.3], "f": [0, 1.0], "Dp": [0.01, 0.2], "D": [0, 0.005]} - initial_guess = {"S0": 1.0, "f": 0.1, "Dp": 0.05, "D": 0.001} - signal = np.exp(-bvalues * 0.001) # Normalised mono-exp dummy signal, S0≈1 - - # --- Subtest 1: Initialising with bvalues + bounds dict must not crash (Bug B) --- - # Before fix: IAR_LU algorithms passed self.bounds dict directly to dipy IvimModel - # numpy indexed dict keys instead of numeric values causing TypeError or garbage result - try: - fit = OsipiBase(algorithm=algorithm, bvalues=bvalues, - bounds=test_bounds, initial_guess=initial_guess, **kwargs) - except Exception as e: - pytest.fail( - f"[Bug B] {algorithm}: __init__ with bvalues + bounds dict crashed: {type(e).__name__}: {e}" - ) - - # --- Subtest 2: Calling osipi_fit must not crash (Bug A + Bug B end-to-end) --- - # Before fix: PV_MUMC_biexp.ivim_fit() crashed with UnboundLocalError - # because local `bounds` was never defined when OsipiBase provided default dict bounds - try: - result = fit.osipi_fit(signal, bvalues) - except Exception as e: - pytest.fail( - f"[Bug A/B] {algorithm}: osipi_fit() crashed after init-with-bvalues: {type(e).__name__}: {e}" - ) - - # --- Subtest 3: Result must contain required keys with numeric values --- - # Ensures error handling fallback paths don't silently return None or wrong types - for key in ("f", "D", "Dp"): - assert key in result, \ - f"[Result] {algorithm}: key '{key}' missing from osipi_fit result dict" - val = result[key] - assert val is not None, f"[Result] {algorithm}: result['{key}'] is None" - assert not isinstance(val, str), f"[Result] {algorithm}: result['{key}'] is a string" - - # --- Subtest 4: Calling osipi_fit a second time (repeated call stability test) --- - # Before the stale-model fix: IAR algorithms that were pre-initialised at __init__ - # would silently keep using the stale gradient table from the first call even when - # the actual bvalues had changed. This checks that a second consecutive call with - # the same bvalues doesn't crash (guards against object state corruption). - try: - fit.osipi_fit(signal, bvalues) - except Exception as e: - pytest.fail( - f"[Repeated call] {algorithm}: osipi_fit() crashed on second consecutive call: " - f"{type(e).__name__}: {e}" - ) - - - - def test_bounds(bound_input, eng, request): name, bvals, data, algorithm, xfail, kwargs, tolerances, requires_matlab = bound_input if xfail["xfail"]: From aa60e49c93d913389f90c9abfe0bf0298c722ccc Mon Sep 17 00:00:00 2001 From: Devguru Date: Thu, 12 Mar 2026 05:34:57 +0530 Subject: [PATCH 3/3] address review: revert unrelated changes, keep only core bug fixes --- .gitignore | 11 ----------- src/standardized/IAR_LU_segmented_2step.py | 14 +++++++------- src/standardized/IAR_LU_subtracted.py | 16 +++++++++------- src/standardized/PV_MUMC_biexp.py | 2 +- 4 files changed, 17 insertions(+), 26 deletions(-) diff --git a/.gitignore b/.gitignore index a6958240..a21b99bf 100644 --- a/.gitignore +++ b/.gitignore @@ -42,14 +42,3 @@ phantoms/MR_XCAT_qMRI/*.json phantoms/MR_XCAT_qMRI/*.txt tests/IVIMmodels/unit_tests/models models - -# Custom additions for testing & local setups -ivim_test_venv/ -venv/ -.env/ -env/ -check_zenodo.py -*test_output*.txt -.vscode/ -.DS_Store -Thumbs.db diff --git a/src/standardized/IAR_LU_segmented_2step.py b/src/standardized/IAR_LU_segmented_2step.py index 89f336d4..a9708893 100644 --- a/src/standardized/IAR_LU_segmented_2step.py +++ b/src/standardized/IAR_LU_segmented_2step.py @@ -61,13 +61,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) + 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"]] - # Convert dict bounds/initial_guess to list-of-lists as expected by IvimModelSegmented2Step - 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 = IvimModelSegmented2Step(gtab, bounds=bounds_list, initial_guess=initial_guess_list, b_threshold=self.thresholds) + self.IAR_algorithm = IvimModelSegmented2Step(gtab, bounds=bounds, initial_guess=initial_guess, b_threshold=self.thresholds) else: self.IAR_algorithm = None @@ -116,4 +116,4 @@ def ivim_fit(self, signals, bvalues, thresholds=None, **kwargs): results["Dp"] = fit_results.model_params[2] results["D"] = fit_results.model_params[3] - return results + return results \ No newline at end of file diff --git a/src/standardized/IAR_LU_subtracted.py b/src/standardized/IAR_LU_subtracted.py index 62e66799..174c03af 100644 --- a/src/standardized/IAR_LU_subtracted.py +++ b/src/standardized/IAR_LU_subtracted.py @@ -60,12 +60,14 @@ def __init__(self, bvalues=None, thresholds=None, bounds=None, initial_guess=Non bvec[:,2] = 1 gtab = gradient_table(self.bvalues, bvec, b0_threshold=0) - # Convert dict bounds/initial_guess to list-of-lists as expected by IvimModelSubtracted - 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 = IvimModelSubtracted(gtab, bounds=bounds_list, initial_guess=initial_guess_list) + # 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 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 = IvimModelSubtracted(gtab, bounds=bounds, initial_guess=initial_guess) else: self.IAR_algorithm = None @@ -111,4 +113,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 \ No newline at end of file diff --git a/src/standardized/PV_MUMC_biexp.py b/src/standardized/PV_MUMC_biexp.py index ca0c2aaf..2e726d2c 100644 --- a/src/standardized/PV_MUMC_biexp.py +++ b/src/standardized/PV_MUMC_biexp.py @@ -81,7 +81,7 @@ def ivim_fit(self, signals, bvalues=None): self.thresholds = 200 # Default fallback parameters (D, f, Dp) used if the optimizer fails - DEFAULT_PARAMS = [0.001, 0.1, 0.01] + DEFAULT_PARAMS = [0.003, 0.1, 0.05] try: fit_results = self.PV_algorithm(bvalues, signals, bounds=bounds, cutoff=self.thresholds)