Skip to content

feat: Add body-part aware initial guesses for IVIM fitting (Feature #87)#149

Open
Devguru-codes wants to merge 3 commits intoOSIPI:mainfrom
Devguru-codes:feature-body-part-initial-guesses
Open

feat: Add body-part aware initial guesses for IVIM fitting (Feature #87)#149
Devguru-codes wants to merge 3 commits intoOSIPI:mainfrom
Devguru-codes:feature-body-part-initial-guesses

Conversation

@Devguru-codes
Copy link
Copy Markdown
Contributor

@Devguru-codes Devguru-codes commented Mar 1, 2026

Hello @etpeterson - I saw no updates were made on this issue so I worked on it. If there is need of improvement, comment it and i will update this pr.

Description

Closes #87. Adds organ-specific initial guesses, bounds, and thresholds for IVIM fitting, sourced from peer-reviewed literature. This enables faster convergence, reduced local minima, and more physiologically plausible results when scanning specific body parts.

Problem

The current generic defaults (f=0.1, D=0.001, Dp=0.01) are reasonable for the brain but significantly off for other organs:

Parameter Current Generic Brain (actual) Liver (actual) Kidney (actual)
f 0.10 0.03–0.06 0.10–0.15 0.18–0.25
D (×10⁻³) 1.00 0.70–0.80 0.96–1.19 1.78–1.89
Dp (×10⁻³) 10.0 7–25 40–80 ❌ 6× off 25–30 ❌ 3× off

Solution: Literature-Sourced Lookup Table

Body Part f D (×10⁻³) Dp (×10⁻³) Source
Brain 0.05 0.80 10.0 Federau 2017 (DOI)
Liver 0.12 1.00 60.0 Dyvorne 2013 (DOI), Guiu 2012 (DOI)
Kidney 0.20 1.90 30.0 Li 2017 (DOI), Ljimani 2020 (DOI)
Prostate 0.08 1.50 25.0 Kuru 2014 (DOI)
Pancreas 0.18 1.20 20.0 Barbieri 2020 (DOI)
Head & Neck 0.15 1.00 25.0 Sumi 2012 (DOI)
Breast 0.10 1.40 20.0 Lee 2018 (DOI)
Placenta 0.28 1.70 40.0 Zhu 2023 (DOI)

Each body part also includes organ-specific bounds (tighter than the generic ones) to constrain the optimizer to physiologically plausible regions.

Justification for chosen values:

  • Brain: Federau 2017 reports f = 0.03–0.06, D ≈ 0.8, D* ≈ 7–25. Our f=0.05, D=0.8, Dp=10.0 is perfectly centered.
  • Liver: Dyvorne 2013/Guiu 2012 report f = 13–25%, D ≈ 1.2, D* ≈ 55–82. Our f=0.12, D=1.0, Dp=60.0 is standard for liver.
  • Kidney: Ljimani 2020 (consensus paper) reports Cortex D=1.89, f=20.7%. Li 2017 reports D* ≈ 28. Our f=0.20, D=1.90, Dp=30.0 matches the cortex.
  • Prostate: Kuru 2014 reports f=8.6%, D=1.46, D*=28.7. Our f=0.08, D=1.50, Dp=25.0 is excellent.
  • Breast: Lee 2018 confirms f ≈ 10%, D ≈ 1.40. Our Dp=20.0 is standard for fibroglandular tissue.
  • Placenta: Zhu 2023 indicates f ≈ 28% and D* ≈ 40. Our values reflect this exactly.

Changes Made

Action File Description
NEW src/wrappers/ivim_body_part_defaults.py Lookup table with 8 body parts + generic, get_body_part_defaults(), get_available_body_parts()
MODIFY src/wrappers/OsipiBase.py Add body_part param to __init__(), support initial_guess as string
NEW tests/.../test_body_part_defaults.py 22 unit tests

API Usage

# New: body_part parameter (sets both initial_guess AND bounds)
fit = OsipiBase(algorithm="IAR_LU_biexp", bvalues=bvals, body_part="liver")

# New: string-based initial_guess (as suggested by maintainer)
fit = OsipiBase(algorithm="IAR_LU_biexp", bvalues=bvals, initial_guess="brain")

# User overrides always win
fit = OsipiBase(algorithm="IAR_LU_biexp", bvalues=bvals,
                body_part="liver", initial_guess=custom_dict)
# → custom_dict is used for initial guess, liver bounds still applied

# 100% backward compatible — default behavior unchanged
fit = OsipiBase(algorithm="IAR_LU_biexp", bvalues=bvals)

Testing

  • 22 new unit tests: all pass — covers lookup correctness, case insensitivity, error handling, user override priority, string-based initial_guess, data integrity (all guesses within bounds)
  • Full regression suite: 1127 passed, 167 skipped, 22 xfailed, 6 xpassed — identical to main baseline
  • End-to-end fitting test with body_part="liver" produced near-perfect results:
    True:   f=0.120  D=0.0010  Dp=0.060
    Fitted: f=0.120  D=0.0010  Dp=0.060
    
  • Zero regressions

Known Issue (Pre-existing, Not Introduced by This PR)

When using body_part= together with algorithm="IAR_LU_biexp", the IAR_LU_biexp algorithm's internal set_bounds() crashes with KeyError: 0 because it expects bounds as list-of-lists (bounds[0], bounds[1]) but receives dict-format bounds. This is the same bug on main that is already documented in Issue #86 and fixed in PR #142 — it reproduces identically with body_part=None (generic defaults). Once PR #142 is merged, this will work seamlessly.

Backward Compatibility

  • body_part=None (default) → identical to current behavior
  • initial_guess=dict → identical to current behavior
  • User-provided bounds/initial_guess always override body-part defaults
  • No existing API signatures changed

Checklist

  • Self-review of changed code
  • Added automated tests — 22 unit tests in tests/IVIMmodels/unit_tests/test_body_part_defaults.py
  • 100% backward compatible — no existing API signatures changed
  • Literature-sourced values with DOI references for all 8 body parts
  • No new dependencies (numpy, scipy already required)
  • Tested against full regression suite — zero regressions

…SIPI#87)

Add literature-sourced default initial guesses, bounds, and thresholds
for 8 anatomical regions: brain, liver, kidney, prostate, pancreas,
head_and_neck, breast, placenta.

New files:
- src/wrappers/ivim_body_part_defaults.py: lookup table module with
  get_body_part_defaults() and get_available_body_parts()
- tests/IVIMmodels/unit_tests/test_body_part_defaults.py: 22 unit tests

Modified files:
- src/wrappers/OsipiBase.py: add body_part parameter to __init__(),
  support initial_guess as string (e.g. initial_guess='liver')

API usage:
  OsipiBase(algorithm='X', bvalues=b, body_part='liver')
  OsipiBase(algorithm='X', bvalues=b, initial_guess='brain')

100% backward compatible: body_part=None uses original generic defaults.
User-provided bounds/initial_guess always take priority.

No regressions: 1127 passed, 167 skipped, 22 xfailed, 6 xpassed.
Copy link
Copy Markdown
Collaborator

@oliverchampion oliverchampion left a comment

Choose a reason for hiding this comment

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

Hey DevGuru, thanks for implementing this! Some general comments. As some of these implementation also requiere expert input and consensus (bounds, initial guesses) I have asked the community to comment too.

@Devguru-codes
Copy link
Copy Markdown
Contributor Author

I will make all updates at once when the expert's review will come.

@DKuppens

This comment was marked as duplicate.

@Devguru-codes
Copy link
Copy Markdown
Contributor Author

Devguru-codes commented Mar 11, 2026

I have updated the file. Updates are -

  1. Initial Guesses: I updated the initial guesses for all organs to exactly match the expected healthy tissue means from the upcoming Sigmund et al. consensus recommendations, mapping to the references provided by @DKuppens sir.
  2. Organ-Specific Bounds: As suggested by Ivan and Oliver, we are keeping the organ-specific bounds structure but setting them universally to broad, physical limits to ensure we don't inadvertently restrict lesion contrast. The bounds applied to all organs (and the generic fallback) are now :{"S0": [0.5, 1.5], "f": [0, 1.0], "Dp": [0.005, 0.2], "D": [0, 0.005]}
  3. Warning message:Added a UserWarning that triggers whenever an organ-specific preset is selected.
  4. Bounds are -
    {
    "brain": {
    "initial_guess": {"S0": 1.0, "f": 0.0764, "Dp": 0.01088, "D": 0.00083},
    "bounds": {"S0": [0.5, 1.5], "f": [0, 1.0], "Dp": [0.005, 0.2], "D": [0, 0.005]},
    "thresholds": [200]
    },
    "kidney": {
    "initial_guess": {"S0": 1.0, "f": 0.1888, "Dp": 0.04053, "D": 0.00189},
    "bounds": {"S0": [0.5, 1.5], "f": [0, 1.0], "Dp": [0.005, 0.2], "D": [0, 0.005]},
    "thresholds": [200]
    },
    "liver": {
    "initial_guess": {"S0": 1.0, "f": 0.2305, "Dp": 0.07002, "D": 0.00109},
    "bounds": {"S0": [0.5, 1.5], "f": [0, 1.0], "Dp": [0.005, 0.2], "D": [0, 0.005]},
    "thresholds": [200]
    },
    "muscle": {
    "initial_guess": {"S0": 1.0, "f": 0.1034, "Dp": 0.03088, "D": 0.00147},
    "bounds": {"S0": [0.5, 1.5], "f": [0, 1.0], "Dp": [0.005, 0.2], "D": [0, 0.005]},
    "thresholds": [200]
    },
    "breast_benign": {
    "initial_guess": {"S0": 1.0, "f": 0.0700, "Dp": 0.05233, "D": 0.00143},
    "bounds": {"S0": [0.5, 1.5], "f": [0, 1.0], "Dp": [0.005, 0.2], "D": [0, 0.005]},
    "thresholds": [200]
    },
    "breast_malignant": {
    "initial_guess": {"S0": 1.0, "f": 0.1131, "Dp": 0.03776, "D": 0.00097},
    "bounds": {"S0": [0.5, 1.5], "f": [0, 1.0], "Dp": [0.005, 0.2], "D": [0, 0.005]},
    "thresholds": [200]
    },
    "pancreas_benign": {
    "initial_guess": {"S0": 1.0, "f": 0.2003, "Dp": 0.02539, "D": 0.00141},
    "bounds": {"S0": [0.5, 1.5], "f": [0, 1.0], "Dp": [0.005, 0.2], "D": [0, 0.005]},
    "thresholds": [200]
    },
    "pancreas_malignant": {
    "initial_guess": {"S0": 1.0, "f": 0.1239, "Dp": 0.02216, "D": 0.00140},
    "bounds": {"S0": [0.5, 1.5], "f": [0, 1.0], "Dp": [0.005, 0.2], "D": [0, 0.005]},
    "thresholds": [200]
    },
    "generic": {
    "initial_guess": {"S0": 1.0, "f": 0.1, "Dp": 0.01, "D": 0.001},
    "bounds": {"S0": [0.5, 1.5], "f": [0, 1.0], "Dp": [0.005, 0.2], "D": [0, 0.005]},
    "thresholds": [200]
    }
    }
    Thank you

@Devguru-codes
Copy link
Copy Markdown
Contributor Author

Hello @oliverchampion sir ,the CI tests failed because they have older initial guess values in the tests. This caused the tests to fail. I am going to update the test file that is test_body_part_defaults.py. Now, coming to values are -
Brain:
Old test_brain_initial_guess: f=0.05, D=0.0008, Dp=0.01
New values: f=0.0764, D=0.00083, Dp=0.01088

Liver:
Old test_liver_initial_guess: f=0.12, D=0.001, Dp=0.06
New values: f=0.2305, D=0.00109, Dp=0.07002

Kidney:
Old test_kidney_initial_guess: f=0.20, D=0.0019, Dp=0.03
New values: f=0.1888, D=0.00189, Dp=0.04053

Bounds Updated (to match the Broad Physical Bounds logic):
Old test_liver_bounds_differ_from_generic: Asserted that liver D upper bound was tighter than generic.
New test logic test_all_organs_use_broad_physical_bounds: Asserts that every organ correctly returns the exact same broad physical bounds we agreed on:
{S0: [0.5, 1.5], f: [0, 1.0], Dp: [0.005, 0.2], D: [0, 0.005]}

Normalization Test Updated (Removed Organs):
Old test_spaces_and_hyphens_normalized: Used head_and_neck to test string normalization.
New logic: Uses breast_benign instead (because head_and_neck was removed.)

I will wait to update it. I want your thoughts on these values. Thank you.

@etpeterson
Copy link
Copy Markdown
Contributor

@Devguru-codes Thanks, this is a good improvement. I do see failing tests so those should be fixed too.

@Devguru-codes
Copy link
Copy Markdown
Contributor Author

Devguru-codes commented Mar 12, 2026

@Devguru-codes Thanks, this is a good improvement. I do see failing tests so those should be fixed too.

Ok @etpeterson sir, @DKuppens sir, @IvanARashid sir, @oliverchampion sir, I just need confirmation on the values i am going to use. I have mentioned the values above. Thank you

@IvanARashid
Copy link
Copy Markdown
Contributor

Hello @oliverchampion sir ,the CI tests failed because they have older initial guess values in the tests. This caused the tests to fail. I am going to update the test file that is test_body_part_defaults.py. Now, coming to values are - Brain: Old test_brain_initial_guess: f=0.05, D=0.0008, Dp=0.01 New values: f=0.0764, D=0.00083, Dp=0.01088

Liver: Old test_liver_initial_guess: f=0.12, D=0.001, Dp=0.06 New values: f=0.2305, D=0.00109, Dp=0.07002

Kidney: Old test_kidney_initial_guess: f=0.20, D=0.0019, Dp=0.03 New values: f=0.1888, D=0.00189, Dp=0.04053

Bounds Updated (to match the Broad Physical Bounds logic): Old test_liver_bounds_differ_from_generic: Asserted that liver D upper bound was tighter than generic. New test logic test_all_organs_use_broad_physical_bounds: Asserts that every organ correctly returns the exact same broad physical bounds we agreed on: {S0: [0.5, 1.5], f: [0, 1.0], Dp: [0.005, 0.2], D: [0, 0.005]}

Normalization Test Updated (Removed Organs): Old test_spaces_and_hyphens_normalized: Used head_and_neck to test string normalization. New logic: Uses breast_benign instead (because head_and_neck was removed.)

I will wait to update it. I want your thoughts on these values. Thank you.

Looks good!

@Devguru-codes
Copy link
Copy Markdown
Contributor Author

Hello @oliverchampion sir ,the CI tests failed because they have older initial guess values in the tests. This caused the tests to fail. I am going to update the test file that is test_body_part_defaults.py. Now, coming to values are - Brain: Old test_brain_initial_guess: f=0.05, D=0.0008, Dp=0.01 New values: f=0.0764, D=0.00083, Dp=0.01088
Liver: Old test_liver_initial_guess: f=0.12, D=0.001, Dp=0.06 New values: f=0.2305, D=0.00109, Dp=0.07002
Kidney: Old test_kidney_initial_guess: f=0.20, D=0.0019, Dp=0.03 New values: f=0.1888, D=0.00189, Dp=0.04053
Bounds Updated (to match the Broad Physical Bounds logic): Old test_liver_bounds_differ_from_generic: Asserted that liver D upper bound was tighter than generic. New test logic test_all_organs_use_broad_physical_bounds: Asserts that every organ correctly returns the exact same broad physical bounds we agreed on: {S0: [0.5, 1.5], f: [0, 1.0], Dp: [0.005, 0.2], D: [0, 0.005]}
Normalization Test Updated (Removed Organs): Old test_spaces_and_hyphens_normalized: Used head_and_neck to test string normalization. New logic: Uses breast_benign instead (because head_and_neck was removed.)
I will wait to update it. I want your thoughts on these values. Thank you.

Looks good!

Thank you @IvanARashid sir for confirming the values. I have now updated those values. Thank you.

@Devguru-codes
Copy link
Copy Markdown
Contributor Author

I have update the values and when i checked those values with the updated bounds, the test cases passed. The tests failed cause they still have the old values present in them. Please review it @oliverchampion sir. Thank you.

@Devguru-codes
Copy link
Copy Markdown
Contributor Author

Devguru-codes commented Mar 21, 2026

@oliverchampion sir, I researched on it and used AI to figure why CI unit test cases were failing only for macos-

  1. Install libomp on macOS (dipy dependency) in unit_test.yml
    Why: On macOS + Python 3.12, dipy's Cython extensions link against OpenMP (libomp.dylib). Without it, any test that imports dipy (even indirectly via IAR_LU_biexp) fails with ImportError: Library not loaded: libomp.dylib
  2. Add -k "not test_deep_learning_algorithms" in unit_test.yml
    Why: The CPU-only PyTorch wheel segfaults inside torch.utils.data.random_split() during IVIM_NEToptim initialization on macOS (both 3.11 and 3.13). This is a known class of PyTorch/macOS compatibility issues — this is not a bug in this project's code.

These changes are suggested by AI in unit_test.yml which I think I will need your permission before I update it in this PR. Thank you.

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.

[Feature] <Add reasonable initial guesses>

5 participants