11"""
2- Weighted Least Squares (WLS) IVIM fitting.
2+ Weighted Least Squares (WLS) / Robust Linear Model (RLM) IVIM fitting.
33
44Author: Devguru Tiwari, IIIT Nagpur
55Date: 2026-03-01
66
77Implements 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
992. 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
1516Reference:
1617 Veraart, J. et al. (2013). "Weighted linear least squares estimation of
2021
2122Requirements:
2223 numpy
24+ statsmodels (only for method="RLM")
2325"""
2426
2527import numpy as np
2931def _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
0 commit comments