Skip to content

Commit 1fbd37e

Browse files
committed
feat: add RLM method toggle to WLS IVIM fitting
1 parent 04b1ac3 commit 1fbd37e

2 files changed

Lines changed: 67 additions & 29 deletions

File tree

src/original/DT_IIITN/wls_ivim_fitting.py

Lines changed: 52 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
"""
2-
Weighted Least Squares (WLS) IVIM fitting.
2+
Weighted Least Squares (WLS) / Robust Linear Model (RLM) IVIM fitting.
33
44
Author: Devguru Tiwari, IIIT Nagpur
55
Date: 2026-03-01
66
77
Implements a segmented approach for IVIM parameter estimation:
8-
1. Estimate D from high b-values using weighted linear regression on log-signal
8+
1. Estimate D from high b-values using weighted/robust linear regression on log-signal
99
2. Estimate f from the intercept of the Step 1 fit
10-
3. Estimate D* from residuals at low b-values using weighted linear regression
10+
3. Estimate D* from residuals at low b-values using weighted/robust linear regression
1111
12-
Weighting follows Veraart et al. (2013): weights = signal^2 to account
13-
for heteroscedasticity introduced by the log-transform.
12+
Two regression methods are available:
13+
- WLS: Weighted Linear Least Squares with Veraart weights (w = S^2)
14+
- RLM: Robust Linear Model using Huber's T norm (statsmodels)
1415
1516
Reference:
1617
Veraart, J. et al. (2013). "Weighted linear least squares estimation of
@@ -20,6 +21,7 @@
2021
2122
Requirements:
2223
numpy
24+
statsmodels (only for method="RLM")
2325
"""
2426

2527
import numpy as np
@@ -29,6 +31,8 @@
2931
def _weighted_linreg(x, y, weights):
3032
"""Fast weighted linear regression: y = a + b*x.
3133
34+
Uses Veraart et al. (2013) approach with weights = S^2.
35+
3236
Args:
3337
x: 1D array, independent variable.
3438
y: 1D array, dependent variable.
@@ -45,26 +49,52 @@ def _weighted_linreg(x, y, weights):
4549
return beta[0], beta[1] # intercept, slope
4650

4751

48-
def wls_ivim_fit(bvalues, signal, cutoff=200):
52+
def _rlm_linreg(x, y):
53+
"""Robust linear regression using statsmodels RLM with Huber's T norm.
54+
55+
RLM down-weights outlier observations via iteratively reweighted least
56+
squares (IRLS), making the fit resistant to corrupted/noisy voxels.
57+
58+
Args:
59+
x: 1D array, independent variable.
60+
y: 1D array, dependent variable.
61+
62+
Returns:
63+
(intercept, slope) tuple.
64+
"""
65+
import statsmodels.api as sm
66+
X = sm.add_constant(x)
67+
model = sm.RLM(y, X, M=sm.robust.norms.HuberT())
68+
result = model.fit()
69+
return result.params[0], result.params[1] # intercept, slope
70+
71+
72+
def wls_ivim_fit(bvalues, signal, cutoff=200, method="WLS"):
4973
"""
50-
Weighted Least Squares IVIM fit (segmented approach).
74+
IVIM fit using WLS or RLM (segmented approach).
5175
52-
Step 1: Fit D from high b-values using WLS on log-signal.
53-
Weights = S(b)^2 (Veraart et al. 2013).
54-
Step 2: Fit D* from residuals at low b-values using WLS.
76+
Step 1: Fit D from high b-values on log-signal.
77+
Step 2: Fit D* from residuals at low b-values.
5578
5679
Args:
5780
bvalues (array-like): 1D array of b-values (s/mm²).
5881
signal (array-like): 1D array of signal intensities (will be normalized).
5982
cutoff (float): b-value threshold separating D from D* fitting.
6083
Default: 200 s/mm².
84+
method (str): Regression method to use.
85+
- "WLS": Weighted Least Squares with Veraart S² weights (default).
86+
- "RLM": Robust Linear Model with Huber's T norm (statsmodels).
6187
6288
Returns:
6389
tuple: (D, f, Dp) where
6490
D (float): True diffusion coefficient (mm²/s).
6591
f (float): Perfusion fraction (0-1).
6692
Dp (float): Pseudo-diffusion coefficient (mm²/s).
6793
"""
94+
method = method.upper()
95+
if method not in ("WLS", "RLM"):
96+
raise ValueError(f"Unknown method '{method}'. Use 'WLS' or 'RLM'.")
97+
6898
bvalues = np.array(bvalues, dtype=float)
6999
signal = np.array(signal, dtype=float)
70100

@@ -80,8 +110,6 @@ def wls_ivim_fit(bvalues, signal, cutoff=200):
80110
# At high b, perfusion component ≈ 0, so:
81111
# S(b) ≈ (1 - f) * exp(-b * D)
82112
# ln(S(b)) = ln(1 - f) - b * D
83-
# Weighted linear fit: weights = S(b)^2 (Veraart correction)
84-
85113
high_mask = bvalues >= cutoff
86114
b_high = bvalues[high_mask]
87115
s_high = signal[high_mask]
@@ -90,11 +118,13 @@ def wls_ivim_fit(bvalues, signal, cutoff=200):
90118
s_high = np.maximum(s_high, 1e-8)
91119
log_s = np.log(s_high)
92120

93-
# Veraart weights: w = S^2 (corrects for noise amplification in log-domain)
94-
weights_high = s_high ** 2
95-
96-
# WLS: ln(S) = intercept + slope * (-b) ⟹ slope = D
97-
intercept, D = _weighted_linreg(-b_high, log_s, weights_high)
121+
if method == "WLS":
122+
# Veraart weights: w = S^2 (corrects for noise in log-domain)
123+
weights_high = s_high ** 2
124+
intercept, D = _weighted_linreg(-b_high, log_s, weights_high)
125+
else:
126+
# RLM: robust regression, no explicit weights needed
127+
intercept, D = _rlm_linreg(-b_high, log_s)
98128

99129
# Extract f from intercept: intercept = ln(1 - f)
100130
f = 1.0 - np.exp(intercept)
@@ -108,7 +138,6 @@ def wls_ivim_fit(bvalues, signal, cutoff=200):
108138
# residual(b) = S(b) - (1 - f) * exp(-b * D)
109139
# ≈ f * exp(-b * D*)
110140
# ln(residual) = ln(f) - b * D*
111-
112141
residual = signal - (1 - f) * np.exp(-bvalues * D)
113142

114143
low_mask = (bvalues < cutoff) & (bvalues > 0)
@@ -119,10 +148,12 @@ def wls_ivim_fit(bvalues, signal, cutoff=200):
119148
r_low = np.maximum(r_low, 1e-8)
120149
log_r = np.log(r_low)
121150

122-
weights_low = r_low ** 2
123-
124151
if len(b_low) >= 2:
125-
_, Dp = _weighted_linreg(-b_low, log_r, weights_low)
152+
if method == "WLS":
153+
weights_low = r_low ** 2
154+
_, Dp = _weighted_linreg(-b_low, log_r, weights_low)
155+
else:
156+
_, Dp = _rlm_linreg(-b_low, log_r)
126157
Dp = np.clip(Dp, 0.005, 0.2)
127158
else:
128159
Dp = 0.01 # fallback

src/standardized/DT_IIITN_WLS.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,15 @@
55

66
class DT_IIITN_WLS(OsipiBase):
77
"""
8-
Weighted Least Squares IVIM fitting using statsmodels Robust Linear Model.
8+
Segmented IVIM fitting with selectable regression method.
9+
10+
Two methods are available:
11+
- WLS: Weighted Least Squares with Veraart S² weights (default)
12+
- RLM: Robust Linear Model with Huber's T norm (statsmodels)
913
1014
Segmented approach:
11-
1. Estimate D from high b-values using robust linear regression on log-signal
12-
2. Estimate D* from residuals at low b-values using robust linear regression
15+
1. Estimate D from high b-values using linear regression on log-signal
16+
2. Estimate D* from residuals at low b-values using linear regression
1317
1418
Author: Devguru Tiwari, IIIT Nagpur
1519
@@ -22,7 +26,7 @@ class DT_IIITN_WLS(OsipiBase):
2226

2327
# Algorithm identification
2428
id_author = "Devguru Tiwari, IIIT Nagpur"
25-
id_algorithm_type = "Weighted least squares segmented fit"
29+
id_algorithm_type = "Weighted least squares / robust linear model segmented fit"
2630
id_return_parameters = "f, D*, D"
2731
id_units = "seconds per milli metre squared or milliseconds per micro metre squared"
2832
id_ref = "https://doi.org/10.1016/j.neuroimage.2013.05.028"
@@ -43,9 +47,9 @@ class DT_IIITN_WLS(OsipiBase):
4347
supported_priors = False
4448

4549
def __init__(self, bvalues=None, thresholds=None,
46-
bounds=None, initial_guess=None):
50+
bounds=None, initial_guess=None, method="WLS"):
4751
"""
48-
Initialize the WLS IVIM fitting algorithm.
52+
Initialize the IVIM fitting algorithm.
4953
5054
Args:
5155
bvalues (array-like, optional): b-values for the fitted signals.
@@ -54,14 +58,16 @@ def __init__(self, bvalues=None, thresholds=None,
5458
and low b-values. Default: 200 s/mm².
5559
bounds (dict, optional): Not used by this algorithm.
5660
initial_guess (dict, optional): Not used by this algorithm.
61+
method (str): Regression method — "WLS" (default) or "RLM".
5762
"""
5863
super(DT_IIITN_WLS, self).__init__(
5964
bvalues=bvalues, bounds=bounds,
6065
initial_guess=initial_guess, thresholds=thresholds
6166
)
67+
self.method = method.upper()
6268

6369
def ivim_fit(self, signals, bvalues, **kwargs):
64-
"""Perform the IVIM fit using WLS.
70+
"""Perform the IVIM fit using the selected method (WLS or RLM).
6571
6672
Args:
6773
signals (array-like): Signal intensities at each b-value.
@@ -77,7 +83,8 @@ def ivim_fit(self, signals, bvalues, **kwargs):
7783
if self.thresholds is not None and len(self.thresholds) > 0:
7884
cutoff = self.thresholds[0]
7985

80-
D, f, Dp = wls_ivim_fit(bvalues, signals, cutoff=cutoff)
86+
D, f, Dp = wls_ivim_fit(bvalues, signals, cutoff=cutoff,
87+
method=self.method)
8188

8289
results = {}
8390
results["D"] = D

0 commit comments

Comments
 (0)