Skip to content

Commit

Permalink
Merge pull request UKRIN-MAPS#231 from UKRIN-MAPS/rel/v0.7.3
Browse files Browse the repository at this point in the history
rel/v0.7.3
  • Loading branch information
alexdaniel654 authored Oct 30, 2024
2 parents b06f185 + 9a7a716 commit a6e6645
Show file tree
Hide file tree
Showing 10 changed files with 81 additions and 40 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/python_CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ jobs:
run: |
pytest --cov-report=xml --cov=ukat
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v4
with:
env_vars: OS,PYTHON
fail_ci_if_error: true
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
## [0.7.3] - 2024-10-30

### Changed
* Signals to input to mapping classes are now normalised before fitting, then M0 rescaled back to the original scale. This
means M0 bounds and initialisation are more consistent/appropriate for data from different vendors #226
* The limits of efficiency have been widened for MOLLI T1 mapping #229
* Upgrade codecov action to v4

## [0.7.2] - 2024-07-05

### Added
Expand Down
7 changes: 5 additions & 2 deletions CITATION.cff
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,14 @@ authors:
- family-names: "Buchanan"
given-names: "Charlotte E"
orcid: "https://orcid.org/0000-0003-3010-6079"
- family-names: "Sourbron"
given-names: "Steven"
orcid: "https://orcid.org/0000-0002-3374-3973"
- family-names: "Francis"
given-names: "Susan T"
orcid: "https://orcid.org/0000-0003-0903-7507"
title: "UKRIN Kidney Analysis Toolbox"
version: 0.7.2
version: 0.7.3
doi: 10.5281/zenodo.4742470
date-released: 2024-07-05
date-released: 2024-10-30
url: "https://github.com/UKRIN-MAPS/ukat"
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

setup(
name="ukat",
version="0.7.2",
version="0.7.3",
description="UKRIN Kidney Analysis Toolbox",
long_description=long_description,
long_description_content_type="text/markdown",
Expand Down
29 changes: 21 additions & 8 deletions ukat/mapping/t1.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

class T1Model(fitting.Model):
def __init__(self, pixel_array, ti, parameters=2, mask=None, tss=0,
tss_axis=-2, multithread=True):
tss_axis=-2, molli=False, multithread=True):
"""
A class containing the T1 fitting model
Expand Down Expand Up @@ -51,6 +51,7 @@ def __init__(self, pixel_array, ti, parameters=2, mask=None, tss=0,
self.parameters = parameters
self.tss = tss
self.tss_axis = tss_axis
self.molli = molli

# Assume the data has been magnitude corrected if the first
# percentile of the first inversion time is negative.
Expand Down Expand Up @@ -92,8 +93,8 @@ def __init__(self, pixel_array, ti, parameters=2, mask=None, tss=0,
self.t1_eq = two_param_abs_eq
super().__init__(pixel_array, ti, self.t1_eq, mask,
multithread)
self.bounds = ([0, 0], [5000, 1000000000])
self.initial_guess = [1000, 30000]
self.bounds = ([0, 0], [5000, 100])
self.initial_guess = [1000, 1]
elif self.parameters == 3:
if self.mag_corr:
self.t1_eq = three_param_eq
Expand All @@ -103,8 +104,12 @@ def __init__(self, pixel_array, ti, parameters=2, mask=None, tss=0,
self.t1_eq = three_param_abs_eq
super().__init__(pixel_array, ti, self.t1_eq, mask,
multithread)
self.bounds = ([0, 0, 1], [5000, 1000000000, 2])
self.initial_guess = [1000, 30000, 2]
if self.molli:
self.bounds = ([0, 0, 0], [5000, 100, 3])
self.initial_guess = [1000, 1, 2]
else:
self.bounds = ([0, 0, 1], [5000, 100, 2])
self.initial_guess = [1000, 1, 2]
else:
raise ValueError(f'Parameters can be 2 or 3 only. You specified '
f'{parameters}.')
Expand Down Expand Up @@ -207,8 +212,10 @@ def __init__(self, pixel_array, inversion_list, affine, tss=0, tss_axis=-2,
or multithread == 'auto', f'multithreaded must be True,' \
f'False or auto. You entered ' \
f'{multithread}'
# Normalise the data so its roughly in the same range across vendors
self.scale = np.nanmax(pixel_array)
self.pixel_array = pixel_array / self.scale

self.pixel_array = pixel_array
self.shape = pixel_array.shape[:-1]
self.dimensions = len(pixel_array.shape)
self.n_ti = pixel_array.shape[-1]
Expand Down Expand Up @@ -256,7 +263,8 @@ def __init__(self, pixel_array, inversion_list, affine, tss=0, tss_axis=-2,
# Fit Data
self.fitting_model = T1Model(self.pixel_array, self.inversion_list,
self.parameters, self.mask, self.tss,
self.tss_axis, self.multithread)
self.tss_axis, self.molli,
self.multithread)
popt, error, r2 = fitting.fit_image(self.fitting_model)
self.t1_map = popt[0]
self.m0_map = popt[1]
Expand Down Expand Up @@ -287,11 +295,16 @@ def __init__(self, pixel_array, inversion_list, affine, tss=0, tss_axis=-2,

# Do MOLLI correction
if self.molli:
correction_factor = (self.m0_map * self.eff_map) / self.m0_map - 1
correction_factor = (((self.m0_map * self.eff_map) / self.m0_map)
- 1)
percentage_error = self.t1_err / self.t1_map
self.t1_map = np.nan_to_num(self.t1_map * correction_factor)
self.t1_err = np.nan_to_num(self.t1_map * percentage_error)

# Scale the data back to the original scale
self.m0_map *= self.scale
self.m0_err *= self.scale

def r1_map(self):
"""
Generates the R1 map from the T1 map output by initialising this
Expand Down
23 changes: 17 additions & 6 deletions ukat/mapping/t2.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,14 @@ def __init__(self, pixel_array, te, method='2p_exp', mask=None,
if self.method == '2p_exp':
self.t2_eq = two_param_eq
super().__init__(pixel_array, te, self.t2_eq, mask, multithread)
self.bounds = ([0, 0], [1000, 100000000])
self.initial_guess = [20, 10000]
self.bounds = ([0, 0], [1000, 100])
self.initial_guess = [20, 1]
elif self.method == '3p_exp':
self.t2_eq = three_param_eq
super().__init__(pixel_array, te, self.t2_eq, mask,
multithread)
self.bounds = ([0, 0, 0], [1000, 100000000, 1000000])
self.initial_guess = [20, 10000, 500]
self.bounds = ([0, 0, 0], [1000, 100, 1])
self.initial_guess = [20, 1, 5e-4]

self.generate_lists()

Expand Down Expand Up @@ -153,7 +153,10 @@ def __init__(self, pixel_array, echo_list, affine, mask=None,
raise ValueError(f'method can be 2p_exp or 3p_exp only. You '
f'specified {method}')

self.pixel_array = pixel_array
# Normalise the data so its roughly in the same range across vendors
self.scale = np.nanmax(pixel_array)
self.pixel_array = pixel_array / self.scale

self.shape = pixel_array.shape[:-1]
self.n_te = pixel_array.shape[-1]
self.n_vox = np.prod(self.shape)
Expand All @@ -166,7 +169,7 @@ def __init__(self, pixel_array, echo_list, affine, mask=None,

# Don't process any nan values
self.mask[np.isnan(np.sum(pixel_array, axis=-1))] = False
self.noise_threshold = noise_threshold
self.noise_threshold = noise_threshold / self.scale
self.method = method
self.echo_list = echo_list
# Auto multithreading conditions
Expand Down Expand Up @@ -210,6 +213,14 @@ def __init__(self, pixel_array, echo_list, affine, mask=None,
self.b_map[bounds_mask] = 0
self.b_err[bounds_mask] = 0

self.b_map *= self.scale
self.b_err *= self.scale

# Scale the data back to the original scale
self.m0_map *= self.scale
self.m0_err *= self.scale
self.noise_threshold *= self.scale

def to_nifti(self, output_directory=os.getcwd(), base_file_name='Output',
maps='all'):
"""Exports some of the T2 class attributes to NIFTI.
Expand Down
14 changes: 11 additions & 3 deletions ukat/mapping/t2star.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ def __init__(self, pixel_array, te, mask=None, multithread=True):
"""

super().__init__(pixel_array, te, two_param_eq, mask, multithread)
self.bounds = ([0, 0], [700, 100000000])
self.initial_guess = [20, 10000]
self.bounds = ([0, 0], [700, 100])
self.initial_guess = [20, 1]
self.generate_lists()


Expand Down Expand Up @@ -117,7 +117,11 @@ def __init__(self, pixel_array, echo_list, affine, mask=None,
or multithread is False \
or multithread == 'auto', f'multithreaded must be True, False ' \
f'or auto. You entered {multithread}'
self.pixel_array = pixel_array

# Normalise the data so its roughly in the same range across vendors
self.scale = np.nanmax(pixel_array)
self.pixel_array = pixel_array / self.scale

self.shape = pixel_array.shape[:-1]
self.n_te = pixel_array.shape[-1]
self.n_vox = np.prod(self.shape)
Expand Down Expand Up @@ -185,6 +189,10 @@ def __init__(self, pixel_array, echo_list, affine, mask=None,
'interest, consider using the 2p_exp fitting'
' method'.format(proportion_less_than_20))

# Scale the data back to the original range
self.m0_map *= self.scale
self.m0_err *= self.scale

def _loglin_fit(self):
if self.multithread:
with ProcessPool() as executor:
Expand Down
27 changes: 14 additions & 13 deletions ukat/mapping/tests/test_t1.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ class TestT1:
595.68345494, 1014.80958915,
1394.05059827]]
])
# Signal with all 9 elements equal to -5000 between 200 and 1000 ms
signal_fail_fit = -5000 * np.ones(9)
# Make some silly data that the code won't be able to fit any values to.
signal_fail_fit = np.arange(0, 9) % 2
affine = np.eye(4)

def test_two_param_eq(self):
Expand Down Expand Up @@ -126,7 +126,7 @@ def test_three_param_fit(self):
multithread=True)
assert mapper.shape == signal_array.shape[:-1]
npt.assert_almost_equal(mapper.t1_map.mean(), self.t1)
npt.assert_almost_equal(mapper.m0_map.mean(), self.m0)
npt.assert_almost_equal(mapper.m0_map.mean(), self.m0, decimal=4)
npt.assert_almost_equal(mapper.eff_map.mean(), self.eff)
npt.assert_almost_equal(mapper.r1_map().mean(), 1 / self.t1)
npt.assert_almost_equal(mapper.r2.mean(), 1)
Expand All @@ -136,7 +136,7 @@ def test_three_param_fit(self):
multithread=False)
assert mapper.shape == signal_array.shape[:-1]
npt.assert_almost_equal(mapper.t1_map.mean(), self.t1)
npt.assert_almost_equal(mapper.m0_map.mean(), self.m0)
npt.assert_almost_equal(mapper.m0_map.mean(), self.m0, decimal=4)
npt.assert_almost_equal(mapper.eff_map.mean(), self.eff)
npt.assert_almost_equal(mapper.r1_map().mean(), 1 / self.t1)
npt.assert_almost_equal(mapper.r2.mean(), 1)
Expand Down Expand Up @@ -164,7 +164,7 @@ def test_failed_fit(self):
signal_array = np.tile(self.signal_fail_fit, (10, 10, 3, 1))

# Fail to fit using the 2 parameter equation
mapper_two_param = T1(signal_array, self.t, self.affine,
mapper_two_param = T1(signal_array[..., :2], self.t[:2], self.affine,
parameters=2, multithread=True)
assert mapper_two_param.shape == signal_array.shape[:-1]
# Voxels that fail to fit are set to zero
Expand All @@ -175,8 +175,8 @@ def test_failed_fit(self):
npt.assert_equal(mapper_two_param.r2.mean(), 0)

# Fail to fit using the 3 parameter equation
mapper_three_param = T1(signal_array, self.t, self.affine,
parameters=3, multithread=True)
mapper_three_param = T1(signal_array[..., :2], self.t[:2],
self.affine, parameters=3, multithread=True)
assert mapper_three_param.shape == signal_array.shape[:-1]
# Voxels that fail to fit are set to zero
npt.assert_equal(mapper_three_param.t1_map.mean(), 0)
Expand Down Expand Up @@ -311,10 +311,11 @@ def test_real_data(self):
image_molli = image_molli[70:90, 100:120, :2, :]

# Gold standard statistics
gold_standard_2p = [1041.581031, 430.129308, 241.512336, 2603.911794]
gold_standard_3p = [1416.989523, 722.097507, 0.0, 4909.693108]
gold_standard_3p_single = [1379.242715, 714.21752, 0.0, 4308.23814]
gold_standard_molli = [1647.83798691, 741.68317391, 0.0, 4706.6919605]
gold_standard_2p = [1040.259477, 429.506592, 241.512334, 2603.911796]
gold_standard_3p = [1388.640507, 677.167604, 0.0, 4909.689015]
gold_standard_3p_single = [1347.824169, 657.254769, 0.0, 3948.24018]
gold_standard_molli = [1554.586501, 606.863022, -170.611303,
6025.763663]

# Two parameter method
mapper = T1(magnitude, ti, affine, parameters=2, tss=tss)
Expand Down Expand Up @@ -431,8 +432,8 @@ def test_get_fit_signal(self):
stats = arraystats.ArrayStats(fit_signal).calculate()
npt.assert_allclose([stats["mean"]["4D"], stats["std"]["4D"],
stats["min"]["4D"], stats["max"]["4D"]],
[5398.618239796042, 3125.641946762129,
0.0, 12565.465983508042],
[5.469067e+03, 2.982727e+03,
2.613584e+00, 1.284273e+04],
rtol=1e-6, atol=1e-4)


Expand Down
3 changes: 1 addition & 2 deletions ukat/mapping/tests/test_t2.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,7 @@ def test_real_data(self):
# Gold standard statistics
gold_standard_2p_exp = [105.63945, 39.616205,
0.0, 568.160604]
gold_standard_3p_exp = [9.881218e+01, 4.294529e+01,
3.489657e-02, 5.681606e+02]
gold_standard_3p_exp = [98.812108, 42.945342, 0.0, 568.160625]
gold_standard_thresh = [106.351332, 39.904419,
0.0, 568.160832]

Expand Down
6 changes: 2 additions & 4 deletions ukat/mapping/tests/test_t2star.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,10 +270,8 @@ def test_real_data(self):
image = image[30:60, 50:90, 2, :]

# Gold standard statistics for each method
gold_standard_loglin = [32.2660346964308, 18.499243841743308,
0.0, 239.07407841896983]
gold_standard_2p_exp = [30.724443852557155, 22.156366883080896,
0.0, 529.8640757093401]
gold_standard_loglin = [32.727562, 23.862456, 6.739695, 581.272145]
gold_standard_2p_exp = [30.724469, 22.156475, 0.0, 529.870475]

# loglin method
mapper = T2Star(image, te, self.affine, method='loglin')
Expand Down

0 comments on commit a6e6645

Please sign in to comment.