Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
67 changes: 50 additions & 17 deletions src/wrappers/OsipiBase.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,14 @@ class OsipiBase:
Parameter bounds for constrained optimization. Should be a dict with keys
like "S0", "f", "Dp", "D" and values as [lower, upper] lists or arrays.
E.g. {"S0" : [0.7, 1.3], "f" : [0, 1], "Dp" : [0.005, 0.2], "D" : [0, 0.005]}.
initial_guess : dict, optional
Initial parameter estimates for the IVIM fit. Should be a dict with keys
like "S0", "f", "Dp", "D" and float values.
E.g. {"S0" : 1, "f" : 0.1, "Dp" : 0.01, "D" : 0.001}.
initial_guess : dict or str, optional
Initial parameter estimates for the IVIM fit. Can be:
- A dict with keys like "S0", "f", "Dp", "D" and float values.
E.g. {"S0" : 1, "f" : 0.1, "Dp" : 0.01, "D" : 0.001}.
- A string naming a body part (e.g., "brain", "liver", "kidney").
The string is looked up in the body-part defaults table and
replaced with the corresponding dict. If bounds are not provided,
body-part-specific bounds are also applied.
algorithm : str, optional
Name of an algorithm module in ``src/standardized`` to load dynamically.
If supplied, the instance is immediately converted to that algorithm’s
Expand All @@ -43,6 +47,14 @@ class OsipiBase:
"Dp":[0.005, 0.2], "D":[0, 0.005]}.
To prevent this, set this bool to False. Default initial guess
{"S0" : 1, "f": 0.1, "Dp": 0.01, "D": 0.001}.
body_part : str, optional
Name of the anatomical region being scanned (e.g., "brain", "liver",
"kidney", "prostate", "pancreas", "head_and_neck", "breast",
"placenta"). When provided, body-part-specific initial guesses,
bounds, and thresholds are used as defaults instead of the generic
ones. User-provided bounds/initial_guess always take priority.
See :mod:`src.wrappers.ivim_body_part_defaults` for available
body parts and their literature-sourced parameter values.
**kwargs
Additional keyword arguments forwarded to the selected algorithm’s
initializer if ``algorithm`` is provided.
Expand Down Expand Up @@ -102,7 +114,14 @@ class OsipiBase:
f_map = results["f"]
"""

def __init__(self, bvalues=None, thresholds=None, bounds=None, initial_guess=None, algorithm=None, force_default_settings=True, **kwargs):
def __init__(self, bvalues=None, thresholds=None, bounds=None, initial_guess=None, algorithm=None, force_default_settings=True, body_part=None, **kwargs):
from src.wrappers.ivim_body_part_defaults import get_body_part_defaults
Comment thread
Devguru-codes marked this conversation as resolved.

# If initial_guess is a string, treat it as a body part name
if isinstance(initial_guess, str):
body_part = initial_guess
initial_guess = None

# Define the attributes as numpy arrays only if they are not None
self.bvalues = np.asarray(bvalues) if bvalues is not None else None
self.thresholds = np.asarray(thresholds) if thresholds is not None else None
Expand All @@ -113,20 +132,34 @@ def __init__(self, bvalues=None, thresholds=None, bounds=None, initial_guess=Non
self.deep_learning = False
self.supervised = False
self.stochastic = False
self.body_part = body_part # Store for reference

if force_default_settings:
if self.bounds is None:
print('warning, no bounds were defined, so default bounds are used of [0, 0, 0.005, 0.7],[0.005, 1.0, 0.2, 1.3]')
self.bounds = {"S0" : [0.7, 1.3], "f" : [0, 1.0], "Dp" : [0.005, 0.2], "D" : [0, 0.005]} # These are defined as [lower, upper]
self.forced_default_bounds = True

if self.initial_guess is None:
print('warning, no initial guesses were defined, so default initial guesses are used of [0.001, 0.001, 0.01, 1]')
self.initial_guess = {"S0" : 1, "f" : 0.1, "Dp" : 0.01, "D" : 0.001}
self.forced_default_initial_guess = True

if self.thresholds is None:
self.thresholds = np.array([200])
if body_part is not None:
Comment thread
Devguru-codes marked this conversation as resolved.
# Use body-part-specific defaults from the literature-sourced lookup table
bp_defaults = get_body_part_defaults(body_part)
if self.bounds is None:
self.bounds = bp_defaults["bounds"]
self.forced_default_bounds = True
if self.initial_guess is None:
self.initial_guess = bp_defaults["initial_guess"]
self.forced_default_initial_guess = True
if self.thresholds is None:
self.thresholds = np.array(bp_defaults["thresholds"])
else:
# Generic defaults (original behavior)
if self.bounds is None:
print('warning, no bounds were defined, so default bounds are used of [0, 0, 0.005, 0.7],[0.005, 1.0, 0.2, 1.3]')
self.bounds = {"S0" : [0.7, 1.3], "f" : [0, 1.0], "Dp" : [0.005, 0.2], "D" : [0, 0.005]} # These are defined as [lower, upper]
self.forced_default_bounds = True

if self.initial_guess is None:
print('warning, no initial guesses were defined, so default initial guesses are used of [0.001, 0.001, 0.01, 1]')
self.initial_guess = {"S0" : 1, "f" : 0.1, "Dp" : 0.01, "D" : 0.001}
self.forced_default_initial_guess = True

if self.thresholds is None:
self.thresholds = np.array([200])

self.osipi_bounds = self.bounds # Variable that stores the original bounds before they are passed to the algorithm
self.osipi_initial_guess = self.initial_guess # Variable that stores the original initial guesses before they are passed to the algorithm
Expand Down
147 changes: 147 additions & 0 deletions src/wrappers/ivim_body_part_defaults.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
"""
Body-part specific IVIM parameter defaults.

Literature-based initial guesses, bounds, and thresholds for
different anatomical regions. Used by OsipiBase when the user
specifies a body_part parameter.

References:
Brain: Federau 2017 (DOI: 10.1002/nbm.3780)
Liver: Dyvorne 2013 (DOI: 10.1016/j.ejrad.2013.03.003),
Comment thread
Devguru-codes marked this conversation as resolved.
Outdated
Guiu 2012 (DOI: 10.1002/jmri.23762)
Comment thread
Devguru-codes marked this conversation as resolved.
Outdated
Kidney: Li 2017 (DOI: 10.1002/jmri.25571),
Comment thread
Devguru-codes marked this conversation as resolved.
Outdated
Ljimani 2020 (DOI: 10.1007/s10334-019-00802-0)
Comment thread
Devguru-codes marked this conversation as resolved.
Outdated
Prostate: Kuru 2014 (DOI: 10.1007/s00330-014-3165-y)
Comment thread
Devguru-codes marked this conversation as resolved.
Outdated
Pancreas: Barbieri 2020 (DOI: 10.1002/mrm.27910)
Head/Neck: Sumi 2012 (DOI: 10.1259/dmfr/15696758)
Comment thread
Devguru-codes marked this conversation as resolved.
Outdated
Breast: Lee 2018 (DOI: 10.1097/RCT.0000000000000661)
Comment thread
Devguru-codes marked this conversation as resolved.
Outdated
Placenta: Zhu 2023 (DOI: 10.1002/jmri.28858)
Comment thread
Devguru-codes marked this conversation as resolved.
Outdated
"""

IVIM_BODY_PART_DEFAULTS = {
"brain": {
"initial_guess": {"S0": 1.0, "f": 0.05, "Dp": 0.01, "D": 0.0008},
Comment thread
Devguru-codes marked this conversation as resolved.
Outdated
"bounds": {
Comment thread
Devguru-codes marked this conversation as resolved.
Outdated
"S0": [0.7, 1.3],
"f": [0.0, 0.15],
"Dp": [0.005, 0.05],
"D": [0.0003, 0.002],
},
"thresholds": [200],
Comment thread
Devguru-codes marked this conversation as resolved.
},
"liver": {
"initial_guess": {"S0": 1.0, "f": 0.12, "Dp": 0.06, "D": 0.001},
"bounds": {
"S0": [0.7, 1.3],
Comment thread
Devguru-codes marked this conversation as resolved.
Outdated
"f": [0.0, 0.40],
Comment thread
Devguru-codes marked this conversation as resolved.
Outdated
"Dp": [0.01, 0.15],
"D": [0.0003, 0.003],
},
"thresholds": [200],
},
"kidney": {
"initial_guess": {"S0": 1.0, "f": 0.20, "Dp": 0.03, "D": 0.0019},
"bounds": {
"S0": [0.7, 1.3],
"f": [0.0, 0.50],
"Dp": [0.01, 0.08],
"D": [0.0005, 0.004],
},
"thresholds": [200],
},
"prostate": {
"initial_guess": {"S0": 1.0, "f": 0.08, "Dp": 0.025, "D": 0.0015},
"bounds": {
"S0": [0.7, 1.3],
"f": [0.0, 0.25],
"Dp": [0.005, 0.06],
"D": [0.0003, 0.003],
},
"thresholds": [200],
},
"pancreas": {
"initial_guess": {"S0": 1.0, "f": 0.18, "Dp": 0.02, "D": 0.0012},
"bounds": {
"S0": [0.7, 1.3],
"f": [0.0, 0.50],
"Dp": [0.005, 0.06],
"D": [0.0003, 0.003],
},
"thresholds": [200],
},
"head_and_neck": {
"initial_guess": {"S0": 1.0, "f": 0.15, "Dp": 0.025, "D": 0.001},
"bounds": {
"S0": [0.7, 1.3],
"f": [0.0, 0.40],
"Dp": [0.005, 0.08],
"D": [0.0003, 0.003],
},
"thresholds": [200],
},
"breast": {
"initial_guess": {"S0": 1.0, "f": 0.10, "Dp": 0.02, "D": 0.0014},
"bounds": {
"S0": [0.7, 1.3],
"f": [0.0, 0.30],
"Dp": [0.005, 0.06],
"D": [0.0004, 0.003],
},
"thresholds": [200],
},
"placenta": {
"initial_guess": {"S0": 1.0, "f": 0.28, "Dp": 0.04, "D": 0.0017},
"bounds": {
"S0": [0.7, 1.3],
"f": [0.05, 0.60],
"Dp": [0.01, 0.1],
"D": [0.0005, 0.004],
},
"thresholds": [200],
},
}

# Keep the current universal defaults as "generic"
IVIM_BODY_PART_DEFAULTS["generic"] = {
"initial_guess": {"S0": 1.0, "f": 0.1, "Dp": 0.01, "D": 0.001},
"bounds": {
"S0": [0.7, 1.3],
"f": [0, 1.0],
"Dp": [0.005, 0.2],
"D": [0, 0.005],
},
"thresholds": [200],
}


def get_body_part_defaults(body_part):
"""Get IVIM default parameters for a given body part.

Args:
body_part (str): Name of the body part (e.g., "brain", "liver", "kidney").
Case-insensitive. Spaces and hyphens are normalized to
underscores (e.g., "head and neck" -> "head_and_neck").

Returns:
dict: Dictionary with keys "initial_guess", "bounds", and "thresholds".

Raises:
ValueError: If the body part is not in the lookup table.
"""
key = body_part.lower().replace(" ", "_").replace("-", "_")
if key not in IVIM_BODY_PART_DEFAULTS:
available = ", ".join(sorted(IVIM_BODY_PART_DEFAULTS.keys()))
raise ValueError(
f"Unknown body part '{body_part}'. "
f"Available body parts: {available}"
Comment thread
Devguru-codes marked this conversation as resolved.
)
return IVIM_BODY_PART_DEFAULTS[key]


def get_available_body_parts():
"""Return a sorted list of all available body part names.

Returns:
list: Sorted list of body part name strings.
"""
return sorted(IVIM_BODY_PART_DEFAULTS.keys())
Loading