Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
79 commits
Select commit Hold shift + click to select a range
b0e686e
Update simple_test_run_of_algorithm.py
IvanARashid Sep 25, 2025
5b4f00e
Merge branch 'main' into wrapper_dev
IvanARashid Sep 25, 2025
6d2bec5
Added default bounds and initial guess
IvanARashid Sep 25, 2025
9df65d8
Merge branch 'main' into wrapper_dev
IvanARashid Oct 15, 2025
29f19c0
Added default bounds and initial guess as dictionaries in OsipiBase
IvanARashid Oct 15, 2025
7dca19a
Dictionary implementation of bounds and initial guesses in testing code
IvanARashid Oct 15, 2025
22585e3
Bug fix
IvanARashid Oct 16, 2025
a3e36e6
Some bounds handling stuff
IvanARashid Oct 17, 2025
57b9381
Defined new variable, self.osipi_bounds which is used here. It is the…
IvanARashid Oct 17, 2025
ebb6be7
Bugfixes to my own submissions
IvanARashid Oct 17, 2025
0aa925d
Bounds adapted to IAR submissions (hopefully)
IvanARashid Oct 17, 2025
f6f6c81
Adapted bounds. But matlab testing is not working for me, function is…
IvanARashid Oct 20, 2025
965289a
Dictionary bounds
IvanARashid Oct 20, 2025
fa70dcd
Dictionary bounds and initial guess
IvanARashid Oct 21, 2025
558de67
Dictionary bounds and initial guesses
IvanARashid Oct 21, 2025
db3eb09
Dictionary bounds and initial guesses where applicable
IvanARashid Oct 21, 2025
f6db6b6
Adjusted documentation for dictionary bounds and initial guesses
IvanARashid Oct 21, 2025
0f6dedc
Merge branch 'main' into wrapper_dev
IvanARashid Oct 21, 2025
d7c67dd
Update simple_test_run_of_algorithm.py
IvanARashid Oct 21, 2025
f662836
Bug fixes to bounds and initial guesses in my algorithms
IvanARashid Nov 19, 2025
5b92c3a
Dictionary update of use_bounds and use_initial_guess
IvanARashid Nov 19, 2025
bf52071
Bounds update
IvanARashid Nov 19, 2025
51541d5
Bounds update
IvanARashid Nov 19, 2025
0eafe9d
Dictionary update to use_initial_guess and use_bounds
IvanARashid Nov 19, 2025
155eb5f
Dictionary update for use_bounds and use_initial_guess, and added def…
IvanARashid Nov 19, 2025
f72458f
Merge branch 'main' into wrapper_dev
IvanARashid Nov 19, 2025
cba7921
testing implemented and algorithms updated
oliverchampion Nov 19, 2025
c4fafb7
Update TCML_TechnionIIT_lsq_sls_BOBYQA.py
oliverchampion Nov 19, 2025
df05f70
Merge pull request #131 from OSIPI/wrapper_dev_testing
IvanARashid Nov 21, 2025
34450ba
Fix type for blim
IvanARashid Nov 24, 2025
6193df2
ignore failures. Tinkered with IVIMNET bounds
oliverchampion Dec 12, 2025
6f833d8
fix AI failures
oliverchampion Dec 12, 2025
c75bd32
floating point error solved
oliverchampion Dec 16, 2025
848a106
Merge pull request #133 from OSIPI/wrapper_dev_testing
oliverchampion Jan 20, 2026
558165a
Merge branch 'main' into wrapper_dev
IvanARashid Jan 22, 2026
7bf6d2c
testing a fix by turning bounds into floats for the failing algorithms
IvanARashid Jan 23, 2026
0283659
testing new fix.. This is already passing locally but not on github
IvanARashid Jan 23, 2026
9c76169
One more try
IvanARashid Jan 23, 2026
eb831f1
Update TCML_TechnionIIT_lsq_sls_BOBYQA.py
IvanARashid Jan 23, 2026
83b3019
Update TCML_TechnionIIT_lsq_sls_BOBYQA.py
IvanARashid Jan 23, 2026
dd30546
Update TCML_TechnionIIT_lsq_sls_trf.py
IvanARashid Jan 23, 2026
73bc88e
Update TCML_TechnionIIT_lsq_sls_lm.py
IvanARashid Jan 23, 2026
5092b4f
Update TCML_TechnionIIT_lsqBOBYQA.py
IvanARashid Jan 23, 2026
f5804ca
Update TCML_TechnionIIT_lsqlm.py
IvanARashid Jan 23, 2026
2024ccf
Update TCML_TechnionIIT_lsqtrf.py
IvanARashid Jan 23, 2026
cd5b488
Update TCML_TechnionIIT_SLS.py
IvanARashid Jan 23, 2026
b4e9fc3
Update xfail logic and test skipping for TCML_TechnionIIT_lsqBOBYQA
oliverchampion Jan 28, 2026
ba924e8
Merge pull request #137 from OSIPI/wrapper_dev_testing
IvanARashid Jan 29, 2026
7e8f0cc
Merge pull request #126 from OSIPI/wrapper_dev
IvanARashid Feb 2, 2026
61f59b5
Update IAR_LU_modified_mix.py
IvanARashid Feb 13, 2026
7393704
Filter f extremes and deduplicate generic signals
oliverchampion Feb 13, 2026
9af0ab7
Merge pull request #138 from OSIPI/fix-iar_lu_modified_mix
IvanARashid Feb 16, 2026
f198339
Fixed typo
IvanARashid Feb 16, 2026
db49ed7
Merge pull request #140 from OSIPI/fix-iar_lu_modified_mix
IvanARashid Feb 16, 2026
54b2bf4
Open PR for analysis
etpeterson Feb 16, 2026
1f24f97
Add algorithms to the exclude list
etpeterson Feb 16, 2026
d45d9fe
Merge pull request #139 from OSIPI/wrapper_dev_testing
etpeterson Feb 26, 2026
b8971d7
fix: replace bare except with except Exception in osipi_fit_full_volume
Devguru-codes Feb 27, 2026
04b1ac3
feat: add WLS IVIM fitting algorithm (Feature #110)
Devguru-codes Mar 1, 2026
c1ef262
Merge pull request #145 from Devguru-codes/fix-bare-except-full-volume
oliverchampion Mar 10, 2026
19d5f75
Merge pull request #141 from OSIPI/analysis/update-spreadsheet
etpeterson Mar 12, 2026
1fbd37e
feat: add RLM method toggle to WLS IVIM fitting
Devguru-codes Mar 12, 2026
edd5352
set required_bounds_optional and required_initial_guess_optional to F…
Devguru-codes Mar 13, 2026
a70e516
Add CITATION.cff for software citation information
oliverchampion Apr 28, 2026
198f252
Add GitHub Actions workflow for PyPI publishing
oliverchampion Apr 28, 2026
7aa2511
Update pyproject.toml with authors and dependencies
oliverchampion Apr 28, 2026
2357772
Update build backend for setuptools in pyproject.toml
oliverchampion Apr 28, 2026
79794b6
Merge pull request #148 from Devguru-codes/feature-wls-ivim-fitting
oliverchampion Apr 28, 2026
3a6c9f9
Added fitting and preprocessing folders in /src/original, and adjuste…
IvanARashid May 4, 2026
3800568
Merge branch 'main' into preproc-main-merge-prep
IvanARashid May 4, 2026
2435de7
added nipype
IvanARashid May 4, 2026
58d664e
added itk
IvanARashid May 4, 2026
a6ba403
Update requirements.txt
IvanARashid May 4, 2026
22e1804
Holding the implementation of some stuff, a package doesn't seem to w…
IvanARashid May 4, 2026
4a87c64
moved to fitting folder
IvanARashid May 4, 2026
3bd5287
renamed so that theyre not being run
IvanARashid May 4, 2026
e6f5df4
renamed
IvanARashid May 4, 2026
1b341bf
Ignore tests that are wip
IvanARashid May 4, 2026
d958c20
Update DT_IIITN_WLS.py
IvanARashid May 4, 2026
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
32 changes: 32 additions & 0 deletions .github/workflows/PyPi_installer.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Publishes to TestPyPI when you push a tag starting with "test-v"
on:
push:
tags:
- "test-v*"

# Publishes to real PyPI when you create a proper GitHub release
release:
types: [published]

jobs:
publish:
runs-on: ubuntu-latest
permissions:
id-token: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install build
- run: python -m build

- name: Publish to TestPyPI
if: startsWith(github.ref, 'refs/tags/test-v')
uses: pypa/gh-action-pypi-publish@release/v1
with:
repository-url: https://test.pypi.org/legacy/

- name: Publish to PyPI
if: github.event_name == 'release'
uses: pypa/gh-action-pypi-publish@release/v1
56 changes: 56 additions & 0 deletions CITATION.cff
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
cff-version: 1.2.0
message: "If you use this software, please cite it as below."
title: "OSIPI TF2.4 IVIM-MRI Code Collection"
version: 0.1.0
date-released: 2026
license: Apache-2.0
repository-code: "https://github.com/OSIPI/TF2.4_IVIM-MRI_CodeCollection"
authors:
- family-names: Jalnefjord
given-names: Oscar
orcid: "https://orcid.org/XXXX-XXXX-XXXX-XXXX"
affiliation: "University of Gothenburg"
- family-names: Rashid
given-names: Ivan A.
affiliation: "Lund University"
- family-names: Kuppens
given-names: Daan
affiliation: "Amsterdam University Medical Center"
- family-names: van der Thiel
given-names: Merel M.
affiliation: "Maastricht University Medical Center"
- family-names: van Houdt
given-names: Petra J.
affiliation: "Netherlands Cancer Institute"
- family-names: Voorter
given-names: Paulien H.M.
affiliation: "Maastricht University Medical Center"
- family-names: Peterson
given-names: Eric T.
affiliation: "SRI International"
- family-names: Gurney-Champion
given-names: Oliver J.
affiliation: "Amsterdam University Medical Center"
preferred-citation:
type: article
title: "An open-source code repository for intravoxel incoherent motion analysis: ISMRM Open Science Initiative for Perfusion Imaging (OSIPI)"
authors:
- family-names: Jalnefjord
given-names: Oscar
- family-names: Rashid
given-names: Ivan A.
- family-names: Kuppens
given-names: Daan
- family-names: van der Thiel
given-names: Merel M.
- family-names: van Houdt
given-names: Petra J.
- family-names: Voorter
given-names: Paulien H.M.
- family-names: Peterson
given-names: Eric T.
- family-names: Gurney-Champion
given-names: Oliver J.
journal: "Magnetic Resonance in Medicine"
year: 2026
doi: "10.XXXX/XXXXXXX" # fill in once published
4 changes: 2 additions & 2 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,8 +282,8 @@ def bound_input(datafile, algorithms):
for algorithm in algorithms["algorithms"]:
algorithm_dict = algorithms.get(algorithm, {})
if not algorithm_dict.get('deep_learning',False):
xfail = {"xfail": name in algorithm_dict.get("xfail_names", {}),
"strict": algorithm_dict.get("xfail_names", {}).get(name, True)}
xfail = {"xfail": name in algorithm_dict.get("xfail_names", {}) or "bounds" in algorithm_dict.get("xfail_names", {}),
"strict": algorithm_dict.get("xfail_names", {}).get("bounds", algorithm_dict.get("xfail_names", {}).get(name,True))}
kwargs = algorithm_dict.get("options", {})
tolerances = algorithm_dict.get("tolerances", {})
requires_matlab = algorithm_dict.get("requires_matlab", False)
Expand Down
16 changes: 15 additions & 1 deletion phantoms/MR_XCAT_qMRI/sim_ivim_sig.py
Original file line number Diff line number Diff line change
Expand Up @@ -451,15 +451,29 @@ def parse_bvalues_file(file_path):

voxel_selector_fraction = 0.5
D, f, Ds = contrast_curve_calc()
ignore = np.isnan(D)
ignore = np.logical_or(np.logical_or(np.isnan(D),f<0.03),f>0.97)
generic_data = {}
seen_combinations = set()

for level, name in legend.items():
if len(ignore) > level and ignore[level]:
continue
selector = XCAT == level
voxels = sig[selector]
if len(voxels) < 1:
continue
D_val = np.median(Dim[selector], axis=0)
f_val = np.median(fim[selector], axis=0)
Dp_val = np.median(Dpim[selector], axis=0)

combo = (tuple(np.atleast_1d(D_val)),
tuple(np.atleast_1d(f_val)),
tuple(np.atleast_1d(Dp_val)))

if combo in seen_combinations:
continue

seen_combinations.add(combo)
signals = np.squeeze(voxels[int(voxels.shape[0] * voxel_selector_fraction)]).tolist()
generic_data[name] = {
'noise': noise,
Expand Down
61 changes: 61 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
[build-system]
requires = ["setuptools>=61", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "osipi-ivim"
version = "0.1.0"
description = "OSIPI TF2.4 IVIM-MRI Code Collection"
license = {text = "Apache-2.0"}
readme = "README.md"
requires-python = ">=3.11"
authors = [
{name = "Oscar Jalnefjord", email = "oscar.jalnefjord@gu.se"},
{name = "Ivan A. Rashid"},
{name = "Daan Kuppens"},
{name = "Merel M. van der Thiel"},
{name = "Petra J. van Houdt"},
{name = "Paulien H.M. Voorter"},
{name = "Eric T. Peterson"},
{name = "Oliver J. Gurney-Champion", email = "o.j.gurney-champion@amsterdamumc.nl"},
]
dependencies = [
"numpy",
"scipy",
"nibabel",
"torch",
"torchio",
"joblib",
"dipy",
"cvxpy",
"nlopt",
"tqdm",
"pandas",
"statsmodels",
"ivimnet",
"super-ivim-dc>1.0.0",
"zenodo-get",
]

[project.optional-dependencies]
test = [
"pytest",
"pytest-json-report",
]
docs = [
"sphinx",
"sphinx_rtd_theme",
]
plot = [
"matplotlib",
"scienceplots",
]
all = [
"osipi-ivim[test,docs,plot]",
]

[project.urls]
Repository = "https://github.com/OSIPI/TF2.4_IVIM-MRI_CodeCollection"

[tool.setuptools.packages.find]
where = ["src"]
1 change: 1 addition & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ markers =
slow: marks tests as slow (deselect with '-m "not slow"')
addopts =
-m 'not slow'
--ignore-glob=**/wip_*.py
testpaths = tests
filterwarnings =
ignore::Warning
5 changes: 4 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,7 @@ sphinx_rtd_theme
pytest-json-report
statsmodels
ivimnet
nlopt
nlopt
nipype
itk
#ivim-mri
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
1 change: 1 addition & 0 deletions src/original/fitting/DT_IIITN/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# WLS IVIM fitting by Devguru Tiwari, IIIT Nagpur
170 changes: 170 additions & 0 deletions src/original/fitting/DT_IIITN/wls_ivim_fitting.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
"""
Weighted Least Squares (WLS) / Robust Linear Model (RLM) IVIM fitting.

Author: Devguru Tiwari, IIIT Nagpur
Date: 2026-03-01

Implements a segmented approach for IVIM parameter estimation:
1. Estimate D from high b-values using weighted/robust linear regression on log-signal
2. Estimate f from the intercept of the Step 1 fit
3. Estimate D* from residuals at low b-values using weighted/robust linear regression

Two regression methods are available:
- WLS: Weighted Linear Least Squares with Veraart weights (w = S^2)
- RLM: Robust Linear Model using Huber's T norm (statsmodels)

Reference:
Veraart, J. et al. (2013). "Weighted linear least squares estimation of
diffusion MRI parameters: strengths, limitations, and pitfalls."
NeuroImage, 81, 335-346.
DOI: 10.1016/j.neuroimage.2013.05.028

Requirements:
numpy
statsmodels (only for method="RLM")
"""

import numpy as np
import warnings


def _weighted_linreg(x, y, weights):
"""Fast weighted linear regression: y = a + b*x.

Uses Veraart et al. (2013) approach with weights = S^2.

Args:
x: 1D array, independent variable.
y: 1D array, dependent variable.
weights: 1D array, weights for each observation.

Returns:
(intercept, slope) tuple.
"""
W = np.diag(weights)
X = np.column_stack([np.ones_like(x), x])
# Weighted normal equations: (X^T W X) beta = X^T W y
XtW = X.T @ W
beta = np.linalg.solve(XtW @ X, XtW @ y)
return beta[0], beta[1] # intercept, slope


def _rlm_linreg(x, y):
"""Robust linear regression using statsmodels RLM with Huber's T norm.

RLM down-weights outlier observations via iteratively reweighted least
squares (IRLS), making the fit resistant to corrupted/noisy voxels.

Args:
x: 1D array, independent variable.
y: 1D array, dependent variable.

Returns:
(intercept, slope) tuple.
"""
import statsmodels.api as sm
X = sm.add_constant(x)
model = sm.RLM(y, X, M=sm.robust.norms.HuberT())
result = model.fit()
return result.params[0], result.params[1] # intercept, slope


def wls_ivim_fit(bvalues, signal, cutoff=200, method="WLS"):
"""
IVIM fit using WLS or RLM (segmented approach).

Step 1: Fit D from high b-values on log-signal.
Step 2: Fit D* from residuals at low b-values.

Args:
bvalues (array-like): 1D array of b-values (s/mm²).
signal (array-like): 1D array of signal intensities (will be normalized).
cutoff (float): b-value threshold separating D from D* fitting.
Default: 200 s/mm².
method (str): Regression method to use.
- "WLS": Weighted Least Squares with Veraart S² weights (default).
- "RLM": Robust Linear Model with Huber's T norm (statsmodels).

Returns:
tuple: (D, f, Dp) where
D (float): True diffusion coefficient (mm²/s).
f (float): Perfusion fraction (0-1).
Dp (float): Pseudo-diffusion coefficient (mm²/s).
"""
method = method.upper()
if method not in ("WLS", "RLM"):
raise ValueError(f"Unknown method '{method}'. Use 'WLS' or 'RLM'.")

bvalues = np.array(bvalues, dtype=float)
signal = np.array(signal, dtype=float)

# Normalize signal to S(b=0)
s0_vals = signal[bvalues == 0]
if len(s0_vals) == 0 or np.mean(s0_vals) <= 0:
return 0.0, 0.0, 0.0
s0 = np.mean(s0_vals)
signal = signal / s0

try:
# ── Step 1: Estimate D from high b-values ─────────────────────
# At high b, perfusion component ≈ 0, so:
# S(b) ≈ (1 - f) * exp(-b * D)
# ln(S(b)) = ln(1 - f) - b * D
high_mask = bvalues >= cutoff
b_high = bvalues[high_mask]
s_high = signal[high_mask]

# Guard against zero/negative signal values
s_high = np.maximum(s_high, 1e-8)
log_s = np.log(s_high)

if method == "WLS":
# Veraart weights: w = S^2 (corrects for noise in log-domain)
weights_high = s_high ** 2
intercept, D = _weighted_linreg(-b_high, log_s, weights_high)
else:
# RLM: robust regression, no explicit weights needed
intercept, D = _rlm_linreg(-b_high, log_s)

# Extract f from intercept: intercept = ln(1 - f)
f = 1.0 - np.exp(intercept)

# Clamp to physically meaningful ranges
D = np.clip(D, 0, 0.005)
f = np.clip(f, 0, 1)

# ── Step 2: Estimate D* from low b-value residuals ────────────
# Subtract the diffusion component:
# residual(b) = S(b) - (1 - f) * exp(-b * D)
# ≈ f * exp(-b * D*)
# ln(residual) = ln(f) - b * D*
residual = signal - (1 - f) * np.exp(-bvalues * D)

low_mask = (bvalues < cutoff) & (bvalues > 0)
b_low = bvalues[low_mask]
r_low = residual[low_mask]

# Guard against zero/negative residuals
r_low = np.maximum(r_low, 1e-8)
log_r = np.log(r_low)

if len(b_low) >= 2:
if method == "WLS":
weights_low = r_low ** 2
_, Dp = _weighted_linreg(-b_low, log_r, weights_low)
else:
_, Dp = _rlm_linreg(-b_low, log_r)
Dp = np.clip(Dp, 0.005, 0.2)
else:
Dp = 0.01 # fallback

# Ensure D* > D (by convention)
if Dp < D:
D, Dp = Dp, D
f = 1 - f

return D, f, Dp

except Exception:
# If fit fails, return zeros (consistent with other algorithms)
return 0.0, 0.0, 0.0
Empty file.
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,9 @@ def __init__(self, gtab, bounds=None, maxiter=10, xtol=1e-8, rescale_units=False
(bounds[0][1]*1000, bounds[1][1]*1000), \
(bounds[0][2]*1000, bounds[1][2]*1000)])
else: # Finally, if units if µm2/ms are already used
self.bounds = np.array([(bounds[0][0], bounds[1][0], \
self.bounds = np.array([(bounds[0][0], bounds[1][0]), \
(bounds[0][1], bounds[1][1]), \
(bounds[0][2], bounds[1][2]))])
(bounds[0][2], bounds[1][2])])

@multi_voxel_fit
def fit(self, data, bounds_de=None):
Expand Down
Loading
Loading