Skip to content

Comments

Fix #86: Correct bounds/initial_guess passing in 5 wrapped algorithms#142

Open
Devguru-codes wants to merge 1 commit intoOSIPI:mainfrom
Devguru-codes:fix-issue-86
Open

Fix #86: Correct bounds/initial_guess passing in 5 wrapped algorithms#142
Devguru-codes wants to merge 1 commit intoOSIPI:mainfrom
Devguru-codes:fix-issue-86

Conversation

@Devguru-codes
Copy link

Fix #86: Correct bounds/initial_guess passing in 5 wrapped algorithms

Description

This PR addresses Issue #86, where several wrapped IVIM algorithms crashed or returned mathematically incorrect results when processing real-world MRI data (which, unlike simulated test data, heavily tests boundary validation and fallback paths).

Root Causes & Bug Fixes

During my investigation of the algorithms acting irregularly on real data, I discovered two distinct classes of Python bugs in the wrappers:

  1. PV_MUMC_biexp.py — UnboundLocalError Crash

    • Bug: The ivim_fit() method had an if/else block where the local bounds variable was only assigned in the else branch. Under OsipiBase, self.bounds defaults to a Python dictionary, meaning the code executed the if branch, never created the bounds variable, and crashed.
    • Fix: Restructured the condition to always extract the self.bounds dictionary values into the ([S0_min, D_min...], [S0_max, D_max...]) array formats expected by the underlying fit_least_squares algorithm.
  2. 4 IAR_LU_* Algorithms — Invalid Constraints (Dict vs List-of-Lists)

    • Bug: When bvalues were provided at init, the wrappers passed the self.bounds Python dictionary directly to the dipy IvimModel* constructors. Internally, DIPY expects arrays and indexes them via bounds[0] and bounds[1]. Indexing a dictionary by integer yields its keys ("S0", "f"), completely breaking the optimizer constraints and silently producing garbage results.
    • Fix: Explicitly convert the dictionary into a [[lower...], [upper...]] array structure whenever the IAR_algorithm is initialized.
    • Affected files: IAR_LU_biexp.py, IAR_LU_segmented_2step.py, IAR_LU_segmented_3step.py, IAR_LU_subtracted.py.

Edge Case Hardening Added

To ensure these algorithms robustly handle diverse real-world data and usage patterns, I added several layers of error handling and edge-case protection:

  • Stale-Model Guard: IAR_LU algorithms now automatically detect if the bvalues array changes between init and ivim_fit(), and gracefully rebuild their internal gradient table/model to prevent silent corruption.
  • Missing D_and_Ds_swap: Added the missing parameter order swap check to IAR_LU_segmented_2step.
  • bvalues=None Guards: All 5 algorithms now properly fall back to self.bvalues, raising a clear error if neither is available.
  • Graceful Fallbacks: Wrapped underlying scipy optimization calls in robust try/except blocks to print a warning and return default initial guesses (fail gracefully) if max evaluations are exceeded or optimization fails.

Testing & Validation

  • New Regression Test: Added 4 explicit sub-tests to test_init_bvalues in tests/IVIMmodels/unit_tests/test_ivim_fit.py. It specifically catches the init failure path, the osipi_fit crash path, result key validity, and the stale-model guard.
  • Test Suite Results: 1148 passed, 0 failures (Verified manually through full suite execution natively). Note: TCML_TechnionIIT_lsqBOBYQA bounds x-fails are pre-existing, tolerated algorithm limitations (strict=False) and are completely unrelated to these fixes.

…ithms

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] <Some wrapped algorithms don't seem to be working>

1 participant