Skip to content

Commit 11445d7

Browse files
Merge pull request #7 from OSIPI/main
add abdominal phantom
2 parents 55fb980 + 0313b0b commit 11445d7

File tree

9 files changed

+290
-11
lines changed

9 files changed

+290
-11
lines changed

.github/workflows/unit_test.yml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@ jobs:
66
build:
77

88
runs-on: ${{ matrix.os }}
9-
9+
continue-on-error: false
1010
strategy:
11+
fail-fast: false
1112
matrix:
1213
os: [ubuntu-latest, macos-latest, windows-latest]
13-
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
14+
python-version: ["3.8", "3.9", "3.10", "3.11"]
1415
# exclude:
1516
# - os: macos-latest
1617
# python-version: "3.7"
@@ -32,4 +33,4 @@ jobs:
3233
- name: Test with pytest
3334
run: |
3435
pip install pytest pytest-cov
35-
pytest tests.py --doctest-modules --junitxml=junit/test-results.xml --cov=com --cov-report=xml --cov-report=html
36+
python -m pytest --doctest-modules --junitxml=junit/test-results.xml --cov=com --cov-report=xml --cov-report=html

requirements.txt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,9 @@ numpy
22
scipy
33
torchio
44
torch
5-
logging
5+
logging
6+
joblib
7+
dipy
8+
matplotlib
9+
scienceplots
10+
cvxpy

src/original/ETP_SRI/LinearFitting.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import numpy as np
2+
import numpy.polynomial.polynomial as poly
3+
4+
from utils.data_simulation.GenerateData import GenerateData
5+
6+
7+
8+
class LinearFit:
9+
"""
10+
Performs linear fits of exponential data
11+
"""
12+
def __init__(self, linear_cutoff=500):
13+
"""
14+
Parameters
15+
----------
16+
linear_cutoff : float
17+
The b-value after which it can be assumed that the perfusion value is negligible
18+
"""
19+
self.linear_cutoff = linear_cutoff
20+
21+
def linear_fit(self, bvalues, signal, weighting=None, stats=False):
22+
"""
23+
Fit a single line
24+
25+
Parameters
26+
----------
27+
bvalues : list or array of float
28+
The diffusion (b-values)
29+
signal : list or array of float
30+
The acquired signal to fit. It is assumed to be linearized at this point.
31+
weighting : list or array fo float
32+
Weights to pass into polyfit. None is no weighting.
33+
stats : boolean
34+
If true, return the polyfit statistics
35+
"""
36+
assert bvalues.size == signal.size, "Signal and b-values don't have the same number of values"
37+
if stats:
38+
D, stats = poly.polyfit(np.asarray(bvalues), signal, 1, full=True, w=weighting)
39+
return [np.exp(D[0]), *-D[1:]], stats
40+
else:
41+
D = poly.polyfit(np.asarray(bvalues), signal, 1, w=weighting)
42+
return [np.exp(D[0]), *-D[1:]]
43+
44+
def ivim_fit(self, bvalues, signal):
45+
"""
46+
Fit an IVIM curve
47+
This fits a bi-exponential curve using linear fitting only
48+
49+
50+
Parameters
51+
----------
52+
bvalues : list or array of float
53+
The diffusion (b-values)
54+
signal : list or array of float
55+
The acquired signal to fit. It is assumed to be exponential at this point
56+
"""
57+
bvalues = np.asarray(bvalues)
58+
assert bvalues.size > 1, 'Too few b-values'
59+
signal = np.asarray(signal)
60+
assert bvalues.size == signal.size, "Signal and b-values don't have the same number of values"
61+
gd = GenerateData()
62+
lt_cutoff = bvalues <= self.linear_cutoff
63+
gt_cutoff = bvalues >= self.linear_cutoff
64+
linear_signal = np.log(signal)
65+
D = self.linear_fit(bvalues[gt_cutoff], linear_signal[gt_cutoff])
66+
67+
if lt_cutoff.sum() > 0:
68+
signal_Dp = linear_signal[lt_cutoff] - gd.linear_signal(D[1], bvalues[lt_cutoff], np.log(D[0]))
69+
70+
Dp_prime = self.linear_fit(bvalues[lt_cutoff], np.log(signal_Dp))
71+
72+
if np.any(np.asarray(Dp_prime) < 0) or not np.all(np.isfinite(Dp_prime)):
73+
print('Perfusion fit failed')
74+
Dp_prime = [0, 0]
75+
f = signal[0] - D[0]
76+
else:
77+
print("This doesn't seem to be an IVIM set of b-values")
78+
f = 1
79+
Dp_prime = [0, 0]
80+
D = D[1]
81+
Dp = D + Dp_prime[1]
82+
if np.allclose(f, 0):
83+
Dp = 0
84+
elif np.allclose(f, 1):
85+
D = 0
86+
return [f, D, Dp]

src/original/ETP_SRI/__init__.py

Whitespace-only changes.

src/original/OGC_AmsterdamUMC/LSQ_fitting.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -494,7 +494,7 @@ def empirical_neg_log_prior(Dt0, Fp0, Dp0, S00=None):
494494
# define the prior
495495
def neg_log_prior(p):
496496
# depends on whether S0 is fitted or not
497-
if len(p) is 4:
497+
if len(p) == 4:
498498
Dt, Fp, Dp, S0 = p[0], p[1], p[2], p[3]
499499
else:
500500
Dt, Fp, Dp = p[0], p[1], p[2]
@@ -507,7 +507,7 @@ def neg_log_prior(p):
507507
Dt_prior = stats.lognorm.pdf(Dt, Dt_shape, scale=Dt_scale)
508508
Fp_prior = stats.beta.pdf(Fp, Fp_a, Fp_b)
509509
# determine and return the prior for D, f and D* (and S0)
510-
if len(p) is 4:
510+
if len(p) == 4:
511511
S0_prior = stats.beta.pdf(S0 / 2, S0_a, S0_b)
512512
return -np.log(Dp_prior + eps) - np.log(Dt_prior + eps) - np.log(Fp_prior + eps) - np.log(
513513
S0_prior + eps)
@@ -525,7 +525,7 @@ def neg_log_likelihood(p, bvalues, dw_data):
525525
:param dw_data: 1D Array diffusion-weighted data
526526
:returns: the log-likelihood of the parameters given the data
527527
"""
528-
if len(p) is 4:
528+
if len(p) == 4:
529529
return 0.5 * (len(bvalues) + 1) * np.log(
530530
np.sum((ivim(bvalues, p[0], p[1], p[2], p[3]) - dw_data) ** 2)) # 0.5*sum simplified
531531
else:
@@ -705,7 +705,7 @@ def goodness_of_fit(bvalues, Dt, Fp, Dp, S0, dw_data, Fp2=None, Dp2=None):
705705
# plt.show()
706706
# print(R2[vox])
707707
return R2, adjust
708-
ed_R2
708+
# ed_R2
709709

710710
def MSE(bvalues, Dt, Fp, Dp, S0, dw_data):
711711
"""

src/wrappers/ivim_fit.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ def ivim_fit(author, signals=None, bvalues=None, data=None, initial_guess=None,
2424

2525

2626
# Some implementations can only fit a voxel at a time (i.e. all inputs must be 2-dimensional)
27-
requires_2D = True if author in []
28-
requires_4D = True if author in []
27+
requires_2D = True if author in [] else False
28+
requires_4D = True if author in [] else False
2929

3030
# Bounds and initial guess for parameters
3131
initial_guess = []
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import numpy as np
2+
import numpy.testing as npt
3+
import pytest
4+
import torch
5+
6+
from utils.data_simulation.GenerateData import GenerateData
7+
from src.original.ETP_SRI.LinearFitting import LinearFit
8+
9+
10+
#run using python -m pytest from the root folder
11+
12+
test_linear_data = [
13+
pytest.param(0, np.linspace(0, 1000, 11), id='0'),
14+
pytest.param(0.01, np.linspace(0, 1000, 11), id='0.1'),
15+
pytest.param(0.02, np.linspace(0, 1000, 11), id='0.2'),
16+
pytest.param(0.03, np.linspace(0, 1000, 11), id='0.3'),
17+
pytest.param(0.04, np.linspace(0, 1000, 11), id='0.4'),
18+
pytest.param(0.05, np.linspace(0, 1000, 11), id='0.5'),
19+
pytest.param(0.08, np.linspace(0, 1000, 11), id='0.8'),
20+
pytest.param(0.1, np.linspace(0, 1000, 11), id='1'),
21+
]
22+
@pytest.mark.parametrize("D, bvals", test_linear_data)
23+
def test_linear_fit(D, bvals):
24+
gd = GenerateData()
25+
gd_signal = gd.exponential_signal(D, bvals)
26+
print(gd_signal)
27+
fit = LinearFit()
28+
D_fit = fit.linear_fit(bvals, np.log(gd_signal))
29+
npt.assert_allclose([1, D], D_fit)
30+
31+
test_ivim_data = [
32+
pytest.param(0, 0.01, 0.05, np.linspace(0, 1000, 11), id='0'),
33+
pytest.param(0.1, 0.01, 0.05, np.linspace(0, 1000, 11), id='0.1'),
34+
pytest.param(0.2, 0.01, 0.05, np.linspace(0, 1000, 11), id='0.2'),
35+
pytest.param(0.1, 0.05, 0.1, np.linspace(0, 1000, 11), id='0.3'),
36+
pytest.param(0.4, 0.001, 0.05, np.linspace(0, 1000, 11), id='0.4'),
37+
pytest.param(0.5, 0.001, 0.05, np.linspace(0, 1000, 11), id='0.5'),
38+
]
39+
@pytest.mark.parametrize("f, D, Dp, bvals", test_ivim_data)
40+
def test_ivim_fit(f, D, Dp, bvals):
41+
gd = GenerateData()
42+
gd_signal = gd.ivim_signal(D, Dp, f, 1, bvals)
43+
fit = LinearFit()
44+
[f_fit, D_fit, Dp_fit] = fit.ivim_fit(bvals, gd_signal)
45+
npt.assert_allclose([f, D], [f_fit, D_fit], atol=1e-5)
46+
if not np.allclose(f, 0):
47+
npt.assert_allclose(Dp, Dp_fit, rtol=1e-2, atol=1e-3)

utils/data_simulation/GenerateData.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import numpy as np
2+
3+
class GenerateData:
4+
"""
5+
Generate exponential and linear data
6+
"""
7+
def __init__(self, operator=None):
8+
"""
9+
Parameters
10+
----------
11+
operator : numpy or torch
12+
Must provide mathematical operators
13+
"""
14+
if operator is None:
15+
self._op = np
16+
else:
17+
self._op = operator
18+
19+
def ivim_signal(self, D, Dp, f, S0, bvalues, snr=None):
20+
"""
21+
Generates IVIM (biexponential) signal
22+
23+
Parameters
24+
----------
25+
D : float
26+
The tissue diffusion value
27+
Dp : float
28+
The pseudo perfusion value
29+
f : float
30+
The fraction of the signal from perfusion
31+
S0 : float
32+
The baseline signal (magnitude at no diffusion)
33+
bvalues : list or array of float
34+
The diffusion (b-values)
35+
"""
36+
signal = self.multiexponential_signal([D, Dp], [1 - f, f], S0, self._op.asarray(bvalues, dtype='float64'))
37+
return self.add_rician_noise(signal, snr)
38+
39+
def exponential_signal(self, D, bvalues):
40+
"""
41+
Generates exponential signal
42+
43+
Parameters
44+
----------
45+
D : float
46+
The tissue diffusion value
47+
bvalues : list or array of float
48+
The diffusion (b-values)
49+
"""
50+
assert D >= 0, 'D must be >= 0'
51+
return self._op.exp(-self._op.asarray(bvalues, dtype='float64') * D)
52+
53+
def multiexponential_signal(self, D, F, S0, bvalues):
54+
"""
55+
Generates multiexponential signal
56+
The combination of exponential signals
57+
58+
Parameters
59+
----------
60+
D : list or arrray of float
61+
The tissue diffusion value
62+
F : list or array of float
63+
The fraction of the signal from perfusion
64+
S0 : list or array of float
65+
The baseline signal (magnitude at no diffusion)
66+
bvalues : list or array of float
67+
The diffusion (b-values)
68+
"""
69+
assert len(D) == len(F), 'D and F must be the same length'
70+
signal = self._op.zeros_like(bvalues)
71+
for [d, f] in zip(D, F):
72+
signal += f * self.exponential_signal(d, bvalues)
73+
signal *= S0
74+
return signal
75+
76+
def add_rician_noise(self, real_signal, snr=None, imag_signal=None):
77+
"""
78+
Adds Rician noise to a real or complex signal
79+
80+
Parameters
81+
----------
82+
real_signal : list or array of float
83+
The real channel float
84+
snr : float
85+
The signal to noise ratio
86+
imag_signal : list or array of float
87+
The imaginary channel float
88+
"""
89+
if imag_signal is None:
90+
imag_signal = self._op.zeros_like(real_signal)
91+
if snr is None:
92+
noisy_data = self._op.sqrt(self._op.power(real_signal, 2) + self._op.power(imag_signal, 2))
93+
else:
94+
real_noise = self._op.random.normal(0, 1 / snr, real_signal.shape)
95+
imag_noise = self._op.random.normal(0, 1 / snr, imag_signal.shape)
96+
noisy_data = self._op.sqrt(self._op.power(real_signal + real_noise, 2) + self._op.power(imag_signal + imag_noise, 2))
97+
return noisy_data
98+
99+
def linear_signal(self, D, bvalues, offset=0):
100+
"""
101+
Generates linear signal
102+
103+
Parameters
104+
----------
105+
D : float
106+
The tissue diffusion value
107+
bvalues : list or array of float
108+
The diffusion (b-values)
109+
offset : float
110+
The signal offset
111+
"""
112+
assert D >= 0, 'D must be >= 0'
113+
data = -D * np.asarray(bvalues)
114+
return data + offset
115+
116+
def multilinear_signal(self, D, F, S0, bvalues, offset=0):
117+
"""
118+
Generates multilinear signal
119+
The combination of multiple linear signals
120+
121+
Parameters
122+
----------
123+
D : list or arrray of float
124+
The tissue diffusion value
125+
F : list or array of float
126+
The fraction of the signal from perfusion
127+
S0 : list or array of float
128+
The baseline signal (magnitude at no diffusion)
129+
bvalues : list or array of float
130+
The diffusion (b-values)
131+
offset : float
132+
The signal offset
133+
"""
134+
assert len(D) == len(F), 'D and F must be the same length'
135+
signal = self._op.zeros_like(bvalues)
136+
for [d, f] in zip(D, F):
137+
signal += f * self.linear_signal(d, bvalues)
138+
signal *= S0
139+
signal += offset
140+
return signal

utils/data_simulation/ivim_simulation.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import torch
22
import numpy as np
33

4-
from utils.ivim.ivim_fit import ivim_parameters_to_signal
4+
from utils.ivim.forward_model import ivim_parameters_to_signal
55

66

77
def simulate_ivim_signal(D, Dp, f, S0, bvalues, SNR_array, rg):

0 commit comments

Comments
 (0)