From 3b2d19e2abfb99afa0027be346fd852d875bd404 Mon Sep 17 00:00:00 2001 From: alexdaniel654 Date: Thu, 7 Sep 2023 16:44:12 +0000 Subject: [PATCH] Reference Manual 20230907 --- data/fetch.html | 419 +++++- data/tests/test_fetch.html | 160 ++ mapping/b0.html | 4 +- mapping/diffusion.html | 204 ++- mapping/fitting/index.html | 76 + mapping/fitting/relaxation.html | 541 +++++++ mapping/fitting/tests/index.html | 71 + mapping/fitting/tests/test_relaxation.html | 700 +++++++++ mapping/index.html | 24 +- mapping/mtr.html | 10 +- mapping/resources/index.html | 65 + mapping/resources/t2_stimfit/index.html | 65 + mapping/resources/t2_stimfit/rf_pulses.html | 319 ++++ mapping/t1.html | 721 +++++---- mapping/t2.html | 747 +++++----- mapping/t2_stimfit.html | 1483 +++++++++++++++++++ mapping/t2star.html | 643 ++++---- mapping/tests/index.html | 8 +- mapping/tests/test_b0.html | 19 +- mapping/tests/test_diffusion.html | 38 +- mapping/tests/test_t1.html | 264 ++-- mapping/tests/test_t2.html | 64 +- mapping/tests/test_t2_stimfit.html | 1161 +++++++++++++++ mapping/tests/test_t2star.html | 55 +- qa/tests/test_snr.html | 6 +- segmentation/whole_kidney.html | 18 +- utils/arraystats.html | 13 +- utils/tests/test_ge.html | 1 - utils/tests/test_tools.html | 18 +- utils/tools.html | 4 +- 30 files changed, 6739 insertions(+), 1182 deletions(-) create mode 100644 mapping/fitting/index.html create mode 100644 mapping/fitting/relaxation.html create mode 100644 mapping/fitting/tests/index.html create mode 100644 mapping/fitting/tests/test_relaxation.html create mode 100644 mapping/resources/index.html create mode 100644 mapping/resources/t2_stimfit/index.html create mode 100644 mapping/resources/t2_stimfit/rf_pulses.html create mode 100644 mapping/t2_stimfit.html create mode 100644 mapping/tests/test_t2_stimfit.html diff --git a/data/fetch.html b/data/fetch.html index d9397ab0..f7d0837e 100644 --- a/data/fetch.html +++ b/data/fetch.html @@ -294,14 +294,41 @@

Module ukat.data.fetch

['02f90f0fc8277e09144c21d3fc75a8b7'], doc='Downloading Philips T1W data') -fetch_t2_philips = _make_fetcher('fetch_t2_philips', - pjoin(ukat_home, 't2_philips'), - 'https://zenodo.org/record/4762380/files/', - ['philips_1.zip'], - ['philips_1.zip'], - ['a8adc351219339737b3f0a50404e2c54'], - unzip=True, - doc='Downloading Philips T2 data') +fetch_t2_ge_1 = _make_fetcher('fetch_t2_ge_1', + pjoin(ukat_home, 't2_ge_1'), + 'https://zenodo.org/record/8160807/files/', + ['ge_t2.zip'], + ['ge_t2.zip'], + ['164997465af0cb55c58022f8f8773b04'], + unzip=True, + doc='Downloading GE T2 data') + +fetch_t2_philips_1 = _make_fetcher('fetch_t2_philips_1', + pjoin(ukat_home, 't2_philips_1'), + 'https://zenodo.org/record/4762380/files/', + ['philips_1.zip'], + ['philips_1.zip'], + ['a8adc351219339737b3f0a50404e2c54'], + unzip=True, + doc='Downloading Philips T2 data') + +fetch_t2_philips_2 = _make_fetcher('fetch_t2_philips_2', + pjoin(ukat_home, 't2_philips_2'), + 'https://zenodo.org/record/8160764/files/', + ['philips_2.zip'], + ['philips_2.zip'], + ['5ce51450e37da30d562443ed03c23274'], + unzip=True, + doc='Downloading Philips T2 data') + +fetch_t2_siemens_1 = _make_fetcher('fetch_t2_siemens_1', + pjoin(ukat_home, 't2_siemens_1'), + 'https://zenodo.org/record/8160856/files/', + ['siemens_t2.zip'], + ['siemens_t2.zip'], + ['77b726b9b6c0ed61ffc5ff9f091d7de5'], + unzip=True, + doc='Downloading Siemens T2 data') fetch_t2star_ge = _make_fetcher('fetch_t2star_ge', pjoin(ukat_home, 't2star_ge'), @@ -443,8 +470,23 @@

Module ukat.data.fetch

fnames = sorted(glob.glob(pjoin(folder, '*'))) return fnames - elif name == 't2_philips': - files, folder = fetch_t2_philips() + elif name == 't2_ge_1': + files, folder = fetch_t2_ge_1() + fnames = sorted(glob.glob(pjoin(folder, '*'))) + return fnames + + elif name == 't2_philips_1': + files, folder = fetch_t2_philips_1() + fnames = sorted(glob.glob(pjoin(folder, '*'))) + return fnames + + elif name == 't2_philips_2': + files, folder = fetch_t2_philips_2() + fnames = sorted(glob.glob(pjoin(folder, '*RespTrig_SE*'))) + return fnames + + elif name == 't2_siemens_1': + files, folder = fetch_t2_siemens_1() fnames = sorted(glob.glob(pjoin(folder, '*'))) return fnames @@ -693,7 +735,7 @@

Module ukat.data.fetch

velocity_encoding = 100 for file in fnames: if ((file.endswith(".nii.gz") and "_ph_" in file) or - file.endswith("_ph.nii.gz")): + file.endswith("_ph.nii.gz")): # Load NIfTI and only save the phase data data = nib.load(file) phase.append(np.squeeze(data.get_fdata())) @@ -733,7 +775,7 @@

Module ukat.data.fetch

for file in fnames: if ((file.endswith(".nii.gz") and "_ph_" in file) or - file.endswith("_ph.nii.gz")): + file.endswith("_ph.nii.gz")): # Load NIfTI and only save the phase data data = nib.load(file) phase.append(np.squeeze(data.get_fdata())) @@ -862,6 +904,60 @@

Module ukat.data.fetch

return image, data.affine +def t2_ge(dataset_id=1): + """Fetches t2/ge_{dataset_id} dataset + dataset_id : int + Number of the dataset to load: + - dataset_id = 1 to load "t2/ge_1" + Returns + ------- + numpy.ndarray + image data + numpy.ndarray + affine matrix for image data + numpy.ndarray + array of echo times, in seconds + """ + possible_dataset_ids = [1] + + if dataset_id not in possible_dataset_ids: + error_msg = f"`dataset_id` must be one of {possible_dataset_ids}" + raise ValueError(error_msg) + + # See README.md in ukat/data/t2 for information about the acquisition. + if dataset_id == 1: + fnames = get_fnames('t2_ge_1') + # Load magnitude data and corresponding echo times (in the orig) + magnitude = [] + echo_list = [] + for file in fnames: + + if file.endswith(".nii.gz"): + + # Load NIfTI + data = nib.load(file) + magnitude.append(data.get_fdata()) + + elif file.endswith(".json"): + + # Retrieve list of echo times in the original order + with open(file, 'r') as json_file: + hdr = json.load(json_file) + echo_list.append(hdr["EchoTime"]) + + # Move echo dimension to 4th dimension + magnitude = np.moveaxis(np.array(magnitude), 0, -1) + echo_list = np.array(echo_list) + + # Sort by increasing echo time + sort_idxs = np.argsort(echo_list) + echo_list = echo_list[sort_idxs] + magnitude = magnitude[:, :, :, sort_idxs] + affine = data.affine + + return magnitude, affine, echo_list + + def t2_philips(dataset_id=1): """Fetches t2/philips_{dataset_id} dataset dataset_id : int @@ -885,7 +981,93 @@

Module ukat.data.fetch

# See README.md in ukat/data/t2 for information about the acquisition. if dataset_id == 1: - fnames = get_fnames('t2_philips') + fnames = get_fnames('t2_philips_1') + # Load magnitude data and corresponding echo times (in the orig) + magnitude = [] + echo_list = [] + for file in fnames: + + if file.endswith(".nii.gz"): + + # Load NIfTI + data = nib.load(file) + magnitude.append(data.get_fdata()) + + elif file.endswith(".json"): + + # Retrieve list of echo times in the original order + with open(file, 'r') as json_file: + hdr = json.load(json_file) + echo_list.append(hdr["EchoTime"]) + + # Move echo dimension to 4th dimension + magnitude = np.moveaxis(np.array(magnitude), 0, -1) + echo_list = np.array(echo_list) + + # Sort by increasing echo time + sort_idxs = np.argsort(echo_list) + echo_list = echo_list[sort_idxs] + magnitude = magnitude[:, :, :, sort_idxs] + affine = data.affine + + return magnitude, affine, echo_list + + elif dataset_id == 2: + fnames = get_fnames('t2_philips_2') + # Load magnitude data and corresponding echo times (in the orig) + magnitude = [] + echo_list = [] + for file in fnames: + + if file.endswith(".nii.gz"): + + # Load NIfTI + data = nib.load(file) + magnitude.append(data.get_fdata()) + + elif file.endswith(".json"): + + # Retrieve list of echo times in the original order + with open(file, 'r') as json_file: + hdr = json.load(json_file) + echo_list.append(hdr["EchoTime"]) + + # Move echo dimension to 4th dimension + magnitude = np.moveaxis(np.array(magnitude), 0, -1) + echo_list = np.array(echo_list) + + # Sort by increasing echo time + sort_idxs = np.argsort(echo_list) + echo_list = echo_list[sort_idxs] + magnitude = magnitude[:, :, :, sort_idxs] + affine = data.affine + + return magnitude, affine, echo_list + + +def t2_siemens(dataset_id=1): + """Fetches t2/siemens_{dataset_id} dataset + dataset_id : int + Number of the dataset to load: + - dataset_id = 1 to load "t2/siemens_1" + Returns + ------- + numpy.ndarray + image data + numpy.ndarray + affine matrix for image data + numpy.ndarray + array of echo times, in seconds + """ + possible_dataset_ids = [1] + + if dataset_id not in possible_dataset_ids: + error_msg = f"`dataset_id` must be one of {possible_dataset_ids}" + raise ValueError(error_msg) + + # See README.md in ukat/data/t2 for information about the acquisition. + if dataset_id == 1: + fnames = get_fnames('t2_siemens_1') # Load magnitude data and corresponding echo times (in the orig) magnitude = [] echo_list = [] @@ -1566,8 +1748,23 @@

Returns

fnames = sorted(glob.glob(pjoin(folder, '*'))) return fnames - elif name == 't2_philips': - files, folder = fetch_t2_philips() + elif name == 't2_ge_1': + files, folder = fetch_t2_ge_1() + fnames = sorted(glob.glob(pjoin(folder, '*'))) + return fnames + + elif name == 't2_philips_1': + files, folder = fetch_t2_philips_1() + fnames = sorted(glob.glob(pjoin(folder, '*'))) + return fnames + + elif name == 't2_philips_2': + files, folder = fetch_t2_philips_2() + fnames = sorted(glob.glob(pjoin(folder, '*RespTrig_SE*'))) + return fnames + + elif name == 't2_siemens_1': + files, folder = fetch_t2_siemens_1() fnames = sorted(glob.glob(pjoin(folder, '*'))) return fnames @@ -1676,7 +1873,7 @@

Returns

velocity_encoding = 100 for file in fnames: if ((file.endswith(".nii.gz") and "_ph_" in file) or - file.endswith("_ph.nii.gz")): + file.endswith("_ph.nii.gz")): # Load NIfTI and only save the phase data data = nib.load(file) phase.append(np.squeeze(data.get_fdata())) @@ -1737,7 +1934,7 @@

Returns

for file in fnames: if ((file.endswith(".nii.gz") and "_ph_" in file) or - file.endswith("_ph.nii.gz")): + file.endswith("_ph.nii.gz")): # Load NIfTI and only save the phase data data = nib.load(file) phase.append(np.squeeze(data.get_fdata())) @@ -1927,6 +2124,82 @@

Returns

return image, data.affine +
+def t2_ge(dataset_id=1) +
+
+

Fetches t2/ge_{dataset_id} dataset +dataset_id : int +Number of the dataset to load: +- dataset_id = 1 to load "t2/ge_1" +Returns

+
+
+
numpy.ndarray
+
image data
+
numpy.ndarray
+
affine matrix for image data
+
numpy.ndarray
+
array of echo times, in seconds
+
+
+ +Expand source code + +
def t2_ge(dataset_id=1):
+    """Fetches t2/ge_{dataset_id} dataset
+        dataset_id : int
+                Number of the dataset to load:
+                - dataset_id = 1 to load "t2/ge_1"
+        Returns
+        -------
+        numpy.ndarray
+            image data
+        numpy.ndarray
+            affine matrix for image data
+        numpy.ndarray
+            array of echo times, in seconds
+        """
+    possible_dataset_ids = [1]
+
+    if dataset_id not in possible_dataset_ids:
+        error_msg = f"`dataset_id` must be one of {possible_dataset_ids}"
+        raise ValueError(error_msg)
+
+    # See README.md in ukat/data/t2 for information about the acquisition.
+    if dataset_id == 1:
+        fnames = get_fnames('t2_ge_1')
+        # Load magnitude data and corresponding echo times (in the orig)
+        magnitude = []
+        echo_list = []
+        for file in fnames:
+
+            if file.endswith(".nii.gz"):
+
+                # Load NIfTI
+                data = nib.load(file)
+                magnitude.append(data.get_fdata())
+
+            elif file.endswith(".json"):
+
+                # Retrieve list of echo times in the original order
+                with open(file, 'r') as json_file:
+                    hdr = json.load(json_file)
+                echo_list.append(hdr["EchoTime"])
+
+        # Move echo dimension to 4th dimension
+        magnitude = np.moveaxis(np.array(magnitude), 0, -1)
+        echo_list = np.array(echo_list)
+
+        # Sort by increasing echo time
+        sort_idxs = np.argsort(echo_list)
+        echo_list = echo_list[sort_idxs]
+        magnitude = magnitude[:, :, :, sort_idxs]
+        affine = data.affine
+
+        return magnitude, affine, echo_list
+
+
def t2_philips(dataset_id=1)
@@ -1972,7 +2245,115 @@

Returns

# See README.md in ukat/data/t2 for information about the acquisition. if dataset_id == 1: - fnames = get_fnames('t2_philips') + fnames = get_fnames('t2_philips_1') + # Load magnitude data and corresponding echo times (in the orig) + magnitude = [] + echo_list = [] + for file in fnames: + + if file.endswith(".nii.gz"): + + # Load NIfTI + data = nib.load(file) + magnitude.append(data.get_fdata()) + + elif file.endswith(".json"): + + # Retrieve list of echo times in the original order + with open(file, 'r') as json_file: + hdr = json.load(json_file) + echo_list.append(hdr["EchoTime"]) + + # Move echo dimension to 4th dimension + magnitude = np.moveaxis(np.array(magnitude), 0, -1) + echo_list = np.array(echo_list) + + # Sort by increasing echo time + sort_idxs = np.argsort(echo_list) + echo_list = echo_list[sort_idxs] + magnitude = magnitude[:, :, :, sort_idxs] + affine = data.affine + + return magnitude, affine, echo_list + + elif dataset_id == 2: + fnames = get_fnames('t2_philips_2') + # Load magnitude data and corresponding echo times (in the orig) + magnitude = [] + echo_list = [] + for file in fnames: + + if file.endswith(".nii.gz"): + + # Load NIfTI + data = nib.load(file) + magnitude.append(data.get_fdata()) + + elif file.endswith(".json"): + + # Retrieve list of echo times in the original order + with open(file, 'r') as json_file: + hdr = json.load(json_file) + echo_list.append(hdr["EchoTime"]) + + # Move echo dimension to 4th dimension + magnitude = np.moveaxis(np.array(magnitude), 0, -1) + echo_list = np.array(echo_list) + + # Sort by increasing echo time + sort_idxs = np.argsort(echo_list) + echo_list = echo_list[sort_idxs] + magnitude = magnitude[:, :, :, sort_idxs] + affine = data.affine + + return magnitude, affine, echo_list + + +
+def t2_siemens(dataset_id=1) +
+
+

Fetches t2/siemens_{dataset_id} dataset +dataset_id : int +Number of the dataset to load: +- dataset_id = 1 to load "t2/siemens_1" +Returns

+
+
+
numpy.ndarray
+
image data
+
numpy.ndarray
+
affine matrix for image data
+
numpy.ndarray
+
array of echo times, in seconds
+
+
+ +Expand source code + +
def t2_siemens(dataset_id=1):
+    """Fetches t2/siemens_{dataset_id} dataset
+        dataset_id : int
+                Number of the dataset to load:
+                - dataset_id = 1 to load "t2/siemens_1"
+        Returns
+        -------
+        numpy.ndarray
+            image data
+        numpy.ndarray
+            affine matrix for image data
+        numpy.ndarray
+            array of echo times, in seconds
+        """
+    possible_dataset_ids = [1]
+
+    if dataset_id not in possible_dataset_ids:
+        error_msg = f"`dataset_id` must be one of {possible_dataset_ids}"
+        raise ValueError(error_msg)
+
+    # See README.md in ukat/data/t2 for information about the acquisition.
+    if dataset_id == 1:
+        fnames = get_fnames('t2_siemens_1')
         # Load magnitude data and corresponding echo times (in the orig)
         magnitude = []
         echo_list = []
@@ -2263,7 +2644,9 @@ 

Index

  • t1_molli_philips
  • t1_philips
  • t1w_volume_philips
  • +
  • t2_ge
  • t2_philips
  • +
  • t2_siemens
  • t2star_ge
  • t2star_philips
  • t2star_siemens
  • diff --git a/data/tests/test_fetch.html b/data/tests/test_fetch.html index bc3b7ce6..65fce5a3 100644 --- a/data/tests/test_fetch.html +++ b/data/tests/test_fetch.html @@ -271,6 +271,23 @@

    Module ukat.data.tests.test_fetch

    assert len(np.shape(image)) == 3 assert np.shape(affine) == (4, 4) + def test_ge_t2(self): + # Test if the fetch function works + magnitude, affine, echo_times = fetch.t2_ge() + + # Check the format of the outputs + assert isinstance(magnitude, np.ndarray) + assert np.unique(np.isnan(magnitude)) != [True] + assert isinstance(affine, np.ndarray) + assert isinstance(echo_times, np.ndarray) + assert len(np.shape(magnitude)) == 4 + assert np.shape(affine) == (4, 4) + assert len(np.shape(echo_times)) == 1 + + # If an incorrect dataset_id is given + with pytest.raises(ValueError): + magnitude, affine, echo_times = fetch.t2_ge(2) + def test_philips_t2(self): # Test if the fetch function works magnitude, affine, echo_times = fetch.t2_philips(1) @@ -284,10 +301,39 @@

    Module ukat.data.tests.test_fetch

    assert np.shape(affine) == (4, 4) assert len(np.shape(echo_times)) == 1 + # Test if the fetch function works + magnitude, affine, echo_times = fetch.t2_philips(2) + + # Check the format of the outputs + assert isinstance(magnitude, np.ndarray) + assert np.unique(np.isnan(magnitude)) != [True] + assert isinstance(affine, np.ndarray) + assert isinstance(echo_times, np.ndarray) + assert len(np.shape(magnitude)) == 4 + assert np.shape(affine) == (4, 4) + assert len(np.shape(echo_times)) == 1 + # If an incorrect dataset_id is given with pytest.raises(ValueError): magnitude, affine, echo_times = fetch.t2_philips(3) + def test_siemens_t2(self): + # Test if the fetch function works + magnitude, affine, echo_times = fetch.t2_siemens() + + # Check the format of the outputs + assert isinstance(magnitude, np.ndarray) + assert np.unique(np.isnan(magnitude)) != [True] + assert isinstance(affine, np.ndarray) + assert isinstance(echo_times, np.ndarray) + assert len(np.shape(magnitude)) == 4 + assert np.shape(affine) == (4, 4) + assert len(np.shape(echo_times)) == 1 + + # If an incorrect dataset_id is given + with pytest.raises(ValueError): + magnitude, affine, echo_times = fetch.t2_siemens(2) + def test_ge_t2star(self): # Test if the fetch function works magnitude, affine, echo_times = fetch.t2star_ge() @@ -617,6 +663,23 @@

    Classes

    assert len(np.shape(image)) == 3 assert np.shape(affine) == (4, 4) + def test_ge_t2(self): + # Test if the fetch function works + magnitude, affine, echo_times = fetch.t2_ge() + + # Check the format of the outputs + assert isinstance(magnitude, np.ndarray) + assert np.unique(np.isnan(magnitude)) != [True] + assert isinstance(affine, np.ndarray) + assert isinstance(echo_times, np.ndarray) + assert len(np.shape(magnitude)) == 4 + assert np.shape(affine) == (4, 4) + assert len(np.shape(echo_times)) == 1 + + # If an incorrect dataset_id is given + with pytest.raises(ValueError): + magnitude, affine, echo_times = fetch.t2_ge(2) + def test_philips_t2(self): # Test if the fetch function works magnitude, affine, echo_times = fetch.t2_philips(1) @@ -630,10 +693,39 @@

    Classes

    assert np.shape(affine) == (4, 4) assert len(np.shape(echo_times)) == 1 + # Test if the fetch function works + magnitude, affine, echo_times = fetch.t2_philips(2) + + # Check the format of the outputs + assert isinstance(magnitude, np.ndarray) + assert np.unique(np.isnan(magnitude)) != [True] + assert isinstance(affine, np.ndarray) + assert isinstance(echo_times, np.ndarray) + assert len(np.shape(magnitude)) == 4 + assert np.shape(affine) == (4, 4) + assert len(np.shape(echo_times)) == 1 + # If an incorrect dataset_id is given with pytest.raises(ValueError): magnitude, affine, echo_times = fetch.t2_philips(3) + def test_siemens_t2(self): + # Test if the fetch function works + magnitude, affine, echo_times = fetch.t2_siemens() + + # Check the format of the outputs + assert isinstance(magnitude, np.ndarray) + assert np.unique(np.isnan(magnitude)) != [True] + assert isinstance(affine, np.ndarray) + assert isinstance(echo_times, np.ndarray) + assert len(np.shape(magnitude)) == 4 + assert np.shape(affine) == (4, 4) + assert len(np.shape(echo_times)) == 1 + + # If an incorrect dataset_id is given + with pytest.raises(ValueError): + magnitude, affine, echo_times = fetch.t2_siemens(2) + def test_ge_t2star(self): # Test if the fetch function works magnitude, affine, echo_times = fetch.t2star_ge() @@ -759,6 +851,33 @@

    Methods

    assert (np.shape(bvecs)[0] == 3 or np.shape(bvecs)[1] == 3)
    +
    +def test_ge_t2(self) +
    +
    +
    +
    + +Expand source code + +
    def test_ge_t2(self):
    +    # Test if the fetch function works
    +    magnitude, affine, echo_times = fetch.t2_ge()
    +
    +    # Check the format of the outputs
    +    assert isinstance(magnitude, np.ndarray)
    +    assert np.unique(np.isnan(magnitude)) != [True]
    +    assert isinstance(affine, np.ndarray)
    +    assert isinstance(echo_times, np.ndarray)
    +    assert len(np.shape(magnitude)) == 4
    +    assert np.shape(affine) == (4, 4)
    +    assert len(np.shape(echo_times)) == 1
    +
    +    # If an incorrect dataset_id is given
    +    with pytest.raises(ValueError):
    +        magnitude, affine, echo_times = fetch.t2_ge(2)
    +
    +
    def test_ge_t2star(self)
    @@ -1034,6 +1153,18 @@

    Methods

    assert np.shape(affine) == (4, 4) assert len(np.shape(echo_times)) == 1 + # Test if the fetch function works + magnitude, affine, echo_times = fetch.t2_philips(2) + + # Check the format of the outputs + assert isinstance(magnitude, np.ndarray) + assert np.unique(np.isnan(magnitude)) != [True] + assert isinstance(affine, np.ndarray) + assert isinstance(echo_times, np.ndarray) + assert len(np.shape(magnitude)) == 4 + assert np.shape(affine) == (4, 4) + assert len(np.shape(echo_times)) == 1 + # If an incorrect dataset_id is given with pytest.raises(ValueError): magnitude, affine, echo_times = fetch.t2_philips(3) @@ -1189,6 +1320,33 @@

    Methods

    assert (np.shape(bvecs)[0] == 3 or np.shape(bvecs)[1] == 3) +
    +def test_siemens_t2(self) +
    +
    +
    +
    + +Expand source code + +
    def test_siemens_t2(self):
    +    # Test if the fetch function works
    +    magnitude, affine, echo_times = fetch.t2_siemens()
    +
    +    # Check the format of the outputs
    +    assert isinstance(magnitude, np.ndarray)
    +    assert np.unique(np.isnan(magnitude)) != [True]
    +    assert isinstance(affine, np.ndarray)
    +    assert isinstance(echo_times, np.ndarray)
    +    assert len(np.shape(magnitude)) == 4
    +    assert np.shape(affine) == (4, 4)
    +    assert len(np.shape(echo_times)) == 1
    +
    +    # If an incorrect dataset_id is given
    +    with pytest.raises(ValueError):
    +        magnitude, affine, echo_times = fetch.t2_siemens(2)
    +
    +
    def test_siemens_t2star(self)
    @@ -1235,6 +1393,7 @@

  • test_ge_b0
  • test_ge_dwi
  • +
  • test_ge_t2
  • test_ge_t2star
  • test_philips_b0
  • test_philips_dwi
  • @@ -1250,6 +1409,7 @@

    test_philips_tsnr
  • test_siemens_b0
  • test_siemens_dwi
  • +
  • test_siemens_t2
  • test_siemens_t2star
  • diff --git a/mapping/b0.html b/mapping/b0.html index cffc023a..aaa45206 100644 --- a/mapping/b0.html +++ b/mapping/b0.html @@ -139,7 +139,7 @@

    Module ukat.mapping.b0

    # B0 Map Offset Correction self.b0_map -= (np.round(mean_central_b0 / b0_offset_step)) * \ b0_offset_step - + # Mask B0 Map self.b0_map[np.squeeze(~self.mask)] = 0 else: @@ -370,7 +370,7 @@

    Parameters

    # B0 Map Offset Correction self.b0_map -= (np.round(mean_central_b0 / b0_offset_step)) * \ b0_offset_step - + # Mask B0 Map self.b0_map[np.squeeze(~self.mask)] = 0 else: diff --git a/mapping/diffusion.html b/mapping/diffusion.html index 20591243..c374305d 100644 --- a/mapping/diffusion.html +++ b/mapping/diffusion.html @@ -38,6 +38,7 @@

    Module ukat.mapping.diffusion

    from dipy.core.gradients import gradient_table, unique_bvals_tolerance from dipy.reconst.dti import TensorModel +from sklearn.metrics import r2_score from tqdm import tqdm @@ -118,8 +119,15 @@

    Module ukat.mapping.diffusion

    ---------- adc : np.ndarray The estimated ADC in mm^2/s. + s0 : np.ndarray + The estimated S0. adc_err : np.ndarray The certainty in the fit of `adc` in mm^2/s. + s0_err : np.ndarray + The certainty in the fit of `s0`. + r2 : np.ndarray + The R-Squared value of the fit, values close to 1 indicate a good + fit, lower values indicate a poorer fit shape : tuple The shape of the ADC map. n_vox : int @@ -199,7 +207,8 @@

    Module ukat.mapping.diffusion

    self.pixel_array_mean = self.__mean_over_directions__() - self.adc, self.adc_err = self.__fit__() + self.adc, self.s0, self.adc_err, self.s0_err, self.r2 = \ + self.__fit__() def __mean_over_directions__(self): """ @@ -222,7 +231,10 @@

    Module ukat.mapping.diffusion

    def __fit__(self): # Initialise maps adc_map = np.zeros(self.n_vox) + s0_map = np.zeros(self.n_vox) adc_err = np.zeros(self.n_vox) + s0_err = np.zeros(self.n_vox) + r2 = np.zeros(self.n_vox) mask = self.mask.flatten() signal = self.pixel_array_mean.reshape(-1, self.n_bvals) @@ -230,17 +242,24 @@

    Module ukat.mapping.diffusion

    with tqdm(total=idx.size) as progress: for ind in idx: sig = signal[ind, :] - adc_map[ind], adc_err[ind] = \ + adc_map[ind], s0_map[ind], adc_err[ind], s0_err[ind], \ + r2[ind] = \ self.__fit_signal__(sig, self.u_bvals) progress.update(1) adc_map[adc_map < 0] = 0 + s0_map[adc_map < 0] = 0 adc_err[adc_map < 0] = 0 + s0_err[adc_map < 0] = 0 + r2[adc_map < 0] = 0 # Reshape results into raw data shape adc_map = adc_map.reshape(self.shape) + s0_map = s0_map.reshape(self.shape) adc_err = adc_err.reshape(self.shape) + s0_err = s0_err.reshape(self.shape) + r2 = r2.reshape(self.shape) - return adc_map, adc_err + return adc_map, s0_map, adc_err, s0_err, r2 @staticmethod def __fit_signal__(sig, bvals): @@ -248,12 +267,18 @@

    Module ukat.mapping.diffusion

    popt, pvar = np.polyfit(bvals[sig > 0], np.log(sig[sig > 0]), 1, cov=True) adc = -popt[0] + s0 = np.exp(popt[1]) adc_err = np.sqrt(pvar[0, 0]) + s0_err = np.exp(np.sqrt(pvar[1, 1])) except np.linalg.LinAlgError: adc = 0 + s0 = 0 adc_err = 0 + s0_err = 0 - return adc, adc_err + fit_sig = adc_eq(bvals, adc, s0) + r2 = r2_score(sig, fit_sig) + return adc, s0, adc_err, s0_err, r2 def to_nifti(self, output_directory=os.getcwd(), base_file_name='Output', maps='all'): @@ -268,21 +293,34 @@

    Module ukat.mapping.diffusion

    Eg., base_file_name = 'Output' will result in 'Output.nii.gz'. maps : list or 'all', optional List of maps to save to NIFTI. This should either the string "all" - or a list of maps from ["adc", "adc_err", "mask"]. + or a list of maps from ["adc", "s0", "adc_err", "s0_err", + "r2", "mask"]. """ os.makedirs(output_directory, exist_ok=True) base_path = os.path.join(output_directory, base_file_name) if maps == 'all' or maps == ['all']: - maps = ['adc', 'adc_err', 'mask'] + maps = ['adc', 's0', 'adc_err', 's0_err', 'r2', 'mask'] if isinstance(maps, list): for result in maps: if result == 'adc' or result == 'adc_map': adc_nifti = nib.Nifti1Image(self.adc, affine=self.affine) nib.save(adc_nifti, base_path + '_adc_map.nii.gz') + elif result == 's0' or result == 's0_map': + s0_nifti = nib.Nifti1Image(self.s0, + affine=self.affine) + nib.save(s0_nifti, base_path + '_s0_map.nii.gz') elif result == 'adc_err' or result == 'adc_err_map': adc_err_nifti = nib.Nifti1Image(self.adc_err, affine=self.affine) nib.save(adc_err_nifti, base_path + '_adc_err.nii.gz') + elif result == 's0_err' or result == 's0_err_map': + s0_err_nifti = nib.Nifti1Image(self.s0_err, + affine=self.affine) + nib.save(s0_err_nifti, base_path + '_s0_err.nii.gz') + elif result == 'r2' or result == 'r2_map': + r2_nifti = nib.Nifti1Image(self.r2, + affine=self.affine) + nib.save(r2_nifti, base_path + '_r2.nii.gz') elif result == 'mask': mask_nifti = nib.Nifti1Image(self.mask.astype(np.uint16), affine=self.affine) @@ -293,6 +331,29 @@

    Module ukat.mapping.diffusion

    '"["adc", "adc_err", "mask"]".') +def adc_eq(bvals, adc, s0): + """ + The ADC equation. + + Parameters + ---------- + bvals : np.ndarray + The b-values used in the experiment in s/mm^2. + adc : float + The estimated ADC value in mm^2/s. + s0 : float + The estimated S0 value. + + Returns + ------- + signal : np.ndarray + The estimated signal values. + """ + with np.errstate(divide='ignore'): + signal = s0 * np.exp(-bvals * adc) + return signal + + class DTI: """ Attributes @@ -371,8 +432,8 @@

    Module ukat.mapping.diffusion

    if bvecs.shape[1] != 3 and bvecs.shape[0] == 3: bvecs = bvecs.T warnings.warn(f'bvecs should be (N, 3). Because your bvecs array ' - 'is {bvecs.shape} it has been transposed to {' - 'bvecs.T.shape}.') + f'is {bvecs.shape} it has been transposed to ' + f'{bvecs.T.shape}.') assert (bvecs.shape[1] == 3) assert (pixel_array.shape[-1] == bvecs.shape[0]), 'Number of bvecs ' \ 'does not match ' \ @@ -451,6 +512,52 @@

    Module ukat.mapping.diffusion

    Functions

    +
    +def adc_eq(bvals, adc, s0) +
    +
    +

    The ADC equation.

    +

    Parameters

    +
    +
    bvals : np.ndarray
    +
    The b-values used in the experiment in s/mm^2.
    +
    adc : float
    +
    The estimated ADC value in mm^2/s.
    +
    s0 : float
    +
    The estimated S0 value.
    +
    +

    Returns

    +
    +
    signal : np.ndarray
    +
    The estimated signal values.
    +
    +
    + +Expand source code + +
    def adc_eq(bvals, adc, s0):
    +    """
    +    The ADC equation.
    +
    +    Parameters
    +    ----------
    +    bvals : np.ndarray
    +        The b-values used in the experiment in s/mm^2.
    +    adc : float
    +        The estimated ADC value in mm^2/s.
    +    s0 : float
    +        The estimated S0 value.
    +
    +    Returns
    +    -------
    +    signal : np.ndarray
    +        The estimated signal values.
    +    """
    +    with np.errstate(divide='ignore'):
    +        signal = s0 * np.exp(-bvals * adc)
    +    return signal
    +
    +
    def make_gradient_scheme(bvals, bvecs, normalize=True, one_bzero=True)
    @@ -587,8 +694,15 @@

    Classes

    adc : np.ndarray
    The estimated ADC in mm^2/s.
    +
    s0 : np.ndarray
    +
    The estimated S0.
    adc_err : np.ndarray
    The certainty in the fit of adc in mm^2/s.
    +
    s0_err : np.ndarray
    +
    The certainty in the fit of s0.
    +
    r2 : np.ndarray
    +
    The R-Squared value of the fit, values close to 1 indicate a good +fit, lower values indicate a poorer fit
    shape : tuple
    The shape of the ADC map.
    n_vox : int
    @@ -645,8 +759,15 @@

    Parameters

    ---------- adc : np.ndarray The estimated ADC in mm^2/s. + s0 : np.ndarray + The estimated S0. adc_err : np.ndarray The certainty in the fit of `adc` in mm^2/s. + s0_err : np.ndarray + The certainty in the fit of `s0`. + r2 : np.ndarray + The R-Squared value of the fit, values close to 1 indicate a good + fit, lower values indicate a poorer fit shape : tuple The shape of the ADC map. n_vox : int @@ -726,7 +847,8 @@

    Parameters

    self.pixel_array_mean = self.__mean_over_directions__() - self.adc, self.adc_err = self.__fit__() + self.adc, self.s0, self.adc_err, self.s0_err, self.r2 = \ + self.__fit__() def __mean_over_directions__(self): """ @@ -749,7 +871,10 @@

    Parameters

    def __fit__(self): # Initialise maps adc_map = np.zeros(self.n_vox) + s0_map = np.zeros(self.n_vox) adc_err = np.zeros(self.n_vox) + s0_err = np.zeros(self.n_vox) + r2 = np.zeros(self.n_vox) mask = self.mask.flatten() signal = self.pixel_array_mean.reshape(-1, self.n_bvals) @@ -757,17 +882,24 @@

    Parameters

    with tqdm(total=idx.size) as progress: for ind in idx: sig = signal[ind, :] - adc_map[ind], adc_err[ind] = \ + adc_map[ind], s0_map[ind], adc_err[ind], s0_err[ind], \ + r2[ind] = \ self.__fit_signal__(sig, self.u_bvals) progress.update(1) adc_map[adc_map < 0] = 0 + s0_map[adc_map < 0] = 0 adc_err[adc_map < 0] = 0 + s0_err[adc_map < 0] = 0 + r2[adc_map < 0] = 0 # Reshape results into raw data shape adc_map = adc_map.reshape(self.shape) + s0_map = s0_map.reshape(self.shape) adc_err = adc_err.reshape(self.shape) + s0_err = s0_err.reshape(self.shape) + r2 = r2.reshape(self.shape) - return adc_map, adc_err + return adc_map, s0_map, adc_err, s0_err, r2 @staticmethod def __fit_signal__(sig, bvals): @@ -775,12 +907,18 @@

    Parameters

    popt, pvar = np.polyfit(bvals[sig > 0], np.log(sig[sig > 0]), 1, cov=True) adc = -popt[0] + s0 = np.exp(popt[1]) adc_err = np.sqrt(pvar[0, 0]) + s0_err = np.exp(np.sqrt(pvar[1, 1])) except np.linalg.LinAlgError: adc = 0 + s0 = 0 adc_err = 0 + s0_err = 0 - return adc, adc_err + fit_sig = adc_eq(bvals, adc, s0) + r2 = r2_score(sig, fit_sig) + return adc, s0, adc_err, s0_err, r2 def to_nifti(self, output_directory=os.getcwd(), base_file_name='Output', maps='all'): @@ -795,21 +933,34 @@

    Parameters

    Eg., base_file_name = 'Output' will result in 'Output.nii.gz'. maps : list or 'all', optional List of maps to save to NIFTI. This should either the string "all" - or a list of maps from ["adc", "adc_err", "mask"]. + or a list of maps from ["adc", "s0", "adc_err", "s0_err", + "r2", "mask"]. """ os.makedirs(output_directory, exist_ok=True) base_path = os.path.join(output_directory, base_file_name) if maps == 'all' or maps == ['all']: - maps = ['adc', 'adc_err', 'mask'] + maps = ['adc', 's0', 'adc_err', 's0_err', 'r2', 'mask'] if isinstance(maps, list): for result in maps: if result == 'adc' or result == 'adc_map': adc_nifti = nib.Nifti1Image(self.adc, affine=self.affine) nib.save(adc_nifti, base_path + '_adc_map.nii.gz') + elif result == 's0' or result == 's0_map': + s0_nifti = nib.Nifti1Image(self.s0, + affine=self.affine) + nib.save(s0_nifti, base_path + '_s0_map.nii.gz') elif result == 'adc_err' or result == 'adc_err_map': adc_err_nifti = nib.Nifti1Image(self.adc_err, affine=self.affine) nib.save(adc_err_nifti, base_path + '_adc_err.nii.gz') + elif result == 's0_err' or result == 's0_err_map': + s0_err_nifti = nib.Nifti1Image(self.s0_err, + affine=self.affine) + nib.save(s0_err_nifti, base_path + '_s0_err.nii.gz') + elif result == 'r2' or result == 'r2_map': + r2_nifti = nib.Nifti1Image(self.r2, + affine=self.affine) + nib.save(r2_nifti, base_path + '_r2.nii.gz') elif result == 'mask': mask_nifti = nib.Nifti1Image(self.mask.astype(np.uint16), affine=self.affine) @@ -835,7 +986,8 @@

    Parameters

    Eg., base_file_name = 'Output' will result in 'Output.nii.gz'.
    maps : list or 'all', optional
    List of maps to save to NIFTI. This should either the string "all" -or a list of maps from ["adc", "adc_err", "mask"].
    +or a list of maps from ["adc", "s0", "adc_err", "s0_err", +"r2", "mask"].
    @@ -854,21 +1006,34 @@

    Parameters

    Eg., base_file_name = 'Output' will result in 'Output.nii.gz'. maps : list or 'all', optional List of maps to save to NIFTI. This should either the string "all" - or a list of maps from ["adc", "adc_err", "mask"]. + or a list of maps from ["adc", "s0", "adc_err", "s0_err", + "r2", "mask"]. """ os.makedirs(output_directory, exist_ok=True) base_path = os.path.join(output_directory, base_file_name) if maps == 'all' or maps == ['all']: - maps = ['adc', 'adc_err', 'mask'] + maps = ['adc', 's0', 'adc_err', 's0_err', 'r2', 'mask'] if isinstance(maps, list): for result in maps: if result == 'adc' or result == 'adc_map': adc_nifti = nib.Nifti1Image(self.adc, affine=self.affine) nib.save(adc_nifti, base_path + '_adc_map.nii.gz') + elif result == 's0' or result == 's0_map': + s0_nifti = nib.Nifti1Image(self.s0, + affine=self.affine) + nib.save(s0_nifti, base_path + '_s0_map.nii.gz') elif result == 'adc_err' or result == 'adc_err_map': adc_err_nifti = nib.Nifti1Image(self.adc_err, affine=self.affine) nib.save(adc_err_nifti, base_path + '_adc_err.nii.gz') + elif result == 's0_err' or result == 's0_err_map': + s0_err_nifti = nib.Nifti1Image(self.s0_err, + affine=self.affine) + nib.save(s0_err_nifti, base_path + '_s0_err.nii.gz') + elif result == 'r2' or result == 'r2_map': + r2_nifti = nib.Nifti1Image(self.r2, + affine=self.affine) + nib.save(r2_nifti, base_path + '_r2.nii.gz') elif result == 'mask': mask_nifti = nib.Nifti1Image(self.mask.astype(np.uint16), affine=self.affine) @@ -1033,8 +1198,8 @@

    Parameters

    if bvecs.shape[1] != 3 and bvecs.shape[0] == 3: bvecs = bvecs.T warnings.warn(f'bvecs should be (N, 3). Because your bvecs array ' - 'is {bvecs.shape} it has been transposed to {' - 'bvecs.T.shape}.') + f'is {bvecs.shape} it has been transposed to ' + f'{bvecs.T.shape}.') assert (bvecs.shape[1] == 3) assert (pixel_array.shape[-1] == bvecs.shape[0]), 'Number of bvecs ' \ 'does not match ' \ @@ -1187,6 +1352,7 @@

    Index

  • Functions

  • diff --git a/mapping/fitting/index.html b/mapping/fitting/index.html new file mode 100644 index 00000000..1d9e2099 --- /dev/null +++ b/mapping/fitting/index.html @@ -0,0 +1,76 @@ + + + + + + +ukat.mapping.fitting API documentation + + + + + + + + + + + +
    + + +
    + + + \ No newline at end of file diff --git a/mapping/fitting/relaxation.html b/mapping/fitting/relaxation.html new file mode 100644 index 00000000..b2e78f06 --- /dev/null +++ b/mapping/fitting/relaxation.html @@ -0,0 +1,541 @@ + + + + + + +ukat.mapping.fitting.relaxation API documentation + + + + + + + + + + + +
    +
    +
    +

    Module ukat.mapping.fitting.relaxation

    +
    +
    +
    + +Expand source code + +
    import inspect
    +import numpy as np
    +
    +from pathos.pools import ProcessPool
    +from scipy.optimize import curve_fit
    +from sklearn.metrics import r2_score
    +from tqdm import tqdm
    +
    +
    +class Model:
    +    def __init__(self, pixel_array, x, eq, mask=None, multithread=True):
    +        """
    +        A template class for fitting models to pixel arrays.
    +
    +        Parameters
    +        ----------
    +        pixel_array : np.ndarray
    +            An array containing the signal from each voxel with the last
    +            dimension being the dependent variable axis
    +        x : np.ndarray
    +            An array containing the dependent variable e.g. time
    +        eq : function
    +            A function that takes the dependent variable as the first
    +            argument and the parameters to fit as the remaining arguments
    +        mask : np.ndarray, optional
    +            A boolean mask of voxels to fit. Should be the shape of the desired
    +            map rather than the raw data i.e. omit the dependent variable axis
    +        multithread : bool, optional
    +            Default True
    +            If True, the fitting will be performed in parallel using all
    +            available cores
    +        """
    +        # Attributes that can be set from default inputs
    +        self.pixel_array = pixel_array
    +        self.map_shape = pixel_array.shape[:-1]
    +        self.x = x
    +        self.eq = eq
    +        self.mask = mask
    +        self.multithread = multithread
    +        self.n_x = pixel_array.shape[-1]
    +        self.n_params = self._get_n_params()
    +
    +        # Placeholder attributes that will be overwritten by the child class
    +        self.initial_guess = None
    +        self.signal_list = None
    +        self.x_list = None
    +        self.p0_list = None
    +        self.mask_list = None
    +
    +    def generate_lists(self):
    +        """
    +        Generate the lists of data, dependent variables, initial guesses and
    +        masks to be used in the fitting process
    +        """
    +        self.signal_list = self.pixel_array.reshape(-1, self.n_x).tolist()
    +        self.x_list = [self.x] * len(self.signal_list)
    +        self.p0_list = [self.initial_guess] * len(self.signal_list)
    +        self.mask_list = self._get_mask_list()
    +
    +    def _get_n_params(self):
    +        """
    +        Get the number of parameters to fit
    +
    +        Returns
    +        -------
    +        n_params : int
    +            The number of parameters to fit
    +        """
    +        n_params = len(inspect.signature(self.eq).parameters) - 1
    +        return n_params
    +
    +    def _get_mask_list(self):
    +        """
    +        Get a list of masks to be used in the fitting process, if no mask
    +        has been specified it will be a list of True i.e. all voxels will be
    +        fit
    +
    +        Returns
    +        -------
    +        mask_list : list
    +            A list of booleans indicating whether to fit a voxel or not
    +        """
    +        if self.mask is None:
    +            mask_list = [True] * len(self.signal_list)
    +            return mask_list
    +        else:
    +            mask_list = self.mask.reshape(-1).tolist()
    +            return mask_list
    +
    +
    +def fit_image(model):
    +    """
    +    Fit an image to a relaxometry curve fitting model
    +
    +    Parameters
    +    ----------
    +    model : ukat.mapping.fitting.relaxation.Model
    +        A model object containing the data and model to fit to
    +
    +    Returns
    +    -------
    +    popt_list : list
    +        A list of nD arrays containing the fitted parameters
    +    error_list : list
    +        A list of nD arrays containing the error in the fitted parameters
    +    r2 : np.ndarray
    +        An nD array containing the R2 value of the fit
    +    """
    +    if model.multithread:
    +        with ProcessPool() as executor:
    +            results = executor.map(fit_signal,
    +                                   model.signal_list,
    +                                   model.x_list,
    +                                   model.p0_list,
    +                                   model.mask_list,
    +                                   [model] * len(model.signal_list))
    +    else:
    +        results = list(tqdm(map(fit_signal,
    +                                model.signal_list,
    +                                model.x_list,
    +                                model.p0_list,
    +                                model.mask_list,
    +                                [model] * len(model.signal_list)),
    +                            total=len(model.signal_list)))
    +
    +    popt_array = np.array([result[0] for result in results])
    +    popt_list = [popt_array[:, p].reshape(model.map_shape) for p in range(
    +        model.n_params)]
    +    error_array = np.array([result[1] for result in results])
    +    error_list = [error_array[:, p].reshape(model.map_shape) for p in range(
    +        model.n_params)]
    +    r2 = np.array([result[2] for result in results]).reshape(model.map_shape)
    +    return popt_list, error_list, r2
    +
    +
    +def fit_signal(sig, x, p0, mask, model):
    +    """
    +    Fit a signal to a model
    +
    +    Parameters
    +    ----------
    +    sig : np.array
    +        Numpy array containing the signal to fit
    +    x : np.array
    +        Numpy array containing the x values for the signal (e.g. TE)
    +    p0 : np.array
    +        Numpy array containing the initial guess for the parameters
    +    mask : bool
    +        A boolean indicating whether to fit the signal or not
    +    model : Model
    +        A Model object containing the model to fit to
    +
    +    Returns
    +    -------
    +    popt : np.array
    +        Numpy array containing the fitted parameters
    +    error : np.array
    +        Numpy array containing the standard error of the fitted parameters
    +    r2 : float
    +        The R^2 value of the fit
    +    """
    +    if mask is True:
    +        try:
    +            popt, pcov = curve_fit(model.eq, x, sig, p0=p0,
    +                                   bounds=model.bounds)
    +            fit_sig = model.eq(x, *popt)
    +            r2 = r2_score(sig, fit_sig)
    +        except (RuntimeError, ValueError):
    +            popt = np.zeros(model.n_params)
    +            pcov = np.zeros((model.n_params, model.n_params))
    +            r2 = -1E6
    +    else:
    +        popt = np.zeros(model.n_params)
    +        pcov = np.zeros((model.n_params, model.n_params))
    +        r2 = -1E6
    +
    +    error = np.sqrt(np.diag(pcov))
    +    return popt, error, r2
    +
    +
    +
    +
    +
    +
    +
    +

    Functions

    +
    +
    +def fit_image(model) +
    +
    +

    Fit an image to a relaxometry curve fitting model

    +

    Parameters

    +
    +
    model : Model
    +
    A model object containing the data and model to fit to
    +
    +

    Returns

    +
    +
    popt_list : list
    +
    A list of nD arrays containing the fitted parameters
    +
    error_list : list
    +
    A list of nD arrays containing the error in the fitted parameters
    +
    r2 : np.ndarray
    +
    An nD array containing the R2 value of the fit
    +
    +
    + +Expand source code + +
    def fit_image(model):
    +    """
    +    Fit an image to a relaxometry curve fitting model
    +
    +    Parameters
    +    ----------
    +    model : ukat.mapping.fitting.relaxation.Model
    +        A model object containing the data and model to fit to
    +
    +    Returns
    +    -------
    +    popt_list : list
    +        A list of nD arrays containing the fitted parameters
    +    error_list : list
    +        A list of nD arrays containing the error in the fitted parameters
    +    r2 : np.ndarray
    +        An nD array containing the R2 value of the fit
    +    """
    +    if model.multithread:
    +        with ProcessPool() as executor:
    +            results = executor.map(fit_signal,
    +                                   model.signal_list,
    +                                   model.x_list,
    +                                   model.p0_list,
    +                                   model.mask_list,
    +                                   [model] * len(model.signal_list))
    +    else:
    +        results = list(tqdm(map(fit_signal,
    +                                model.signal_list,
    +                                model.x_list,
    +                                model.p0_list,
    +                                model.mask_list,
    +                                [model] * len(model.signal_list)),
    +                            total=len(model.signal_list)))
    +
    +    popt_array = np.array([result[0] for result in results])
    +    popt_list = [popt_array[:, p].reshape(model.map_shape) for p in range(
    +        model.n_params)]
    +    error_array = np.array([result[1] for result in results])
    +    error_list = [error_array[:, p].reshape(model.map_shape) for p in range(
    +        model.n_params)]
    +    r2 = np.array([result[2] for result in results]).reshape(model.map_shape)
    +    return popt_list, error_list, r2
    +
    +
    +
    +def fit_signal(sig, x, p0, mask, model) +
    +
    +

    Fit a signal to a model

    +

    Parameters

    +
    +
    sig : np.array
    +
    Numpy array containing the signal to fit
    +
    x : np.array
    +
    Numpy array containing the x values for the signal (e.g. TE)
    +
    p0 : np.array
    +
    Numpy array containing the initial guess for the parameters
    +
    mask : bool
    +
    A boolean indicating whether to fit the signal or not
    +
    model : Model
    +
    A Model object containing the model to fit to
    +
    +

    Returns

    +
    +
    popt : np.array
    +
    Numpy array containing the fitted parameters
    +
    error : np.array
    +
    Numpy array containing the standard error of the fitted parameters
    +
    r2 : float
    +
    The R^2 value of the fit
    +
    +
    + +Expand source code + +
    def fit_signal(sig, x, p0, mask, model):
    +    """
    +    Fit a signal to a model
    +
    +    Parameters
    +    ----------
    +    sig : np.array
    +        Numpy array containing the signal to fit
    +    x : np.array
    +        Numpy array containing the x values for the signal (e.g. TE)
    +    p0 : np.array
    +        Numpy array containing the initial guess for the parameters
    +    mask : bool
    +        A boolean indicating whether to fit the signal or not
    +    model : Model
    +        A Model object containing the model to fit to
    +
    +    Returns
    +    -------
    +    popt : np.array
    +        Numpy array containing the fitted parameters
    +    error : np.array
    +        Numpy array containing the standard error of the fitted parameters
    +    r2 : float
    +        The R^2 value of the fit
    +    """
    +    if mask is True:
    +        try:
    +            popt, pcov = curve_fit(model.eq, x, sig, p0=p0,
    +                                   bounds=model.bounds)
    +            fit_sig = model.eq(x, *popt)
    +            r2 = r2_score(sig, fit_sig)
    +        except (RuntimeError, ValueError):
    +            popt = np.zeros(model.n_params)
    +            pcov = np.zeros((model.n_params, model.n_params))
    +            r2 = -1E6
    +    else:
    +        popt = np.zeros(model.n_params)
    +        pcov = np.zeros((model.n_params, model.n_params))
    +        r2 = -1E6
    +
    +    error = np.sqrt(np.diag(pcov))
    +    return popt, error, r2
    +
    +
    +
    +
    +
    +

    Classes

    +
    +
    +class Model +(pixel_array, x, eq, mask=None, multithread=True) +
    +
    +

    A template class for fitting models to pixel arrays.

    +

    Parameters

    +
    +
    pixel_array : np.ndarray
    +
    An array containing the signal from each voxel with the last +dimension being the dependent variable axis
    +
    x : np.ndarray
    +
    An array containing the dependent variable e.g. time
    +
    eq : function
    +
    A function that takes the dependent variable as the first +argument and the parameters to fit as the remaining arguments
    +
    mask : np.ndarray, optional
    +
    A boolean mask of voxels to fit. Should be the shape of the desired +map rather than the raw data i.e. omit the dependent variable axis
    +
    multithread : bool, optional
    +
    Default True +If True, the fitting will be performed in parallel using all +available cores
    +
    +
    + +Expand source code + +
    class Model:
    +    def __init__(self, pixel_array, x, eq, mask=None, multithread=True):
    +        """
    +        A template class for fitting models to pixel arrays.
    +
    +        Parameters
    +        ----------
    +        pixel_array : np.ndarray
    +            An array containing the signal from each voxel with the last
    +            dimension being the dependent variable axis
    +        x : np.ndarray
    +            An array containing the dependent variable e.g. time
    +        eq : function
    +            A function that takes the dependent variable as the first
    +            argument and the parameters to fit as the remaining arguments
    +        mask : np.ndarray, optional
    +            A boolean mask of voxels to fit. Should be the shape of the desired
    +            map rather than the raw data i.e. omit the dependent variable axis
    +        multithread : bool, optional
    +            Default True
    +            If True, the fitting will be performed in parallel using all
    +            available cores
    +        """
    +        # Attributes that can be set from default inputs
    +        self.pixel_array = pixel_array
    +        self.map_shape = pixel_array.shape[:-1]
    +        self.x = x
    +        self.eq = eq
    +        self.mask = mask
    +        self.multithread = multithread
    +        self.n_x = pixel_array.shape[-1]
    +        self.n_params = self._get_n_params()
    +
    +        # Placeholder attributes that will be overwritten by the child class
    +        self.initial_guess = None
    +        self.signal_list = None
    +        self.x_list = None
    +        self.p0_list = None
    +        self.mask_list = None
    +
    +    def generate_lists(self):
    +        """
    +        Generate the lists of data, dependent variables, initial guesses and
    +        masks to be used in the fitting process
    +        """
    +        self.signal_list = self.pixel_array.reshape(-1, self.n_x).tolist()
    +        self.x_list = [self.x] * len(self.signal_list)
    +        self.p0_list = [self.initial_guess] * len(self.signal_list)
    +        self.mask_list = self._get_mask_list()
    +
    +    def _get_n_params(self):
    +        """
    +        Get the number of parameters to fit
    +
    +        Returns
    +        -------
    +        n_params : int
    +            The number of parameters to fit
    +        """
    +        n_params = len(inspect.signature(self.eq).parameters) - 1
    +        return n_params
    +
    +    def _get_mask_list(self):
    +        """
    +        Get a list of masks to be used in the fitting process, if no mask
    +        has been specified it will be a list of True i.e. all voxels will be
    +        fit
    +
    +        Returns
    +        -------
    +        mask_list : list
    +            A list of booleans indicating whether to fit a voxel or not
    +        """
    +        if self.mask is None:
    +            mask_list = [True] * len(self.signal_list)
    +            return mask_list
    +        else:
    +            mask_list = self.mask.reshape(-1).tolist()
    +            return mask_list
    +
    +

    Subclasses

    + +

    Methods

    +
    +
    +def generate_lists(self) +
    +
    +

    Generate the lists of data, dependent variables, initial guesses and +masks to be used in the fitting process

    +
    + +Expand source code + +
    def generate_lists(self):
    +    """
    +    Generate the lists of data, dependent variables, initial guesses and
    +    masks to be used in the fitting process
    +    """
    +    self.signal_list = self.pixel_array.reshape(-1, self.n_x).tolist()
    +    self.x_list = [self.x] * len(self.signal_list)
    +    self.p0_list = [self.initial_guess] * len(self.signal_list)
    +    self.mask_list = self._get_mask_list()
    +
    +
    +
    +
    +
    +
    +
    + +
    + + + \ No newline at end of file diff --git a/mapping/fitting/tests/index.html b/mapping/fitting/tests/index.html new file mode 100644 index 00000000..9effce7e --- /dev/null +++ b/mapping/fitting/tests/index.html @@ -0,0 +1,71 @@ + + + + + + +ukat.mapping.fitting.tests API documentation + + + + + + + + + + + +
    + + +
    + + + \ No newline at end of file diff --git a/mapping/fitting/tests/test_relaxation.html b/mapping/fitting/tests/test_relaxation.html new file mode 100644 index 00000000..7ccac23b --- /dev/null +++ b/mapping/fitting/tests/test_relaxation.html @@ -0,0 +1,700 @@ + + + + + + +ukat.mapping.fitting.tests.test_relaxation API documentation + + + + + + + + + + + +
    +
    +
    +

    Module ukat.mapping.fitting.tests.test_relaxation

    +
    +
    +
    + +Expand source code + +
    import numpy as np
    +import numpy.testing as npt
    +
    +from ukat.mapping.fitting import Model, fit_image, fit_signal
    +
    +
    +class TestModel:
    +    pixel_array = np.zeros((10, 10, 3, 8))
    +    x = np.linspace(0, 1000, 8)
    +    mask = np.ones((10, 10, 3), dtype=bool)
    +    mask[:5] = False
    +
    +    @staticmethod
    +    def two_param_eq(x, a, b):
    +        return a * x + b
    +
    +    @staticmethod
    +    def three_param_eq(x, a, b, c):
    +        return a * x + (b * c)
    +
    +    def test_init(self):
    +        model = Model(self.pixel_array, self.x, self.two_param_eq, self.mask,
    +                      multithread=True)
    +        assert model.map_shape == (10, 10, 3)
    +        assert model.n_x == 8
    +
    +    def test_n_params(self):
    +        model = Model(self.pixel_array, self.x, self.two_param_eq, self.mask,
    +                      multithread=True)
    +        assert model.n_params == 2
    +
    +        model = Model(self.pixel_array, self.x, self.three_param_eq, self.mask,
    +                      multithread=True)
    +        assert model.n_params == 3
    +
    +    def test_generate_lists(self):
    +        model = Model(self.pixel_array, self.x, self.two_param_eq, self.mask,
    +                      multithread=True)
    +        model.initial_guess = [1, 1]
    +        model.generate_lists()
    +        assert type(model.signal_list) == list
    +        assert type(model.x_list) == list
    +        assert type(model.p0_list) == list
    +        assert type(model.mask_list) == list
    +
    +        assert len(model.signal_list) == 300
    +        assert len(model.x_list) == 300
    +        assert len(model.p0_list) == 300
    +        assert len(model.mask_list) == 300
    +
    +        assert len(model.signal_list[0]) == 8
    +        assert len(model.x_list[0]) == 8
    +        assert len(model.p0_list[0]) == 2
    +
    +        model = Model(self.pixel_array, self.x, self.two_param_eq,
    +                      multithread=True)
    +        model.initial_guess = [1, 1]
    +        model.generate_lists()
    +        assert type(model.mask_list) == list
    +        assert len(model.mask_list) == 300
    +        assert model.mask_list[0] is True
    +
    +
    +class TestFitSignal:
    +    x = np.arange(1, 9)
    +
    +    @staticmethod
    +    def linear_eq(x, m, c):
    +        return m * x + c
    +
    +    def test_fit_signal(self):
    +        sig = np.array([1, 2, 3, 4, 5, 6, 7, 8])
    +        pixel_array = np.tile(sig, (10, 10, 3, 1))
    +        model = Model(pixel_array, self.x, self.linear_eq,
    +                      multithread=True)
    +        model.initial_guess = [0.9, 0.9]
    +        model.bounds = ([0, 0], [2, 2])
    +        model.generate_lists()
    +        popt, error, r2 = fit_signal(sig, self.x, model.initial_guess, True,
    +                                     model)
    +        npt.assert_allclose(popt, [1, 0], rtol=1e-5, atol=1e4)
    +        npt.assert_allclose(error, [0, 0], rtol=1e-5, atol=1e4)
    +        npt.assert_almost_equal(r2, 1)
    +
    +    def test_mask(self):
    +        sig = np.array([1, 2, 3, 4, 5, 6, 7, 8])
    +        pixel_array = np.tile(sig, (10, 10, 3, 1))
    +        model = Model(pixel_array, self.x, self.linear_eq,
    +                      multithread=True)
    +        model.initial_guess = [0.9, 0.9]
    +        model.bounds = ([0, 0], [2, 2])
    +        model.generate_lists()
    +        popt, error, r2 = fit_signal(sig, self.x, model.initial_guess, False,
    +                                     model)
    +        npt.assert_allclose(popt, [0, 0])
    +        npt.assert_allclose(error, [0, 0])
    +        npt.assert_almost_equal(r2, -1E6)
    +
    +
    +class TestFitImage:
    +    x = np.arange(1, 9)
    +    sig = np.array([1, 2, 3, 4, 5, 6, 7, 8])
    +    pixel_array = np.tile(sig, (10, 10, 3, 1))
    +
    +    @staticmethod
    +    def linear_eq(x, m, c):
    +        return m * x + c
    +
    +    def test_single_threaded(self):
    +        model = Model(self.pixel_array, self.x, self.linear_eq,
    +                      multithread=False)
    +        model.initial_guess = [0.9, 0.9]
    +        model.bounds = ([0, 0], [2, 2])
    +        model.generate_lists()
    +        popt, error, r2 = fit_image(model)
    +
    +        assert len(popt) == 2
    +        assert len(error) == 2
    +
    +        assert popt[0].shape == (10, 10, 3)
    +        assert error[0].shape == (10, 10, 3)
    +        assert r2.shape == (10, 10, 3)
    +
    +        npt.assert_almost_equal(popt[0].mean(), 1, decimal=5)
    +        npt.assert_almost_equal(error[0].mean(), 0, decimal=5)
    +        npt.assert_almost_equal(r2.mean(), 1, decimal=5)
    +
    +    def test_multi_threaded(self):
    +        model = Model(self.pixel_array, self.x, self.linear_eq,
    +                      multithread=True)
    +        model.initial_guess = [0.9, 0.9]
    +        model.bounds = ([0, 0], [2, 2])
    +        model.generate_lists()
    +        popt, error, r2 = fit_image(model)
    +
    +        assert len(popt) == 2
    +        assert len(error) == 2
    +
    +        assert popt[0].shape == (10, 10, 3)
    +        assert error[0].shape == (10, 10, 3)
    +        assert r2.shape == (10, 10, 3)
    +
    +        npt.assert_almost_equal(popt[0].mean(), 1, decimal=5)
    +        npt.assert_almost_equal(error[0].mean(), 0, decimal=5)
    +        npt.assert_almost_equal(r2.mean(), 1, decimal=5)
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +

    Classes

    +
    +
    +class TestFitImage +
    +
    +
    +
    + +Expand source code + +
    class TestFitImage:
    +    x = np.arange(1, 9)
    +    sig = np.array([1, 2, 3, 4, 5, 6, 7, 8])
    +    pixel_array = np.tile(sig, (10, 10, 3, 1))
    +
    +    @staticmethod
    +    def linear_eq(x, m, c):
    +        return m * x + c
    +
    +    def test_single_threaded(self):
    +        model = Model(self.pixel_array, self.x, self.linear_eq,
    +                      multithread=False)
    +        model.initial_guess = [0.9, 0.9]
    +        model.bounds = ([0, 0], [2, 2])
    +        model.generate_lists()
    +        popt, error, r2 = fit_image(model)
    +
    +        assert len(popt) == 2
    +        assert len(error) == 2
    +
    +        assert popt[0].shape == (10, 10, 3)
    +        assert error[0].shape == (10, 10, 3)
    +        assert r2.shape == (10, 10, 3)
    +
    +        npt.assert_almost_equal(popt[0].mean(), 1, decimal=5)
    +        npt.assert_almost_equal(error[0].mean(), 0, decimal=5)
    +        npt.assert_almost_equal(r2.mean(), 1, decimal=5)
    +
    +    def test_multi_threaded(self):
    +        model = Model(self.pixel_array, self.x, self.linear_eq,
    +                      multithread=True)
    +        model.initial_guess = [0.9, 0.9]
    +        model.bounds = ([0, 0], [2, 2])
    +        model.generate_lists()
    +        popt, error, r2 = fit_image(model)
    +
    +        assert len(popt) == 2
    +        assert len(error) == 2
    +
    +        assert popt[0].shape == (10, 10, 3)
    +        assert error[0].shape == (10, 10, 3)
    +        assert r2.shape == (10, 10, 3)
    +
    +        npt.assert_almost_equal(popt[0].mean(), 1, decimal=5)
    +        npt.assert_almost_equal(error[0].mean(), 0, decimal=5)
    +        npt.assert_almost_equal(r2.mean(), 1, decimal=5)
    +
    +

    Class variables

    +
    +
    var pixel_array
    +
    +
    +
    +
    var sig
    +
    +
    +
    +
    var x
    +
    +
    +
    +
    +

    Static methods

    +
    +
    +def linear_eq(x, m, c) +
    +
    +
    +
    + +Expand source code + +
    @staticmethod
    +def linear_eq(x, m, c):
    +    return m * x + c
    +
    +
    +
    +

    Methods

    +
    +
    +def test_multi_threaded(self) +
    +
    +
    +
    + +Expand source code + +
    def test_multi_threaded(self):
    +    model = Model(self.pixel_array, self.x, self.linear_eq,
    +                  multithread=True)
    +    model.initial_guess = [0.9, 0.9]
    +    model.bounds = ([0, 0], [2, 2])
    +    model.generate_lists()
    +    popt, error, r2 = fit_image(model)
    +
    +    assert len(popt) == 2
    +    assert len(error) == 2
    +
    +    assert popt[0].shape == (10, 10, 3)
    +    assert error[0].shape == (10, 10, 3)
    +    assert r2.shape == (10, 10, 3)
    +
    +    npt.assert_almost_equal(popt[0].mean(), 1, decimal=5)
    +    npt.assert_almost_equal(error[0].mean(), 0, decimal=5)
    +    npt.assert_almost_equal(r2.mean(), 1, decimal=5)
    +
    +
    +
    +def test_single_threaded(self) +
    +
    +
    +
    + +Expand source code + +
    def test_single_threaded(self):
    +    model = Model(self.pixel_array, self.x, self.linear_eq,
    +                  multithread=False)
    +    model.initial_guess = [0.9, 0.9]
    +    model.bounds = ([0, 0], [2, 2])
    +    model.generate_lists()
    +    popt, error, r2 = fit_image(model)
    +
    +    assert len(popt) == 2
    +    assert len(error) == 2
    +
    +    assert popt[0].shape == (10, 10, 3)
    +    assert error[0].shape == (10, 10, 3)
    +    assert r2.shape == (10, 10, 3)
    +
    +    npt.assert_almost_equal(popt[0].mean(), 1, decimal=5)
    +    npt.assert_almost_equal(error[0].mean(), 0, decimal=5)
    +    npt.assert_almost_equal(r2.mean(), 1, decimal=5)
    +
    +
    +
    +
    +
    +class TestFitSignal +
    +
    +
    +
    + +Expand source code + +
    class TestFitSignal:
    +    x = np.arange(1, 9)
    +
    +    @staticmethod
    +    def linear_eq(x, m, c):
    +        return m * x + c
    +
    +    def test_fit_signal(self):
    +        sig = np.array([1, 2, 3, 4, 5, 6, 7, 8])
    +        pixel_array = np.tile(sig, (10, 10, 3, 1))
    +        model = Model(pixel_array, self.x, self.linear_eq,
    +                      multithread=True)
    +        model.initial_guess = [0.9, 0.9]
    +        model.bounds = ([0, 0], [2, 2])
    +        model.generate_lists()
    +        popt, error, r2 = fit_signal(sig, self.x, model.initial_guess, True,
    +                                     model)
    +        npt.assert_allclose(popt, [1, 0], rtol=1e-5, atol=1e4)
    +        npt.assert_allclose(error, [0, 0], rtol=1e-5, atol=1e4)
    +        npt.assert_almost_equal(r2, 1)
    +
    +    def test_mask(self):
    +        sig = np.array([1, 2, 3, 4, 5, 6, 7, 8])
    +        pixel_array = np.tile(sig, (10, 10, 3, 1))
    +        model = Model(pixel_array, self.x, self.linear_eq,
    +                      multithread=True)
    +        model.initial_guess = [0.9, 0.9]
    +        model.bounds = ([0, 0], [2, 2])
    +        model.generate_lists()
    +        popt, error, r2 = fit_signal(sig, self.x, model.initial_guess, False,
    +                                     model)
    +        npt.assert_allclose(popt, [0, 0])
    +        npt.assert_allclose(error, [0, 0])
    +        npt.assert_almost_equal(r2, -1E6)
    +
    +

    Class variables

    +
    +
    var x
    +
    +
    +
    +
    +

    Static methods

    +
    +
    +def linear_eq(x, m, c) +
    +
    +
    +
    + +Expand source code + +
    @staticmethod
    +def linear_eq(x, m, c):
    +    return m * x + c
    +
    +
    +
    +

    Methods

    +
    +
    +def test_fit_signal(self) +
    +
    +
    +
    + +Expand source code + +
    def test_fit_signal(self):
    +    sig = np.array([1, 2, 3, 4, 5, 6, 7, 8])
    +    pixel_array = np.tile(sig, (10, 10, 3, 1))
    +    model = Model(pixel_array, self.x, self.linear_eq,
    +                  multithread=True)
    +    model.initial_guess = [0.9, 0.9]
    +    model.bounds = ([0, 0], [2, 2])
    +    model.generate_lists()
    +    popt, error, r2 = fit_signal(sig, self.x, model.initial_guess, True,
    +                                 model)
    +    npt.assert_allclose(popt, [1, 0], rtol=1e-5, atol=1e4)
    +    npt.assert_allclose(error, [0, 0], rtol=1e-5, atol=1e4)
    +    npt.assert_almost_equal(r2, 1)
    +
    +
    +
    +def test_mask(self) +
    +
    +
    +
    + +Expand source code + +
    def test_mask(self):
    +    sig = np.array([1, 2, 3, 4, 5, 6, 7, 8])
    +    pixel_array = np.tile(sig, (10, 10, 3, 1))
    +    model = Model(pixel_array, self.x, self.linear_eq,
    +                  multithread=True)
    +    model.initial_guess = [0.9, 0.9]
    +    model.bounds = ([0, 0], [2, 2])
    +    model.generate_lists()
    +    popt, error, r2 = fit_signal(sig, self.x, model.initial_guess, False,
    +                                 model)
    +    npt.assert_allclose(popt, [0, 0])
    +    npt.assert_allclose(error, [0, 0])
    +    npt.assert_almost_equal(r2, -1E6)
    +
    +
    +
    +
    +
    +class TestModel +
    +
    +
    +
    + +Expand source code + +
    class TestModel:
    +    pixel_array = np.zeros((10, 10, 3, 8))
    +    x = np.linspace(0, 1000, 8)
    +    mask = np.ones((10, 10, 3), dtype=bool)
    +    mask[:5] = False
    +
    +    @staticmethod
    +    def two_param_eq(x, a, b):
    +        return a * x + b
    +
    +    @staticmethod
    +    def three_param_eq(x, a, b, c):
    +        return a * x + (b * c)
    +
    +    def test_init(self):
    +        model = Model(self.pixel_array, self.x, self.two_param_eq, self.mask,
    +                      multithread=True)
    +        assert model.map_shape == (10, 10, 3)
    +        assert model.n_x == 8
    +
    +    def test_n_params(self):
    +        model = Model(self.pixel_array, self.x, self.two_param_eq, self.mask,
    +                      multithread=True)
    +        assert model.n_params == 2
    +
    +        model = Model(self.pixel_array, self.x, self.three_param_eq, self.mask,
    +                      multithread=True)
    +        assert model.n_params == 3
    +
    +    def test_generate_lists(self):
    +        model = Model(self.pixel_array, self.x, self.two_param_eq, self.mask,
    +                      multithread=True)
    +        model.initial_guess = [1, 1]
    +        model.generate_lists()
    +        assert type(model.signal_list) == list
    +        assert type(model.x_list) == list
    +        assert type(model.p0_list) == list
    +        assert type(model.mask_list) == list
    +
    +        assert len(model.signal_list) == 300
    +        assert len(model.x_list) == 300
    +        assert len(model.p0_list) == 300
    +        assert len(model.mask_list) == 300
    +
    +        assert len(model.signal_list[0]) == 8
    +        assert len(model.x_list[0]) == 8
    +        assert len(model.p0_list[0]) == 2
    +
    +        model = Model(self.pixel_array, self.x, self.two_param_eq,
    +                      multithread=True)
    +        model.initial_guess = [1, 1]
    +        model.generate_lists()
    +        assert type(model.mask_list) == list
    +        assert len(model.mask_list) == 300
    +        assert model.mask_list[0] is True
    +
    +

    Class variables

    +
    +
    var mask
    +
    +
    +
    +
    var pixel_array
    +
    +
    +
    +
    var x
    +
    +
    +
    +
    +

    Static methods

    +
    +
    +def three_param_eq(x, a, b, c) +
    +
    +
    +
    + +Expand source code + +
    @staticmethod
    +def three_param_eq(x, a, b, c):
    +    return a * x + (b * c)
    +
    +
    +
    +def two_param_eq(x, a, b) +
    +
    +
    +
    + +Expand source code + +
    @staticmethod
    +def two_param_eq(x, a, b):
    +    return a * x + b
    +
    +
    +
    +

    Methods

    +
    +
    +def test_generate_lists(self) +
    +
    +
    +
    + +Expand source code + +
    def test_generate_lists(self):
    +    model = Model(self.pixel_array, self.x, self.two_param_eq, self.mask,
    +                  multithread=True)
    +    model.initial_guess = [1, 1]
    +    model.generate_lists()
    +    assert type(model.signal_list) == list
    +    assert type(model.x_list) == list
    +    assert type(model.p0_list) == list
    +    assert type(model.mask_list) == list
    +
    +    assert len(model.signal_list) == 300
    +    assert len(model.x_list) == 300
    +    assert len(model.p0_list) == 300
    +    assert len(model.mask_list) == 300
    +
    +    assert len(model.signal_list[0]) == 8
    +    assert len(model.x_list[0]) == 8
    +    assert len(model.p0_list[0]) == 2
    +
    +    model = Model(self.pixel_array, self.x, self.two_param_eq,
    +                  multithread=True)
    +    model.initial_guess = [1, 1]
    +    model.generate_lists()
    +    assert type(model.mask_list) == list
    +    assert len(model.mask_list) == 300
    +    assert model.mask_list[0] is True
    +
    +
    +
    +def test_init(self) +
    +
    +
    +
    + +Expand source code + +
    def test_init(self):
    +    model = Model(self.pixel_array, self.x, self.two_param_eq, self.mask,
    +                  multithread=True)
    +    assert model.map_shape == (10, 10, 3)
    +    assert model.n_x == 8
    +
    +
    +
    +def test_n_params(self) +
    +
    +
    +
    + +Expand source code + +
    def test_n_params(self):
    +    model = Model(self.pixel_array, self.x, self.two_param_eq, self.mask,
    +                  multithread=True)
    +    assert model.n_params == 2
    +
    +    model = Model(self.pixel_array, self.x, self.three_param_eq, self.mask,
    +                  multithread=True)
    +    assert model.n_params == 3
    +
    +
    +
    +
    +
    +
    +
    + +
    + + + \ No newline at end of file diff --git a/mapping/index.html b/mapping/index.html index 70daff5d..97af37e7 100644 --- a/mapping/index.html +++ b/mapping/index.html @@ -26,7 +26,14 @@

    Module ukat.mapping

    Expand source code -
    from . import b0, diffusion, mtr, t1, t2, t2star
    +
    from . import b0, diffusion, mtr, t1, t2, t2_stimfit, t2star
    +from .b0 import B0
    +from .diffusion import ADC, DTI
    +from .mtr import MTR
    +from .t1 import T1
    +from .t2 import T2
    +from .t2_stimfit import StimFitModel, T2StimFit
    +from .t2star import T2Star
    @@ -40,10 +47,18 @@

    Sub-modules

    Diffusion imaging module

    +
    ukat.mapping.fitting
    +
    +
    +
    ukat.mapping.mtr
    +
    ukat.mapping.resources
    +
    +
    +
    ukat.mapping.t1
    @@ -52,6 +67,10 @@

    Sub-modules

    +
    ukat.mapping.t2_stimfit
    +
    +
    +
    ukat.mapping.t2star
    @@ -84,9 +103,12 @@

    Index

    diff --git a/mapping/mtr.html b/mapping/mtr.html index 791d9985..b93ecfda 100644 --- a/mapping/mtr.html +++ b/mapping/mtr.html @@ -77,7 +77,7 @@

    Module ukat.mapping.mtr

    'dimension of the input ' \ 'pixel_array must be 2.' if np.sum(pixel_array[..., 1]) >= np.sum(pixel_array[..., 0]): - warnings.warn(f'The average intensity of the MT_ON image is more ' + warnings.warn('The average intensity of the MT_ON image is more ' 'than the average intensity of the MT_OFF image. ' 'This will lead to negative MTR values which is not ' 'usually desirable. Please check that you\'ve input ' @@ -97,7 +97,8 @@

    Module ukat.mapping.mtr

    self.mt_on = np.squeeze(self.pixel_array[..., 1] * self.mask) # Magnetisation Transfer Ratio calculation self.mtr_map = np.squeeze(np.nan_to_num(((self.mt_off - self.mt_on) / - self.mt_off), posinf=0, neginf=0)) + self.mt_off), + posinf=0, neginf=0)) def to_nifti(self, output_directory=os.getcwd(), base_file_name='Output', maps='all'): @@ -244,7 +245,7 @@

    Parameters

    'dimension of the input ' \ 'pixel_array must be 2.' if np.sum(pixel_array[..., 1]) >= np.sum(pixel_array[..., 0]): - warnings.warn(f'The average intensity of the MT_ON image is more ' + warnings.warn('The average intensity of the MT_ON image is more ' 'than the average intensity of the MT_OFF image. ' 'This will lead to negative MTR values which is not ' 'usually desirable. Please check that you\'ve input ' @@ -264,7 +265,8 @@

    Parameters

    self.mt_on = np.squeeze(self.pixel_array[..., 1] * self.mask) # Magnetisation Transfer Ratio calculation self.mtr_map = np.squeeze(np.nan_to_num(((self.mt_off - self.mt_on) / - self.mt_off), posinf=0, neginf=0)) + self.mt_off), + posinf=0, neginf=0)) def to_nifti(self, output_directory=os.getcwd(), base_file_name='Output', maps='all'): diff --git a/mapping/resources/index.html b/mapping/resources/index.html new file mode 100644 index 00000000..d19560e8 --- /dev/null +++ b/mapping/resources/index.html @@ -0,0 +1,65 @@ + + + + + + +ukat.mapping.resources API documentation + + + + + + + + + + + +
    + + +
    + + + \ No newline at end of file diff --git a/mapping/resources/t2_stimfit/index.html b/mapping/resources/t2_stimfit/index.html new file mode 100644 index 00000000..5945b63c --- /dev/null +++ b/mapping/resources/t2_stimfit/index.html @@ -0,0 +1,65 @@ + + + + + + +ukat.mapping.resources.t2_stimfit API documentation + + + + + + + + + + + +
    + + +
    + + + \ No newline at end of file diff --git a/mapping/resources/t2_stimfit/rf_pulses.html b/mapping/resources/t2_stimfit/rf_pulses.html new file mode 100644 index 00000000..f5b6fbdb --- /dev/null +++ b/mapping/resources/t2_stimfit/rf_pulses.html @@ -0,0 +1,319 @@ + + + + + + +ukat.mapping.resources.t2_stimfit.rf_pulses API documentation + + + + + + + + + + + +
    +
    +
    +

    Module ukat.mapping.resources.t2_stimfit.rf_pulses

    +
    +
    +
    + +Expand source code + +
    import numpy as np
    +
    +ge_90 = np.array([0.000000000000000000e+00,
    +                  0.000000000000000000e+00,
    +                  0.000000000000000000e+00,
    +                  0.000000000000000000e+00,
    +                  0.000000000000000000e+00,
    +                  0.000000000000000000e+00,
    +                  -1.266768749857178165e+02,
    +                  -3.124522490114486573e+04,
    +                  -9.531547336557127710e+04,
    +                  -9.721048020529435598e+04,
    +                  -1.179229383718024765e+05,
    +                  -1.456733869440756971e+05,
    +                  -1.721247993207764812e+05,
    +                  -1.952492802694011480e+05,
    +                  -2.129515095199028729e+05,
    +                  -2.212653870648781303e+05,
    +                  -2.176550316485261428e+05,
    +                  -1.986209674288319075e+05,
    +                  -1.608037579581205500e+05,
    +                  -1.015676326677684265e+05,
    +                  -1.764346979966967410e+04,
    +                  9.146498080684282468e+04,
    +                  2.305132406684918096e+05,
    +                  3.992299908046545461e+05,
    +                  5.973996433025186416e+05,
    +                  8.234028000181980897e+05,
    +                  1.076599458585872315e+06,
    +                  1.353354095860138303e+06,
    +                  1.648617102658187039e+06,
    +                  1.960097276725076837e+06,
    +                  2.279507916006019339e+06,
    +                  2.598834868547144346e+06,
    +                  2.914556274034465663e+06,
    +                  3.215262450139994733e+06,
    +                  3.492515551519702189e+06,
    +                  3.743626426648607478e+06,
    +                  3.955383802293300163e+06,
    +                  4.122795531721453648e+06,
    +                  4.245528126540713944e+06,
    +                  4.312751237523826770e+06,
    +                  4.325134271489643492e+06,
    +                  4.287280772804215550e+06,
    +                  4.193747129049188923e+06,
    +                  4.051372835346973035e+06,
    +                  3.868604124899462331e+06,
    +                  3.644595798674946185e+06,
    +                  3.390452242988429498e+06,
    +                  3.116637709659715649e+06,
    +                  2.824276558167716488e+06,
    +                  2.525030573960933834e+06,
    +                  2.228156430497014429e+06,
    +                  1.934771772715587635e+06,
    +                  1.653738064701012336e+06,
    +                  1.391487375758768991e+06,
    +                  1.147340323923531920e+06,
    +                  9.273275685164701426e+05,
    +                  7.332337711992086843e+05,
    +                  5.631421786069724476e+05,
    +                  4.191517663029067335e+05,
    +                  3.006847090521778446e+05,
    +                  2.252239997526510851e+05,
    +                  1.585568372310573177e+05,
    +                  2.501030593846463671e+04,
    +                  1.223713984113714659e+01])
    +
    +ge_180 = np.array([2.628067099684659042e+05,
    +                   1.812536453020582558e+05,
    +                   5.814370884872322495e+04,
    +                   -4.110320687328568602e+04,
    +                   -1.374178347772504785e+05,
    +                   -2.306666085725722369e+05,
    +                   -3.182438513767363620e+05,
    +                   -3.943806634919878561e+05,
    +                   -4.532441780498687876e+05,
    +                   -4.879111396010195022e+05,
    +                   -4.922629923736716155e+05,
    +                   -4.584898011221469496e+05,
    +                   -3.801620427679839777e+05,
    +                   -2.525084479537750303e+05,
    +                   -6.868080375989120512e+04,
    +                   1.734608747904833872e+05,
    +                   4.798435415184603771e+05,
    +                   8.509221243205775972e+05,
    +                   1.286536533087235410e+06,
    +                   1.785914593650705181e+06,
    +                   2.343766314660373144e+06,
    +                   2.955048761526284739e+06,
    +                   3.610983570142784156e+06,
    +                   4.298010399946169928e+06,
    +                   5.002087344977931120e+06,
    +                   5.701459471400410868e+06,
    +                   6.373684782939250581e+06,
    +                   6.991767440126724541e+06,
    +                   7.526621337772894651e+06,
    +                   7.951718050802586600e+06,
    +                   8.241118240750433877e+06,
    +                   8.378189295735327527e+06,
    +                   8.354026371893344447e+06,
    +                   8.170212088494279422e+06,
    +                   7.837891504246501252e+06,
    +                   7.377730451304072514e+06,
    +                   6.815066800326613709e+06,
    +                   6.177579216060877778e+06,
    +                   5.494161889734255150e+06,
    +                   4.790092515738011338e+06,
    +                   4.088859494925408624e+06,
    +                   3.408608516213723458e+06,
    +                   2.764577993263987359e+06,
    +                   2.168364262965844013e+06,
    +                   1.626976905518055893e+06,
    +                   1.147140237310670083e+06,
    +                   7.305744826368239010e+05,
    +                   3.791304701537272194e+05,
    +                   9.221733370468940120e+04,
    +                   -1.313390917462947546e+05,
    +                   -2.978557992391907610e+05,
    +                   -4.099621404116821941e+05,
    +                   -4.737573230025937082e+05,
    +                   -4.947706974138531368e+05,
    +                   -4.808528154712727410e+05,
    +                   -4.383707892266485142e+05,
    +                   -3.731866142973536626e+05,
    +                   -2.931676076587194111e+05,
    +                   -2.034967062652181776e+05,
    +                   -1.086659747288654326e+05,
    +                   -1.190326363416896857e+04,
    +                   9.080151677670537902e+04,
    +                   2.349733636560339364e+05,
    +                   -2.059012535436650978e+03])
    +
    +philips_90 = np.array([6.600000000000000000e+01,
    +                       -2.642857142857142776e+02,
    +                       -6.307142857142857792e+02,
    +                       -1.032142857142857338e+03,
    +                       -1.458000000000000000e+03,
    +                       -1.902285714285714221e+03,
    +                       -2.353000000000000000e+03,
    +                       -2.800000000000000000e+03,
    +                       -3.227428571428571558e+03,
    +                       -3.625142857142856883e+03,
    +                       -3.974000000000000000e+03,
    +                       -4.260714285714285325e+03,
    +                       -4.471142857142856883e+03,
    +                       -4.582000000000000000e+03,
    +                       -4.591000000000000000e+03,
    +                       -4.463714285714286234e+03,
    +                       -4.206714285714286234e+03,
    +                       -3.794857142857142208e+03,
    +                       -3.225142857142857338e+03,
    +                       -2.491999999999999545e+03,
    +                       -1.580000000000000909e+03,
    +                       -5.100000000000000000e+02,
    +                       7.482857142857126291e+02,
    +                       2.157857142857139024e+03,
    +                       3.728857142857144026e+03,
    +                       5.440285714285713766e+03,
    +                       7.275999999999996362e+03,
    +                       9.221857142857144936e+03,
    +                       1.124700000000000000e+04,
    +                       1.333671428571428260e+04,
    +                       1.545514285714285143e+04,
    +                       1.757542857142857247e+04,
    +                       1.966757142857142753e+04,
    +                       2.170200000000000728e+04,
    +                       2.364200000000000364e+04,
    +                       2.547100000000000000e+04,
    +                       2.713457142857142753e+04,
    +                       2.863385714285714494e+04,
    +                       2.992314285714285506e+04,
    +                       3.099285714285714494e+04,
    +                       3.182485714285714494e+04,
    +                       3.239100000000000000e+04,
    +                       3.271200000000000000e+04,
    +                       3.273557142857142753e+04,
    +                       3.250800000000000000e+04,
    +                       3.197042857142856519e+04,
    +                       3.124314285714286234e+04,
    +                       3.024142857142856519e+04,
    +                       2.900242857142856883e+04,
    +                       2.757000000000000000e+04,
    +                       2.593514285714285870e+04,
    +                       2.415500000000000728e+04,
    +                       2.224042857142856155e+04,
    +                       2.022942857142856519e+04,
    +                       1.815042857142856519e+04,
    +                       1.603457142857142571e+04,
    +                       1.391100000000000000e+04,
    +                       1.181442857142857429e+04,
    +                       9.765714285714289872e+03,
    +                       7.798000000000007276e+03,
    +                       5.929714285714278958e+03,
    +                       4.179714285714280777e+03,
    +                       2.184857142857139024e+03,
    +                       2.660000000000000000e+02])
    +
    +philips_180 = np.array([1.148000000000000000e+03,
    +                        1.754761904761904816e+03,
    +                        2.009746031746031804e+03,
    +                        2.101238095238095411e+03,
    +                        2.124507936507936392e+03,
    +                        2.076793650793650613e+03,
    +                        1.934238095238095184e+03,
    +                        1.694222222222222399e+03,
    +                        1.279492063492063608e+03,
    +                        6.062857142857147892e+02,
    +                        -3.720476190476181273e+02,
    +                        -1.550380952380951157e+03,
    +                        -2.703190476190476147e+03,
    +                        -3.637793650793649249e+03,
    +                        -4.326333333333333030e+03,
    +                        -4.843904761904761472e+03,
    +                        -5.259507936507936392e+03,
    +                        -5.505158730158730577e+03,
    +                        -5.302714285714286234e+03,
    +                        -4.534444444444445253e+03,
    +                        -3.094619047619050434e+03,
    +                        -1.095000000000006821e+03,
    +                        1.362269841269835979e+03,
    +                        4.290158730158726030e+03,
    +                        7.697095238095237619e+03,
    +                        1.166841269841270150e+04,
    +                        1.591801587301586005e+04,
    +                        2.024557142857142026e+04,
    +                        2.438911111111110949e+04,
    +                        2.811252380952380918e+04,
    +                        3.091819047619046614e+04,
    +                        3.250747619047618718e+04,
    +                        3.250747619047618718e+04,
    +                        3.091819047619046614e+04,
    +                        2.811252380952380918e+04,
    +                        2.438966666666666424e+04,
    +                        2.024557142857142026e+04,
    +                        1.591874603174601907e+04,
    +                        1.166809523809524035e+04,
    +                        7.697000000000000000e+03,
    +                        4.290158730158726030e+03,
    +                        1.362349206349189672e+03,
    +                        -1.094666666666673564e+03,
    +                        -3.093619047619046796e+03,
    +                        -4.533444444444450710e+03,
    +                        -5.302285714285715585e+03,
    +                        -5.504174603174604272e+03,
    +                        -5.259507936507935483e+03,
    +                        -4.843904761904761472e+03,
    +                        -4.325555555555557476e+03,
    +                        -3.636428571428570194e+03,
    +                        -2.702142857142859157e+03,
    +                        -1.550380952380948429e+03,
    +                        -3.720476190476193779e+02,
    +                        6.062857142857187682e+02,
    +                        1.280492063492064290e+03,
    +                        1.695222222222221490e+03,
    +                        1.935238095238095866e+03,
    +                        2.076793650793650613e+03,
    +                        2.124507936507936392e+03,
    +                        2.102238095238094957e+03,
    +                        2.009920634920635166e+03,
    +                        1.755761904761903224e+03,
    +                        1.149000000000000000e+03])
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    + + + \ No newline at end of file diff --git a/mapping/t1.html b/mapping/t1.html index c3bc8b5a..6a0f68d1 100644 --- a/mapping/t1.html +++ b/mapping/t1.html @@ -26,13 +26,95 @@

    Module ukat.mapping.t1

    Expand source code -
    import concurrent.futures
    -import nibabel as nib
    +
    import nibabel as nib
     import numpy as np
     import os
     import warnings
    -from tqdm import tqdm
    -from scipy.optimize import curve_fit
    +
    +from . import fitting
    +
    +
    +class T1Model(fitting.Model):
    +    def __init__(self, pixel_array, ti, parameters=2, mask=None, tss=0,
    +                 tss_axis=-2, multithread=True):
    +        """
    +        A class containing the T1 fitting model
    +
    +        Parameters
    +        ----------
    +        pixel_array : np.ndarray
    +            An array containing the signal from each voxel at each echo
    +            time with the last dimension being time i.e. the array needed to
    +            generate a 3D T1 map would have dimensions [x, y, z, TE].
    +        ti : np.ndarray
    +            An array of the inversion times used for the last dimension of the
    +            pixel_array. In milliseconds.
    +        parameters : {2, 3}, optional
    +            Default `2`
    +            The number of parameters to fit the data to. A two parameter fit
    +            will estimate S0 and T1 while a three parameter fit will also
    +            estimate the inversion efficiency.
    +        mask : np.ndarray, optional
    +            A boolean mask of the voxels to fit. Should be the shape of the
    +            desired T1 map rather than the raw data i.e. omit the time
    +            dimension.
    +        tss : float, optional
    +            Default 0
    +            The temporal slice spacing is the delay between acquisition of
    +            slices in a T1 map. Including this information means the
    +            inversion time is correct for each slice in a multi-slice T1
    +            map. In milliseconds.
    +        tss_axis : int, optional
    +            Default -2 i.e. last spatial axis
    +            The axis over which the temporal slice spacing is applied. This
    +            axis is relative to the full 4D pixel array i.e. tss_axis=-1
    +            would be along the TI axis and would be meaningless.
    +            If `pixel_array` is single slice (dimensions [x, y, TI]),
    +            then this should be set to None.
    +        multithread : bool, optional
    +            Default True
    +            If True, the fitting will be performed in parallel using all
    +            available cores
    +        """
    +        self.parameters = parameters
    +        self.tss = tss
    +        self.tss_axis = tss_axis
    +
    +        if np.min(pixel_array) < 0:
    +            self.mag_corr = True
    +        else:
    +            self.mag_corr = False
    +
    +        if self.parameters == 2:
    +            if self.mag_corr:
    +                super().__init__(pixel_array, ti, two_param_eq, mask,
    +                                 multithread)
    +            else:
    +                super().__init__(pixel_array, ti, two_param_abs_eq, mask,
    +                                 multithread)
    +            self.bounds = ([0, 0], [5000, 1000000000])
    +            self.initial_guess = [1000, 30000]
    +        elif self.parameters == 3:
    +            if self.mag_corr:
    +                super().__init__(pixel_array, ti, three_param_eq, mask,
    +                                 multithread)
    +            else:
    +                super().__init__(pixel_array, ti, three_param_abs_eq, mask,
    +                                 multithread)
    +            self.bounds = ([0, 0, 1], [5000, 1000000000, 2])
    +            self.initial_guess = [1000, 30000, 2]
    +        else:
    +            raise ValueError(f'Parameters can be 2 or 3 only. You specified '
    +                             f'{parameters}.')
    +
    +        self.generate_lists()
    +        if self.tss != 0:
    +            self._tss_correct_ti()
    +
    +    def _tss_correct_ti(self):
    +        slices = np.indices(self.map_shape)[self.tss_axis].ravel()
    +        for ind, (ti, slice) in enumerate(zip(self.x_list, slices)):
    +            self.x_list[ind] = np.array(ti) + self.tss * slice
     
     
     class T1:
    @@ -52,10 +134,16 @@ 

    Module ukat.mapping.t1

    pulse and 2 represents a 180 degree inversion eff_err : np.ndarray The certainty in the fit of `eff` + r2 : np.ndarray + The R-Squared value of the fit, values close to 1 indicate a good + fit, lower values indicate a poorer fit shape : tuple The shape of the T1 map n_ti : int The number of TI used to calculate the map + n_vox : int + The number of voxels in the map i.e. the product of all dimensions + apart from TI """ def __init__(self, pixel_array, inversion_list, affine, tss=0, tss_axis=-2, @@ -99,8 +187,8 @@

    Module ukat.mapping.t1

    molli : bool, optional Default False. Apply MOLLI corrections to T1. - multithread : bool, optional - Default True. + multithread : bool or 'auto', optional + Default 'auto'. If True, fitting will be distributed over all cores available on the node. If False, fitting will be carried out on a single thread. Multithreading is useful when calculating the T1 for a large @@ -109,18 +197,26 @@

    Module ukat.mapping.t1

    amounts of data e.g. a mean T1 signal decay over a ROI when the overheads of multithreading are more of a hindrance than the increase in speed distributing the calculation would generate. + 'auto' attempts to apply multithreading where appropriate based + on the number of voxels being fit. """ + assert multithread is True \ + or multithread is False \ + or multithread == 'auto', f'multithreaded must be True,' \ + f'False or auto. You entered ' \ + f'{multithread}' self.pixel_array = pixel_array self.shape = pixel_array.shape[:-1] self.dimensions = len(pixel_array.shape) self.n_ti = pixel_array.shape[-1] + self.n_vox = np.prod(self.shape) self.affine = affine # Generate a mask if there isn't one specified if mask is None: self.mask = np.ones(self.shape, dtype=bool) else: - self.mask = mask + self.mask = mask.astype(bool) # Don't process any nan values self.mask[np.isnan(np.sum(pixel_array, axis=-1))] = False self.inversion_list = inversion_list @@ -132,6 +228,11 @@

    Module ukat.mapping.t1

    self.tss = 0 self.parameters = parameters self.molli = molli + if multithread == 'auto': + if self.n_vox > 20: + multithread = True + else: + multithread = False self.multithread = multithread # Some sanity checks @@ -150,160 +251,43 @@

    Module ukat.mapping.t1

    warnings.warn('MOLLI requires a three parameter fit, ' 'using parameters=3.') + # Fit Data + fitting_model = T1Model(self.pixel_array, self.inversion_list, + self.parameters, self.mask, self.tss, + self.tss_axis, self.multithread) + popt, error, r2 = fitting.fit_image(fitting_model) + self.t1_map = popt[0] + self.m0_map = popt[1] + self.t1_err = error[0] + self.m0_err = error[1] + self.r2 = r2 - # Initialise output attributes - self.t1_map = np.zeros(self.shape) - self.t1_err = np.zeros(self.shape) - self.m0_map = np.zeros(self.shape) - self.m0_err = np.zeros(self.shape) - self.eff_map = np.zeros(self.shape) - self.eff_err = np.zeros(self.shape) - - # Fit data - if self.parameters == 2: - self.t1_map, self.t1_err, self.m0_map, self.m0_err = self.__fit__() - elif self.parameters == 3: - self.t1_map, self.t1_err, self.m0_map, self.m0_err, \ - self.eff_map, self.eff_err = self.__fit__() - else: - raise ValueError('Parameters can be 2 or 3 only. You specified ' - '{}'.format(self.parameters)) + if self.parameters == 3: + self.eff_map = popt[2] + self.eff_err = error[2] + + # Filter values that are very close to models upper bounds of T1 or + # M0 out. Not filtering based on eff as this should ideally be at + # the upper bound! + threshold = 0.999 # 99.9% of the upper bound + bounds_mask = ((self.t1_map > fitting_model.bounds[1][0] * threshold) | + (self.m0_map > fitting_model.bounds[1][1] * threshold)) + self.t1_map[bounds_mask] = 0 + self.m0_map[bounds_mask] = 0 + self.t1_err[bounds_mask] = 0 + self.m0_err[bounds_mask] = 0 + self.r2[bounds_mask] = 0 + if self.parameters == 3: + self.eff_map[bounds_mask] = 0 + self.eff_err[bounds_mask] = 0 + # Do MOLLI correction if self.molli: 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) - def __fit__(self): - n_vox = np.prod(self.shape) - # Initialise maps - t1_map = np.zeros(n_vox) - m0_map = np.zeros(n_vox) - t1_err = np.zeros(n_vox) - m0_err = np.zeros(n_vox) - if self.parameters == 3: - eff_map = np.zeros(n_vox) - eff_err = np.zeros(n_vox) - mask = self.mask.flatten() - signal = self.pixel_array.reshape(-1, self.n_ti) - slices = np.indices(self.shape)[self.tss_axis].ravel() - # Get indices of voxels to process - idx = np.argwhere(mask).squeeze() - - # Multithreaded method - if self.multithread: - with concurrent.futures.ProcessPoolExecutor() as pool: - with tqdm(total=idx.size) as progress: - futures = [] - - for ind in idx: - ti_slice_corrected = self.inversion_list + \ - slices[ind] * self.tss - future = pool.submit(self.__fit_signal__, - signal[ind, :], - ti_slice_corrected, - self.parameters) - future.add_done_callback(lambda p: progress.update()) - futures.append(future) - - results = [] - for future in futures: - result = future.result() - results.append(result) - - if self.parameters == 2: - t1_map[idx], t1_err[idx], \ - m0_map[idx], m0_err[idx] = [np.array(row) - for row in zip(*results)] - elif self.parameters == 3: - t1_map[idx], t1_err[idx], \ - m0_map[idx], m0_err[idx], \ - eff_map[idx], eff_err[idx] = [np.array(row) - for row in zip(*results)] - - # Single threaded method - else: - with tqdm(total=idx.size) as progress: - for ind in idx: - sig = signal[ind, :] - ti_slice_corrected = self.inversion_list + \ - slices[ind] * self.tss - if self.parameters == 2: - t1_map[ind], t1_err[ind], \ - m0_map[ind], m0_err[ind] = \ - self.__fit_signal__(sig, - ti_slice_corrected, - self.parameters) - elif self.parameters == 3: - t1_map[ind], t1_err[ind], \ - m0_map[ind], m0_err[ind], \ - eff_map[ind], eff_err[ind] = \ - self.__fit_signal__(sig, - ti_slice_corrected, - self.parameters) - progress.update(1) - - # Reshape results to raw data shape - t1_map = t1_map.reshape(self.shape) - m0_map = m0_map.reshape(self.shape) - t1_err = t1_err.reshape(self.shape) - m0_err = m0_err.reshape(self.shape) - - if self.parameters == 2: - return t1_map, t1_err, m0_map, m0_err - - elif self.parameters == 3: - eff_map = eff_map.reshape(self.shape) - eff_err = eff_err.reshape(self.shape) - return t1_map, t1_err, m0_map, m0_err, eff_map, eff_err - - def __fit_signal__(self, sig, t, parameters): - - # Initialise parameters and specify equation to fit to - if parameters == 2: - bounds = ([0, 0], [5000, 1000000000]) - initial_guess = [1000, 30000] - if sig.min() >= 0: - eq = two_param_abs_eq - else: - eq = two_param_eq - elif parameters == 3: - bounds = ([0, 0, 1], [5000, 1000000000, 2]) - initial_guess = [1000, 30000, 2] - if sig.min() >= 0: - eq = three_param_abs_eq - else: - eq = three_param_eq - - # Fit data to equation - try: - popt, pcov = curve_fit(eq, t, sig, - p0=initial_guess, bounds=bounds) - except RuntimeError: - popt = np.zeros(self.parameters) - pcov = np.zeros((self.parameters, self.parameters)) - - # Extract fits and errors from result variable - if popt[0] < bounds[1][0] - 1: - t1 = popt[0] - m0 = popt[1] - err = np.sqrt(np.diag(pcov)) - t1_err = err[0] - m0_err = err[1] - if self.parameters == 3: - eff = popt[2] - eff_err = err[2] - else: - t1, m0, t1_err, m0_err = 0, 0, 0, 0 - if self.parameters == 3: - eff, eff_err = 0, 0 - - if self.parameters == 2: - return t1, t1_err, m0, m0_err - elif self.parameters == 3: - return t1, t1_err, m0, m0_err, eff, eff_err - def r1_map(self): """ Generates the R1 map from the T1 map output by initialising this @@ -319,7 +303,10 @@

    Module ukat.mapping.t1

    An array containing the R1 map generated by the function with R1 measured in ms. """ - return np.nan_to_num(np.reciprocal(self.t1_map), posinf=0, neginf=0) + with np.errstate(divide='ignore'): + r1_map = np.nan_to_num(np.reciprocal(self.t1_map), posinf=0, + neginf=0) + return r1_map def to_nifti(self, output_directory=os.getcwd(), base_file_name='Output', maps='all'): @@ -335,13 +322,13 @@

    Module ukat.mapping.t1

    maps : list or 'all', optional List of maps to save to NIFTI. This should either the string "all" or a list of maps from ["t1", "t1_err", "m0", "m0_err", "eff", - "eff_err", "r1", "mask"] + "eff_err", "r1", "r2", "mask"] """ os.makedirs(output_directory, exist_ok=True) base_path = os.path.join(output_directory, base_file_name) if maps == 'all' or maps == ['all']: maps = ['t1', 't1_err', 'm0', 'm0_err', 'eff', 'eff_err', 'r1_map', - 'mask'] + 'r2', 'mask'] if isinstance(maps, list): for result in maps: if result == 't1' or result == 't1_map': @@ -371,6 +358,10 @@

    Module ukat.mapping.t1

    r1_nifti = nib.Nifti1Image(T1.r1_map(self), affine=self.affine) nib.save(r1_nifti, base_path + '_r1_map.nii.gz') + elif result == 'r2': + r2_nifti = nib.Nifti1Image(self.r2, + affine=self.affine) + nib.save(r2_nifti, base_path + '_r2.nii.gz') elif result == 'mask': mask_nifti = nib.Nifti1Image(self.mask.astype(np.uint16), affine=self.affine) @@ -402,7 +393,9 @@

    Module ukat.mapping.t1

    ------- signal: ndarray """ - return np.abs(m0 * (1 - 2 * np.exp(-t / t1))) + with np.errstate(divide='ignore'): + signal = np.abs(m0 * (1 - 2 * np.exp(-t / t1))) + return signal def two_param_eq(t, t1, m0): @@ -423,7 +416,9 @@

    Module ukat.mapping.t1

    ------- signal: ndarray """ - return m0 * (1 - 2 * np.exp(-t / t1)) + with np.errstate(divide='ignore'): + signal = m0 * (1 - 2 * np.exp(-t / t1)) + return signal def three_param_abs_eq(t, t1, m0, eff): @@ -447,7 +442,9 @@

    Module ukat.mapping.t1

    ------- signal: ndarray """ - return np.abs(m0 * (1 - eff * np.exp(-t / t1))) + with np.errstate(divide='ignore'): + signal = np.abs(m0 * (1 - eff * np.exp(-t / t1))) + return signal def three_param_eq(t, t1, m0, eff): @@ -471,7 +468,9 @@

    Module ukat.mapping.t1

    ------- signal: ndarray """ - return m0 * (1 - eff * np.exp(-t / t1)) + with np.errstate(divide='ignore'): + signal = m0 * (1 - eff * np.exp(-t / t1)) + return signal def magnitude_correct(pixel_array): @@ -641,7 +640,9 @@

    Returns

    ------- signal: ndarray """ - return np.abs(m0 * (1 - eff * np.exp(-t / t1)))
    + with np.errstate(divide='ignore'): + signal = np.abs(m0 * (1 - eff * np.exp(-t / t1))) + return signal
    @@ -692,7 +693,9 @@

    Returns

    ------- signal: ndarray """ - return m0 * (1 - eff * np.exp(-t / t1))
    + with np.errstate(divide='ignore'): + signal = m0 * (1 - eff * np.exp(-t / t1)) + return signal
    @@ -737,7 +740,9 @@

    Returns

    ------- signal: ndarray """ - return np.abs(m0 * (1 - 2 * np.exp(-t / t1)))
    + with np.errstate(divide='ignore'): + signal = np.abs(m0 * (1 - 2 * np.exp(-t / t1))) + return signal
    @@ -782,7 +787,9 @@

    Returns

    ------- signal: ndarray """ - return m0 * (1 - 2 * np.exp(-t / t1))
    + with np.errstate(divide='ignore'): + signal = m0 * (1 - 2 * np.exp(-t / t1)) + return signal @@ -810,10 +817,16 @@

    Classes

    pulse and 2 represents a 180 degree inversion
    eff_err : np.ndarray
    The certainty in the fit of eff
    +
    r2 : np.ndarray
    +
    The R-Squared value of the fit, values close to 1 indicate a good +fit, lower values indicate a poorer fit
    shape : tuple
    The shape of the T1 map
    n_ti : int
    The number of TI used to calculate the map
    +
    n_vox : int
    +
    The number of voxels in the map i.e. the product of all dimensions +apart from TI

    Initialise a T1 class instance.

    Parameters

    @@ -853,8 +866,8 @@

    Parameters

    molli : bool, optional
    Default False. Apply MOLLI corrections to T1.
    -
    multithread : bool, optional
    -
    Default True. +
    multithread : bool or 'auto', optional
    +
    Default 'auto'. If True, fitting will be distributed over all cores available on the node. If False, fitting will be carried out on a single thread. Multithreading is useful when calculating the T1 for a large @@ -862,7 +875,9 @@

    Parameters

    Turning off multithreading can be useful when fitting very small amounts of data e.g. a mean T1 signal decay over a ROI when the overheads of multithreading are more of a hindrance than the -increase in speed distributing the calculation would generate.
    +increase in speed distributing the calculation would generate. +'auto' attempts to apply multithreading where appropriate based +on the number of voxels being fit.
    @@ -885,10 +900,16 @@

    Parameters

    pulse and 2 represents a 180 degree inversion eff_err : np.ndarray The certainty in the fit of `eff` + r2 : np.ndarray + The R-Squared value of the fit, values close to 1 indicate a good + fit, lower values indicate a poorer fit shape : tuple The shape of the T1 map n_ti : int The number of TI used to calculate the map + n_vox : int + The number of voxels in the map i.e. the product of all dimensions + apart from TI """ def __init__(self, pixel_array, inversion_list, affine, tss=0, tss_axis=-2, @@ -932,8 +953,8 @@

    Parameters

    molli : bool, optional Default False. Apply MOLLI corrections to T1. - multithread : bool, optional - Default True. + multithread : bool or 'auto', optional + Default 'auto'. If True, fitting will be distributed over all cores available on the node. If False, fitting will be carried out on a single thread. Multithreading is useful when calculating the T1 for a large @@ -942,18 +963,26 @@

    Parameters

    amounts of data e.g. a mean T1 signal decay over a ROI when the overheads of multithreading are more of a hindrance than the increase in speed distributing the calculation would generate. + 'auto' attempts to apply multithreading where appropriate based + on the number of voxels being fit. """ + assert multithread is True \ + or multithread is False \ + or multithread == 'auto', f'multithreaded must be True,' \ + f'False or auto. You entered ' \ + f'{multithread}' self.pixel_array = pixel_array self.shape = pixel_array.shape[:-1] self.dimensions = len(pixel_array.shape) self.n_ti = pixel_array.shape[-1] + self.n_vox = np.prod(self.shape) self.affine = affine # Generate a mask if there isn't one specified if mask is None: self.mask = np.ones(self.shape, dtype=bool) else: - self.mask = mask + self.mask = mask.astype(bool) # Don't process any nan values self.mask[np.isnan(np.sum(pixel_array, axis=-1))] = False self.inversion_list = inversion_list @@ -965,6 +994,11 @@

    Parameters

    self.tss = 0 self.parameters = parameters self.molli = molli + if multithread == 'auto': + if self.n_vox > 20: + multithread = True + else: + multithread = False self.multithread = multithread # Some sanity checks @@ -983,160 +1017,43 @@

    Parameters

    warnings.warn('MOLLI requires a three parameter fit, ' 'using parameters=3.') + # Fit Data + fitting_model = T1Model(self.pixel_array, self.inversion_list, + self.parameters, self.mask, self.tss, + self.tss_axis, self.multithread) + popt, error, r2 = fitting.fit_image(fitting_model) + self.t1_map = popt[0] + self.m0_map = popt[1] + self.t1_err = error[0] + self.m0_err = error[1] + self.r2 = r2 - # Initialise output attributes - self.t1_map = np.zeros(self.shape) - self.t1_err = np.zeros(self.shape) - self.m0_map = np.zeros(self.shape) - self.m0_err = np.zeros(self.shape) - self.eff_map = np.zeros(self.shape) - self.eff_err = np.zeros(self.shape) - - # Fit data - if self.parameters == 2: - self.t1_map, self.t1_err, self.m0_map, self.m0_err = self.__fit__() - elif self.parameters == 3: - self.t1_map, self.t1_err, self.m0_map, self.m0_err, \ - self.eff_map, self.eff_err = self.__fit__() - else: - raise ValueError('Parameters can be 2 or 3 only. You specified ' - '{}'.format(self.parameters)) + if self.parameters == 3: + self.eff_map = popt[2] + self.eff_err = error[2] + + # Filter values that are very close to models upper bounds of T1 or + # M0 out. Not filtering based on eff as this should ideally be at + # the upper bound! + threshold = 0.999 # 99.9% of the upper bound + bounds_mask = ((self.t1_map > fitting_model.bounds[1][0] * threshold) | + (self.m0_map > fitting_model.bounds[1][1] * threshold)) + self.t1_map[bounds_mask] = 0 + self.m0_map[bounds_mask] = 0 + self.t1_err[bounds_mask] = 0 + self.m0_err[bounds_mask] = 0 + self.r2[bounds_mask] = 0 + if self.parameters == 3: + self.eff_map[bounds_mask] = 0 + self.eff_err[bounds_mask] = 0 + # Do MOLLI correction if self.molli: 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) - def __fit__(self): - n_vox = np.prod(self.shape) - # Initialise maps - t1_map = np.zeros(n_vox) - m0_map = np.zeros(n_vox) - t1_err = np.zeros(n_vox) - m0_err = np.zeros(n_vox) - if self.parameters == 3: - eff_map = np.zeros(n_vox) - eff_err = np.zeros(n_vox) - mask = self.mask.flatten() - signal = self.pixel_array.reshape(-1, self.n_ti) - slices = np.indices(self.shape)[self.tss_axis].ravel() - # Get indices of voxels to process - idx = np.argwhere(mask).squeeze() - - # Multithreaded method - if self.multithread: - with concurrent.futures.ProcessPoolExecutor() as pool: - with tqdm(total=idx.size) as progress: - futures = [] - - for ind in idx: - ti_slice_corrected = self.inversion_list + \ - slices[ind] * self.tss - future = pool.submit(self.__fit_signal__, - signal[ind, :], - ti_slice_corrected, - self.parameters) - future.add_done_callback(lambda p: progress.update()) - futures.append(future) - - results = [] - for future in futures: - result = future.result() - results.append(result) - - if self.parameters == 2: - t1_map[idx], t1_err[idx], \ - m0_map[idx], m0_err[idx] = [np.array(row) - for row in zip(*results)] - elif self.parameters == 3: - t1_map[idx], t1_err[idx], \ - m0_map[idx], m0_err[idx], \ - eff_map[idx], eff_err[idx] = [np.array(row) - for row in zip(*results)] - - # Single threaded method - else: - with tqdm(total=idx.size) as progress: - for ind in idx: - sig = signal[ind, :] - ti_slice_corrected = self.inversion_list + \ - slices[ind] * self.tss - if self.parameters == 2: - t1_map[ind], t1_err[ind], \ - m0_map[ind], m0_err[ind] = \ - self.__fit_signal__(sig, - ti_slice_corrected, - self.parameters) - elif self.parameters == 3: - t1_map[ind], t1_err[ind], \ - m0_map[ind], m0_err[ind], \ - eff_map[ind], eff_err[ind] = \ - self.__fit_signal__(sig, - ti_slice_corrected, - self.parameters) - progress.update(1) - - # Reshape results to raw data shape - t1_map = t1_map.reshape(self.shape) - m0_map = m0_map.reshape(self.shape) - t1_err = t1_err.reshape(self.shape) - m0_err = m0_err.reshape(self.shape) - - if self.parameters == 2: - return t1_map, t1_err, m0_map, m0_err - - elif self.parameters == 3: - eff_map = eff_map.reshape(self.shape) - eff_err = eff_err.reshape(self.shape) - return t1_map, t1_err, m0_map, m0_err, eff_map, eff_err - - def __fit_signal__(self, sig, t, parameters): - - # Initialise parameters and specify equation to fit to - if parameters == 2: - bounds = ([0, 0], [5000, 1000000000]) - initial_guess = [1000, 30000] - if sig.min() >= 0: - eq = two_param_abs_eq - else: - eq = two_param_eq - elif parameters == 3: - bounds = ([0, 0, 1], [5000, 1000000000, 2]) - initial_guess = [1000, 30000, 2] - if sig.min() >= 0: - eq = three_param_abs_eq - else: - eq = three_param_eq - - # Fit data to equation - try: - popt, pcov = curve_fit(eq, t, sig, - p0=initial_guess, bounds=bounds) - except RuntimeError: - popt = np.zeros(self.parameters) - pcov = np.zeros((self.parameters, self.parameters)) - - # Extract fits and errors from result variable - if popt[0] < bounds[1][0] - 1: - t1 = popt[0] - m0 = popt[1] - err = np.sqrt(np.diag(pcov)) - t1_err = err[0] - m0_err = err[1] - if self.parameters == 3: - eff = popt[2] - eff_err = err[2] - else: - t1, m0, t1_err, m0_err = 0, 0, 0, 0 - if self.parameters == 3: - eff, eff_err = 0, 0 - - if self.parameters == 2: - return t1, t1_err, m0, m0_err - elif self.parameters == 3: - return t1, t1_err, m0, m0_err, eff, eff_err - def r1_map(self): """ Generates the R1 map from the T1 map output by initialising this @@ -1152,7 +1069,10 @@

    Parameters

    An array containing the R1 map generated by the function with R1 measured in ms. """ - return np.nan_to_num(np.reciprocal(self.t1_map), posinf=0, neginf=0) + with np.errstate(divide='ignore'): + r1_map = np.nan_to_num(np.reciprocal(self.t1_map), posinf=0, + neginf=0) + return r1_map def to_nifti(self, output_directory=os.getcwd(), base_file_name='Output', maps='all'): @@ -1168,13 +1088,13 @@

    Parameters

    maps : list or 'all', optional List of maps to save to NIFTI. This should either the string "all" or a list of maps from ["t1", "t1_err", "m0", "m0_err", "eff", - "eff_err", "r1", "mask"] + "eff_err", "r1", "r2", "mask"] """ os.makedirs(output_directory, exist_ok=True) base_path = os.path.join(output_directory, base_file_name) if maps == 'all' or maps == ['all']: maps = ['t1', 't1_err', 'm0', 'm0_err', 'eff', 'eff_err', 'r1_map', - 'mask'] + 'r2', 'mask'] if isinstance(maps, list): for result in maps: if result == 't1' or result == 't1_map': @@ -1204,6 +1124,10 @@

    Parameters

    r1_nifti = nib.Nifti1Image(T1.r1_map(self), affine=self.affine) nib.save(r1_nifti, base_path + '_r1_map.nii.gz') + elif result == 'r2': + r2_nifti = nib.Nifti1Image(self.r2, + affine=self.affine) + nib.save(r2_nifti, base_path + '_r2.nii.gz') elif result == 'mask': mask_nifti = nib.Nifti1Image(self.mask.astype(np.uint16), affine=self.affine) @@ -1251,7 +1175,10 @@

    Returns

    An array containing the R1 map generated by the function with R1 measured in ms. """ - return np.nan_to_num(np.reciprocal(self.t1_map), posinf=0, neginf=0) + with np.errstate(divide='ignore'): + r1_map = np.nan_to_num(np.reciprocal(self.t1_map), posinf=0, + neginf=0) + return r1_map
    @@ -1269,7 +1196,7 @@

    Parameters

    maps : list or 'all', optional
    List of maps to save to NIFTI. This should either the string "all" or a list of maps from ["t1", "t1_err", "m0", "m0_err", "eff", -"eff_err", "r1", "mask"]
    +"eff_err", "r1", "r2", "mask"]
    @@ -1289,13 +1216,13 @@

    Parameters

    maps : list or 'all', optional List of maps to save to NIFTI. This should either the string "all" or a list of maps from ["t1", "t1_err", "m0", "m0_err", "eff", - "eff_err", "r1", "mask"] + "eff_err", "r1", "r2", "mask"] """ os.makedirs(output_directory, exist_ok=True) base_path = os.path.join(output_directory, base_file_name) if maps == 'all' or maps == ['all']: maps = ['t1', 't1_err', 'm0', 'm0_err', 'eff', 'eff_err', 'r1_map', - 'mask'] + 'r2', 'mask'] if isinstance(maps, list): for result in maps: if result == 't1' or result == 't1_map': @@ -1325,6 +1252,10 @@

    Parameters

    r1_nifti = nib.Nifti1Image(T1.r1_map(self), affine=self.affine) nib.save(r1_nifti, base_path + '_r1_map.nii.gz') + elif result == 'r2': + r2_nifti = nib.Nifti1Image(self.r2, + affine=self.affine) + nib.save(r2_nifti, base_path + '_r2.nii.gz') elif result == 'mask': mask_nifti = nib.Nifti1Image(self.mask.astype(np.uint16), affine=self.affine) @@ -1340,6 +1271,147 @@

    Parameters

    +
    +class T1Model +(pixel_array, ti, parameters=2, mask=None, tss=0, tss_axis=-2, multithread=True) +
    +
    +

    A class containing the T1 fitting model

    +

    Parameters

    +
    +
    pixel_array : np.ndarray
    +
    An array containing the signal from each voxel at each echo +time with the last dimension being time i.e. the array needed to +generate a 3D T1 map would have dimensions [x, y, z, TE].
    +
    ti : np.ndarray
    +
    An array of the inversion times used for the last dimension of the +pixel_array. In milliseconds.
    +
    parameters : {2, 3}, optional
    +
    Default 2 +The number of parameters to fit the data to. A two parameter fit +will estimate S0 and T1 while a three parameter fit will also +estimate the inversion efficiency.
    +
    mask : np.ndarray, optional
    +
    A boolean mask of the voxels to fit. Should be the shape of the +desired T1 map rather than the raw data i.e. omit the time +dimension.
    +
    tss : float, optional
    +
    Default 0 +The temporal slice spacing is the delay between acquisition of +slices in a T1 map. Including this information means the +inversion time is correct for each slice in a multi-slice T1 +map. In milliseconds.
    +
    tss_axis : int, optional
    +
    Default -2 i.e. last spatial axis +The axis over which the temporal slice spacing is applied. This +axis is relative to the full 4D pixel array i.e. tss_axis=-1 +would be along the TI axis and would be meaningless. +If pixel_array is single slice (dimensions [x, y, TI]), +then this should be set to None.
    +
    multithread : bool, optional
    +
    Default True +If True, the fitting will be performed in parallel using all +available cores
    +
    +
    + +Expand source code + +
    class T1Model(fitting.Model):
    +    def __init__(self, pixel_array, ti, parameters=2, mask=None, tss=0,
    +                 tss_axis=-2, multithread=True):
    +        """
    +        A class containing the T1 fitting model
    +
    +        Parameters
    +        ----------
    +        pixel_array : np.ndarray
    +            An array containing the signal from each voxel at each echo
    +            time with the last dimension being time i.e. the array needed to
    +            generate a 3D T1 map would have dimensions [x, y, z, TE].
    +        ti : np.ndarray
    +            An array of the inversion times used for the last dimension of the
    +            pixel_array. In milliseconds.
    +        parameters : {2, 3}, optional
    +            Default `2`
    +            The number of parameters to fit the data to. A two parameter fit
    +            will estimate S0 and T1 while a three parameter fit will also
    +            estimate the inversion efficiency.
    +        mask : np.ndarray, optional
    +            A boolean mask of the voxels to fit. Should be the shape of the
    +            desired T1 map rather than the raw data i.e. omit the time
    +            dimension.
    +        tss : float, optional
    +            Default 0
    +            The temporal slice spacing is the delay between acquisition of
    +            slices in a T1 map. Including this information means the
    +            inversion time is correct for each slice in a multi-slice T1
    +            map. In milliseconds.
    +        tss_axis : int, optional
    +            Default -2 i.e. last spatial axis
    +            The axis over which the temporal slice spacing is applied. This
    +            axis is relative to the full 4D pixel array i.e. tss_axis=-1
    +            would be along the TI axis and would be meaningless.
    +            If `pixel_array` is single slice (dimensions [x, y, TI]),
    +            then this should be set to None.
    +        multithread : bool, optional
    +            Default True
    +            If True, the fitting will be performed in parallel using all
    +            available cores
    +        """
    +        self.parameters = parameters
    +        self.tss = tss
    +        self.tss_axis = tss_axis
    +
    +        if np.min(pixel_array) < 0:
    +            self.mag_corr = True
    +        else:
    +            self.mag_corr = False
    +
    +        if self.parameters == 2:
    +            if self.mag_corr:
    +                super().__init__(pixel_array, ti, two_param_eq, mask,
    +                                 multithread)
    +            else:
    +                super().__init__(pixel_array, ti, two_param_abs_eq, mask,
    +                                 multithread)
    +            self.bounds = ([0, 0], [5000, 1000000000])
    +            self.initial_guess = [1000, 30000]
    +        elif self.parameters == 3:
    +            if self.mag_corr:
    +                super().__init__(pixel_array, ti, three_param_eq, mask,
    +                                 multithread)
    +            else:
    +                super().__init__(pixel_array, ti, three_param_abs_eq, mask,
    +                                 multithread)
    +            self.bounds = ([0, 0, 1], [5000, 1000000000, 2])
    +            self.initial_guess = [1000, 30000, 2]
    +        else:
    +            raise ValueError(f'Parameters can be 2 or 3 only. You specified '
    +                             f'{parameters}.')
    +
    +        self.generate_lists()
    +        if self.tss != 0:
    +            self._tss_correct_ti()
    +
    +    def _tss_correct_ti(self):
    +        slices = np.indices(self.map_shape)[self.tss_axis].ravel()
    +        for ind, (ti, slice) in enumerate(zip(self.x_list, slices)):
    +            self.x_list[ind] = np.array(ti) + self.tss * slice
    +
    +

    Ancestors

    + +

    Inherited members

    + +
    @@ -1372,6 +1444,9 @@

    T1
  • to_nifti
  • +
  • +

    T1Model

    +
  • diff --git a/mapping/t2.html b/mapping/t2.html index 1c5475cf..5570ad6a 100644 --- a/mapping/t2.html +++ b/mapping/t2.html @@ -27,11 +27,79 @@

    Module ukat.mapping.t2

    Expand source code
    import os
    +
     import nibabel as nib
     import numpy as np
    -import concurrent.futures
    -from tqdm import tqdm
    -from scipy.optimize import curve_fit
    +
    +from . import fitting
    +
    +
    +class T2Model(fitting.Model):
    +    def __init__(self, pixel_array, te, method='2p_exp', mask=None,
    +                 multithread=True):
    +        """
    +        A class containing the T2 fitting model
    +
    +        Parameters
    +        ----------
    +        pixel_array : np.ndarray
    +            An array containing the signal from each voxel at each echo
    +            time with the last dimension being time i.e. the array needed to
    +            generate a 3D T2 map would have dimensions [x, y, z, TE].
    +        te : np.ndarray
    +            An array of the echo times used for the last dimension of the
    +            pixel_array. In milliseconds.
    +        method : {'2p_exp', '3p_exp'}, optional
    +            Default '2p_exp'
    +            The model the data is fit to. 2p_exp uses a two parameter
    +            exponential model (S = S0 * exp(-t / T2)) whereas 3p_exp uses a
    +            three parameter exponential model (S = S0 * exp(-t / T2) + b) to
    +            fit for noise/very long T2 components of the signal.
    +        mask : np.ndarray, optional
    +            A boolean mask of the voxels to fit. Should be the shape of the
    +            desired T2 map rather than the raw data i.e. omit the time
    +            dimension.
    +        multithread : bool, optional
    +            Default True
    +            If True, the fitting will be performed in parallel using all
    +            available cores
    +        """
    +        self.method = method
    +
    +        if self.method == '2p_exp':
    +            super().__init__(pixel_array, te, two_param_eq, mask, multithread)
    +            self.bounds = ([0, 0], [1000, 100000000])
    +            self.initial_guess = [20, 10000]
    +        elif self.method == '3p_exp':
    +            super().__init__(pixel_array, te, three_param_eq, mask,
    +                             multithread)
    +            self.bounds = ([0, 0, 0], [1000, 100000000, 1000000])
    +            self.initial_guess = [20, 10000, 500]
    +
    +        self.generate_lists()
    +
    +    def threshold_noise(self, threshold=0):
    +        """
    +        Remove voxel values below a certain threshold from the fitting
    +        process, useful if long echo times have been collected and thus
    +        thermal noise is being measured below a certain threshold rather
    +        than the T2 decay.
    +
    +        Parameters
    +        ----------
    +        threshold : float, optional
    +            Default 0
    +            The threshold below which to remove values
    +        """
    +        for ind, (sig, te, p0) in enumerate(zip(self.signal_list,
    +                                                self.x_list,
    +                                                self.p0_list)):
    +            self.signal_list[ind] = np.array(
    +                [x for (x, b) in zip(sig, np.array(sig) > threshold) if b])
    +            self.x_list[ind] = np.array(
    +                [x for (x, b) in zip(te, np.array(sig) > threshold) if b])
    +            self.p0_list[ind] = np.array(
    +                [x for (x, b) in zip(p0, np.array(sig) > threshold) if b])
     
     
     class T2:
    @@ -46,6 +114,9 @@ 

    Module ukat.mapping.t2

    The estimated M0 values m0_err : np.ndarray The certainty in the fit of `m0` + r2 : np.ndarray + The R-Squared value of the fit, values close to 1 indicate a good + fit, lower values indicate a poorer fit shape : tuple The shape of the T2 map n_te : int @@ -99,13 +170,14 @@

    Module ukat.mapping.t2

    'number of time frames on the last axis ' \ 'of pixel_array' assert multithread is True \ - or multithread is False \ - or multithread == 'auto', 'multithreaded must be True, ' \ - 'False or auto. You entered {}' \ - .format(multithread) + or multithread is False \ + or multithread == 'auto', f'multithreaded must be True,' \ + f'False or auto. You entered ' \ + f'{multithread}' + if method != '2p_exp' and method != '3p_exp': - raise ValueError('method can be 2p_exp or 3p_exp only. You ' - 'specified {}'.format(method)) + raise ValueError(f'method can be 2p_exp or 3p_exp only. You ' + f'specified {method}') self.pixel_array = pixel_array self.shape = pixel_array.shape[:-1] @@ -116,8 +188,9 @@

    Module ukat.mapping.t2

    if mask is None: self.mask = np.ones(self.shape, dtype=bool) else: - self.mask = mask - # Don't process any nan values + self.mask = mask.astype(bool) + + # Don't process any nan values self.mask[np.isnan(np.sum(pixel_array, axis=-1))] = False self.noise_threshold = noise_threshold self.method = method @@ -131,163 +204,40 @@

    Module ukat.mapping.t2

    self.multithread = multithread # Fit data - if self.method == '2p_exp': - self.t2_map, self.t2_err, \ - self.m0_map, self.m0_err \ - = self.__fit__() - elif self.method == '3p_exp': - self.t2_map, self.t2_err, \ - self.m0_map, self.m0_err, \ - self.b_map, self.b_err \ - = self.__fit__() - - def __fit__(self): - - # Initialise maps - t2_map = np.zeros(self.n_vox) - t2_err = np.zeros(self.n_vox) - m0_map = np.zeros(self.n_vox) - m0_err = np.zeros(self.n_vox) - b_map = np.zeros(self.n_vox) - b_err = np.zeros(self.n_vox) - mask = self.mask.flatten() - signal = self.pixel_array.reshape(-1, self.n_te) - # Get indices of voxels to process - idx = np.argwhere(mask).squeeze() - - # Multithreaded method - if self.multithread: - with concurrent.futures.ProcessPoolExecutor() as pool: - with tqdm(total=idx.size) as progress: - futures = [] - - for ind in idx: - signal_thresh = signal[ind, :][ - signal[ind, :] > self.noise_threshold] - echo_list_thresh = self.echo_list[ - signal[ind, :] > self.noise_threshold] - future = pool.submit(self.__fit_signal__, - signal_thresh, - echo_list_thresh) - future.add_done_callback(lambda p: progress.update()) - futures.append(future) - - results = [] - for future in futures: - result = future.result() - results.append(result) - - if self.method == '2p_exp': - t2_map[idx], t2_err[idx], m0_map[idx], m0_err[idx] = [np.array( - row) for row in zip(*results)] - elif self.method == '3p_exp': - t2_map[idx], t2_err[idx], \ - m0_map[idx], m0_err[idx], \ - b_map[idx], b_err[idx] = \ - [np.array(row) for row in zip(*results)] - - # Single threaded method - else: - with tqdm(total=idx.size) as progress: - for ind in idx: - signal_thresh = signal[ind, :][ - signal[ind, :] > self.noise_threshold] - echo_list_thresh = self.echo_list[ - signal[ind, :] > self.noise_threshold] - if self.method == '2p_exp': - t2_map[ind], t2_err[ind], \ - m0_map[ind], m0_err[ind] \ - = self.__fit_signal__(signal_thresh, - echo_list_thresh) - elif self.method == '3p_exp': - t2_map[ind], t2_err[ind], \ - m0_map[ind], m0_err[ind], \ - b_map[ind], b_err[ind] \ - = self.__fit_signal__(signal_thresh, - echo_list_thresh) - progress.update(1) - - # Reshape results to raw data shape - t2_map = t2_map.reshape(self.shape) - t2_err = t2_err.reshape(self.shape) - m0_map = m0_map.reshape(self.shape) - m0_err = m0_err.reshape(self.shape) - - if self.method == '2p_exp': - return t2_map, t2_err, m0_map, m0_err - elif self.method == '3p_exp': - b_map = b_map.reshape(self.shape) - b_err = b_err.reshape(self.shape) - return t2_map, t2_err, m0_map, m0_err, b_map, b_err - - def __fit_signal__(self, sig, te): - - # Initialise parameters - if self.method == '2p_exp': - eq = two_param_eq - bounds = ([0, 0], [1000, 100000000]) - initial_guess = [20, 10000] - elif self.method == '3p_exp': - eq = three_param_eq - bounds = ([0, 0, 0], [1000, 100000000, 1000000]) - initial_guess = [20, 10000, 500] - - # Fit data to equation - try: - popt, pcov = curve_fit(eq, te, sig, p0=initial_guess, - bounds=bounds) - except (RuntimeError, ValueError): - popt = np.zeros(3) - pcov = np.zeros((3, 3)) - - # Extract fits and errors from result variables - if self.method == '2p_exp': - if popt[0] < bounds[1][0] - 1: - t2 = popt[0] - m0 = popt[1] - err = np.sqrt(np.diag(pcov)) - t2_err = err[0] - m0_err = err[1] - else: - t2, m0, t2_err, m0_err = 0, 0, 0, 0 - - return t2, t2_err, m0, m0_err - - elif self.method == '3p_exp': - if popt[0] < bounds[1][0] - 1: - t2 = popt[0] - m0 = popt[1] - b = popt[2] - err = np.sqrt(np.diag(pcov)) - t2_err = err[0] - m0_err = err[1] - b_err = err[2] - else: - t2, m0, t2_err, m0_err, b, b_err = 0, 0, 0, 0, 0, 0 - - return t2, t2_err, m0, m0_err, b, b_err - - def r2_map(self): - """ - Generates the R2 map from the T2 map output by initialising this - class. - - Parameters - ---------- - See class attributes in __init__ - - Returns - ------- - r2 : np.ndarray - An array containing the R2 map generated - by the function with R2 measured in ms. - """ - return np.reciprocal(self.t2_map) + fitting_model = T2Model(self.pixel_array, self.echo_list, + self.method, self.mask, self.multithread) + + if self.noise_threshold > 0: + fitting_model.threshold_noise(self.noise_threshold) + popt, error, r2 = fitting.fit_image(fitting_model) + self.t2_map = popt[0] + self.m0_map = popt[1] + self.t2_err = error[0] + self.m0_err = error[1] + self.r2 = r2 + + if self.method == '3p_exp': + self.b_map = popt[2] + self.b_err = error[2] + + # Filter values that are very close to models upper bounds of T2 or + # M0 out. + threshold = 0.999 # 99.9% of the upper bound + bounds_mask = ((self.t2_map > fitting_model.bounds[1][0] * threshold) | + (self.m0_map > fitting_model.bounds[1][1] * threshold)) + self.t2_map[bounds_mask] = 0 + self.m0_map[bounds_mask] = 0 + self.t2_err[bounds_mask] = 0 + self.m0_err[bounds_mask] = 0 + self.r2[bounds_mask] = 0 + if self.method == '3p_exp': + self.b_map[bounds_mask] = 0 + self.b_err[bounds_mask] = 0 def to_nifti(self, output_directory=os.getcwd(), base_file_name='Output', maps='all'): """Exports some of the T2 class attributes to NIFTI. - + Parameters ---------- output_directory : string, optional @@ -304,6 +254,9 @@

    Module ukat.mapping.t2

    base_path = os.path.join(output_directory, base_file_name) if maps == 'all' or maps == ['all']: maps = ['t2', 't2_err', 'm0', 'm0_err', 'r2', 'mask'] + if self.method == '3p_exp': + maps.append('b') + maps.append('b_err') if isinstance(maps, list): for result in maps: if result == 't2' or result == 't2_map': @@ -321,19 +274,26 @@

    Module ukat.mapping.t2

    affine=self.affine) nib.save(m0_err_nifti, base_path + '_m0_err.nii.gz') elif result == 'r2' or result == 'r2_map': - r2_nifti = nib.Nifti1Image(T2.r2_map(self), + r2_nifti = nib.Nifti1Image(self.r2, affine=self.affine) nib.save(r2_nifti, base_path + '_r2_map.nii.gz') elif result == 'mask': mask_nifti = nib.Nifti1Image(self.mask.astype(np.uint16), affine=self.affine) nib.save(mask_nifti, base_path + '_mask.nii.gz') + elif result == 'b' or result == 'b_map': + b_nifti = nib.Nifti1Image(self.b_map, + affine=self.affine) + nib.save(b_nifti, base_path + '_b_map.nii.gz') + elif result == 'b_err': + b_err_nifti = nib.Nifti1Image(self.b_err, + affine=self.affine) + nib.save(b_err_nifti, base_path + '_b_err.nii.gz') else: raise ValueError('No NIFTI file saved. The variable "maps" ' 'should be "all" or a list of maps from ' '"["t2", "t2_err", "m0", "m0_err", "r2", ' '"mask"]".') - return @@ -356,7 +316,9 @@

    Module ukat.mapping.t2

    signal: np.ndarray The expected signal """ - return np.sqrt(np.square(m0 * np.exp(-t / t2))) + with np.errstate(divide='ignore'): + signal = m0 * np.exp(-t / t2) + return signal def three_param_eq(t, t2, m0, b): @@ -380,7 +342,9 @@

    Module ukat.mapping.t2

    signal: np.ndarray The expected signal """ - return np.sqrt(np.square(m0 * np.exp(-t / t2) + b))
    + with np.errstate(divide='ignore'): + signal = m0 * np.exp(-t / t2) + b + return signal
    @@ -437,7 +401,9 @@

    Returns

    signal: np.ndarray The expected signal """ - return np.sqrt(np.square(m0 * np.exp(-t / t2) + b)) + with np.errstate(divide='ignore'): + signal = m0 * np.exp(-t / t2) + b + return signal
    @@ -483,7 +449,9 @@

    Returns

    signal: np.ndarray The expected signal """ - return np.sqrt(np.square(m0 * np.exp(-t / t2)))
    + with np.errstate(divide='ignore'): + signal = m0 * np.exp(-t / t2) + return signal @@ -506,6 +474,9 @@

    Classes

    The estimated M0 values
    m0_err : np.ndarray
    The certainty in the fit of m0
    +
    r2 : np.ndarray
    +
    The R-Squared value of the fit, values close to 1 indicate a good +fit, lower values indicate a poorer fit
    shape : tuple
    The shape of the T2 map
    n_te : int
    @@ -565,6 +536,9 @@

    Parameters

    The estimated M0 values m0_err : np.ndarray The certainty in the fit of `m0` + r2 : np.ndarray + The R-Squared value of the fit, values close to 1 indicate a good + fit, lower values indicate a poorer fit shape : tuple The shape of the T2 map n_te : int @@ -618,13 +592,14 @@

    Parameters

    'number of time frames on the last axis ' \ 'of pixel_array' assert multithread is True \ - or multithread is False \ - or multithread == 'auto', 'multithreaded must be True, ' \ - 'False or auto. You entered {}' \ - .format(multithread) + or multithread is False \ + or multithread == 'auto', f'multithreaded must be True,' \ + f'False or auto. You entered ' \ + f'{multithread}' + if method != '2p_exp' and method != '3p_exp': - raise ValueError('method can be 2p_exp or 3p_exp only. You ' - 'specified {}'.format(method)) + raise ValueError(f'method can be 2p_exp or 3p_exp only. You ' + f'specified {method}') self.pixel_array = pixel_array self.shape = pixel_array.shape[:-1] @@ -635,8 +610,9 @@

    Parameters

    if mask is None: self.mask = np.ones(self.shape, dtype=bool) else: - self.mask = mask - # Don't process any nan values + self.mask = mask.astype(bool) + + # Don't process any nan values self.mask[np.isnan(np.sum(pixel_array, axis=-1))] = False self.noise_threshold = noise_threshold self.method = method @@ -650,163 +626,40 @@

    Parameters

    self.multithread = multithread # Fit data - if self.method == '2p_exp': - self.t2_map, self.t2_err, \ - self.m0_map, self.m0_err \ - = self.__fit__() - elif self.method == '3p_exp': - self.t2_map, self.t2_err, \ - self.m0_map, self.m0_err, \ - self.b_map, self.b_err \ - = self.__fit__() - - def __fit__(self): - - # Initialise maps - t2_map = np.zeros(self.n_vox) - t2_err = np.zeros(self.n_vox) - m0_map = np.zeros(self.n_vox) - m0_err = np.zeros(self.n_vox) - b_map = np.zeros(self.n_vox) - b_err = np.zeros(self.n_vox) - mask = self.mask.flatten() - signal = self.pixel_array.reshape(-1, self.n_te) - # Get indices of voxels to process - idx = np.argwhere(mask).squeeze() - - # Multithreaded method - if self.multithread: - with concurrent.futures.ProcessPoolExecutor() as pool: - with tqdm(total=idx.size) as progress: - futures = [] - - for ind in idx: - signal_thresh = signal[ind, :][ - signal[ind, :] > self.noise_threshold] - echo_list_thresh = self.echo_list[ - signal[ind, :] > self.noise_threshold] - future = pool.submit(self.__fit_signal__, - signal_thresh, - echo_list_thresh) - future.add_done_callback(lambda p: progress.update()) - futures.append(future) - - results = [] - for future in futures: - result = future.result() - results.append(result) - - if self.method == '2p_exp': - t2_map[idx], t2_err[idx], m0_map[idx], m0_err[idx] = [np.array( - row) for row in zip(*results)] - elif self.method == '3p_exp': - t2_map[idx], t2_err[idx], \ - m0_map[idx], m0_err[idx], \ - b_map[idx], b_err[idx] = \ - [np.array(row) for row in zip(*results)] - - # Single threaded method - else: - with tqdm(total=idx.size) as progress: - for ind in idx: - signal_thresh = signal[ind, :][ - signal[ind, :] > self.noise_threshold] - echo_list_thresh = self.echo_list[ - signal[ind, :] > self.noise_threshold] - if self.method == '2p_exp': - t2_map[ind], t2_err[ind], \ - m0_map[ind], m0_err[ind] \ - = self.__fit_signal__(signal_thresh, - echo_list_thresh) - elif self.method == '3p_exp': - t2_map[ind], t2_err[ind], \ - m0_map[ind], m0_err[ind], \ - b_map[ind], b_err[ind] \ - = self.__fit_signal__(signal_thresh, - echo_list_thresh) - progress.update(1) - - # Reshape results to raw data shape - t2_map = t2_map.reshape(self.shape) - t2_err = t2_err.reshape(self.shape) - m0_map = m0_map.reshape(self.shape) - m0_err = m0_err.reshape(self.shape) - - if self.method == '2p_exp': - return t2_map, t2_err, m0_map, m0_err - elif self.method == '3p_exp': - b_map = b_map.reshape(self.shape) - b_err = b_err.reshape(self.shape) - return t2_map, t2_err, m0_map, m0_err, b_map, b_err - - def __fit_signal__(self, sig, te): - - # Initialise parameters - if self.method == '2p_exp': - eq = two_param_eq - bounds = ([0, 0], [1000, 100000000]) - initial_guess = [20, 10000] - elif self.method == '3p_exp': - eq = three_param_eq - bounds = ([0, 0, 0], [1000, 100000000, 1000000]) - initial_guess = [20, 10000, 500] - - # Fit data to equation - try: - popt, pcov = curve_fit(eq, te, sig, p0=initial_guess, - bounds=bounds) - except (RuntimeError, ValueError): - popt = np.zeros(3) - pcov = np.zeros((3, 3)) - - # Extract fits and errors from result variables - if self.method == '2p_exp': - if popt[0] < bounds[1][0] - 1: - t2 = popt[0] - m0 = popt[1] - err = np.sqrt(np.diag(pcov)) - t2_err = err[0] - m0_err = err[1] - else: - t2, m0, t2_err, m0_err = 0, 0, 0, 0 - - return t2, t2_err, m0, m0_err - - elif self.method == '3p_exp': - if popt[0] < bounds[1][0] - 1: - t2 = popt[0] - m0 = popt[1] - b = popt[2] - err = np.sqrt(np.diag(pcov)) - t2_err = err[0] - m0_err = err[1] - b_err = err[2] - else: - t2, m0, t2_err, m0_err, b, b_err = 0, 0, 0, 0, 0, 0 - - return t2, t2_err, m0, m0_err, b, b_err - - def r2_map(self): - """ - Generates the R2 map from the T2 map output by initialising this - class. - - Parameters - ---------- - See class attributes in __init__ - - Returns - ------- - r2 : np.ndarray - An array containing the R2 map generated - by the function with R2 measured in ms. - """ - return np.reciprocal(self.t2_map) + fitting_model = T2Model(self.pixel_array, self.echo_list, + self.method, self.mask, self.multithread) + + if self.noise_threshold > 0: + fitting_model.threshold_noise(self.noise_threshold) + popt, error, r2 = fitting.fit_image(fitting_model) + self.t2_map = popt[0] + self.m0_map = popt[1] + self.t2_err = error[0] + self.m0_err = error[1] + self.r2 = r2 + + if self.method == '3p_exp': + self.b_map = popt[2] + self.b_err = error[2] + + # Filter values that are very close to models upper bounds of T2 or + # M0 out. + threshold = 0.999 # 99.9% of the upper bound + bounds_mask = ((self.t2_map > fitting_model.bounds[1][0] * threshold) | + (self.m0_map > fitting_model.bounds[1][1] * threshold)) + self.t2_map[bounds_mask] = 0 + self.m0_map[bounds_mask] = 0 + self.t2_err[bounds_mask] = 0 + self.m0_err[bounds_mask] = 0 + self.r2[bounds_mask] = 0 + if self.method == '3p_exp': + self.b_map[bounds_mask] = 0 + self.b_err[bounds_mask] = 0 def to_nifti(self, output_directory=os.getcwd(), base_file_name='Output', maps='all'): """Exports some of the T2 class attributes to NIFTI. - + Parameters ---------- output_directory : string, optional @@ -823,6 +676,9 @@

    Parameters

    base_path = os.path.join(output_directory, base_file_name) if maps == 'all' or maps == ['all']: maps = ['t2', 't2_err', 'm0', 'm0_err', 'r2', 'mask'] + if self.method == '3p_exp': + maps.append('b') + maps.append('b_err') if isinstance(maps, list): for result in maps: if result == 't2' or result == 't2_map': @@ -840,59 +696,30 @@

    Parameters

    affine=self.affine) nib.save(m0_err_nifti, base_path + '_m0_err.nii.gz') elif result == 'r2' or result == 'r2_map': - r2_nifti = nib.Nifti1Image(T2.r2_map(self), + r2_nifti = nib.Nifti1Image(self.r2, affine=self.affine) nib.save(r2_nifti, base_path + '_r2_map.nii.gz') elif result == 'mask': mask_nifti = nib.Nifti1Image(self.mask.astype(np.uint16), affine=self.affine) nib.save(mask_nifti, base_path + '_mask.nii.gz') + elif result == 'b' or result == 'b_map': + b_nifti = nib.Nifti1Image(self.b_map, + affine=self.affine) + nib.save(b_nifti, base_path + '_b_map.nii.gz') + elif result == 'b_err': + b_err_nifti = nib.Nifti1Image(self.b_err, + affine=self.affine) + nib.save(b_err_nifti, base_path + '_b_err.nii.gz') else: raise ValueError('No NIFTI file saved. The variable "maps" ' 'should be "all" or a list of maps from ' '"["t2", "t2_err", "m0", "m0_err", "r2", ' '"mask"]".') - return

    Methods

    -
    -def r2_map(self) -
    -
    -

    Generates the R2 map from the T2 map output by initialising this -class.

    -

    Parameters

    -

    See class attributes in init

    -

    Returns

    -
    -
    r2 : np.ndarray
    -
    An array containing the R2 map generated -by the function with R2 measured in ms.
    -
    -
    - -Expand source code - -
    def r2_map(self):
    -    """
    -    Generates the R2 map from the T2 map output by initialising this
    -    class.
    -
    -    Parameters
    -    ----------
    -    See class attributes in __init__
    -
    -    Returns
    -    -------
    -    r2 : np.ndarray
    -        An array containing the R2 map generated
    -        by the function with R2 measured in ms.
    -    """
    -    return np.reciprocal(self.t2_map)
    -
    -
    def to_nifti(self, output_directory='/home/runner/work/ukat/ukat', base_file_name='Output', maps='all')
    @@ -917,7 +744,7 @@

    Parameters

    def to_nifti(self, output_directory=os.getcwd(), base_file_name='Output',
                  maps='all'):
         """Exports some of the T2 class attributes to NIFTI.
    -                    
    +
         Parameters
         ----------
         output_directory : string, optional
    @@ -934,6 +761,9 @@ 

    Parameters

    base_path = os.path.join(output_directory, base_file_name) if maps == 'all' or maps == ['all']: maps = ['t2', 't2_err', 'm0', 'm0_err', 'r2', 'mask'] + if self.method == '3p_exp': + maps.append('b') + maps.append('b_err') if isinstance(maps, list): for result in maps: if result == 't2' or result == 't2_map': @@ -951,24 +781,190 @@

    Parameters

    affine=self.affine) nib.save(m0_err_nifti, base_path + '_m0_err.nii.gz') elif result == 'r2' or result == 'r2_map': - r2_nifti = nib.Nifti1Image(T2.r2_map(self), + r2_nifti = nib.Nifti1Image(self.r2, affine=self.affine) nib.save(r2_nifti, base_path + '_r2_map.nii.gz') elif result == 'mask': mask_nifti = nib.Nifti1Image(self.mask.astype(np.uint16), affine=self.affine) nib.save(mask_nifti, base_path + '_mask.nii.gz') + elif result == 'b' or result == 'b_map': + b_nifti = nib.Nifti1Image(self.b_map, + affine=self.affine) + nib.save(b_nifti, base_path + '_b_map.nii.gz') + elif result == 'b_err': + b_err_nifti = nib.Nifti1Image(self.b_err, + affine=self.affine) + nib.save(b_err_nifti, base_path + '_b_err.nii.gz') else: raise ValueError('No NIFTI file saved. The variable "maps" ' 'should be "all" or a list of maps from ' '"["t2", "t2_err", "m0", "m0_err", "r2", ' '"mask"]".') - return
    +
    +class T2Model +(pixel_array, te, method='2p_exp', mask=None, multithread=True) +
    +
    +

    A class containing the T2 fitting model

    +

    Parameters

    +
    +
    pixel_array : np.ndarray
    +
    An array containing the signal from each voxel at each echo +time with the last dimension being time i.e. the array needed to +generate a 3D T2 map would have dimensions [x, y, z, TE].
    +
    te : np.ndarray
    +
    An array of the echo times used for the last dimension of the +pixel_array. In milliseconds.
    +
    method : {'2p_exp', '3p_exp'}, optional
    +
    Default '2p_exp' +The model the data is fit to. 2p_exp uses a two parameter +exponential model (S = S0 * exp(-t / T2)) whereas 3p_exp uses a +three parameter exponential model (S = S0 * exp(-t / T2) + b) to +fit for noise/very long T2 components of the signal.
    +
    mask : np.ndarray, optional
    +
    A boolean mask of the voxels to fit. Should be the shape of the +desired T2 map rather than the raw data i.e. omit the time +dimension.
    +
    multithread : bool, optional
    +
    Default True +If True, the fitting will be performed in parallel using all +available cores
    +
    +
    + +Expand source code + +
    class T2Model(fitting.Model):
    +    def __init__(self, pixel_array, te, method='2p_exp', mask=None,
    +                 multithread=True):
    +        """
    +        A class containing the T2 fitting model
    +
    +        Parameters
    +        ----------
    +        pixel_array : np.ndarray
    +            An array containing the signal from each voxel at each echo
    +            time with the last dimension being time i.e. the array needed to
    +            generate a 3D T2 map would have dimensions [x, y, z, TE].
    +        te : np.ndarray
    +            An array of the echo times used for the last dimension of the
    +            pixel_array. In milliseconds.
    +        method : {'2p_exp', '3p_exp'}, optional
    +            Default '2p_exp'
    +            The model the data is fit to. 2p_exp uses a two parameter
    +            exponential model (S = S0 * exp(-t / T2)) whereas 3p_exp uses a
    +            three parameter exponential model (S = S0 * exp(-t / T2) + b) to
    +            fit for noise/very long T2 components of the signal.
    +        mask : np.ndarray, optional
    +            A boolean mask of the voxels to fit. Should be the shape of the
    +            desired T2 map rather than the raw data i.e. omit the time
    +            dimension.
    +        multithread : bool, optional
    +            Default True
    +            If True, the fitting will be performed in parallel using all
    +            available cores
    +        """
    +        self.method = method
    +
    +        if self.method == '2p_exp':
    +            super().__init__(pixel_array, te, two_param_eq, mask, multithread)
    +            self.bounds = ([0, 0], [1000, 100000000])
    +            self.initial_guess = [20, 10000]
    +        elif self.method == '3p_exp':
    +            super().__init__(pixel_array, te, three_param_eq, mask,
    +                             multithread)
    +            self.bounds = ([0, 0, 0], [1000, 100000000, 1000000])
    +            self.initial_guess = [20, 10000, 500]
    +
    +        self.generate_lists()
    +
    +    def threshold_noise(self, threshold=0):
    +        """
    +        Remove voxel values below a certain threshold from the fitting
    +        process, useful if long echo times have been collected and thus
    +        thermal noise is being measured below a certain threshold rather
    +        than the T2 decay.
    +
    +        Parameters
    +        ----------
    +        threshold : float, optional
    +            Default 0
    +            The threshold below which to remove values
    +        """
    +        for ind, (sig, te, p0) in enumerate(zip(self.signal_list,
    +                                                self.x_list,
    +                                                self.p0_list)):
    +            self.signal_list[ind] = np.array(
    +                [x for (x, b) in zip(sig, np.array(sig) > threshold) if b])
    +            self.x_list[ind] = np.array(
    +                [x for (x, b) in zip(te, np.array(sig) > threshold) if b])
    +            self.p0_list[ind] = np.array(
    +                [x for (x, b) in zip(p0, np.array(sig) > threshold) if b])
    +
    +

    Ancestors

    + +

    Methods

    +
    +
    +def threshold_noise(self, threshold=0) +
    +
    +

    Remove voxel values below a certain threshold from the fitting +process, useful if long echo times have been collected and thus +thermal noise is being measured below a certain threshold rather +than the T2 decay.

    +

    Parameters

    +
    +
    threshold : float, optional
    +
    Default 0 +The threshold below which to remove values
    +
    +
    + +Expand source code + +
    def threshold_noise(self, threshold=0):
    +    """
    +    Remove voxel values below a certain threshold from the fitting
    +    process, useful if long echo times have been collected and thus
    +    thermal noise is being measured below a certain threshold rather
    +    than the T2 decay.
    +
    +    Parameters
    +    ----------
    +    threshold : float, optional
    +        Default 0
    +        The threshold below which to remove values
    +    """
    +    for ind, (sig, te, p0) in enumerate(zip(self.signal_list,
    +                                            self.x_list,
    +                                            self.p0_list)):
    +        self.signal_list[ind] = np.array(
    +            [x for (x, b) in zip(sig, np.array(sig) > threshold) if b])
    +        self.x_list[ind] = np.array(
    +            [x for (x, b) in zip(te, np.array(sig) > threshold) if b])
    +        self.p0_list[ind] = np.array(
    +            [x for (x, b) in zip(p0, np.array(sig) > threshold) if b])
    +
    +
    +
    +

    Inherited members

    + +
    @@ -994,10 +990,15 @@

    Index

  • T2

  • +
  • +

    T2Model

    + +
  • diff --git a/mapping/t2_stimfit.html b/mapping/t2_stimfit.html new file mode 100644 index 00000000..e9399364 --- /dev/null +++ b/mapping/t2_stimfit.html @@ -0,0 +1,1483 @@ + + + + + + +ukat.mapping.t2_stimfit API documentation + + + + + + + + + + + +
    +
    +
    +

    Module ukat.mapping.t2_stimfit

    +
    +
    +
    + +Expand source code + +
    import os
    +import warnings
    +
    +import nibabel as nib
    +import numpy as np
    +from numba import jit
    +from pathos.pools import ProcessPool
    +from scipy import optimize
    +from sklearn.metrics import r2_score
    +from tqdm import tqdm
    +
    +from .resources.t2_stimfit import rf_pulses
    +from ukat.mapping.t2 import two_param_eq
    +
    +
    +class StimFitModel:
    +    def __init__(self, mode='non_selective', n_comp=1, ukrin_vendor=None):
    +        """
    +        A class to set up the T2 StimFit model.
    +
    +        This model generates an optimisation dictionary (`opt`) containing the
    +        model parameters and fitting options.
    +
    +        Parameters
    +        ----------
    +        mode : {'non_selective', 'selective'}, optional
    +            Default 'non_selective'
    +            Choose whether the refocusing pulses are selective on
    +            non-selective.
    +        n_comp : {1, 2, 3}, optional
    +            Default 1
    +            The number of components to fit e.g. if n_comp=2, the model will
    +            estimate two T2 values, two M0 values and one B1 value per voxel.
    +        ukrin_vendor : {None, 'ge', 'philips', 'siemens'}, optional
    +            Default None
    +            The vendor of the MRI scanner used to acquire the data if the UKRIN
    +            protocol was used. Specifying a vendor at this stage overrides
    +            the relevant parameters in the model with those from the UKRIN
    +            protocol. If no vendor is specified, the default parameters are
    +            used but can be manually updated after instantiation.
    +
    +        Key Parameters in Options Dictionary
    +        ----------
    +        mode : {'non_selective', 'selective'}
    +            Choose whether the refocusing pulses are slice selective or
    +            non-selective.
    +        esp : float
    +            The echo spacing in seconds.
    +        etl : int
    +            The echo train length.
    +        T1 : float
    +            The approximate T1 value in seconds.
    +        Dz : list
    +            The start and end position of each slice in cm.
    +        Nz : int
    +            The number of positions along the slice profile to simulate signal
    +            decay for.
    +        Nrf : int
    +            The number of resampled points in the RF waveform.
    +        RFe : dict
    +            The excitation pulse parameters, outlined below.
    +        RFr : dict
    +            The refocusing pulse parameters, outlined below.
    +        lsq : dict
    +            The least squares fitting parameters, outlined below.
    +
    +        Key Parameters in RFe Dictionary
    +        ----------
    +        RF : np.ndarray
    +            The excitation pulse shape.
    +        G : float
    +            The amplitude of the excitation pulse in Gauss/cm.
    +        tau : float
    +            The excitation pulse duration in seconds.
    +        phase : float
    +            The relative phase of the excitation pulse in degrees (0 in CPMG).
    +        angle : float
    +            The flip angle of the excitation pulse in degrees (typically 90).
    +        ref : float
    +            The rephasing gradient fraction, times two. Near unity for
    +            excitation.
    +        alpha : list, optional
    +            The actual tip angle distribution across the slice (degrees). If
    +            not specified, the tip angle distribution is calculated.
    +
    +        Key Parameters in RFr Dictionary
    +        ----------
    +        RF : np.ndarray
    +            The refocusing pulse shape.
    +        G : float
    +            The amplitude of the refocusing pulse in Gauss/cm.
    +        tau : float
    +            The refocusing pulse duration in seconds.
    +        phase : float
    +            The relative phase of the refocusing pulse in degrees (90 in CPMG).
    +        angle : float
    +            The flip angle of the refocusing pulse in degrees (typically 180).
    +        ref : float
    +            The rephasing gradient fraction, times two. Typically, 0 for
    +            refocusing.
    +        alpha : list, optional
    +            The actual refocusing angle distribution across the slice
    +            (degrees). If not specified, the tip angle distribution is
    +            calculated.
    +
    +        Key Parameters in lsq Dictionary
    +        ----------
    +        Ncomp : int
    +            The number of components to fit.
    +        X0 : list
    +            The initial guess for the fitting parameters in the order
    +            [[T2_comp, M0_comp] * Ncomp, B1].
    +        XL : list
    +            The lower bounds for the fitting parameters in the order
    +            [[T2_comp, M0_comp] * Ncomp, B1].
    +        XU : list
    +            The upper bounds for the fitting parameters in the order
    +            [[T2_comp, M0_comp] * Ncomp, B1].
    +        xtol : float
    +            Tolerance for termination by the change of the independent
    +            variables.
    +        ftol : float
    +            Tolerance for termination by the change of the cost function.
    +        """
    +        if mode != 'non_selective' and mode != 'selective':
    +            raise ValueError(f'mode must be either "non_selective" or '
    +                             f'"selective". You specified {mode}.')
    +        self.mode = mode
    +        if n_comp not in [1, 2, 3]:
    +            raise ValueError(f'n_comp must be either 1, 2 or 3. You specified '
    +                             f'{n_comp}.')
    +        self.n_comp = n_comp
    +        if ukrin_vendor not in ['ge', 'philips', 'siemens']:
    +            warnings.warn('ukrin_vendor was not specified. Using default '
    +                          'pulse sequence parameters.')
    +        self.opt = dict()
    +        self.opt['mode'] = self.mode
    +        self.opt['esp'] = 10e-3
    +        self.opt['etl'] = 20
    +        self.opt['T1'] = 3
    +
    +        self.opt['RFe'] = dict()
    +        self.opt['RFr'] = dict()
    +        if self.mode == 'selective':
    +            self.opt['Dz'] = [-0.5, 0.5]
    +            self.opt['Nz'] = 51
    +            self.opt['Nrf'] = 64
    +            self.opt['RFe'] = {'RF': [],
    +                               'tau': 2e-3,
    +                               'G': 0.5,
    +                               'phase': 0,
    +                               'ref': 1,
    +                               'alpha': [],
    +                               'angle': 90}
    +            self.opt['RFr'] = {'RF': [],
    +                               'tau': 2e-3,
    +                               'G': 0.5,
    +                               'phase': 90,
    +                               'ref': 0,
    +                               'alpha': [],
    +                               'angle': 180,
    +                               'FA_array': np.ones(self.opt['etl'])}
    +        else:
    +            self.opt['RFe'] = {'angle': 90}
    +            self.opt['RFr'] = {'angle': 180,
    +                               'FA_array': np.ones(self.opt['etl'])}
    +        # Curve fitting parameters
    +        self.opt['lsq'] = {'Ncomp': n_comp,
    +                           'xtol': 5e-4,
    +                           'ftol': 1e-9}
    +        if self.opt['lsq']['Ncomp'] == 1:
    +            # [T2(sec), amp, B1]
    +            self.opt['lsq']['X0'] = [0.06, 0.1, 1]
    +            self.opt['lsq']['XU'] = [3, 1e+3, 1.8]
    +            self.opt['lsq']['XL'] = [0.015, 0, 0.2]
    +        elif self.opt['lsq']['Ncomp'] == 2:
    +            # [T2, amp, T2, amp, B1]
    +            self.opt['lsq']['X0'] = [0.02, 0.1, 0.331, 0.1, 1]
    +            self.opt['lsq']['XU'] = [0.25, 1e+3, 3, 1e+3, 1.8]
    +            self.opt['lsq']['XL'] = [0.015, 0, 0.25, 0, 0.2]
    +        elif self.opt['lsq']['Ncomp'] == 3:
    +            # [T2, amp, T2, amp, T2, amp, B1]
    +            self.opt['lsq']['X0'] = [0.02, 0.1, 0.036, 0.1, 0.131, 0.1, 1]
    +            self.opt['lsq']['XU'] = [0.035, 1e+3, 0.13, 1e3, 3, 1e+3, 1.8]
    +            self.opt['lsq']['XL'] = [0.015, 0, 0.035, 0, 0.13, 0, 0.2]
    +
    +        if ukrin_vendor is not None:
    +            self._set_ukrin_vendor(ukrin_vendor)
    +            if self.mode == 'selective':
    +                self.opt['RFe'] = self._set_rf(self.opt['RFe'])
    +                self.opt['RFr'] = self._set_rf(self.opt['RFr'])
    +
    +    def get_opt(self):
    +        return self.opt
    +
    +    def get_lsq(self):
    +        return self.opt['lsq']
    +
    +    def get_rfe(self):
    +        return self.opt['RFe']
    +
    +    def get_rfr(self):
    +        return self.opt['RFr']
    +
    +    def _set_ukrin_vendor(self, vendor):
    +        self.vendor = vendor
    +        self.opt['T1'] = 1.5
    +        self.opt['esp'] = 0.0129
    +        self.opt['etl'] = 10
    +        self.opt['te'] = (np.arange(self.opt['etl']) + 1) * self.opt['esp']
    +        self.opt['RFr']['FA_array'] = np.ones(self.opt['etl'])
    +        if self.vendor == 'ge':
    +            self.opt['RFe']['tau'] = 2000 / 1e6  # Duration
    +            self.opt['RFe']['G'] = 0.751599  # Amplitude
    +            self.opt['RFr']['tau'] = 3136 / 1e6
    +            self.opt['RFr']['G'] = 0.276839
    +            self.opt['RFe']['RF'] = rf_pulses.ge_90
    +            self.opt['RFr']['RF'] = rf_pulses.ge_180
    +            self.opt['Dz'] = [0, 0.45]  # Slice thickness
    +        elif self.vendor == 'philips':
    +            self.opt['RFe']['tau'] = 3820 / 1e6
    +            self.opt['RFe']['G'] = 0.392
    +            self.opt['RFr']['tau'] = 6010 / 1e6
    +            self.opt['RFr']['G'] = 0.327
    +            self.opt['RFe']['RF'] = rf_pulses.philips_90
    +            self.opt['RFr']['RF'] = rf_pulses.philips_180
    +            self.opt['Dz'] = [0, 0.45]
    +        elif self.vendor == 'siemens':
    +            self.opt['RFe']['tau'] = 3072 / 1e6
    +            self.opt['RFe']['G'] = 0.417
    +            self.opt['RFr']['tau'] = 3000 / 1e6
    +            self.opt['RFr']['G'] = 0.326
    +            self.opt['RFe']['RF'] = rf_pulses.ge_90
    +            self.opt['RFr']['RF'] = rf_pulses.ge_180
    +            self.opt['Dz'] = [0, 0.5]
    +        else:
    +            warnings.warn(f'{self.vendor} is not implemented. Please '
    +                          f'manually specify the models parameters.')
    +
    +    def _set_rf(self, rf):
    +        dz = self.opt['Dz']
    +        nz = self.opt['Nz']
    +        nrf = self.opt['Nrf']
    +
    +        gamma = 2 * np.pi * 42.575e6 / 10000  # Gauss
    +        z = np.linspace(dz[0], dz[1], nz)
    +        scale = rf['angle'] / (gamma * rf['tau'] * abs(np.sum(rf['RF'])) / len(
    +            rf['RF']) * 180 / np.pi)
    +        rf['RF'] *= scale
    +
    +        m = np.zeros([3, nz])
    +        m[2, :] = 1
    +        rf['RF'] = 1e-4 * rf['RF']  # approximation for
    +        # small tip angle
    +
    +        phi = gamma * rf['G'] * z * rf['tau'] / nrf
    +        cphi = np.cos(phi)
    +        sphi = np.sin(phi)
    +        cp_rf = np.cos(rf['phase'] * np.pi / 180)
    +        sp_rf = np.sin(rf['phase'] * np.pi / 180)
    +        theta_rf = gamma * rf['RF'] * rf['tau'] / nrf
    +        ct_rf = np.cos(theta_rf)
    +        st_rf = np.sin(theta_rf)
    +
    +        for i in range(nrf):
    +            for j in range(nz):
    +                rz = np.array([[cphi[j], sphi[j], 0],
    +                               [-sphi[j], cphi[j], 0],
    +                               [0, 0, 1]])
    +                m[:, j] = np.dot(rz, m[:, j])
    +
    +            r = np.array([[1, 0, 0],
    +                          [0, ct_rf[i], st_rf[i]],
    +                          [0, -st_rf[i], ct_rf[i]]])
    +            if rf['phase'] != 0:
    +                rz = np.array([[cp_rf, sp_rf, 0],
    +                               [-sp_rf, cp_rf, 0],
    +                               [0, 0, 1]])
    +                rzm = np.array([[cp_rf, -sp_rf, 0],
    +                                [sp_rf, cp_rf, 0],
    +                                [0, 0, 1]])
    +                r = np.dot(rzm, np.dot(r, rz))
    +            m = np.dot(r, m)
    +
    +        if rf['ref'] > 0:
    +            psi = -rf['ref'] / 2 * gamma * rf['G'] * z * rf['tau']
    +            for j in range(nz):
    +                rz = np.array([[np.cos(psi[j]), np.sin(psi[j]), 0],
    +                               [-np.sin(psi[j]), np.cos(psi[j]), 0],
    +                               [0, 0, 1]])
    +                m[:, j] = np.dot(rz, m[:, j])
    +
    +        rf['RF'] = 1e4 * rf['RF']
    +        rf['alpha'] = 1e4 * np.arccos(m[2, :])
    +        return rf
    +
    +
    +class T2StimFit:
    +    """
    +    Attributes
    +    ----------
    +    t2_map : np.ndarray
    +        The estimated T2 values in ms
    +    m0_map : np.ndarray
    +        The estimated M0 values
    +    r2_map : np.ndarray
    +        The R-Squared value of the fit, values close to 1 indicate a good
    +        fit, lower values indicate a poorer fit
    +    shape : tuple
    +        The shape of the T2 map
    +    n_vox : int
    +        The number of voxels in the map i.e. the product of all dimensions
    +        apart from TE
    +    """
    +    def __init__(self, pixel_array, affine, model,
    +                 mask=None, multithread='auto', norm=True):
    +        """
    +        Class for performing stimulated echo T2 fitting as in Marc Lebel R.
    +        StimFit: A Toolbox for Robust T2 Mapping with Stimulated Echo
    +        Compensation. In: Proc. Intl. Soc. Mag. Reson. Med. 20. Melbourne;
    +        2012:2558. https://archive.ismrm.org/2012/2558.html.
    +
    +        Parameters
    +        ----------
    +        pixel_array : np.ndarray
    +            An array containing the signal from each voxel at each echo
    +            time with the last dimension being time i.e. the array needed to
    +            generate a 3D T2 map would have dimensions [x, y, z, TE].
    +        affine : np.ndarray
    +            A matrix giving the relationship between voxel coordinates and
    +            world coordinates.
    +        model : StimFitModel
    +            A StimFitModel object containing the model parameters.
    +        mask : np.ndarray, optional
    +            A boolean mask of the voxels to fit. Should be the shape of the
    +            desired T2 map rather than the raw data i.e. omit the time
    +            dimension.
    +        multithread : bool or 'auto', optional
    +            Default 'auto'.
    +            If True, fitting will be distributed over all cores available on
    +            the node. If False, fitting will be carried out on a single thread.
    +            'auto' attempts to apply multithreading where appropriate based
    +            on the number of voxels being fit.
    +        norm : bool, optional
    +            Default True.
    +            StimFit is performed on normalised data. If norm is False,
    +            it is assumed that the data has already been normalised. If norm
    +            is True, the data will be normalised before fitting.
    +        """
    +        self.pixel_array = np.copy(pixel_array)
    +        self.shape = pixel_array.shape[:-1]
    +        self.n_vox = np.prod(self.shape)
    +        self.affine = affine
    +        self.model = model
    +
    +        assert multithread is True \
    +               or multithread is False \
    +               or multithread == 'auto', f'multithreaded must be True,' \
    +                                         f'False or auto. You entered ' \
    +                                         f'{multithread}'
    +        if multithread == 'auto':
    +            if self.n_vox > 20:
    +                multithread = True
    +            else:
    +                multithread = False
    +        self.multithread = multithread
    +
    +        # Generate a mask if there isn't one specified
    +        if mask is None:
    +            self.mask = np.ones(self.shape, dtype=bool)
    +        else:
    +            self.mask = mask
    +            # Don't process any nan values
    +        self.mask[np.isnan(np.sum(pixel_array, axis=-1))] = False
    +
    +        # Normalise the data
    +        if norm:
    +            self.pixel_array /= np.nanmax(self.pixel_array)
    +
    +        if np.nanmax(self.pixel_array) > 1:
    +            warnings.warn('Pixel array contains values greater than 1. '
    +                          'Data should be normalised, please set norm=True '
    +                          'or manually normalise your data.')
    +
    +        # Perform the fit
    +        self._fit()
    +
    +    def to_nifti(self, output_directory=os.getcwd(), base_file_name='Output',
    +                 maps='all'):
    +        """Exports some of the T2StimFit class attributes to NIFTI.
    +
    +        Parameters
    +        ----------
    +        output_directory : string, optional
    +            Path to the folder where the NIFTI files will be saved.
    +        base_file_name : string, optional
    +            Filename of the resulting NIFTI. This code appends the extension.
    +            Eg., base_file_name = 'Output' will result in 'Output.nii.gz'.
    +        maps : list or 'all', optional
    +            List of maps to save to NIFTI. This should either the string "all"
    +            or a list of maps from ["t2", "m0", "b1", "r2", "mask"].
    +        """
    +        os.makedirs(output_directory, exist_ok=True)
    +        base_path = os.path.join(output_directory, base_file_name)
    +        if maps == 'all' or maps == ['all']:
    +            maps = ['t2', 'm0', 'b1', 'r2', 'mask']
    +        if isinstance(maps, list):
    +            for result in maps:
    +                if result == 't2' or result == 't2_map':
    +                    t2_nifti = nib.Nifti1Image(self.t2_map, affine=self.affine)
    +                    nib.save(t2_nifti, base_path + '_t2_map.nii.gz')
    +                elif result == 'm0' or result == 'm0_map':
    +                    m0_nifti = nib.Nifti1Image(self.m0_map, affine=self.affine)
    +                    nib.save(m0_nifti, base_path + '_m0_map.nii.gz')
    +                elif result == 'b1':
    +                    m0_err_nifti = nib.Nifti1Image(self.b1_map,
    +                                                   affine=self.affine)
    +                    nib.save(m0_err_nifti, base_path + '_b1_map.nii.gz')
    +                elif result == 'r2' or result == 'r2_map':
    +                    r2_nifti = nib.Nifti1Image(self.r2_map,
    +                                               affine=self.affine)
    +                    nib.save(r2_nifti, base_path + '_r2_map.nii.gz')
    +                elif result == 'mask':
    +                    mask_nifti = nib.Nifti1Image(self.mask.astype(np.uint16),
    +                                                 affine=self.affine)
    +                    nib.save(mask_nifti, base_path + '_mask.nii.gz')
    +        else:
    +            raise ValueError('No NIFTI file saved. The variable "maps" '
    +                             'should be "all" or a list of maps from '
    +                             '"["t2", "m0", "b1", "r2", '
    +                             '"mask"]".')
    +
    +    def _fit(self):
    +        mask = self.mask.flatten()
    +        signal = self.pixel_array.reshape(self.n_vox, self.model.opt['etl'])
    +        idx = np.argwhere(mask).squeeze()
    +        signal = signal[idx, :]
    +
    +        if self.multithread:
    +            with ProcessPool() as executor:
    +                results = executor.map(self._fit_signal, signal)
    +        else:
    +            results = list(tqdm(map(self._fit_signal, signal),
    +                                total=np.sum(self.mask)))
    +        t2 = np.array([result[0] for result in results])
    +        m0 = np.array([result[1] for result in results])
    +        b1 = np.array([result[2] for result in results])
    +        r2 = np.array([result[3] for result in results])
    +
    +        if self.model.n_comp > 1:
    +            t2_map = np.zeros((self.n_vox, self.model.n_comp))
    +            m0_map = np.zeros((self.n_vox, self.model.n_comp))
    +            r2_map = np.zeros((self.n_vox, self.model.n_comp))
    +        else:
    +            t2_map = np.zeros(self.n_vox)
    +            m0_map = np.zeros(self.n_vox)
    +            r2_map = np.zeros(self.n_vox)
    +        b1_map = np.zeros(self.n_vox)
    +        t2_map[idx] = t2 * 1000  # Convert to ms
    +        m0_map[idx] = m0
    +        b1_map[idx] = b1
    +        r2_map[idx] = r2
    +        self.t2_map = np.squeeze(t2_map.reshape((*self.shape,
    +                                                 self.model.n_comp)))
    +        self.m0_map = np.squeeze(m0_map.reshape((*self.shape,
    +                                                 self.model.n_comp)))
    +        self.b1_map = b1_map.reshape(self.shape)
    +        self.r2_map = np.squeeze(r2_map.reshape((*self.shape,
    +                                                 self.model.n_comp)))
    +
    +    def _fit_signal(self, signal):
    +        if len(signal) != self.model.opt['etl']:
    +            raise Exception('Inconsistent echo train length')
    +
    +        # Two component fitting
    +        if self.model.opt['lsq']['Ncomp'] == 2:
    +            x = optimize.least_squares(self._residual2,
    +                                       self.model.opt['lsq']['X0'],
    +                                       args=(signal, self.model.opt,
    +                                             self.model.mode),
    +                                       bounds=(self.model.opt['lsq']['XL'],
    +                                               self.model.opt['lsq']['XU']),
    +                                       xtol=self.model.opt['lsq']['xtol'],
    +                                       ftol=self.model.opt['lsq']['ftol']).x
    +            t2, amp, b1 = [x[0], x[2]], [x[1], x[3]], x[4]
    +            r2 = [r2_score(signal, two_param_eq(self.model.opt['te'], t2[0],
    +                                                amp[0])),
    +                  r2_score(signal, two_param_eq(self.model.opt['te'], t2[1],
    +                                                amp[1]))]
    +
    +        # Three component fitting
    +        elif self.model.opt['lsq']['Ncomp'] == 3:
    +            x = optimize.least_squares(self._residual3,
    +                                       self.model.opt['lsq']['X0'],
    +                                       args=(signal, self.model.opt,
    +                                             self.model.mode),
    +                                       bounds=(self.model.opt['lsq']['XL'],
    +                                               self.model.opt['lsq']['XU']),
    +                                       xtol=self.model.opt['lsq']['xtol'],
    +                                       ftol=self.model.opt['lsq']['ftol']).x
    +            t2, amp, b1 = [x[0], x[2], x[4]], [x[1], x[3], x[5]], x[6]
    +            r2 = [r2_score(signal, two_param_eq(self.model.opt['te'], t2[0],
    +                                                amp[0])),
    +                  r2_score(signal, two_param_eq(self.model.opt['te'], t2[1],
    +                                                amp[1])),
    +                  r2_score(signal, two_param_eq(self.model.opt['te'], t2[2],
    +                                                amp[2]))]
    +
    +        # One component fitting
    +        else:
    +            x = optimize.least_squares(self._residual1,
    +                                       self.model.opt['lsq']['X0'],
    +                                       args=(signal, self.model.opt,
    +                                             self.model.mode),
    +                                       bounds=(self.model.opt['lsq']['XL'],
    +                                               self.model.opt['lsq']['XU']),
    +                                       xtol=self.model.opt['lsq']['xtol'],
    +                                       ftol=self.model.opt['lsq']['ftol']).x
    +            t2, amp, b1 = x
    +            fit_sig = two_param_eq(self.model.opt['te'], t2, amp)
    +            r2 = r2_score(signal, fit_sig)
    +        return t2, amp, b1, r2
    +
    +    @staticmethod
    +    def _residual1(p, y, opt, mode):
    +        return y - _epgsig(p[0], p[2], opt, mode) * p[1]
    +
    +    @staticmethod
    +    def _residual2(p, y, opt, mode):
    +        return y - (_epgsig(p[0], p[4], opt, mode) * p[1] -
    +                    _epgsig(p[2], p[4], opt, mode) * p[3])
    +
    +    @staticmethod
    +    def _residual3(p, y, opt, mode):
    +        return y - (_epgsig(p[0], p[6], opt, mode) * p[1] -
    +                    _epgsig(p[4], p[6], opt, mode) * p[5] -
    +                    _epgsig(p[2], p[6], opt, mode) * p[3])
    +
    +
    +def _epgsig(t2, b1, opt, mode):
    +    sig = np.zeros(opt['etl'])
    +    if mode == 'non_selective':
    +        fa = np.pi / 180 * opt['RFr']['angle'] * np.array([
    +            opt['RFr']['FA_array']])
    +        sig = _epg(t2, b1, opt['T1'],
    +                   opt['esp'], fa,
    +                   opt['RFe']['angle'] * np.pi / 180)
    +    elif mode == 'selective':
    +        fa = np.array([opt['RFr']['alpha']]).T * \
    +             opt['RFr']['FA_array']
    +        m = _epg(t2, b1, opt['T1'], opt['esp'],
    +                 fa, opt['RFe']['alpha'])
    +        sig = np.sum(m, 0) / opt['Nz']
    +    return sig.ravel()
    +
    +
    +@jit(nopython=True)
    +def _epg(x2, b1, x1, esp, ar, ae):  # TE = 6.425ms. TR = 1500ms.   90, 175,
    +    # 145, 110, 110, 110.
    +    echo_intensity = np.zeros(ar.shape, dtype=np.float64)
    +    omiga = np.zeros((ar.shape[0], 3, 1 + 2 * ar.shape[1]),
    +                     dtype=np.float64)
    +    ar = b1 * ar
    +    ae = b1 * ae
    +    x2 = np.exp(-0.5 * esp / x2)
    +    x1 = np.exp(-0.5 * esp / x1)
    +
    +    for i in range(omiga.shape[2]):
    +        if i == 0:
    +            omiga[:, 0, i] = np.sin(ae)
    +            omiga[:, 1, i] = np.sin(ae)
    +            omiga[:, 2, i] = np.cos(ae)
    +            continue
    +        omiga[:, 0, 1:i + 1] = omiga[:, 0, 0:i]
    +        omiga[:, 1, 0:i] = omiga[:, 1, 1:i + 1]
    +        omiga[:, 0, 0] = np.conj(omiga[:, 1, 0])
    +        omiga[:, 0:2, :] = x2 * omiga[:, 0:2, :]
    +        omiga[:, 2, :] = x1 * omiga[:, 2, :]
    +        omiga[:, 2, 0] += 1 - x1
    +        if i % 2 == 1:
    +            for runs in range(ar.shape[0]):
    +                ari = ar[runs, i // 2]
    +                t = np.array(
    +                    [[np.cos(0.5 * ari) ** 2, np.sin(0.5 * ari) ** 2,
    +                      np.sin(ari)],
    +                     [np.sin(0.5 * ari) ** 2, np.cos(0.5 * ari) ** 2,
    +                      -np.sin(ari)],
    +                     [-0.5 * np.sin(ari), +0.5 * np.sin(ari),
    +                      np.cos(ari)]], dtype=np.float64)
    +                omiga[runs, :, :] = np.dot(t, np.ascontiguousarray(
    +                    omiga[runs, :, :]))
    +        if i % 2 == 0:
    +            echo_intensity[:, i // 2 - 1] = omiga[:, 0, 0]
    +    return echo_intensity
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +

    Classes

    +
    +
    +class StimFitModel +(mode='non_selective', n_comp=1, ukrin_vendor=None) +
    +
    +

    A class to set up the T2 StimFit model.

    +

    This model generates an optimisation dictionary (opt) containing the +model parameters and fitting options.

    +

    Parameters

    +
    +
    mode : {'non_selective', 'selective'}, optional
    +
    Default 'non_selective' +Choose whether the refocusing pulses are selective on +non-selective.
    +
    n_comp : {1, 2, 3}, optional
    +
    Default 1 +The number of components to fit e.g. if n_comp=2, the model will +estimate two T2 values, two M0 values and one B1 value per voxel.
    +
    ukrin_vendor : {None, 'ge', 'philips', 'siemens'}, optional
    +
    Default None +The vendor of the MRI scanner used to acquire the data if the UKRIN +protocol was used. Specifying a vendor at this stage overrides +the relevant parameters in the model with those from the UKRIN +protocol. If no vendor is specified, the default parameters are +used but can be manually updated after instantiation.
    +
    +

    Key Parameters In Options Dictionary

    +

    mode : {'non_selective', 'selective'} +Choose whether the refocusing pulses are slice selective or +non-selective. +esp : float +The echo spacing in seconds. +etl : int +The echo train length. +T1 : float +The approximate T1 value in seconds. +Dz : list +The start and end position of each slice in cm. +Nz : int +The number of positions along the slice profile to simulate signal +decay for. +Nrf : int +The number of resampled points in the RF waveform. +RFe : dict +The excitation pulse parameters, outlined below. +RFr : dict +The refocusing pulse parameters, outlined below. +lsq : dict +The least squares fitting parameters, outlined below.

    +

    Key Parameters In Rfe Dictionary

    +

    RF : np.ndarray +The excitation pulse shape. +G : float +The amplitude of the excitation pulse in Gauss/cm. +tau : float +The excitation pulse duration in seconds. +phase : float +The relative phase of the excitation pulse in degrees (0 in CPMG). +angle : float +The flip angle of the excitation pulse in degrees (typically 90). +ref : float +The rephasing gradient fraction, times two. Near unity for +excitation. +alpha : list, optional +The actual tip angle distribution across the slice (degrees). If +not specified, the tip angle distribution is calculated.

    +

    Key Parameters In Rfr Dictionary

    +

    RF : np.ndarray +The refocusing pulse shape. +G : float +The amplitude of the refocusing pulse in Gauss/cm. +tau : float +The refocusing pulse duration in seconds. +phase : float +The relative phase of the refocusing pulse in degrees (90 in CPMG). +angle : float +The flip angle of the refocusing pulse in degrees (typically 180). +ref : float +The rephasing gradient fraction, times two. Typically, 0 for +refocusing. +alpha : list, optional +The actual refocusing angle distribution across the slice +(degrees). If not specified, the tip angle distribution is +calculated.

    +

    Key Parameters In Lsq Dictionary

    +

    Ncomp : int +The number of components to fit. +X0 : list +The initial guess for the fitting parameters in the order +[[T2_comp, M0_comp] * Ncomp, B1]. +XL : list +The lower bounds for the fitting parameters in the order +[[T2_comp, M0_comp] * Ncomp, B1]. +XU : list +The upper bounds for the fitting parameters in the order +[[T2_comp, M0_comp] * Ncomp, B1]. +xtol : float +Tolerance for termination by the change of the independent +variables. +ftol : float +Tolerance for termination by the change of the cost function.

    +
    + +Expand source code + +
    class StimFitModel:
    +    def __init__(self, mode='non_selective', n_comp=1, ukrin_vendor=None):
    +        """
    +        A class to set up the T2 StimFit model.
    +
    +        This model generates an optimisation dictionary (`opt`) containing the
    +        model parameters and fitting options.
    +
    +        Parameters
    +        ----------
    +        mode : {'non_selective', 'selective'}, optional
    +            Default 'non_selective'
    +            Choose whether the refocusing pulses are selective on
    +            non-selective.
    +        n_comp : {1, 2, 3}, optional
    +            Default 1
    +            The number of components to fit e.g. if n_comp=2, the model will
    +            estimate two T2 values, two M0 values and one B1 value per voxel.
    +        ukrin_vendor : {None, 'ge', 'philips', 'siemens'}, optional
    +            Default None
    +            The vendor of the MRI scanner used to acquire the data if the UKRIN
    +            protocol was used. Specifying a vendor at this stage overrides
    +            the relevant parameters in the model with those from the UKRIN
    +            protocol. If no vendor is specified, the default parameters are
    +            used but can be manually updated after instantiation.
    +
    +        Key Parameters in Options Dictionary
    +        ----------
    +        mode : {'non_selective', 'selective'}
    +            Choose whether the refocusing pulses are slice selective or
    +            non-selective.
    +        esp : float
    +            The echo spacing in seconds.
    +        etl : int
    +            The echo train length.
    +        T1 : float
    +            The approximate T1 value in seconds.
    +        Dz : list
    +            The start and end position of each slice in cm.
    +        Nz : int
    +            The number of positions along the slice profile to simulate signal
    +            decay for.
    +        Nrf : int
    +            The number of resampled points in the RF waveform.
    +        RFe : dict
    +            The excitation pulse parameters, outlined below.
    +        RFr : dict
    +            The refocusing pulse parameters, outlined below.
    +        lsq : dict
    +            The least squares fitting parameters, outlined below.
    +
    +        Key Parameters in RFe Dictionary
    +        ----------
    +        RF : np.ndarray
    +            The excitation pulse shape.
    +        G : float
    +            The amplitude of the excitation pulse in Gauss/cm.
    +        tau : float
    +            The excitation pulse duration in seconds.
    +        phase : float
    +            The relative phase of the excitation pulse in degrees (0 in CPMG).
    +        angle : float
    +            The flip angle of the excitation pulse in degrees (typically 90).
    +        ref : float
    +            The rephasing gradient fraction, times two. Near unity for
    +            excitation.
    +        alpha : list, optional
    +            The actual tip angle distribution across the slice (degrees). If
    +            not specified, the tip angle distribution is calculated.
    +
    +        Key Parameters in RFr Dictionary
    +        ----------
    +        RF : np.ndarray
    +            The refocusing pulse shape.
    +        G : float
    +            The amplitude of the refocusing pulse in Gauss/cm.
    +        tau : float
    +            The refocusing pulse duration in seconds.
    +        phase : float
    +            The relative phase of the refocusing pulse in degrees (90 in CPMG).
    +        angle : float
    +            The flip angle of the refocusing pulse in degrees (typically 180).
    +        ref : float
    +            The rephasing gradient fraction, times two. Typically, 0 for
    +            refocusing.
    +        alpha : list, optional
    +            The actual refocusing angle distribution across the slice
    +            (degrees). If not specified, the tip angle distribution is
    +            calculated.
    +
    +        Key Parameters in lsq Dictionary
    +        ----------
    +        Ncomp : int
    +            The number of components to fit.
    +        X0 : list
    +            The initial guess for the fitting parameters in the order
    +            [[T2_comp, M0_comp] * Ncomp, B1].
    +        XL : list
    +            The lower bounds for the fitting parameters in the order
    +            [[T2_comp, M0_comp] * Ncomp, B1].
    +        XU : list
    +            The upper bounds for the fitting parameters in the order
    +            [[T2_comp, M0_comp] * Ncomp, B1].
    +        xtol : float
    +            Tolerance for termination by the change of the independent
    +            variables.
    +        ftol : float
    +            Tolerance for termination by the change of the cost function.
    +        """
    +        if mode != 'non_selective' and mode != 'selective':
    +            raise ValueError(f'mode must be either "non_selective" or '
    +                             f'"selective". You specified {mode}.')
    +        self.mode = mode
    +        if n_comp not in [1, 2, 3]:
    +            raise ValueError(f'n_comp must be either 1, 2 or 3. You specified '
    +                             f'{n_comp}.')
    +        self.n_comp = n_comp
    +        if ukrin_vendor not in ['ge', 'philips', 'siemens']:
    +            warnings.warn('ukrin_vendor was not specified. Using default '
    +                          'pulse sequence parameters.')
    +        self.opt = dict()
    +        self.opt['mode'] = self.mode
    +        self.opt['esp'] = 10e-3
    +        self.opt['etl'] = 20
    +        self.opt['T1'] = 3
    +
    +        self.opt['RFe'] = dict()
    +        self.opt['RFr'] = dict()
    +        if self.mode == 'selective':
    +            self.opt['Dz'] = [-0.5, 0.5]
    +            self.opt['Nz'] = 51
    +            self.opt['Nrf'] = 64
    +            self.opt['RFe'] = {'RF': [],
    +                               'tau': 2e-3,
    +                               'G': 0.5,
    +                               'phase': 0,
    +                               'ref': 1,
    +                               'alpha': [],
    +                               'angle': 90}
    +            self.opt['RFr'] = {'RF': [],
    +                               'tau': 2e-3,
    +                               'G': 0.5,
    +                               'phase': 90,
    +                               'ref': 0,
    +                               'alpha': [],
    +                               'angle': 180,
    +                               'FA_array': np.ones(self.opt['etl'])}
    +        else:
    +            self.opt['RFe'] = {'angle': 90}
    +            self.opt['RFr'] = {'angle': 180,
    +                               'FA_array': np.ones(self.opt['etl'])}
    +        # Curve fitting parameters
    +        self.opt['lsq'] = {'Ncomp': n_comp,
    +                           'xtol': 5e-4,
    +                           'ftol': 1e-9}
    +        if self.opt['lsq']['Ncomp'] == 1:
    +            # [T2(sec), amp, B1]
    +            self.opt['lsq']['X0'] = [0.06, 0.1, 1]
    +            self.opt['lsq']['XU'] = [3, 1e+3, 1.8]
    +            self.opt['lsq']['XL'] = [0.015, 0, 0.2]
    +        elif self.opt['lsq']['Ncomp'] == 2:
    +            # [T2, amp, T2, amp, B1]
    +            self.opt['lsq']['X0'] = [0.02, 0.1, 0.331, 0.1, 1]
    +            self.opt['lsq']['XU'] = [0.25, 1e+3, 3, 1e+3, 1.8]
    +            self.opt['lsq']['XL'] = [0.015, 0, 0.25, 0, 0.2]
    +        elif self.opt['lsq']['Ncomp'] == 3:
    +            # [T2, amp, T2, amp, T2, amp, B1]
    +            self.opt['lsq']['X0'] = [0.02, 0.1, 0.036, 0.1, 0.131, 0.1, 1]
    +            self.opt['lsq']['XU'] = [0.035, 1e+3, 0.13, 1e3, 3, 1e+3, 1.8]
    +            self.opt['lsq']['XL'] = [0.015, 0, 0.035, 0, 0.13, 0, 0.2]
    +
    +        if ukrin_vendor is not None:
    +            self._set_ukrin_vendor(ukrin_vendor)
    +            if self.mode == 'selective':
    +                self.opt['RFe'] = self._set_rf(self.opt['RFe'])
    +                self.opt['RFr'] = self._set_rf(self.opt['RFr'])
    +
    +    def get_opt(self):
    +        return self.opt
    +
    +    def get_lsq(self):
    +        return self.opt['lsq']
    +
    +    def get_rfe(self):
    +        return self.opt['RFe']
    +
    +    def get_rfr(self):
    +        return self.opt['RFr']
    +
    +    def _set_ukrin_vendor(self, vendor):
    +        self.vendor = vendor
    +        self.opt['T1'] = 1.5
    +        self.opt['esp'] = 0.0129
    +        self.opt['etl'] = 10
    +        self.opt['te'] = (np.arange(self.opt['etl']) + 1) * self.opt['esp']
    +        self.opt['RFr']['FA_array'] = np.ones(self.opt['etl'])
    +        if self.vendor == 'ge':
    +            self.opt['RFe']['tau'] = 2000 / 1e6  # Duration
    +            self.opt['RFe']['G'] = 0.751599  # Amplitude
    +            self.opt['RFr']['tau'] = 3136 / 1e6
    +            self.opt['RFr']['G'] = 0.276839
    +            self.opt['RFe']['RF'] = rf_pulses.ge_90
    +            self.opt['RFr']['RF'] = rf_pulses.ge_180
    +            self.opt['Dz'] = [0, 0.45]  # Slice thickness
    +        elif self.vendor == 'philips':
    +            self.opt['RFe']['tau'] = 3820 / 1e6
    +            self.opt['RFe']['G'] = 0.392
    +            self.opt['RFr']['tau'] = 6010 / 1e6
    +            self.opt['RFr']['G'] = 0.327
    +            self.opt['RFe']['RF'] = rf_pulses.philips_90
    +            self.opt['RFr']['RF'] = rf_pulses.philips_180
    +            self.opt['Dz'] = [0, 0.45]
    +        elif self.vendor == 'siemens':
    +            self.opt['RFe']['tau'] = 3072 / 1e6
    +            self.opt['RFe']['G'] = 0.417
    +            self.opt['RFr']['tau'] = 3000 / 1e6
    +            self.opt['RFr']['G'] = 0.326
    +            self.opt['RFe']['RF'] = rf_pulses.ge_90
    +            self.opt['RFr']['RF'] = rf_pulses.ge_180
    +            self.opt['Dz'] = [0, 0.5]
    +        else:
    +            warnings.warn(f'{self.vendor} is not implemented. Please '
    +                          f'manually specify the models parameters.')
    +
    +    def _set_rf(self, rf):
    +        dz = self.opt['Dz']
    +        nz = self.opt['Nz']
    +        nrf = self.opt['Nrf']
    +
    +        gamma = 2 * np.pi * 42.575e6 / 10000  # Gauss
    +        z = np.linspace(dz[0], dz[1], nz)
    +        scale = rf['angle'] / (gamma * rf['tau'] * abs(np.sum(rf['RF'])) / len(
    +            rf['RF']) * 180 / np.pi)
    +        rf['RF'] *= scale
    +
    +        m = np.zeros([3, nz])
    +        m[2, :] = 1
    +        rf['RF'] = 1e-4 * rf['RF']  # approximation for
    +        # small tip angle
    +
    +        phi = gamma * rf['G'] * z * rf['tau'] / nrf
    +        cphi = np.cos(phi)
    +        sphi = np.sin(phi)
    +        cp_rf = np.cos(rf['phase'] * np.pi / 180)
    +        sp_rf = np.sin(rf['phase'] * np.pi / 180)
    +        theta_rf = gamma * rf['RF'] * rf['tau'] / nrf
    +        ct_rf = np.cos(theta_rf)
    +        st_rf = np.sin(theta_rf)
    +
    +        for i in range(nrf):
    +            for j in range(nz):
    +                rz = np.array([[cphi[j], sphi[j], 0],
    +                               [-sphi[j], cphi[j], 0],
    +                               [0, 0, 1]])
    +                m[:, j] = np.dot(rz, m[:, j])
    +
    +            r = np.array([[1, 0, 0],
    +                          [0, ct_rf[i], st_rf[i]],
    +                          [0, -st_rf[i], ct_rf[i]]])
    +            if rf['phase'] != 0:
    +                rz = np.array([[cp_rf, sp_rf, 0],
    +                               [-sp_rf, cp_rf, 0],
    +                               [0, 0, 1]])
    +                rzm = np.array([[cp_rf, -sp_rf, 0],
    +                                [sp_rf, cp_rf, 0],
    +                                [0, 0, 1]])
    +                r = np.dot(rzm, np.dot(r, rz))
    +            m = np.dot(r, m)
    +
    +        if rf['ref'] > 0:
    +            psi = -rf['ref'] / 2 * gamma * rf['G'] * z * rf['tau']
    +            for j in range(nz):
    +                rz = np.array([[np.cos(psi[j]), np.sin(psi[j]), 0],
    +                               [-np.sin(psi[j]), np.cos(psi[j]), 0],
    +                               [0, 0, 1]])
    +                m[:, j] = np.dot(rz, m[:, j])
    +
    +        rf['RF'] = 1e4 * rf['RF']
    +        rf['alpha'] = 1e4 * np.arccos(m[2, :])
    +        return rf
    +
    +

    Methods

    +
    +
    +def get_lsq(self) +
    +
    +
    +
    + +Expand source code + +
    def get_lsq(self):
    +    return self.opt['lsq']
    +
    +
    +
    +def get_opt(self) +
    +
    +
    +
    + +Expand source code + +
    def get_opt(self):
    +    return self.opt
    +
    +
    +
    +def get_rfe(self) +
    +
    +
    +
    + +Expand source code + +
    def get_rfe(self):
    +    return self.opt['RFe']
    +
    +
    +
    +def get_rfr(self) +
    +
    +
    +
    + +Expand source code + +
    def get_rfr(self):
    +    return self.opt['RFr']
    +
    +
    +
    +
    +
    +class T2StimFit +(pixel_array, affine, model, mask=None, multithread='auto', norm=True) +
    +
    +

    Attributes

    +
    +
    t2_map : np.ndarray
    +
    The estimated T2 values in ms
    +
    m0_map : np.ndarray
    +
    The estimated M0 values
    +
    r2_map : np.ndarray
    +
    The R-Squared value of the fit, values close to 1 indicate a good +fit, lower values indicate a poorer fit
    +
    shape : tuple
    +
    The shape of the T2 map
    +
    n_vox : int
    +
    The number of voxels in the map i.e. the product of all dimensions +apart from TE
    +
    Class for performing stimulated echo T2 fitting as in Marc Lebel R.
    +
    StimFit : A Toolbox for Robust T2 Mapping with Stimulated Echo
    +
     
    +
    +

    Compensation. In: Proc. Intl. Soc. Mag. Reson. Med. 20. Melbourne; +2012:2558. https://archive.ismrm.org/2012/2558.html.

    +

    Parameters

    +
    +
    pixel_array : np.ndarray
    +
    An array containing the signal from each voxel at each echo +time with the last dimension being time i.e. the array needed to +generate a 3D T2 map would have dimensions [x, y, z, TE].
    +
    affine : np.ndarray
    +
    A matrix giving the relationship between voxel coordinates and +world coordinates.
    +
    model : StimFitModel
    +
    A StimFitModel object containing the model parameters.
    +
    mask : np.ndarray, optional
    +
    A boolean mask of the voxels to fit. Should be the shape of the +desired T2 map rather than the raw data i.e. omit the time +dimension.
    +
    multithread : bool or 'auto', optional
    +
    Default 'auto'. +If True, fitting will be distributed over all cores available on +the node. If False, fitting will be carried out on a single thread. +'auto' attempts to apply multithreading where appropriate based +on the number of voxels being fit.
    +
    norm : bool, optional
    +
    Default True. +StimFit is performed on normalised data. If norm is False, +it is assumed that the data has already been normalised. If norm +is True, the data will be normalised before fitting.
    +
    +
    + +Expand source code + +
    class T2StimFit:
    +    """
    +    Attributes
    +    ----------
    +    t2_map : np.ndarray
    +        The estimated T2 values in ms
    +    m0_map : np.ndarray
    +        The estimated M0 values
    +    r2_map : np.ndarray
    +        The R-Squared value of the fit, values close to 1 indicate a good
    +        fit, lower values indicate a poorer fit
    +    shape : tuple
    +        The shape of the T2 map
    +    n_vox : int
    +        The number of voxels in the map i.e. the product of all dimensions
    +        apart from TE
    +    """
    +    def __init__(self, pixel_array, affine, model,
    +                 mask=None, multithread='auto', norm=True):
    +        """
    +        Class for performing stimulated echo T2 fitting as in Marc Lebel R.
    +        StimFit: A Toolbox for Robust T2 Mapping with Stimulated Echo
    +        Compensation. In: Proc. Intl. Soc. Mag. Reson. Med. 20. Melbourne;
    +        2012:2558. https://archive.ismrm.org/2012/2558.html.
    +
    +        Parameters
    +        ----------
    +        pixel_array : np.ndarray
    +            An array containing the signal from each voxel at each echo
    +            time with the last dimension being time i.e. the array needed to
    +            generate a 3D T2 map would have dimensions [x, y, z, TE].
    +        affine : np.ndarray
    +            A matrix giving the relationship between voxel coordinates and
    +            world coordinates.
    +        model : StimFitModel
    +            A StimFitModel object containing the model parameters.
    +        mask : np.ndarray, optional
    +            A boolean mask of the voxels to fit. Should be the shape of the
    +            desired T2 map rather than the raw data i.e. omit the time
    +            dimension.
    +        multithread : bool or 'auto', optional
    +            Default 'auto'.
    +            If True, fitting will be distributed over all cores available on
    +            the node. If False, fitting will be carried out on a single thread.
    +            'auto' attempts to apply multithreading where appropriate based
    +            on the number of voxels being fit.
    +        norm : bool, optional
    +            Default True.
    +            StimFit is performed on normalised data. If norm is False,
    +            it is assumed that the data has already been normalised. If norm
    +            is True, the data will be normalised before fitting.
    +        """
    +        self.pixel_array = np.copy(pixel_array)
    +        self.shape = pixel_array.shape[:-1]
    +        self.n_vox = np.prod(self.shape)
    +        self.affine = affine
    +        self.model = model
    +
    +        assert multithread is True \
    +               or multithread is False \
    +               or multithread == 'auto', f'multithreaded must be True,' \
    +                                         f'False or auto. You entered ' \
    +                                         f'{multithread}'
    +        if multithread == 'auto':
    +            if self.n_vox > 20:
    +                multithread = True
    +            else:
    +                multithread = False
    +        self.multithread = multithread
    +
    +        # Generate a mask if there isn't one specified
    +        if mask is None:
    +            self.mask = np.ones(self.shape, dtype=bool)
    +        else:
    +            self.mask = mask
    +            # Don't process any nan values
    +        self.mask[np.isnan(np.sum(pixel_array, axis=-1))] = False
    +
    +        # Normalise the data
    +        if norm:
    +            self.pixel_array /= np.nanmax(self.pixel_array)
    +
    +        if np.nanmax(self.pixel_array) > 1:
    +            warnings.warn('Pixel array contains values greater than 1. '
    +                          'Data should be normalised, please set norm=True '
    +                          'or manually normalise your data.')
    +
    +        # Perform the fit
    +        self._fit()
    +
    +    def to_nifti(self, output_directory=os.getcwd(), base_file_name='Output',
    +                 maps='all'):
    +        """Exports some of the T2StimFit class attributes to NIFTI.
    +
    +        Parameters
    +        ----------
    +        output_directory : string, optional
    +            Path to the folder where the NIFTI files will be saved.
    +        base_file_name : string, optional
    +            Filename of the resulting NIFTI. This code appends the extension.
    +            Eg., base_file_name = 'Output' will result in 'Output.nii.gz'.
    +        maps : list or 'all', optional
    +            List of maps to save to NIFTI. This should either the string "all"
    +            or a list of maps from ["t2", "m0", "b1", "r2", "mask"].
    +        """
    +        os.makedirs(output_directory, exist_ok=True)
    +        base_path = os.path.join(output_directory, base_file_name)
    +        if maps == 'all' or maps == ['all']:
    +            maps = ['t2', 'm0', 'b1', 'r2', 'mask']
    +        if isinstance(maps, list):
    +            for result in maps:
    +                if result == 't2' or result == 't2_map':
    +                    t2_nifti = nib.Nifti1Image(self.t2_map, affine=self.affine)
    +                    nib.save(t2_nifti, base_path + '_t2_map.nii.gz')
    +                elif result == 'm0' or result == 'm0_map':
    +                    m0_nifti = nib.Nifti1Image(self.m0_map, affine=self.affine)
    +                    nib.save(m0_nifti, base_path + '_m0_map.nii.gz')
    +                elif result == 'b1':
    +                    m0_err_nifti = nib.Nifti1Image(self.b1_map,
    +                                                   affine=self.affine)
    +                    nib.save(m0_err_nifti, base_path + '_b1_map.nii.gz')
    +                elif result == 'r2' or result == 'r2_map':
    +                    r2_nifti = nib.Nifti1Image(self.r2_map,
    +                                               affine=self.affine)
    +                    nib.save(r2_nifti, base_path + '_r2_map.nii.gz')
    +                elif result == 'mask':
    +                    mask_nifti = nib.Nifti1Image(self.mask.astype(np.uint16),
    +                                                 affine=self.affine)
    +                    nib.save(mask_nifti, base_path + '_mask.nii.gz')
    +        else:
    +            raise ValueError('No NIFTI file saved. The variable "maps" '
    +                             'should be "all" or a list of maps from '
    +                             '"["t2", "m0", "b1", "r2", '
    +                             '"mask"]".')
    +
    +    def _fit(self):
    +        mask = self.mask.flatten()
    +        signal = self.pixel_array.reshape(self.n_vox, self.model.opt['etl'])
    +        idx = np.argwhere(mask).squeeze()
    +        signal = signal[idx, :]
    +
    +        if self.multithread:
    +            with ProcessPool() as executor:
    +                results = executor.map(self._fit_signal, signal)
    +        else:
    +            results = list(tqdm(map(self._fit_signal, signal),
    +                                total=np.sum(self.mask)))
    +        t2 = np.array([result[0] for result in results])
    +        m0 = np.array([result[1] for result in results])
    +        b1 = np.array([result[2] for result in results])
    +        r2 = np.array([result[3] for result in results])
    +
    +        if self.model.n_comp > 1:
    +            t2_map = np.zeros((self.n_vox, self.model.n_comp))
    +            m0_map = np.zeros((self.n_vox, self.model.n_comp))
    +            r2_map = np.zeros((self.n_vox, self.model.n_comp))
    +        else:
    +            t2_map = np.zeros(self.n_vox)
    +            m0_map = np.zeros(self.n_vox)
    +            r2_map = np.zeros(self.n_vox)
    +        b1_map = np.zeros(self.n_vox)
    +        t2_map[idx] = t2 * 1000  # Convert to ms
    +        m0_map[idx] = m0
    +        b1_map[idx] = b1
    +        r2_map[idx] = r2
    +        self.t2_map = np.squeeze(t2_map.reshape((*self.shape,
    +                                                 self.model.n_comp)))
    +        self.m0_map = np.squeeze(m0_map.reshape((*self.shape,
    +                                                 self.model.n_comp)))
    +        self.b1_map = b1_map.reshape(self.shape)
    +        self.r2_map = np.squeeze(r2_map.reshape((*self.shape,
    +                                                 self.model.n_comp)))
    +
    +    def _fit_signal(self, signal):
    +        if len(signal) != self.model.opt['etl']:
    +            raise Exception('Inconsistent echo train length')
    +
    +        # Two component fitting
    +        if self.model.opt['lsq']['Ncomp'] == 2:
    +            x = optimize.least_squares(self._residual2,
    +                                       self.model.opt['lsq']['X0'],
    +                                       args=(signal, self.model.opt,
    +                                             self.model.mode),
    +                                       bounds=(self.model.opt['lsq']['XL'],
    +                                               self.model.opt['lsq']['XU']),
    +                                       xtol=self.model.opt['lsq']['xtol'],
    +                                       ftol=self.model.opt['lsq']['ftol']).x
    +            t2, amp, b1 = [x[0], x[2]], [x[1], x[3]], x[4]
    +            r2 = [r2_score(signal, two_param_eq(self.model.opt['te'], t2[0],
    +                                                amp[0])),
    +                  r2_score(signal, two_param_eq(self.model.opt['te'], t2[1],
    +                                                amp[1]))]
    +
    +        # Three component fitting
    +        elif self.model.opt['lsq']['Ncomp'] == 3:
    +            x = optimize.least_squares(self._residual3,
    +                                       self.model.opt['lsq']['X0'],
    +                                       args=(signal, self.model.opt,
    +                                             self.model.mode),
    +                                       bounds=(self.model.opt['lsq']['XL'],
    +                                               self.model.opt['lsq']['XU']),
    +                                       xtol=self.model.opt['lsq']['xtol'],
    +                                       ftol=self.model.opt['lsq']['ftol']).x
    +            t2, amp, b1 = [x[0], x[2], x[4]], [x[1], x[3], x[5]], x[6]
    +            r2 = [r2_score(signal, two_param_eq(self.model.opt['te'], t2[0],
    +                                                amp[0])),
    +                  r2_score(signal, two_param_eq(self.model.opt['te'], t2[1],
    +                                                amp[1])),
    +                  r2_score(signal, two_param_eq(self.model.opt['te'], t2[2],
    +                                                amp[2]))]
    +
    +        # One component fitting
    +        else:
    +            x = optimize.least_squares(self._residual1,
    +                                       self.model.opt['lsq']['X0'],
    +                                       args=(signal, self.model.opt,
    +                                             self.model.mode),
    +                                       bounds=(self.model.opt['lsq']['XL'],
    +                                               self.model.opt['lsq']['XU']),
    +                                       xtol=self.model.opt['lsq']['xtol'],
    +                                       ftol=self.model.opt['lsq']['ftol']).x
    +            t2, amp, b1 = x
    +            fit_sig = two_param_eq(self.model.opt['te'], t2, amp)
    +            r2 = r2_score(signal, fit_sig)
    +        return t2, amp, b1, r2
    +
    +    @staticmethod
    +    def _residual1(p, y, opt, mode):
    +        return y - _epgsig(p[0], p[2], opt, mode) * p[1]
    +
    +    @staticmethod
    +    def _residual2(p, y, opt, mode):
    +        return y - (_epgsig(p[0], p[4], opt, mode) * p[1] -
    +                    _epgsig(p[2], p[4], opt, mode) * p[3])
    +
    +    @staticmethod
    +    def _residual3(p, y, opt, mode):
    +        return y - (_epgsig(p[0], p[6], opt, mode) * p[1] -
    +                    _epgsig(p[4], p[6], opt, mode) * p[5] -
    +                    _epgsig(p[2], p[6], opt, mode) * p[3])
    +
    +

    Methods

    +
    +
    +def to_nifti(self, output_directory='/home/runner/work/ukat/ukat', base_file_name='Output', maps='all') +
    +
    +

    Exports some of the T2StimFit class attributes to NIFTI.

    +

    Parameters

    +
    +
    output_directory : string, optional
    +
    Path to the folder where the NIFTI files will be saved.
    +
    base_file_name : string, optional
    +
    Filename of the resulting NIFTI. This code appends the extension. +Eg., base_file_name = 'Output' will result in 'Output.nii.gz'.
    +
    maps : list or 'all', optional
    +
    List of maps to save to NIFTI. This should either the string "all" +or a list of maps from ["t2", "m0", "b1", "r2", "mask"].
    +
    +
    + +Expand source code + +
    def to_nifti(self, output_directory=os.getcwd(), base_file_name='Output',
    +             maps='all'):
    +    """Exports some of the T2StimFit class attributes to NIFTI.
    +
    +    Parameters
    +    ----------
    +    output_directory : string, optional
    +        Path to the folder where the NIFTI files will be saved.
    +    base_file_name : string, optional
    +        Filename of the resulting NIFTI. This code appends the extension.
    +        Eg., base_file_name = 'Output' will result in 'Output.nii.gz'.
    +    maps : list or 'all', optional
    +        List of maps to save to NIFTI. This should either the string "all"
    +        or a list of maps from ["t2", "m0", "b1", "r2", "mask"].
    +    """
    +    os.makedirs(output_directory, exist_ok=True)
    +    base_path = os.path.join(output_directory, base_file_name)
    +    if maps == 'all' or maps == ['all']:
    +        maps = ['t2', 'm0', 'b1', 'r2', 'mask']
    +    if isinstance(maps, list):
    +        for result in maps:
    +            if result == 't2' or result == 't2_map':
    +                t2_nifti = nib.Nifti1Image(self.t2_map, affine=self.affine)
    +                nib.save(t2_nifti, base_path + '_t2_map.nii.gz')
    +            elif result == 'm0' or result == 'm0_map':
    +                m0_nifti = nib.Nifti1Image(self.m0_map, affine=self.affine)
    +                nib.save(m0_nifti, base_path + '_m0_map.nii.gz')
    +            elif result == 'b1':
    +                m0_err_nifti = nib.Nifti1Image(self.b1_map,
    +                                               affine=self.affine)
    +                nib.save(m0_err_nifti, base_path + '_b1_map.nii.gz')
    +            elif result == 'r2' or result == 'r2_map':
    +                r2_nifti = nib.Nifti1Image(self.r2_map,
    +                                           affine=self.affine)
    +                nib.save(r2_nifti, base_path + '_r2_map.nii.gz')
    +            elif result == 'mask':
    +                mask_nifti = nib.Nifti1Image(self.mask.astype(np.uint16),
    +                                             affine=self.affine)
    +                nib.save(mask_nifti, base_path + '_mask.nii.gz')
    +    else:
    +        raise ValueError('No NIFTI file saved. The variable "maps" '
    +                         'should be "all" or a list of maps from '
    +                         '"["t2", "m0", "b1", "r2", '
    +                         '"mask"]".')
    +
    +
    +
    +
    +
    +
    +
    + +
    + + + \ No newline at end of file diff --git a/mapping/t2star.html b/mapping/t2star.html index 7878ac4b..50a04e42 100644 --- a/mapping/t2star.html +++ b/mapping/t2star.html @@ -30,9 +30,42 @@

    Module ukat.mapping.t2star

    import warnings import numpy as np import nibabel as nib -import concurrent.futures + +from . import fitting + +from pathos.pools import ProcessPool from tqdm import tqdm -from scipy.optimize import curve_fit +from sklearn.metrics import r2_score + + +class T2StarExpModel(fitting.Model): + def __init__(self, pixel_array, te, mask=None, multithread=True): + """ + A class for fitting T2* data to a mono-exponential model. + + Parameters + ---------- + pixel_array : np.ndarray + An array containing the signal from each voxel at each echo + time with the last dimension being time i.e. the array needed to + generate a 3D T2* map would have dimensions [x, y, z, TE]. + te : np.ndarray + An array of the echo times used for the last dimension of the + pixel_array. In milliseconds. + mask : np.ndarray, optional + A boolean mask of the voxels to fit. Should be the shape of the + desired T2* map rather than the raw data i.e. omit the time + dimension. + multithread : bool, optional + Default True + If True, the fitting will be performed in parallel using all + available cores + """ + + super().__init__(pixel_array, te, two_param_eq, mask, multithread) + self.bounds = ([0, 0], [700, 100000000]) + self.initial_guess = [20, 10000] + self.generate_lists() class T2Star: @@ -49,6 +82,9 @@

    Module ukat.mapping.t2star

    m0_err : np.ndarray The certainty in the fit of `m0_map`. Only returned if `2p_exp` method is used, otherwise is an array of nan + r2 : np.ndarray + The R-Squared value of the fit, values close to 1 indicate a good + fit, lower values indicate a poorer fit shape : tuple The shape of the T2* map n_te : int @@ -103,27 +139,29 @@

    Module ukat.mapping.t2star

    'number of time frames on the last axis ' \ 'of pixel_array' assert method == 'loglin' \ - or method == '2p_exp', 'method must be loglin or 2p_exp. You ' \ - 'entered {}'.format(method) + or method == '2p_exp', f'method must be loglin or 2p_exp. You ' \ + f'entered {method}' assert multithread is True \ or multithread is False \ - or multithread == 'auto', 'multithreaded must be True, False or ' \ - 'auto. You entered {}'\ - .format(multithread) + or multithread == 'auto', f'multithreaded must be True, False ' \ + f'or auto. You entered {multithread}' self.pixel_array = pixel_array self.shape = pixel_array.shape[:-1] self.n_te = pixel_array.shape[-1] self.n_vox = np.prod(self.shape) self.affine = affine + # Generate a mask if there isn't one specified if mask is None: self.mask = np.ones(self.shape, dtype=bool) else: - self.mask = mask - # Don't process any nan values + self.mask = mask.astype(bool) + + # Don't process any nan values self.mask[np.isnan(np.sum(pixel_array, axis=-1))] = False self.echo_list = echo_list self.method = method + # Auto multithreading conditions if multithread == 'auto': if self.method == '2p_exp' and self.n_vox > 20: @@ -133,8 +171,33 @@

    Module ukat.mapping.t2star

    self.multithread = multithread # Fit data - self.t2star_map, self.t2star_err, self.m0_map, self.m0_err\ - = self.__fit__() + + # Initialise an exponential model, even if we're using loglin fit, + # so we're using the same limits etc + self._exp_model = T2StarExpModel(self.pixel_array, self.echo_list, + self.mask, self.multithread) + if self.method == 'loglin': + popt, error, r2 = self._loglin_fit() + else: + popt, error, r2 = fitting.fit_image(self._exp_model) + + self.t2star_map = popt[0] + self.m0_map = popt[1] + self.t2star_err = error[0] + self.m0_err = error[1] + self.r2 = r2 + + # Filter values that are very close to models upper bounds of T2* or + # M0 out. + threshold = 0.999 # 99.9% of the upper bound + bounds_mask = ((self.t2star_map > + self._exp_model.bounds[1][0] * threshold) | + (self.m0_map > self._exp_model.bounds[1][1] * threshold)) + self.t2star_map[bounds_mask] = 0 + self.m0_map[bounds_mask] = 0 + self.t2star_err[bounds_mask] = 0 + self.m0_err[bounds_mask] = 0 + self.r2[bounds_mask] = 0 # Warn if using loglin method to produce a map with a large # proportion of T2* < 20 ms i.e. where loglin isn't as accurate. @@ -150,128 +213,88 @@

    Module ukat.mapping.t2star

    'interest, consider using the 2p_exp fitting' ' method'.format(proportion_less_than_20)) - def __fit__(self): - - # Initialise maps - t2star_map = np.zeros(self.n_vox) - t2star_err = np.zeros(self.n_vox) - m0_map = np.zeros(self.n_vox) - m0_err = np.zeros(self.n_vox) - mask = self.mask.flatten() - signal = self.pixel_array.reshape(-1, self.n_te) - # Get indices of voxels to process - idx = np.argwhere(mask).squeeze() - - # Multithreaded method + def _loglin_fit(self): if self.multithread: - with concurrent.futures.ProcessPoolExecutor() as pool: - with tqdm(total=idx.size) as progress: - futures = [] - - for ind in idx: - future = pool.submit(self.__fit_signal__, - signal[ind, :], - self.echo_list, - self.method) - future.add_done_callback(lambda p: progress.update()) - futures.append(future) - - results = [] - for future in futures: - result = future.result() - results.append(result) - t2star_map[idx], t2star_err[idx], m0_map[idx], m0_err[idx] \ - = [np.array(row) for row in zip(*results)] - - # Single threaded method + with ProcessPool() as executor: + results = executor.map(self._fit_loglin_signal, + self._exp_model.signal_list, + self._exp_model.x_list, + self._exp_model.mask_list, + [self._exp_model] * self.n_vox) else: - with tqdm(total=idx.size) as progress: - for ind in idx: - sig = signal[ind, :] - t2star_map[ind], t2star_err[ind], \ - m0_map[ind], m0_err[ind] \ - = self.__fit_signal__(sig, self.echo_list, self.method) - progress.update(1) - - # Reshape results to raw data shape - t2star_map = t2star_map.reshape(self.shape) - t2star_err = t2star_err.reshape(self.shape) - m0_map = m0_map.reshape(self.shape) - m0_err = m0_err.reshape(self.shape) - - return t2star_map, t2star_err, m0_map, m0_err + results = list(tqdm(map(self._fit_loglin_signal, + self._exp_model.signal_list, + self._exp_model.x_list, + self._exp_model.mask_list, + [self._exp_model] * self.n_vox), + total=self.n_vox)) + popt_array = np.array([result[0] for result in results]) + popt_list = [popt_array[:, p].reshape(self._exp_model.map_shape) for p + in range(self._exp_model.n_params)] + error_array = np.array([result[1] for result in results]) + error_list = [error_array[:, p].reshape(self._exp_model.map_shape) + for p in range(self._exp_model.n_params)] + r2 = np.array([result[2] for result in results]).reshape( + self._exp_model.map_shape) + return popt_list, error_list, r2 @staticmethod - def __fit_signal__(sig, te, method): - if method == 'loglin': - s_w = 0.0 - s_wx = 0.0 - s_wx2 = 0.0 - s_wy = 0.0 - s_wxy = 0.0 - n_te = len(sig) - - noise = sig.sum() / n_te - sd = np.abs(np.sum(sig ** 2) / n_te - noise ** 2) - if sd > 1e-10: - for t in range(n_te): - if sig[t] > 0: - te_tmp = te[t] - if sig[t] > sd: - sigma = np.log(sig[t] / (sig[t] - sd)) - else: - sigma = np.log(sig[t] / 0.0001) - logsig = np.log(sig[t]) - weight = 1 / sigma ** 2 - - s_w += weight - s_wx += weight * te_tmp - s_wx2 += weight * te_tmp ** 2 - s_wy += weight * logsig - s_wxy += weight * te_tmp * logsig - - delta = (s_w * s_wx2) - (s_wx ** 2) - if delta > 1e-5: - a = (1 / delta) * (s_wx2 * s_wy - s_wx * s_wxy) - b = (1 / delta) * (s_w * s_wxy - s_wx * s_wy) - t2star = np.real(-1 / b) - m0 = np.real(np.exp(a)) - if t2star < 0 or t2star > 700 or np.isnan(t2star): + def _fit_loglin_signal(sig, te, mask, model): + if mask is True: + with np.errstate(divide='ignore', invalid='ignore'): + sig = np.array(sig) + te = np.array(te) + s_w = 0.0 + s_wx = 0.0 + s_wx2 = 0.0 + s_wy = 0.0 + s_wxy = 0.0 + n_te = len(sig) + + noise = sig.sum() / n_te + sd = np.abs(np.sum(sig ** 2) / n_te - noise ** 2) + if sd > 1e-10: + for t in range(n_te): + if sig[t] > 0: + te_tmp = te[t] + if sig[t] > sd: + sigma = np.log(sig[t] / (sig[t] - sd)) + else: + sigma = np.log(sig[t] / 0.0001) + logsig = np.log(sig[t]) + weight = 1 / sigma ** 2 + + s_w += weight + s_wx += weight * te_tmp + s_wx2 += weight * te_tmp ** 2 + s_wy += weight * logsig + s_wxy += weight * te_tmp * logsig + + delta = (s_w * s_wx2) - (s_wx ** 2) + if delta > 1e-5: + a = (1 / delta) * (s_wx2 * s_wy - s_wx * s_wxy) + b = (1 / delta) * (s_w * s_wxy - s_wx * s_wy) + t2star = np.real(-1 / b) + m0 = np.real(np.exp(a)) + if t2star < 0 or t2star > model.bounds[1][0] or \ + np.isnan(t2star): + t2star = 0 + m0 = 0 + else: t2star = 0 m0 = 0 else: t2star = 0 m0 = 0 - else: - t2star = 0 - m0 = 0 - t2star_err = np.nan - m0_err = np.nan - - elif method == '2p_exp': - # Initialise parameters - bounds = ([0, 0], [700, 100000000]) - initial_guess = [20, 10000] - - # Fit data to equation - try: - popt, pcov = curve_fit(two_param_eq, te, sig, - p0=initial_guess, bounds=bounds) - except RuntimeError: - popt = np.zeros(2) - pcov = np.zeros((2, 2)) - - # Extract fits and errors from result variables - if popt[0] < bounds[1][0] - 1: - t2star = popt[0] - m0 = popt[1] - err = np.sqrt(np.diag(pcov)) - t2star_err = err[0] - m0_err = err[1] - else: - t2star, m0, t2star_err, m0_err = 0, 0, 0, 0 + else: + t2star = 0 + m0 = 0 - return t2star, t2star_err, m0, m0_err + fit_sig = two_param_eq(te, t2star, m0) + r2 = r2_score(sig, fit_sig) + t2star_err = np.nan + m0_err = np.nan + return (t2star, m0), (t2star_err, m0_err), r2 def r2star_map(self): """ @@ -286,15 +309,17 @@

    Module ukat.mapping.t2star

    ------- r2star_map : np.ndarray An array containing the R2* map generated - by the function with R2* measured in ms. + by the function with R2* measured in ms^-1. """ - return np.nan_to_num(np.reciprocal(self.t2star_map), - posinf=0, neginf=0) + with np.errstate(divide='ignore'): + r2star = np.nan_to_num(np.reciprocal(self.t2star_map), + posinf=0, neginf=0) + return r2star def to_nifti(self, output_directory=os.getcwd(), base_file_name='Output', maps='all'): """Exports some of the T2Star class attributes to NIFTI. - + Parameters ---------- output_directory : string, optional @@ -305,12 +330,12 @@

    Module ukat.mapping.t2star

    maps : list or 'all', optional List of maps to save to NIFTI. This should either the string "all" or a list of maps from ["t2star", "t2star_err", "m0", - "m0_err", "r2star", "mask"]. + "m0_err", "r2star", "r2", "mask"]. """ os.makedirs(output_directory, exist_ok=True) base_path = os.path.join(output_directory, base_file_name) if maps == 'all' or maps == ['all']: - maps = ['t2star', 'm0', 'r2star', 'mask'] + maps = ['t2star', 'm0', 'r2star', 'r2', 'mask'] if self.method == '2p_exp': maps += ['t2star_err', 'm0_err'] if isinstance(maps, list): @@ -345,6 +370,10 @@

    Module ukat.mapping.t2star

    r2star_nifti = nib.Nifti1Image(T2Star.r2star_map(self), affine=self.affine) nib.save(r2star_nifti, base_path + '_r2star_map.nii.gz') + elif result == 'r2' or result == 'r2_map': + r2_nifti = nib.Nifti1Image(self.r2, + affine=self.affine) + nib.save(r2_nifti, base_path + '_r2.nii.gz') elif result == 'mask': mask_nifti = nib.Nifti1Image(self.mask.astype(np.uint16), affine=self.affine) @@ -376,7 +405,9 @@

    Module ukat.mapping.t2star

    ------- signal: np.ndarray """ - return np.sqrt(np.square(m0 * np.exp(-t / t2star))) + with np.errstate(divide='ignore'): + signal = m0 * np.exp(-t / t2star) + return signal
    @@ -428,7 +459,9 @@

    Returns

    ------- signal: np.ndarray """ - return np.sqrt(np.square(m0 * np.exp(-t / t2star))) + with np.errstate(divide='ignore'): + signal = m0 * np.exp(-t / t2star) + return signal @@ -453,6 +486,9 @@

    Classes

    m0_err : np.ndarray
    The certainty in the fit of m0_map. Only returned if 2p_exp method is used, otherwise is an array of nan
    +
    r2 : np.ndarray
    +
    The R-Squared value of the fit, values close to 1 indicate a good +fit, lower values indicate a poorer fit
    shape : tuple
    The shape of the T2* map
    n_te : int
    @@ -516,6 +552,9 @@

    Parameters

    m0_err : np.ndarray The certainty in the fit of `m0_map`. Only returned if `2p_exp` method is used, otherwise is an array of nan + r2 : np.ndarray + The R-Squared value of the fit, values close to 1 indicate a good + fit, lower values indicate a poorer fit shape : tuple The shape of the T2* map n_te : int @@ -570,27 +609,29 @@

    Parameters

    'number of time frames on the last axis ' \ 'of pixel_array' assert method == 'loglin' \ - or method == '2p_exp', 'method must be loglin or 2p_exp. You ' \ - 'entered {}'.format(method) + or method == '2p_exp', f'method must be loglin or 2p_exp. You ' \ + f'entered {method}' assert multithread is True \ or multithread is False \ - or multithread == 'auto', 'multithreaded must be True, False or ' \ - 'auto. You entered {}'\ - .format(multithread) + or multithread == 'auto', f'multithreaded must be True, False ' \ + f'or auto. You entered {multithread}' self.pixel_array = pixel_array self.shape = pixel_array.shape[:-1] self.n_te = pixel_array.shape[-1] self.n_vox = np.prod(self.shape) self.affine = affine + # Generate a mask if there isn't one specified if mask is None: self.mask = np.ones(self.shape, dtype=bool) else: - self.mask = mask - # Don't process any nan values + self.mask = mask.astype(bool) + + # Don't process any nan values self.mask[np.isnan(np.sum(pixel_array, axis=-1))] = False self.echo_list = echo_list self.method = method + # Auto multithreading conditions if multithread == 'auto': if self.method == '2p_exp' and self.n_vox > 20: @@ -600,8 +641,33 @@

    Parameters

    self.multithread = multithread # Fit data - self.t2star_map, self.t2star_err, self.m0_map, self.m0_err\ - = self.__fit__() + + # Initialise an exponential model, even if we're using loglin fit, + # so we're using the same limits etc + self._exp_model = T2StarExpModel(self.pixel_array, self.echo_list, + self.mask, self.multithread) + if self.method == 'loglin': + popt, error, r2 = self._loglin_fit() + else: + popt, error, r2 = fitting.fit_image(self._exp_model) + + self.t2star_map = popt[0] + self.m0_map = popt[1] + self.t2star_err = error[0] + self.m0_err = error[1] + self.r2 = r2 + + # Filter values that are very close to models upper bounds of T2* or + # M0 out. + threshold = 0.999 # 99.9% of the upper bound + bounds_mask = ((self.t2star_map > + self._exp_model.bounds[1][0] * threshold) | + (self.m0_map > self._exp_model.bounds[1][1] * threshold)) + self.t2star_map[bounds_mask] = 0 + self.m0_map[bounds_mask] = 0 + self.t2star_err[bounds_mask] = 0 + self.m0_err[bounds_mask] = 0 + self.r2[bounds_mask] = 0 # Warn if using loglin method to produce a map with a large # proportion of T2* < 20 ms i.e. where loglin isn't as accurate. @@ -617,128 +683,88 @@

    Parameters

    'interest, consider using the 2p_exp fitting' ' method'.format(proportion_less_than_20)) - def __fit__(self): - - # Initialise maps - t2star_map = np.zeros(self.n_vox) - t2star_err = np.zeros(self.n_vox) - m0_map = np.zeros(self.n_vox) - m0_err = np.zeros(self.n_vox) - mask = self.mask.flatten() - signal = self.pixel_array.reshape(-1, self.n_te) - # Get indices of voxels to process - idx = np.argwhere(mask).squeeze() - - # Multithreaded method + def _loglin_fit(self): if self.multithread: - with concurrent.futures.ProcessPoolExecutor() as pool: - with tqdm(total=idx.size) as progress: - futures = [] - - for ind in idx: - future = pool.submit(self.__fit_signal__, - signal[ind, :], - self.echo_list, - self.method) - future.add_done_callback(lambda p: progress.update()) - futures.append(future) - - results = [] - for future in futures: - result = future.result() - results.append(result) - t2star_map[idx], t2star_err[idx], m0_map[idx], m0_err[idx] \ - = [np.array(row) for row in zip(*results)] - - # Single threaded method + with ProcessPool() as executor: + results = executor.map(self._fit_loglin_signal, + self._exp_model.signal_list, + self._exp_model.x_list, + self._exp_model.mask_list, + [self._exp_model] * self.n_vox) else: - with tqdm(total=idx.size) as progress: - for ind in idx: - sig = signal[ind, :] - t2star_map[ind], t2star_err[ind], \ - m0_map[ind], m0_err[ind] \ - = self.__fit_signal__(sig, self.echo_list, self.method) - progress.update(1) - - # Reshape results to raw data shape - t2star_map = t2star_map.reshape(self.shape) - t2star_err = t2star_err.reshape(self.shape) - m0_map = m0_map.reshape(self.shape) - m0_err = m0_err.reshape(self.shape) - - return t2star_map, t2star_err, m0_map, m0_err + results = list(tqdm(map(self._fit_loglin_signal, + self._exp_model.signal_list, + self._exp_model.x_list, + self._exp_model.mask_list, + [self._exp_model] * self.n_vox), + total=self.n_vox)) + popt_array = np.array([result[0] for result in results]) + popt_list = [popt_array[:, p].reshape(self._exp_model.map_shape) for p + in range(self._exp_model.n_params)] + error_array = np.array([result[1] for result in results]) + error_list = [error_array[:, p].reshape(self._exp_model.map_shape) + for p in range(self._exp_model.n_params)] + r2 = np.array([result[2] for result in results]).reshape( + self._exp_model.map_shape) + return popt_list, error_list, r2 @staticmethod - def __fit_signal__(sig, te, method): - if method == 'loglin': - s_w = 0.0 - s_wx = 0.0 - s_wx2 = 0.0 - s_wy = 0.0 - s_wxy = 0.0 - n_te = len(sig) - - noise = sig.sum() / n_te - sd = np.abs(np.sum(sig ** 2) / n_te - noise ** 2) - if sd > 1e-10: - for t in range(n_te): - if sig[t] > 0: - te_tmp = te[t] - if sig[t] > sd: - sigma = np.log(sig[t] / (sig[t] - sd)) - else: - sigma = np.log(sig[t] / 0.0001) - logsig = np.log(sig[t]) - weight = 1 / sigma ** 2 - - s_w += weight - s_wx += weight * te_tmp - s_wx2 += weight * te_tmp ** 2 - s_wy += weight * logsig - s_wxy += weight * te_tmp * logsig - - delta = (s_w * s_wx2) - (s_wx ** 2) - if delta > 1e-5: - a = (1 / delta) * (s_wx2 * s_wy - s_wx * s_wxy) - b = (1 / delta) * (s_w * s_wxy - s_wx * s_wy) - t2star = np.real(-1 / b) - m0 = np.real(np.exp(a)) - if t2star < 0 or t2star > 700 or np.isnan(t2star): + def _fit_loglin_signal(sig, te, mask, model): + if mask is True: + with np.errstate(divide='ignore', invalid='ignore'): + sig = np.array(sig) + te = np.array(te) + s_w = 0.0 + s_wx = 0.0 + s_wx2 = 0.0 + s_wy = 0.0 + s_wxy = 0.0 + n_te = len(sig) + + noise = sig.sum() / n_te + sd = np.abs(np.sum(sig ** 2) / n_te - noise ** 2) + if sd > 1e-10: + for t in range(n_te): + if sig[t] > 0: + te_tmp = te[t] + if sig[t] > sd: + sigma = np.log(sig[t] / (sig[t] - sd)) + else: + sigma = np.log(sig[t] / 0.0001) + logsig = np.log(sig[t]) + weight = 1 / sigma ** 2 + + s_w += weight + s_wx += weight * te_tmp + s_wx2 += weight * te_tmp ** 2 + s_wy += weight * logsig + s_wxy += weight * te_tmp * logsig + + delta = (s_w * s_wx2) - (s_wx ** 2) + if delta > 1e-5: + a = (1 / delta) * (s_wx2 * s_wy - s_wx * s_wxy) + b = (1 / delta) * (s_w * s_wxy - s_wx * s_wy) + t2star = np.real(-1 / b) + m0 = np.real(np.exp(a)) + if t2star < 0 or t2star > model.bounds[1][0] or \ + np.isnan(t2star): + t2star = 0 + m0 = 0 + else: t2star = 0 m0 = 0 else: t2star = 0 m0 = 0 - else: - t2star = 0 - m0 = 0 - t2star_err = np.nan - m0_err = np.nan - - elif method == '2p_exp': - # Initialise parameters - bounds = ([0, 0], [700, 100000000]) - initial_guess = [20, 10000] - - # Fit data to equation - try: - popt, pcov = curve_fit(two_param_eq, te, sig, - p0=initial_guess, bounds=bounds) - except RuntimeError: - popt = np.zeros(2) - pcov = np.zeros((2, 2)) - - # Extract fits and errors from result variables - if popt[0] < bounds[1][0] - 1: - t2star = popt[0] - m0 = popt[1] - err = np.sqrt(np.diag(pcov)) - t2star_err = err[0] - m0_err = err[1] - else: - t2star, m0, t2star_err, m0_err = 0, 0, 0, 0 + else: + t2star = 0 + m0 = 0 - return t2star, t2star_err, m0, m0_err + fit_sig = two_param_eq(te, t2star, m0) + r2 = r2_score(sig, fit_sig) + t2star_err = np.nan + m0_err = np.nan + return (t2star, m0), (t2star_err, m0_err), r2 def r2star_map(self): """ @@ -753,15 +779,17 @@

    Parameters

    ------- r2star_map : np.ndarray An array containing the R2* map generated - by the function with R2* measured in ms. + by the function with R2* measured in ms^-1. """ - return np.nan_to_num(np.reciprocal(self.t2star_map), - posinf=0, neginf=0) + with np.errstate(divide='ignore'): + r2star = np.nan_to_num(np.reciprocal(self.t2star_map), + posinf=0, neginf=0) + return r2star def to_nifti(self, output_directory=os.getcwd(), base_file_name='Output', maps='all'): """Exports some of the T2Star class attributes to NIFTI. - + Parameters ---------- output_directory : string, optional @@ -772,12 +800,12 @@

    Parameters

    maps : list or 'all', optional List of maps to save to NIFTI. This should either the string "all" or a list of maps from ["t2star", "t2star_err", "m0", - "m0_err", "r2star", "mask"]. + "m0_err", "r2star", "r2", "mask"]. """ os.makedirs(output_directory, exist_ok=True) base_path = os.path.join(output_directory, base_file_name) if maps == 'all' or maps == ['all']: - maps = ['t2star', 'm0', 'r2star', 'mask'] + maps = ['t2star', 'm0', 'r2star', 'r2', 'mask'] if self.method == '2p_exp': maps += ['t2star_err', 'm0_err'] if isinstance(maps, list): @@ -812,6 +840,10 @@

    Parameters

    r2star_nifti = nib.Nifti1Image(T2Star.r2star_map(self), affine=self.affine) nib.save(r2star_nifti, base_path + '_r2star_map.nii.gz') + elif result == 'r2' or result == 'r2_map': + r2_nifti = nib.Nifti1Image(self.r2, + affine=self.affine) + nib.save(r2_nifti, base_path + '_r2.nii.gz') elif result == 'mask': mask_nifti = nib.Nifti1Image(self.mask.astype(np.uint16), affine=self.affine) @@ -838,7 +870,7 @@

    Returns

    r2star_map : np.ndarray
    An array containing the R2 map generated -by the function with R2 measured in ms.
    +by the function with R2 measured in ms^-1.
    @@ -857,10 +889,12 @@

    Returns

    ------- r2star_map : np.ndarray An array containing the R2* map generated - by the function with R2* measured in ms. + by the function with R2* measured in ms^-1. """ - return np.nan_to_num(np.reciprocal(self.t2star_map), - posinf=0, neginf=0) + with np.errstate(divide='ignore'): + r2star = np.nan_to_num(np.reciprocal(self.t2star_map), + posinf=0, neginf=0) + return r2star
    @@ -878,7 +912,7 @@

    Parameters

    maps : list or 'all', optional
    List of maps to save to NIFTI. This should either the string "all" or a list of maps from ["t2star", "t2star_err", "m0", -"m0_err", "r2star", "mask"].
    +"m0_err", "r2star", "r2", "mask"].
    @@ -887,7 +921,7 @@

    Parameters

    def to_nifti(self, output_directory=os.getcwd(), base_file_name='Output',
                  maps='all'):
         """Exports some of the T2Star class attributes to NIFTI.
    -            
    +
         Parameters
         ----------
         output_directory : string, optional
    @@ -898,12 +932,12 @@ 

    Parameters

    maps : list or 'all', optional List of maps to save to NIFTI. This should either the string "all" or a list of maps from ["t2star", "t2star_err", "m0", - "m0_err", "r2star", "mask"]. + "m0_err", "r2star", "r2", "mask"]. """ os.makedirs(output_directory, exist_ok=True) base_path = os.path.join(output_directory, base_file_name) if maps == 'all' or maps == ['all']: - maps = ['t2star', 'm0', 'r2star', 'mask'] + maps = ['t2star', 'm0', 'r2star', 'r2', 'mask'] if self.method == '2p_exp': maps += ['t2star_err', 'm0_err'] if isinstance(maps, list): @@ -938,6 +972,10 @@

    Parameters

    r2star_nifti = nib.Nifti1Image(T2Star.r2star_map(self), affine=self.affine) nib.save(r2star_nifti, base_path + '_r2star_map.nii.gz') + elif result == 'r2' or result == 'r2_map': + r2_nifti = nib.Nifti1Image(self.r2, + affine=self.affine) + nib.save(r2_nifti, base_path + '_r2.nii.gz') elif result == 'mask': mask_nifti = nib.Nifti1Image(self.mask.astype(np.uint16), affine=self.affine) @@ -953,6 +991,76 @@

    Parameters

    +
    +class T2StarExpModel +(pixel_array, te, mask=None, multithread=True) +
    +
    +

    A class for fitting T2* data to a mono-exponential model.

    +

    Parameters

    +
    +
    pixel_array : np.ndarray
    +
    An array containing the signal from each voxel at each echo +time with the last dimension being time i.e. the array needed to +generate a 3D T2* map would have dimensions [x, y, z, TE].
    +
    te : np.ndarray
    +
    An array of the echo times used for the last dimension of the +pixel_array. In milliseconds.
    +
    mask : np.ndarray, optional
    +
    A boolean mask of the voxels to fit. Should be the shape of the +desired T2* map rather than the raw data i.e. omit the time +dimension.
    +
    multithread : bool, optional
    +
    Default True +If True, the fitting will be performed in parallel using all +available cores
    +
    +
    + +Expand source code + +
    class T2StarExpModel(fitting.Model):
    +    def __init__(self, pixel_array, te, mask=None, multithread=True):
    +        """
    +        A class for fitting T2* data to a mono-exponential model.
    +
    +        Parameters
    +        ----------
    +        pixel_array : np.ndarray
    +            An array containing the signal from each voxel at each echo
    +            time with the last dimension being time i.e. the array needed to
    +            generate a 3D T2* map would have dimensions [x, y, z, TE].
    +        te : np.ndarray
    +            An array of the echo times used for the last dimension of the
    +            pixel_array. In milliseconds.
    +        mask : np.ndarray, optional
    +            A boolean mask of the voxels to fit. Should be the shape of the
    +            desired T2* map rather than the raw data i.e. omit the time
    +            dimension.
    +        multithread : bool, optional
    +            Default True
    +            If True, the fitting will be performed in parallel using all
    +            available cores
    +        """
    +
    +        super().__init__(pixel_array, te, two_param_eq, mask, multithread)
    +        self.bounds = ([0, 0], [700, 100000000])
    +        self.initial_guess = [20, 10000]
    +        self.generate_lists()
    +
    +

    Ancestors

    + +

    Inherited members

    + +
    @@ -981,6 +1089,9 @@

    to_nifti +
  • +

    T2StarExpModel

    +
  • diff --git a/mapping/tests/index.html b/mapping/tests/index.html index 035c8554..2c087594 100644 --- a/mapping/tests/index.html +++ b/mapping/tests/index.html @@ -26,7 +26,8 @@

    Module ukat.mapping.tests

    Expand source code -
    from . import test_b0, test_diffusion, test_mtr, test_t1, test_t2, test_t2star
    +
    from . import test_b0, test_diffusion, test_mtr, test_t1, test_t2, \
    +    test_t2_stimfit, test_t2star
    @@ -52,6 +53,10 @@

    Sub-modules

    +
    ukat.mapping.tests.test_t2_stimfit
    +
    +
    +
    ukat.mapping.tests.test_t2star
    @@ -83,6 +88,7 @@

    Index

  • ukat.mapping.tests.test_mtr
  • ukat.mapping.tests.test_t1
  • ukat.mapping.tests.test_t2
  • +
  • ukat.mapping.tests.test_t2_stimfit
  • ukat.mapping.tests.test_t2star
  • diff --git a/mapping/tests/test_b0.html b/mapping/tests/test_b0.html index a3285814..f6285a88 100644 --- a/mapping/tests/test_b0.html +++ b/mapping/tests/test_b0.html @@ -46,7 +46,8 @@

    Module ukat.mapping.tests.test_b0

    correct_array = np.angle(np.exp(1j * correct_array)) one_echo_array = np.arange(100).reshape((10, 10, 1)) multiple_echoes_array = (np.concatenate((correct_array, - np.arange(300).reshape((10, 10, 3))), axis=2)) + np.arange(300).reshape( + (10, 10, 3))), axis=2)) affine = np.eye(4) correct_echo_list = [4, 7] one_echo_list = [4] @@ -61,7 +62,7 @@

    Module ukat.mapping.tests.test_b0

    unwrap=False).b0_map b0maps_stats = arraystats.ArrayStats(b0_map_calculated).calculate() npt.assert_allclose([b0maps_stats["mean"], b0maps_stats["std"], - b0maps_stats["min"], b0maps_stats["max"]], + b0maps_stats["min"], b0maps_stats["max"]], self.gold_standard, rtol=1e-7, atol=1e-9) def test_inputs(self): @@ -210,7 +211,7 @@

    Module ukat.mapping.tests.test_b0

    mapper = B0(images, te, affine, unwrap=True) b0map_stats = arraystats.ArrayStats(mapper.b0_map).calculate() npt.assert_allclose([b0map_stats["mean"], b0map_stats["std"], - b0map_stats["min"], b0map_stats["max"]], + b0map_stats["min"], b0map_stats["max"]], gold_standard_b0, rtol=0.01, atol=0) def test_b0_offset_correction(self): @@ -242,6 +243,7 @@

    Module ukat.mapping.tests.test_b0

    # This assertion proves that there was offset correction performed assert (mapper.b0_map != b0_map_without_offset_correction).any() + # Delete the NIFTI test folder recursively if any of the unit tests failed if os.path.exists('test_output'): shutil.rmtree('test_output')
    @@ -273,7 +275,8 @@

    Classes

    correct_array = np.angle(np.exp(1j * correct_array)) one_echo_array = np.arange(100).reshape((10, 10, 1)) multiple_echoes_array = (np.concatenate((correct_array, - np.arange(300).reshape((10, 10, 3))), axis=2)) + np.arange(300).reshape( + (10, 10, 3))), axis=2)) affine = np.eye(4) correct_echo_list = [4, 7] one_echo_list = [4] @@ -288,7 +291,7 @@

    Classes

    unwrap=False).b0_map b0maps_stats = arraystats.ArrayStats(b0_map_calculated).calculate() npt.assert_allclose([b0maps_stats["mean"], b0maps_stats["std"], - b0maps_stats["min"], b0maps_stats["max"]], + b0maps_stats["min"], b0maps_stats["max"]], self.gold_standard, rtol=1e-7, atol=1e-9) def test_inputs(self): @@ -437,7 +440,7 @@

    Classes

    mapper = B0(images, te, affine, unwrap=True) b0map_stats = arraystats.ArrayStats(mapper.b0_map).calculate() npt.assert_allclose([b0map_stats["mean"], b0map_stats["std"], - b0map_stats["min"], b0map_stats["max"]], + b0map_stats["min"], b0map_stats["max"]], gold_standard_b0, rtol=0.01, atol=0) def test_b0_offset_correction(self): @@ -521,7 +524,7 @@

    Methods

    unwrap=False).b0_map b0maps_stats = arraystats.ArrayStats(b0_map_calculated).calculate() npt.assert_allclose([b0maps_stats["mean"], b0maps_stats["std"], - b0maps_stats["min"], b0maps_stats["max"]], + b0maps_stats["min"], b0maps_stats["max"]], self.gold_standard, rtol=1e-7, atol=1e-9)
    @@ -697,7 +700,7 @@

    Methods

    mapper = B0(images, te, affine, unwrap=True) b0map_stats = arraystats.ArrayStats(mapper.b0_map).calculate() npt.assert_allclose([b0map_stats["mean"], b0map_stats["std"], - b0map_stats["min"], b0map_stats["max"]], + b0map_stats["min"], b0map_stats["max"]], gold_standard_b0, rtol=0.01, atol=0) diff --git a/mapping/tests/test_diffusion.html b/mapping/tests/test_diffusion.html index 64571440..4006b22f 100644 --- a/mapping/tests/test_diffusion.html +++ b/mapping/tests/test_diffusion.html @@ -119,11 +119,13 @@

    Module ukat.mapping.tests.test_diffusion

    def test_negative_signal(self): gold_standard_adc = [0.000833, 0.000998, 0.0, 0.004852] gold_standard_adc_err = [7.819414e-05, 1.222237e-04, 0.0, 9.935775e-04] + gold_standard_adc_r2 = [0.398594, 0.434274, -0.03715, 0.999107] negateive_pixel_array = self.pixel_array.copy() negateive_pixel_array[:30, :, :, :] -= 40000 mapper = ADC(negateive_pixel_array, self.affine, self.bvals) adc_stats = arraystats.ArrayStats(mapper.adc).calculate() adc_err_stats = arraystats.ArrayStats(mapper.adc_err).calculate() + adc_r2_stats = arraystats.ArrayStats(mapper.r2).calculate() assert np.sum(mapper.adc[:30, :, :]) == 0 npt.assert_allclose([adc_stats['mean']['3D'], adc_stats['std']['3D'], adc_stats['min']['3D'], adc_stats['max']['3D']], @@ -133,6 +135,11 @@

    Module ukat.mapping.tests.test_diffusion

    adc_err_stats['min']['3D'], adc_err_stats['max']['3D']], gold_standard_adc_err, rtol=5e-3, atol=1e-7) + npt.assert_allclose([adc_r2_stats['mean']['3D'], + adc_r2_stats['std']['3D'], + adc_r2_stats['min']['3D'], + adc_r2_stats['max']['3D']], + gold_standard_adc_r2, rtol=5e-3, atol=1e-7) def test_real_data(self): # Gold standard statistics @@ -182,10 +189,13 @@

    Module ukat.mapping.tests.test_diffusion

    mapper.to_nifti(output_directory='test_output', base_file_name='adc_test', maps='all') output_files = os.listdir('test_output') - assert len(output_files) == 3 + assert len(output_files) == 6 assert 'adc_test_adc_map.nii.gz' in output_files assert 'adc_test_adc_err.nii.gz' in output_files assert 'adc_test_mask.nii.gz' in output_files + assert 'adc_test_r2.nii.gz' in output_files + assert 'adc_test_s0_map.nii.gz' in output_files + assert 'adc_test_s0_err.nii.gz' in output_files for f in os.listdir('test_output'): os.remove(os.path.join('test_output', f)) @@ -378,11 +388,13 @@

    Classes

    def test_negative_signal(self): gold_standard_adc = [0.000833, 0.000998, 0.0, 0.004852] gold_standard_adc_err = [7.819414e-05, 1.222237e-04, 0.0, 9.935775e-04] + gold_standard_adc_r2 = [0.398594, 0.434274, -0.03715, 0.999107] negateive_pixel_array = self.pixel_array.copy() negateive_pixel_array[:30, :, :, :] -= 40000 mapper = ADC(negateive_pixel_array, self.affine, self.bvals) adc_stats = arraystats.ArrayStats(mapper.adc).calculate() adc_err_stats = arraystats.ArrayStats(mapper.adc_err).calculate() + adc_r2_stats = arraystats.ArrayStats(mapper.r2).calculate() assert np.sum(mapper.adc[:30, :, :]) == 0 npt.assert_allclose([adc_stats['mean']['3D'], adc_stats['std']['3D'], adc_stats['min']['3D'], adc_stats['max']['3D']], @@ -392,6 +404,11 @@

    Classes

    adc_err_stats['min']['3D'], adc_err_stats['max']['3D']], gold_standard_adc_err, rtol=5e-3, atol=1e-7) + npt.assert_allclose([adc_r2_stats['mean']['3D'], + adc_r2_stats['std']['3D'], + adc_r2_stats['min']['3D'], + adc_r2_stats['max']['3D']], + gold_standard_adc_r2, rtol=5e-3, atol=1e-7) def test_real_data(self): # Gold standard statistics @@ -441,10 +458,13 @@

    Classes

    mapper.to_nifti(output_directory='test_output', base_file_name='adc_test', maps='all') output_files = os.listdir('test_output') - assert len(output_files) == 3 + assert len(output_files) == 6 assert 'adc_test_adc_map.nii.gz' in output_files assert 'adc_test_adc_err.nii.gz' in output_files assert 'adc_test_mask.nii.gz' in output_files + assert 'adc_test_r2.nii.gz' in output_files + assert 'adc_test_s0_map.nii.gz' in output_files + assert 'adc_test_s0_err.nii.gz' in output_files for f in os.listdir('test_output'): os.remove(os.path.join('test_output', f)) @@ -542,11 +562,13 @@

    Methods

    def test_negative_signal(self):
         gold_standard_adc = [0.000833, 0.000998, 0.0, 0.004852]
         gold_standard_adc_err = [7.819414e-05, 1.222237e-04, 0.0, 9.935775e-04]
    +    gold_standard_adc_r2 = [0.398594, 0.434274, -0.03715, 0.999107]
         negateive_pixel_array = self.pixel_array.copy()
         negateive_pixel_array[:30, :, :, :] -= 40000
         mapper = ADC(negateive_pixel_array, self.affine, self.bvals)
         adc_stats = arraystats.ArrayStats(mapper.adc).calculate()
         adc_err_stats = arraystats.ArrayStats(mapper.adc_err).calculate()
    +    adc_r2_stats = arraystats.ArrayStats(mapper.r2).calculate()
         assert np.sum(mapper.adc[:30, :, :]) == 0
         npt.assert_allclose([adc_stats['mean']['3D'], adc_stats['std']['3D'],
                              adc_stats['min']['3D'], adc_stats['max']['3D']],
    @@ -555,7 +577,12 @@ 

    Methods

    adc_err_stats['std']['3D'], adc_err_stats['min']['3D'], adc_err_stats['max']['3D']], - gold_standard_adc_err, rtol=5e-3, atol=1e-7)
    + gold_standard_adc_err, rtol=5e-3, atol=1e-7) + npt.assert_allclose([adc_r2_stats['mean']['3D'], + adc_r2_stats['std']['3D'], + adc_r2_stats['min']['3D'], + adc_r2_stats['max']['3D']], + gold_standard_adc_r2, rtol=5e-3, atol=1e-7)
    @@ -605,10 +632,13 @@

    Methods

    mapper.to_nifti(output_directory='test_output', base_file_name='adc_test', maps='all') output_files = os.listdir('test_output') - assert len(output_files) == 3 + assert len(output_files) == 6 assert 'adc_test_adc_map.nii.gz' in output_files assert 'adc_test_adc_err.nii.gz' in output_files assert 'adc_test_mask.nii.gz' in output_files + assert 'adc_test_r2.nii.gz' in output_files + assert 'adc_test_s0_map.nii.gz' in output_files + assert 'adc_test_s0_err.nii.gz' in output_files for f in os.listdir('test_output'): os.remove(os.path.join('test_output', f)) diff --git a/mapping/tests/test_t1.html b/mapping/tests/test_t1.html index 0d95d392..8063afa6 100644 --- a/mapping/tests/test_t1.html +++ b/mapping/tests/test_t1.html @@ -124,16 +124,26 @@

    Module ukat.mapping.tests.test_t1

    # Multithread mapper = T1(signal_array, self.t, self.affine, multithread=True) assert mapper.shape == signal_array.shape[:-1] - assert mapper.t1_map.mean() - self.t1 < 0.00001 - assert mapper.m0_map.mean() - self.m0 < 0.00001 - assert mapper.r1_map().mean() - 1 / self.t1 < 0.00001 + 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.r1_map().mean(), 1 / self.t1) + npt.assert_almost_equal(mapper.r2.mean(), 1) # Single Threaded mapper = T1(signal_array, self.t, self.affine, multithread=False) assert mapper.shape == signal_array.shape[:-1] - assert mapper.t1_map.mean() - self.t1 < 0.00001 - assert mapper.m0_map.mean() - self.m0 < 0.00001 - assert mapper.r1_map().mean() - 1 / self.t1 < 0.00001 + 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.r1_map().mean(), 1 / self.t1) + npt.assert_almost_equal(mapper.r2.mean(), 1) + + # Auto Threaded + mapper = T1(signal_array, self.t, self.affine, multithread='auto') + 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.r1_map().mean(), 1 / self.t1) + npt.assert_almost_equal(mapper.r2.mean(), 1) def test_three_param_fit(self): # Make the signal into a 4D array @@ -143,35 +153,39 @@

    Module ukat.mapping.tests.test_t1

    mapper = T1(signal_array, self.t, self.affine, parameters=3, multithread=True) assert mapper.shape == signal_array.shape[:-1] - assert mapper.t1_map.mean() - self.t1 < 0.00001 - assert mapper.m0_map.mean() - self.m0 < 0.00001 - assert mapper.eff_map.mean() - self.eff < 0.00005 - assert mapper.r1_map().mean() - 1 / self.t1 < 0.00001 + 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.eff_map.mean(), self.eff) + npt.assert_almost_equal(mapper.r1_map().mean(), 1 / self.t1) + npt.assert_almost_equal(mapper.r2.mean(), 1) # Single Threaded mapper = T1(signal_array, self.t, self.affine, parameters=3, multithread=False) assert mapper.shape == signal_array.shape[:-1] - assert mapper.t1_map.mean() - self.t1 < 0.00001 - assert mapper.m0_map.mean() - self.m0 < 0.00001 - assert mapper.eff_map.mean() - self.eff < 0.00005 - assert mapper.r1_map().mean() - 1 / self.t1 < 0.00001 + 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.eff_map.mean(), self.eff) + npt.assert_almost_equal(mapper.r1_map().mean(), 1 / self.t1) + npt.assert_almost_equal(mapper.r2.mean(), 1) def test_tss(self): mapper = T1(self.correct_signal_two_param_tss, self.t, self.affine, tss=10) assert mapper.shape == self.correct_signal_two_param_tss.shape[:-1] - assert mapper.t1_map.mean() - self.t1 < 0.00001 - assert mapper.m0_map.mean() - self.m0 < 0.00001 - assert mapper.r1_map().mean() - 1 / self.t1 < 0.00001 + 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.r1_map().mean(), 1 / self.t1) + npt.assert_almost_equal(mapper.r2.mean(), 1) def test_tss_axis(self): signal_array = np.swapaxes(self.correct_signal_two_param_tss, 0, 1) mapper = T1(signal_array, self.t, self.affine, tss=10, tss_axis=0) - assert mapper.t1_map.mean() - self.t1 < 0.00001 - assert mapper.m0_map.mean() - self.m0 < 0.00001 - assert mapper.r1_map().mean() - 1 / self.t1 < 0.00001 + 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.r1_map().mean(), 1 / self.t1) + npt.assert_almost_equal(mapper.r2.mean(), 1) def test_failed_fit(self): # Make the signal, where the fitting is expected to fail, into 4D array @@ -182,20 +196,22 @@

    Module ukat.mapping.tests.test_t1

    parameters=2, multithread=True) assert mapper_two_param.shape == signal_array.shape[:-1] # Voxels that fail to fit are set to zero - assert mapper_two_param.t1_map.mean() == 0.0 - assert mapper_two_param.t1_err.mean() == 0.0 - assert mapper_two_param.m0_map.mean() == 0.0 - assert mapper_two_param.m0_err.mean() == 0.0 + npt.assert_equal(mapper_two_param.t1_map.mean(), 0) + npt.assert_equal(mapper_two_param.t1_err.mean(), 0) + npt.assert_equal(mapper_two_param.m0_map.mean(), 0) + npt.assert_equal(mapper_two_param.m0_err.mean(), 0) + 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) assert mapper_three_param.shape == signal_array.shape[:-1] # Voxels that fail to fit are set to zero - assert mapper_three_param.t1_map.mean() == 0.0 - assert mapper_three_param.t1_err.mean() == 0.0 - assert mapper_three_param.m0_map.mean() == 0.0 - assert mapper_three_param.m0_err.mean() == 0.0 + npt.assert_equal(mapper_three_param.t1_map.mean(), 0) + npt.assert_equal(mapper_three_param.t1_err.mean(), 0) + npt.assert_equal(mapper_three_param.m0_map.mean(), 0) + npt.assert_equal(mapper_three_param.m0_err.mean(), 0) + npt.assert_equal(mapper_two_param.r2.mean(), 0) def test_mask(self): signal_array = np.tile(self.correct_signal_two_param, (10, 10, 3, 1)) @@ -205,16 +221,16 @@

    Module ukat.mapping.tests.test_t1

    mask[:5, ...] = False mapper = T1(signal_array, self.t, self.affine, mask=mask) assert mapper.shape == signal_array.shape[:-1] - assert mapper.t1_map[5:, ...].mean() - self.t1 < 0.00001 - assert mapper.t1_map[:5, ...].mean() < 0.00001 + npt.assert_almost_equal(mapper.t1_map[5:, ...].mean(), self.t1) + npt.assert_equal(mapper.t1_map[:5, ...].mean(), 0) # Int mask mask = np.ones(signal_array.shape[:-1]) mask[:5, ...] = 0 mapper = T1(signal_array, self.t, self.affine, mask=mask) assert mapper.shape == signal_array.shape[:-1] - assert mapper.t1_map[5:, ...].mean() - self.t1 < 0.00001 - assert mapper.t1_map[:5, ...].mean() < 0.00001 + npt.assert_almost_equal(mapper.t1_map[5:, ...].mean(), self.t1) + npt.assert_equal(mapper.t1_map[:5, ...].mean(), 0) def test_mismatched_raw_data_and_inversion_lengths(self): @@ -275,9 +291,10 @@

    Module ukat.mapping.tests.test_t1

    affine=self.affine, tss=1, tss_axis=2) def test_molli_2p_warning(self): + signal_array = np.tile(self.correct_signal_three_param, (10, 10, 3, 1)) with pytest.warns(UserWarning): - mapper = T1(pixel_array=np.zeros((5, 5, 5)), - inversion_list=np.linspace(0, 2000, 5), + mapper = T1(pixel_array=signal_array, + inversion_list=self.t, affine=self.affine, parameters=2, molli=True) def test_real_data(self): @@ -343,13 +360,14 @@

    Module ukat.mapping.tests.test_t1

    mapper.to_nifti(output_directory='test_output', base_file_name='t1test', maps='all') output_files = os.listdir('test_output') - assert len(output_files) == 8 + assert len(output_files) == 9 assert 't1test_eff_err.nii.gz' in output_files assert 't1test_eff_map.nii.gz' in output_files assert 't1test_m0_err.nii.gz' in output_files assert 't1test_m0_map.nii.gz' in output_files assert 't1test_mask.nii.gz' in output_files assert 't1test_r1_map.nii.gz' in output_files + assert 't1test_r2.nii.gz' in output_files assert 't1test_t1_err.nii.gz' in output_files assert 't1test_t1_map.nii.gz' in output_files @@ -833,16 +851,26 @@

    Methods

    # Multithread mapper = T1(signal_array, self.t, self.affine, multithread=True) assert mapper.shape == signal_array.shape[:-1] - assert mapper.t1_map.mean() - self.t1 < 0.00001 - assert mapper.m0_map.mean() - self.m0 < 0.00001 - assert mapper.r1_map().mean() - 1 / self.t1 < 0.00001 + 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.r1_map().mean(), 1 / self.t1) + npt.assert_almost_equal(mapper.r2.mean(), 1) # Single Threaded mapper = T1(signal_array, self.t, self.affine, multithread=False) assert mapper.shape == signal_array.shape[:-1] - assert mapper.t1_map.mean() - self.t1 < 0.00001 - assert mapper.m0_map.mean() - self.m0 < 0.00001 - assert mapper.r1_map().mean() - 1 / self.t1 < 0.00001 + 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.r1_map().mean(), 1 / self.t1) + npt.assert_almost_equal(mapper.r2.mean(), 1) + + # Auto Threaded + mapper = T1(signal_array, self.t, self.affine, multithread='auto') + 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.r1_map().mean(), 1 / self.t1) + npt.assert_almost_equal(mapper.r2.mean(), 1) def test_three_param_fit(self): # Make the signal into a 4D array @@ -852,35 +880,39 @@

    Methods

    mapper = T1(signal_array, self.t, self.affine, parameters=3, multithread=True) assert mapper.shape == signal_array.shape[:-1] - assert mapper.t1_map.mean() - self.t1 < 0.00001 - assert mapper.m0_map.mean() - self.m0 < 0.00001 - assert mapper.eff_map.mean() - self.eff < 0.00005 - assert mapper.r1_map().mean() - 1 / self.t1 < 0.00001 + 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.eff_map.mean(), self.eff) + npt.assert_almost_equal(mapper.r1_map().mean(), 1 / self.t1) + npt.assert_almost_equal(mapper.r2.mean(), 1) # Single Threaded mapper = T1(signal_array, self.t, self.affine, parameters=3, multithread=False) assert mapper.shape == signal_array.shape[:-1] - assert mapper.t1_map.mean() - self.t1 < 0.00001 - assert mapper.m0_map.mean() - self.m0 < 0.00001 - assert mapper.eff_map.mean() - self.eff < 0.00005 - assert mapper.r1_map().mean() - 1 / self.t1 < 0.00001 + 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.eff_map.mean(), self.eff) + npt.assert_almost_equal(mapper.r1_map().mean(), 1 / self.t1) + npt.assert_almost_equal(mapper.r2.mean(), 1) def test_tss(self): mapper = T1(self.correct_signal_two_param_tss, self.t, self.affine, tss=10) assert mapper.shape == self.correct_signal_two_param_tss.shape[:-1] - assert mapper.t1_map.mean() - self.t1 < 0.00001 - assert mapper.m0_map.mean() - self.m0 < 0.00001 - assert mapper.r1_map().mean() - 1 / self.t1 < 0.00001 + 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.r1_map().mean(), 1 / self.t1) + npt.assert_almost_equal(mapper.r2.mean(), 1) def test_tss_axis(self): signal_array = np.swapaxes(self.correct_signal_two_param_tss, 0, 1) mapper = T1(signal_array, self.t, self.affine, tss=10, tss_axis=0) - assert mapper.t1_map.mean() - self.t1 < 0.00001 - assert mapper.m0_map.mean() - self.m0 < 0.00001 - assert mapper.r1_map().mean() - 1 / self.t1 < 0.00001 + 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.r1_map().mean(), 1 / self.t1) + npt.assert_almost_equal(mapper.r2.mean(), 1) def test_failed_fit(self): # Make the signal, where the fitting is expected to fail, into 4D array @@ -891,20 +923,22 @@

    Methods

    parameters=2, multithread=True) assert mapper_two_param.shape == signal_array.shape[:-1] # Voxels that fail to fit are set to zero - assert mapper_two_param.t1_map.mean() == 0.0 - assert mapper_two_param.t1_err.mean() == 0.0 - assert mapper_two_param.m0_map.mean() == 0.0 - assert mapper_two_param.m0_err.mean() == 0.0 + npt.assert_equal(mapper_two_param.t1_map.mean(), 0) + npt.assert_equal(mapper_two_param.t1_err.mean(), 0) + npt.assert_equal(mapper_two_param.m0_map.mean(), 0) + npt.assert_equal(mapper_two_param.m0_err.mean(), 0) + 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) assert mapper_three_param.shape == signal_array.shape[:-1] # Voxels that fail to fit are set to zero - assert mapper_three_param.t1_map.mean() == 0.0 - assert mapper_three_param.t1_err.mean() == 0.0 - assert mapper_three_param.m0_map.mean() == 0.0 - assert mapper_three_param.m0_err.mean() == 0.0 + npt.assert_equal(mapper_three_param.t1_map.mean(), 0) + npt.assert_equal(mapper_three_param.t1_err.mean(), 0) + npt.assert_equal(mapper_three_param.m0_map.mean(), 0) + npt.assert_equal(mapper_three_param.m0_err.mean(), 0) + npt.assert_equal(mapper_two_param.r2.mean(), 0) def test_mask(self): signal_array = np.tile(self.correct_signal_two_param, (10, 10, 3, 1)) @@ -914,16 +948,16 @@

    Methods

    mask[:5, ...] = False mapper = T1(signal_array, self.t, self.affine, mask=mask) assert mapper.shape == signal_array.shape[:-1] - assert mapper.t1_map[5:, ...].mean() - self.t1 < 0.00001 - assert mapper.t1_map[:5, ...].mean() < 0.00001 + npt.assert_almost_equal(mapper.t1_map[5:, ...].mean(), self.t1) + npt.assert_equal(mapper.t1_map[:5, ...].mean(), 0) # Int mask mask = np.ones(signal_array.shape[:-1]) mask[:5, ...] = 0 mapper = T1(signal_array, self.t, self.affine, mask=mask) assert mapper.shape == signal_array.shape[:-1] - assert mapper.t1_map[5:, ...].mean() - self.t1 < 0.00001 - assert mapper.t1_map[:5, ...].mean() < 0.00001 + npt.assert_almost_equal(mapper.t1_map[5:, ...].mean(), self.t1) + npt.assert_equal(mapper.t1_map[:5, ...].mean(), 0) def test_mismatched_raw_data_and_inversion_lengths(self): @@ -984,9 +1018,10 @@

    Methods

    affine=self.affine, tss=1, tss_axis=2) def test_molli_2p_warning(self): + signal_array = np.tile(self.correct_signal_three_param, (10, 10, 3, 1)) with pytest.warns(UserWarning): - mapper = T1(pixel_array=np.zeros((5, 5, 5)), - inversion_list=np.linspace(0, 2000, 5), + mapper = T1(pixel_array=signal_array, + inversion_list=self.t, affine=self.affine, parameters=2, molli=True) def test_real_data(self): @@ -1052,13 +1087,14 @@

    Methods

    mapper.to_nifti(output_directory='test_output', base_file_name='t1test', maps='all') output_files = os.listdir('test_output') - assert len(output_files) == 8 + assert len(output_files) == 9 assert 't1test_eff_err.nii.gz' in output_files assert 't1test_eff_map.nii.gz' in output_files assert 't1test_m0_err.nii.gz' in output_files assert 't1test_m0_map.nii.gz' in output_files assert 't1test_mask.nii.gz' in output_files assert 't1test_r1_map.nii.gz' in output_files + assert 't1test_r2.nii.gz' in output_files assert 't1test_t1_err.nii.gz' in output_files assert 't1test_t1_map.nii.gz' in output_files @@ -1151,20 +1187,22 @@

    Methods

    parameters=2, multithread=True) assert mapper_two_param.shape == signal_array.shape[:-1] # Voxels that fail to fit are set to zero - assert mapper_two_param.t1_map.mean() == 0.0 - assert mapper_two_param.t1_err.mean() == 0.0 - assert mapper_two_param.m0_map.mean() == 0.0 - assert mapper_two_param.m0_err.mean() == 0.0 + npt.assert_equal(mapper_two_param.t1_map.mean(), 0) + npt.assert_equal(mapper_two_param.t1_err.mean(), 0) + npt.assert_equal(mapper_two_param.m0_map.mean(), 0) + npt.assert_equal(mapper_two_param.m0_err.mean(), 0) + 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) assert mapper_three_param.shape == signal_array.shape[:-1] # Voxels that fail to fit are set to zero - assert mapper_three_param.t1_map.mean() == 0.0 - assert mapper_three_param.t1_err.mean() == 0.0 - assert mapper_three_param.m0_map.mean() == 0.0 - assert mapper_three_param.m0_err.mean() == 0.0
    + npt.assert_equal(mapper_three_param.t1_map.mean(), 0) + npt.assert_equal(mapper_three_param.t1_err.mean(), 0) + npt.assert_equal(mapper_three_param.m0_map.mean(), 0) + npt.assert_equal(mapper_three_param.m0_err.mean(), 0) + npt.assert_equal(mapper_two_param.r2.mean(), 0)
    @@ -1184,16 +1222,16 @@

    Methods

    mask[:5, ...] = False mapper = T1(signal_array, self.t, self.affine, mask=mask) assert mapper.shape == signal_array.shape[:-1] - assert mapper.t1_map[5:, ...].mean() - self.t1 < 0.00001 - assert mapper.t1_map[:5, ...].mean() < 0.00001 + npt.assert_almost_equal(mapper.t1_map[5:, ...].mean(), self.t1) + npt.assert_equal(mapper.t1_map[:5, ...].mean(), 0) # Int mask mask = np.ones(signal_array.shape[:-1]) mask[:5, ...] = 0 mapper = T1(signal_array, self.t, self.affine, mask=mask) assert mapper.shape == signal_array.shape[:-1] - assert mapper.t1_map[5:, ...].mean() - self.t1 < 0.00001 - assert mapper.t1_map[:5, ...].mean() < 0.00001
    + npt.assert_almost_equal(mapper.t1_map[5:, ...].mean(), self.t1) + npt.assert_equal(mapper.t1_map[:5, ...].mean(), 0)
    @@ -1228,9 +1266,10 @@

    Methods

    Expand source code
    def test_molli_2p_warning(self):
    +    signal_array = np.tile(self.correct_signal_three_param, (10, 10, 3, 1))
         with pytest.warns(UserWarning):
    -        mapper = T1(pixel_array=np.zeros((5, 5, 5)),
    -                    inversion_list=np.linspace(0, 2000, 5),
    +        mapper = T1(pixel_array=signal_array,
    +                    inversion_list=self.t,
                         affine=self.affine, parameters=2, molli=True)
    @@ -1355,19 +1394,21 @@

    Methods

    mapper = T1(signal_array, self.t, self.affine, parameters=3, multithread=True) assert mapper.shape == signal_array.shape[:-1] - assert mapper.t1_map.mean() - self.t1 < 0.00001 - assert mapper.m0_map.mean() - self.m0 < 0.00001 - assert mapper.eff_map.mean() - self.eff < 0.00005 - assert mapper.r1_map().mean() - 1 / self.t1 < 0.00001 + 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.eff_map.mean(), self.eff) + npt.assert_almost_equal(mapper.r1_map().mean(), 1 / self.t1) + npt.assert_almost_equal(mapper.r2.mean(), 1) # Single Threaded mapper = T1(signal_array, self.t, self.affine, parameters=3, multithread=False) assert mapper.shape == signal_array.shape[:-1] - assert mapper.t1_map.mean() - self.t1 < 0.00001 - assert mapper.m0_map.mean() - self.m0 < 0.00001 - assert mapper.eff_map.mean() - self.eff < 0.00005 - assert mapper.r1_map().mean() - 1 / self.t1 < 0.00001
    + 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.eff_map.mean(), self.eff) + npt.assert_almost_equal(mapper.r1_map().mean(), 1 / self.t1) + npt.assert_almost_equal(mapper.r2.mean(), 1)
    @@ -1392,13 +1433,14 @@

    Methods

    mapper.to_nifti(output_directory='test_output', base_file_name='t1test', maps='all') output_files = os.listdir('test_output') - assert len(output_files) == 8 + assert len(output_files) == 9 assert 't1test_eff_err.nii.gz' in output_files assert 't1test_eff_map.nii.gz' in output_files assert 't1test_m0_err.nii.gz' in output_files assert 't1test_m0_map.nii.gz' in output_files assert 't1test_mask.nii.gz' in output_files assert 't1test_r1_map.nii.gz' in output_files + assert 't1test_r2.nii.gz' in output_files assert 't1test_t1_err.nii.gz' in output_files assert 't1test_t1_map.nii.gz' in output_files @@ -1447,9 +1489,10 @@

    Methods

    mapper = T1(self.correct_signal_two_param_tss, self.t, self.affine, tss=10) assert mapper.shape == self.correct_signal_two_param_tss.shape[:-1] - assert mapper.t1_map.mean() - self.t1 < 0.00001 - assert mapper.m0_map.mean() - self.m0 < 0.00001 - assert mapper.r1_map().mean() - 1 / self.t1 < 0.00001
    + 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.r1_map().mean(), 1 / self.t1) + npt.assert_almost_equal(mapper.r2.mean(), 1)
    @@ -1464,9 +1507,10 @@

    Methods

    def test_tss_axis(self):
         signal_array = np.swapaxes(self.correct_signal_two_param_tss, 0, 1)
         mapper = T1(signal_array, self.t, self.affine, tss=10, tss_axis=0)
    -    assert mapper.t1_map.mean() - self.t1 < 0.00001
    -    assert mapper.m0_map.mean() - self.m0 < 0.00001
    -    assert mapper.r1_map().mean() - 1 / self.t1 < 0.00001
    + 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.r1_map().mean(), 1 / self.t1) + npt.assert_almost_equal(mapper.r2.mean(), 1)
    @@ -1547,16 +1591,26 @@

    Methods

    # Multithread mapper = T1(signal_array, self.t, self.affine, multithread=True) assert mapper.shape == signal_array.shape[:-1] - assert mapper.t1_map.mean() - self.t1 < 0.00001 - assert mapper.m0_map.mean() - self.m0 < 0.00001 - assert mapper.r1_map().mean() - 1 / self.t1 < 0.00001 + 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.r1_map().mean(), 1 / self.t1) + npt.assert_almost_equal(mapper.r2.mean(), 1) # Single Threaded mapper = T1(signal_array, self.t, self.affine, multithread=False) assert mapper.shape == signal_array.shape[:-1] - assert mapper.t1_map.mean() - self.t1 < 0.00001 - assert mapper.m0_map.mean() - self.m0 < 0.00001 - assert mapper.r1_map().mean() - 1 / self.t1 < 0.00001
    + 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.r1_map().mean(), 1 / self.t1) + npt.assert_almost_equal(mapper.r2.mean(), 1) + + # Auto Threaded + mapper = T1(signal_array, self.t, self.affine, multithread='auto') + 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.r1_map().mean(), 1 / self.t1) + npt.assert_almost_equal(mapper.r2.mean(), 1) diff --git a/mapping/tests/test_t2.html b/mapping/tests/test_t2.html index 7142767d..037a6d14 100644 --- a/mapping/tests/test_t2.html +++ b/mapping/tests/test_t2.html @@ -68,19 +68,21 @@

    Module ukat.mapping.tests.test_t2

    assert mapper.shape == signal_array.shape[:-1] npt.assert_almost_equal(mapper.t2_map.mean(), self.t2) npt.assert_almost_equal(mapper.m0_map.mean(), self.m0) - npt.assert_almost_equal(mapper.r2_map().mean(), 1 / self.t2) + npt.assert_almost_equal(mapper.r2.mean(), 1) # Single Threaded mapper = T2(signal_array, self.t, self.affine, multithread=False) assert mapper.shape == signal_array.shape[:-1] npt.assert_almost_equal(mapper.t2_map.mean(), self.t2) npt.assert_almost_equal(mapper.m0_map.mean(), self.m0) + npt.assert_almost_equal(mapper.r2.mean(), 1) # Auto Threaded mapper = T2(signal_array, self.t, self.affine, multithread='auto') assert mapper.shape == signal_array.shape[:-1] npt.assert_almost_equal(mapper.t2_map.mean(), self.t2) npt.assert_almost_equal(mapper.m0_map.mean(), self.m0) + npt.assert_almost_equal(mapper.r2.mean(), 1) # Fail to fit mapper = T2(signal_array[..., ::-1], self.t, self.affine, @@ -100,6 +102,7 @@

    Module ukat.mapping.tests.test_t2

    npt.assert_almost_equal(mapper.t2_map.mean(), self.t2) npt.assert_almost_equal(mapper.m0_map.mean(), self.m0) npt.assert_almost_equal(mapper.b_map.mean(), self.b) + npt.assert_almost_equal(mapper.r2.mean(), 1) # Single Threaded mapper = T2(signal_array, self.t, self.affine, multithread=False, @@ -108,6 +111,7 @@

    Module ukat.mapping.tests.test_t2

    npt.assert_almost_equal(mapper.t2_map.mean(), self.t2) npt.assert_almost_equal(mapper.m0_map.mean(), self.m0) npt.assert_almost_equal(mapper.b_map.mean(), self.b) + npt.assert_almost_equal(mapper.r2.mean(), 1) def test_threshold_fit(self): # Make the signal into a 4D array @@ -119,6 +123,7 @@

    Module ukat.mapping.tests.test_t2

    assert mapper.shape == signal_array.shape[:-1] npt.assert_almost_equal(mapper.t2_map.mean(), self.t2) npt.assert_almost_equal(mapper.m0_map.mean(), self.m0) + npt.assert_almost_equal(mapper.r2.mean(), 1) # Single Threaded mapper = T2(signal_array, self.t, self.affine, multithread=False, @@ -126,6 +131,7 @@

    Module ukat.mapping.tests.test_t2

    assert mapper.shape == signal_array.shape[:-1] npt.assert_almost_equal(mapper.t2_map.mean(), self.t2) npt.assert_almost_equal(mapper.m0_map.mean(), self.m0) + npt.assert_almost_equal(mapper.r2.mean(), 1) def test_mask(self): signal_array = np.tile(self.correct_signal, (10, 10, 3, 1)) @@ -175,8 +181,8 @@

    Module ukat.mapping.tests.test_t2

    0.0, 568.160604] gold_standard_3p_exp = [9.881218e+01, 4.294529e+01, 3.489657e-02, 5.681606e+02] - gold_standard_thresh = [106.354968, 39.894933, - 0.0, 568.160591] + gold_standard_thresh = [106.351332, 39.904419, + 0.0, 568.160832] # 2p_exp method mapper = T2(image, te, self.affine) @@ -202,7 +208,7 @@

    Module ukat.mapping.tests.test_t2

    def test_to_nifti(self): # Create a T2 map instance and test different export to NIFTI scenarios signal_array = np.tile(self.correct_signal, (10, 10, 3, 1)) - mapper = T2(signal_array, self.t, self.affine) + mapper = T2(signal_array, self.t, self.affine, method='3p_exp') if os.path.exists('test_output'): shutil.rmtree('test_output') @@ -212,7 +218,9 @@

    Module ukat.mapping.tests.test_t2

    mapper.to_nifti(output_directory='test_output', base_file_name='t2test', maps='all') output_files = os.listdir('test_output') - assert len(output_files) == 6 + assert len(output_files) == 8 + assert 't2test_b_map.nii.gz' in output_files + assert 't2test_b_err.nii.gz' in output_files assert 't2test_m0_err.nii.gz' in output_files assert 't2test_m0_map.nii.gz' in output_files assert 't2test_mask.nii.gz' in output_files @@ -229,7 +237,7 @@

    Module ukat.mapping.tests.test_t2

    output_files = os.listdir('test_output') assert len(output_files) == 0 - # Check that only t2 and r2 are saved. + # Check that only mask, t2 and r2 are saved. mapper.to_nifti(output_directory='test_output', base_file_name='t2test', maps=['mask', 't2', 'r2']) output_files = os.listdir('test_output') @@ -306,19 +314,21 @@

    Classes

    assert mapper.shape == signal_array.shape[:-1] npt.assert_almost_equal(mapper.t2_map.mean(), self.t2) npt.assert_almost_equal(mapper.m0_map.mean(), self.m0) - npt.assert_almost_equal(mapper.r2_map().mean(), 1 / self.t2) + npt.assert_almost_equal(mapper.r2.mean(), 1) # Single Threaded mapper = T2(signal_array, self.t, self.affine, multithread=False) assert mapper.shape == signal_array.shape[:-1] npt.assert_almost_equal(mapper.t2_map.mean(), self.t2) npt.assert_almost_equal(mapper.m0_map.mean(), self.m0) + npt.assert_almost_equal(mapper.r2.mean(), 1) # Auto Threaded mapper = T2(signal_array, self.t, self.affine, multithread='auto') assert mapper.shape == signal_array.shape[:-1] npt.assert_almost_equal(mapper.t2_map.mean(), self.t2) npt.assert_almost_equal(mapper.m0_map.mean(), self.m0) + npt.assert_almost_equal(mapper.r2.mean(), 1) # Fail to fit mapper = T2(signal_array[..., ::-1], self.t, self.affine, @@ -338,6 +348,7 @@

    Classes

    npt.assert_almost_equal(mapper.t2_map.mean(), self.t2) npt.assert_almost_equal(mapper.m0_map.mean(), self.m0) npt.assert_almost_equal(mapper.b_map.mean(), self.b) + npt.assert_almost_equal(mapper.r2.mean(), 1) # Single Threaded mapper = T2(signal_array, self.t, self.affine, multithread=False, @@ -346,6 +357,7 @@

    Classes

    npt.assert_almost_equal(mapper.t2_map.mean(), self.t2) npt.assert_almost_equal(mapper.m0_map.mean(), self.m0) npt.assert_almost_equal(mapper.b_map.mean(), self.b) + npt.assert_almost_equal(mapper.r2.mean(), 1) def test_threshold_fit(self): # Make the signal into a 4D array @@ -357,6 +369,7 @@

    Classes

    assert mapper.shape == signal_array.shape[:-1] npt.assert_almost_equal(mapper.t2_map.mean(), self.t2) npt.assert_almost_equal(mapper.m0_map.mean(), self.m0) + npt.assert_almost_equal(mapper.r2.mean(), 1) # Single Threaded mapper = T2(signal_array, self.t, self.affine, multithread=False, @@ -364,6 +377,7 @@

    Classes

    assert mapper.shape == signal_array.shape[:-1] npt.assert_almost_equal(mapper.t2_map.mean(), self.t2) npt.assert_almost_equal(mapper.m0_map.mean(), self.m0) + npt.assert_almost_equal(mapper.r2.mean(), 1) def test_mask(self): signal_array = np.tile(self.correct_signal, (10, 10, 3, 1)) @@ -413,8 +427,8 @@

    Classes

    0.0, 568.160604] gold_standard_3p_exp = [9.881218e+01, 4.294529e+01, 3.489657e-02, 5.681606e+02] - gold_standard_thresh = [106.354968, 39.894933, - 0.0, 568.160591] + gold_standard_thresh = [106.351332, 39.904419, + 0.0, 568.160832] # 2p_exp method mapper = T2(image, te, self.affine) @@ -440,7 +454,7 @@

    Classes

    def test_to_nifti(self): # Create a T2 map instance and test different export to NIFTI scenarios signal_array = np.tile(self.correct_signal, (10, 10, 3, 1)) - mapper = T2(signal_array, self.t, self.affine) + mapper = T2(signal_array, self.t, self.affine, method='3p_exp') if os.path.exists('test_output'): shutil.rmtree('test_output') @@ -450,7 +464,9 @@

    Classes

    mapper.to_nifti(output_directory='test_output', base_file_name='t2test', maps='all') output_files = os.listdir('test_output') - assert len(output_files) == 6 + assert len(output_files) == 8 + assert 't2test_b_map.nii.gz' in output_files + assert 't2test_b_err.nii.gz' in output_files assert 't2test_m0_err.nii.gz' in output_files assert 't2test_m0_map.nii.gz' in output_files assert 't2test_mask.nii.gz' in output_files @@ -467,7 +483,7 @@

    Classes

    output_files = os.listdir('test_output') assert len(output_files) == 0 - # Check that only t2 and r2 are saved. + # Check that only mask, t2 and r2 are saved. mapper.to_nifti(output_directory='test_output', base_file_name='t2test', maps=['mask', 't2', 'r2']) output_files = os.listdir('test_output') @@ -535,19 +551,21 @@

    Methods

    assert mapper.shape == signal_array.shape[:-1] npt.assert_almost_equal(mapper.t2_map.mean(), self.t2) npt.assert_almost_equal(mapper.m0_map.mean(), self.m0) - npt.assert_almost_equal(mapper.r2_map().mean(), 1 / self.t2) + npt.assert_almost_equal(mapper.r2.mean(), 1) # Single Threaded mapper = T2(signal_array, self.t, self.affine, multithread=False) assert mapper.shape == signal_array.shape[:-1] npt.assert_almost_equal(mapper.t2_map.mean(), self.t2) npt.assert_almost_equal(mapper.m0_map.mean(), self.m0) + npt.assert_almost_equal(mapper.r2.mean(), 1) # Auto Threaded mapper = T2(signal_array, self.t, self.affine, multithread='auto') assert mapper.shape == signal_array.shape[:-1] npt.assert_almost_equal(mapper.t2_map.mean(), self.t2) npt.assert_almost_equal(mapper.m0_map.mean(), self.m0) + npt.assert_almost_equal(mapper.r2.mean(), 1) # Fail to fit mapper = T2(signal_array[..., ::-1], self.t, self.affine, @@ -577,6 +595,7 @@

    Methods

    npt.assert_almost_equal(mapper.t2_map.mean(), self.t2) npt.assert_almost_equal(mapper.m0_map.mean(), self.m0) npt.assert_almost_equal(mapper.b_map.mean(), self.b) + npt.assert_almost_equal(mapper.r2.mean(), 1) # Single Threaded mapper = T2(signal_array, self.t, self.affine, multithread=False, @@ -584,7 +603,8 @@

    Methods

    assert mapper.shape == signal_array.shape[:-1] npt.assert_almost_equal(mapper.t2_map.mean(), self.t2) npt.assert_almost_equal(mapper.m0_map.mean(), self.m0) - npt.assert_almost_equal(mapper.b_map.mean(), self.b) + npt.assert_almost_equal(mapper.b_map.mean(), self.b) + npt.assert_almost_equal(mapper.r2.mean(), 1)
    @@ -674,8 +694,8 @@

    Methods

    0.0, 568.160604] gold_standard_3p_exp = [9.881218e+01, 4.294529e+01, 3.489657e-02, 5.681606e+02] - gold_standard_thresh = [106.354968, 39.894933, - 0.0, 568.160591] + gold_standard_thresh = [106.351332, 39.904419, + 0.0, 568.160832] # 2p_exp method mapper = T2(image, te, self.affine) @@ -733,13 +753,15 @@

    Methods

    assert mapper.shape == signal_array.shape[:-1] npt.assert_almost_equal(mapper.t2_map.mean(), self.t2) npt.assert_almost_equal(mapper.m0_map.mean(), self.m0) + npt.assert_almost_equal(mapper.r2.mean(), 1) # Single Threaded mapper = T2(signal_array, self.t, self.affine, multithread=False, noise_threshold=1300) assert mapper.shape == signal_array.shape[:-1] npt.assert_almost_equal(mapper.t2_map.mean(), self.t2) - npt.assert_almost_equal(mapper.m0_map.mean(), self.m0)
    + npt.assert_almost_equal(mapper.m0_map.mean(), self.m0) + npt.assert_almost_equal(mapper.r2.mean(), 1)
    @@ -754,7 +776,7 @@

    Methods

    def test_to_nifti(self):
         # Create a T2 map instance and test different export to NIFTI scenarios
         signal_array = np.tile(self.correct_signal, (10, 10, 3, 1))
    -    mapper = T2(signal_array, self.t, self.affine)
    +    mapper = T2(signal_array, self.t, self.affine, method='3p_exp')
     
         if os.path.exists('test_output'):
             shutil.rmtree('test_output')
    @@ -764,7 +786,9 @@ 

    Methods

    mapper.to_nifti(output_directory='test_output', base_file_name='t2test', maps='all') output_files = os.listdir('test_output') - assert len(output_files) == 6 + assert len(output_files) == 8 + assert 't2test_b_map.nii.gz' in output_files + assert 't2test_b_err.nii.gz' in output_files assert 't2test_m0_err.nii.gz' in output_files assert 't2test_m0_map.nii.gz' in output_files assert 't2test_mask.nii.gz' in output_files @@ -781,7 +805,7 @@

    Methods

    output_files = os.listdir('test_output') assert len(output_files) == 0 - # Check that only t2 and r2 are saved. + # Check that only mask, t2 and r2 are saved. mapper.to_nifti(output_directory='test_output', base_file_name='t2test', maps=['mask', 't2', 'r2']) output_files = os.listdir('test_output') diff --git a/mapping/tests/test_t2_stimfit.html b/mapping/tests/test_t2_stimfit.html new file mode 100644 index 00000000..316af54a --- /dev/null +++ b/mapping/tests/test_t2_stimfit.html @@ -0,0 +1,1161 @@ + + + + + + +ukat.mapping.tests.test_t2_stimfit API documentation + + + + + + + + + + + +
    +
    +
    +

    Module ukat.mapping.tests.test_t2_stimfit

    +
    +
    +
    + +Expand source code + +
    import os
    +import shutil
    +import numpy as np
    +import numpy.testing as npt
    +import pytest
    +from ukat.data import fetch
    +from ukat.mapping.t2_stimfit import StimFitModel, T2StimFit, _epgsig
    +from ukat.utils import arraystats
    +
    +
    +class TestStimFitModel:
    +    def test_invalid_model(self):
    +        with pytest.raises(ValueError):
    +            model = StimFitModel(mode='invalid_model')
    +
    +    def test_invalid_comp(self):
    +        with pytest.raises(ValueError):
    +            model = StimFitModel(n_comp=0)
    +
    +        with pytest.raises(ValueError):
    +            model = StimFitModel(n_comp=4)
    +
    +        with pytest.raises(ValueError):
    +            model = StimFitModel(n_comp='one')
    +
    +    def test_invalid_vendor(self):
    +        with pytest.warns(UserWarning):
    +            model = StimFitModel(ukrin_vendor='brucker')
    +
    +    def test_mode_switching(self):
    +        model = StimFitModel(mode='selective')
    +        assert model.mode == 'selective'
    +        assert model.opt['RFe']['angle'] == 90
    +        assert model.opt['Dz'] == [-0.5, 0.5]
    +
    +        model = StimFitModel(mode='non_selective')
    +        assert model.mode == 'non_selective'
    +        assert model.opt['RFe']['angle'] == 90
    +        with pytest.raises(KeyError):
    +            model.opt['Dz']
    +
    +    def test_n_comp_switching(self):
    +        model = StimFitModel(n_comp=1)
    +        assert model.n_comp == 1
    +        assert model.opt['lsq']['Ncomp'] == 1
    +        assert model.opt['lsq']['X0'][2] == 1
    +
    +        model = StimFitModel(n_comp=2)
    +        assert model.n_comp == 2
    +        assert model.opt['lsq']['Ncomp'] == 2
    +        assert model.opt['lsq']['X0'][2] == 0.331
    +
    +        model = StimFitModel(n_comp=3)
    +        assert model.n_comp == 3
    +        assert model.opt['lsq']['Ncomp'] == 3
    +        assert model.opt['lsq']['X0'][2] == 0.036
    +
    +    def test_vendor_switching(self):
    +        model = StimFitModel(mode='selective', ukrin_vendor='ge')
    +        assert model.vendor == 'ge'
    +        assert model.opt['RFe']['tau'] == 2000 / 1e6
    +
    +        model = StimFitModel(mode='selective', ukrin_vendor='philips')
    +        assert model.vendor == 'philips'
    +        assert model.opt['RFe']['tau'] == 3820 / 1e6
    +
    +        model = StimFitModel(mode='selective', ukrin_vendor='siemens')
    +        assert model.vendor == 'siemens'
    +        assert model.opt['RFe']['tau'] == 3072 / 1e6
    +
    +    def test_set_rf(self):
    +        model = StimFitModel(mode='selective', ukrin_vendor='ge')
    +        npt.assert_almost_equal(model.opt['RFe']['RF'][-1], 2.58327955e-07)
    +        npt.assert_almost_equal(model.opt['RFe']['alpha'][-1], 0.04164681,
    +                                decimal=5)
    +        npt.assert_almost_equal(model.opt['RFr']['RF'][-1], -3.55622605e-05)
    +        npt.assert_almost_equal(model.opt['RFr']['alpha'][-1], 1.97107315)
    +
    +        model = StimFitModel(mode='selective', ukrin_vendor='philips')
    +        npt.assert_almost_equal(model.opt['RFe']['RF'][-1], 3.59081850e-04)
    +        npt.assert_almost_equal(model.opt['RFe']['alpha'][-1], 0.05002553,
    +                                decimal=5)
    +        npt.assert_almost_equal(model.opt['RFr']['RF'][-1], 0.00473865)
    +        npt.assert_almost_equal(model.opt['RFr']['alpha'][-1],  0.46764775,
    +                                decimal=5)
    +
    +        model = StimFitModel(mode='selective', ukrin_vendor='siemens')
    +        npt.assert_almost_equal(model.opt['RFe']['RF'][-1], 1.68182263e-07)
    +        npt.assert_almost_equal(model.opt['RFe']['alpha'][-1], 0.07221162,
    +                                decimal=5)
    +        npt.assert_almost_equal(model.opt['RFr']['RF'][-1], -3.71744163e-05)
    +        npt.assert_almost_equal(model.opt['RFr']['alpha'][-1], 1.31133498)
    +
    +    def test_getters(self):
    +        model = StimFitModel(mode='selective', ukrin_vendor='ge')
    +        assert len(model.get_opt()) == 11
    +        assert len(model.get_lsq()) == 6
    +        assert len(model.get_rfe()) == 7
    +        assert len(model.get_rfr()) == 8
    +
    +
    +class TestT2StimFit:
    +    image_ge, affine_ge, te_ge = fetch.t2_ge(1)
    +    image_ge = image_ge[35:45, 50:65, 2:4, :]  # Crop to speed up tests
    +    image_philips, affine_philips, te_philips = fetch.t2_philips(2)
    +    image_philips = image_philips[35:45, 50:65, 2:4, :]
    +    image_siemens, affine_siemens, te_siemens = fetch.t2_siemens(1)
    +    image_siemens = image_siemens[35:45, 40:55, 2:4, :]
    +
    +    # selective
    +    def test_selectiveness(self):
    +        # Selective
    +        model = StimFitModel(mode='selective', ukrin_vendor='ge')
    +        mapper = T2StimFit(self.image_ge, self.affine_ge, model)
    +        stats = arraystats.ArrayStats(mapper.t2_map).calculate()
    +        npt.assert_allclose([stats["mean"]["3D"], stats["std"]["3D"],
    +                             stats["min"]["3D"], stats["max"]["3D"]],
    +                            [164.331581, 199.057747, 51.268116, 1455.551225],
    +                            rtol=1e-2, atol=0.25)
    +
    +        # Non-selective
    +        model = StimFitModel(mode='non_selective', ukrin_vendor='ge')
    +        mapper = T2StimFit(self.image_ge, self.affine_ge, model)
    +        stats = arraystats.ArrayStats(mapper.t2_map).calculate()
    +        npt.assert_allclose([stats["mean"]["3D"], stats["std"]["3D"],
    +                             stats["min"]["3D"], stats["max"]["3D"]],
    +                            [165.994692, 203.583211, 51.827107, 1497.168001],
    +                            rtol=1e-2, atol=0.25)
    +
    +    # n_comp
    +    def test_n_comp(self):
    +        # Two Components
    +        model = StimFitModel(mode='selective', ukrin_vendor='ge', n_comp=2)
    +        mapper = T2StimFit(self.image_ge[0, 14, :, :], self.affine_ge, model)
    +
    +        npt.assert_allclose([mapper.t2_map[0, 0]],
    +                            [117.991529],
    +                            rtol=5e-2, atol=0.1)
    +
    +        # Three Components
    +        # Cant get this to be stable across operating systems so commented out.
    +
    +        # model = StimFitModel(mode='selective', ukrin_vendor='ge', n_comp=3)
    +        # mapper = T2StimFit(self.image_ge[0, 14, :, :], self.affine_ge, model)
    +        # npt.assert_allclose([mapper.t2_map[0, 2]],
    +        #                     [1245.291925],
    +        #                     rtol=5e-2, atol=0.1)
    +
    +    # vendor
    +    def test_vendor(self):
    +        # Philips
    +        model = StimFitModel(mode='selective', ukrin_vendor='philips')
    +        mapper = T2StimFit(self.image_philips, self.affine_philips, model)
    +        stats = arraystats.ArrayStats(mapper.t2_map).calculate()
    +        npt.assert_allclose([stats["mean"]["3D"], stats["std"]["3D"],
    +                             stats["min"]["3D"], stats["max"]["3D"]],
    +                            [281.52594, 596.832203, 36.470879, 3000.0],
    +                            rtol=1e-6, atol=1e-4)
    +
    +        # Siemens
    +        model = StimFitModel(mode='selective', ukrin_vendor='siemens')
    +        mapper = T2StimFit(self.image_siemens, self.affine_siemens, model)
    +        stats = arraystats.ArrayStats(mapper.t2_map).calculate()
    +        npt.assert_allclose([stats["mean"]["3D"], stats["std"]["3D"],
    +                             stats["min"]["3D"], stats["max"]["3D"]],
    +                            [120.47096, 190.454984, 26.621704, 2999.999651],
    +                            rtol=1e-5, atol=1e-2)
    +
    +    # mask
    +    def test_mask(self):
    +        mask = self.image_ge[..., 0] > 3000
    +        model = StimFitModel(mode='non_selective', ukrin_vendor='ge')
    +        mapper = T2StimFit(self.image_ge, self.affine_ge, model, mask=mask)
    +        stats = arraystats.ArrayStats(mapper.t2_map).calculate()
    +        npt.assert_allclose([stats["mean"]["3D"], stats["std"]["3D"],
    +                             stats["min"]["3D"], stats["max"]["3D"]],
    +                            [156.693513, 207.797,  0.0, 1497.168001],
    +                            rtol=1e-2, atol=0.25)
    +
    +    # threading
    +    def test_st(self):
    +        model = StimFitModel(mode='non_selective', ukrin_vendor='ge')
    +        mapper = T2StimFit(self.image_ge, self.affine_ge, model,
    +                           multithread=False)
    +        stats = arraystats.ArrayStats(mapper.t2_map).calculate()
    +        npt.assert_allclose([stats["mean"]["3D"], stats["std"]["3D"],
    +                             stats["min"]["3D"], stats["max"]["3D"]],
    +                            [165.994692, 203.583211, 51.827107, 1497.168001],
    +                            rtol=1e-2, atol=0.25)
    +
    +    # normalisation
    +    def test_normalisation_warning(self):
    +        with pytest.warns(UserWarning):
    +            model = StimFitModel(mode='non_selective', ukrin_vendor='ge')
    +            mapper = T2StimFit(self.image_ge * 2, self.affine_ge, model,
    +                               norm=False)
    +
    +    def test_etl_signal_exception(self):
    +        with pytest.raises(Exception):
    +            model = StimFitModel(mode='non_selective', ukrin_vendor='ge')
    +            mapper = T2StimFit(self.image_ge[..., :-2], self.affine_ge, model)
    +
    +    # to_nifti
    +    def test_to_nifti(self):
    +        mask = self.image_ge[..., 0] > 3000
    +        model = StimFitModel(mode='non_selective', ukrin_vendor='ge')
    +        mapper = T2StimFit(self.image_ge, self.affine_ge, model, mask=mask)
    +
    +        if os.path.exists('test_output'):
    +            shutil.rmtree('test_output')
    +        os.makedirs('test_output', exist_ok=True)
    +
    +        # Check all is saved.
    +        mapper.to_nifti(output_directory='test_output',
    +                        base_file_name='t2stimfittest', maps='all')
    +        output_files = os.listdir('test_output')
    +        assert len(output_files) == 5
    +        assert 't2stimfittest_b1_map.nii.gz' in output_files
    +        assert 't2stimfittest_m0_map.nii.gz' in output_files
    +        assert 't2stimfittest_mask.nii.gz' in output_files
    +        assert 't2stimfittest_r2_map.nii.gz' in output_files
    +        assert 't2stimfittest_t2_map.nii.gz' in output_files
    +
    +        for f in os.listdir('test_output'):
    +            os.remove(os.path.join('test_output', f))
    +
    +        # Check that no files are saved.
    +        mapper.to_nifti(output_directory='test_output',
    +                        base_file_name='t2stimfittest', maps=[])
    +        output_files = os.listdir('test_output')
    +        assert len(output_files) == 0
    +
    +        # Check that only t2, mask and r2 are saved.
    +        mapper.to_nifti(output_directory='test_output',
    +                        base_file_name='t2stimfittest', maps=['mask', 't2',
    +                                                              'r2'])
    +        output_files = os.listdir('test_output')
    +        assert len(output_files) == 3
    +        assert 't2stimfittest_mask.nii.gz' in output_files
    +        assert 't2stimfittest_t2_map.nii.gz' in output_files
    +        assert 't2stimfittest_r2_map.nii.gz' in output_files
    +
    +        for f in os.listdir('test_output'):
    +            os.remove(os.path.join('test_output', f))
    +
    +        # Check that it fails when no maps are given
    +        with pytest.raises(ValueError):
    +            mapper.to_nifti(output_directory='test_output',
    +                            base_file_name='t2stimfittest', maps='')
    +
    +        # Delete 'test_output' folder
    +        shutil.rmtree('test_output')
    +
    +
    +class TestEpg:
    +    t2 = 0.1
    +    b1 = 0.95
    +
    +    def test_selective(self):
    +        model = StimFitModel(mode='selective', ukrin_vendor='ge')
    +        sig = _epgsig(self.t2, self.b1, model.opt, model.mode)
    +        npt.assert_allclose(sig, np.array([0.53193464, 0.48718256, 0.41393849,
    +                                           0.37639148, 0.32247532, 0.29132453,
    +                                           0.25050307, 0.22604609, 0.19430487,
    +                                           0.1755666]),
    +                            rtol=1e-5, atol=1e-5)
    +
    +    def test_non_selective(self):
    +        model = StimFitModel(mode='non_selective', ukrin_vendor='ge')
    +        sig = _epgsig(self.t2, self.b1, model.opt, model.mode)
    +        npt.assert_allclose(sig, np.array([0.87087025, 0.7713902, 0.6727603,
    +                                           0.59694589, 0.51965957, 0.46200336,
    +                                           0.40135138, 0.35760991, 0.30993556,
    +                                           0.27684428]),
    +                            rtol=1e-5, atol=1e-5)
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +

    Classes

    +
    +
    +class TestEpg +
    +
    +
    +
    + +Expand source code + +
    class TestEpg:
    +    t2 = 0.1
    +    b1 = 0.95
    +
    +    def test_selective(self):
    +        model = StimFitModel(mode='selective', ukrin_vendor='ge')
    +        sig = _epgsig(self.t2, self.b1, model.opt, model.mode)
    +        npt.assert_allclose(sig, np.array([0.53193464, 0.48718256, 0.41393849,
    +                                           0.37639148, 0.32247532, 0.29132453,
    +                                           0.25050307, 0.22604609, 0.19430487,
    +                                           0.1755666]),
    +                            rtol=1e-5, atol=1e-5)
    +
    +    def test_non_selective(self):
    +        model = StimFitModel(mode='non_selective', ukrin_vendor='ge')
    +        sig = _epgsig(self.t2, self.b1, model.opt, model.mode)
    +        npt.assert_allclose(sig, np.array([0.87087025, 0.7713902, 0.6727603,
    +                                           0.59694589, 0.51965957, 0.46200336,
    +                                           0.40135138, 0.35760991, 0.30993556,
    +                                           0.27684428]),
    +                            rtol=1e-5, atol=1e-5)
    +
    +

    Class variables

    +
    +
    var b1
    +
    +
    +
    +
    var t2
    +
    +
    +
    +
    +

    Methods

    +
    +
    +def test_non_selective(self) +
    +
    +
    +
    + +Expand source code + +
    def test_non_selective(self):
    +    model = StimFitModel(mode='non_selective', ukrin_vendor='ge')
    +    sig = _epgsig(self.t2, self.b1, model.opt, model.mode)
    +    npt.assert_allclose(sig, np.array([0.87087025, 0.7713902, 0.6727603,
    +                                       0.59694589, 0.51965957, 0.46200336,
    +                                       0.40135138, 0.35760991, 0.30993556,
    +                                       0.27684428]),
    +                        rtol=1e-5, atol=1e-5)
    +
    +
    +
    +def test_selective(self) +
    +
    +
    +
    + +Expand source code + +
    def test_selective(self):
    +    model = StimFitModel(mode='selective', ukrin_vendor='ge')
    +    sig = _epgsig(self.t2, self.b1, model.opt, model.mode)
    +    npt.assert_allclose(sig, np.array([0.53193464, 0.48718256, 0.41393849,
    +                                       0.37639148, 0.32247532, 0.29132453,
    +                                       0.25050307, 0.22604609, 0.19430487,
    +                                       0.1755666]),
    +                        rtol=1e-5, atol=1e-5)
    +
    +
    +
    +
    +
    +class TestStimFitModel +
    +
    +
    +
    + +Expand source code + +
    class TestStimFitModel:
    +    def test_invalid_model(self):
    +        with pytest.raises(ValueError):
    +            model = StimFitModel(mode='invalid_model')
    +
    +    def test_invalid_comp(self):
    +        with pytest.raises(ValueError):
    +            model = StimFitModel(n_comp=0)
    +
    +        with pytest.raises(ValueError):
    +            model = StimFitModel(n_comp=4)
    +
    +        with pytest.raises(ValueError):
    +            model = StimFitModel(n_comp='one')
    +
    +    def test_invalid_vendor(self):
    +        with pytest.warns(UserWarning):
    +            model = StimFitModel(ukrin_vendor='brucker')
    +
    +    def test_mode_switching(self):
    +        model = StimFitModel(mode='selective')
    +        assert model.mode == 'selective'
    +        assert model.opt['RFe']['angle'] == 90
    +        assert model.opt['Dz'] == [-0.5, 0.5]
    +
    +        model = StimFitModel(mode='non_selective')
    +        assert model.mode == 'non_selective'
    +        assert model.opt['RFe']['angle'] == 90
    +        with pytest.raises(KeyError):
    +            model.opt['Dz']
    +
    +    def test_n_comp_switching(self):
    +        model = StimFitModel(n_comp=1)
    +        assert model.n_comp == 1
    +        assert model.opt['lsq']['Ncomp'] == 1
    +        assert model.opt['lsq']['X0'][2] == 1
    +
    +        model = StimFitModel(n_comp=2)
    +        assert model.n_comp == 2
    +        assert model.opt['lsq']['Ncomp'] == 2
    +        assert model.opt['lsq']['X0'][2] == 0.331
    +
    +        model = StimFitModel(n_comp=3)
    +        assert model.n_comp == 3
    +        assert model.opt['lsq']['Ncomp'] == 3
    +        assert model.opt['lsq']['X0'][2] == 0.036
    +
    +    def test_vendor_switching(self):
    +        model = StimFitModel(mode='selective', ukrin_vendor='ge')
    +        assert model.vendor == 'ge'
    +        assert model.opt['RFe']['tau'] == 2000 / 1e6
    +
    +        model = StimFitModel(mode='selective', ukrin_vendor='philips')
    +        assert model.vendor == 'philips'
    +        assert model.opt['RFe']['tau'] == 3820 / 1e6
    +
    +        model = StimFitModel(mode='selective', ukrin_vendor='siemens')
    +        assert model.vendor == 'siemens'
    +        assert model.opt['RFe']['tau'] == 3072 / 1e6
    +
    +    def test_set_rf(self):
    +        model = StimFitModel(mode='selective', ukrin_vendor='ge')
    +        npt.assert_almost_equal(model.opt['RFe']['RF'][-1], 2.58327955e-07)
    +        npt.assert_almost_equal(model.opt['RFe']['alpha'][-1], 0.04164681,
    +                                decimal=5)
    +        npt.assert_almost_equal(model.opt['RFr']['RF'][-1], -3.55622605e-05)
    +        npt.assert_almost_equal(model.opt['RFr']['alpha'][-1], 1.97107315)
    +
    +        model = StimFitModel(mode='selective', ukrin_vendor='philips')
    +        npt.assert_almost_equal(model.opt['RFe']['RF'][-1], 3.59081850e-04)
    +        npt.assert_almost_equal(model.opt['RFe']['alpha'][-1], 0.05002553,
    +                                decimal=5)
    +        npt.assert_almost_equal(model.opt['RFr']['RF'][-1], 0.00473865)
    +        npt.assert_almost_equal(model.opt['RFr']['alpha'][-1],  0.46764775,
    +                                decimal=5)
    +
    +        model = StimFitModel(mode='selective', ukrin_vendor='siemens')
    +        npt.assert_almost_equal(model.opt['RFe']['RF'][-1], 1.68182263e-07)
    +        npt.assert_almost_equal(model.opt['RFe']['alpha'][-1], 0.07221162,
    +                                decimal=5)
    +        npt.assert_almost_equal(model.opt['RFr']['RF'][-1], -3.71744163e-05)
    +        npt.assert_almost_equal(model.opt['RFr']['alpha'][-1], 1.31133498)
    +
    +    def test_getters(self):
    +        model = StimFitModel(mode='selective', ukrin_vendor='ge')
    +        assert len(model.get_opt()) == 11
    +        assert len(model.get_lsq()) == 6
    +        assert len(model.get_rfe()) == 7
    +        assert len(model.get_rfr()) == 8
    +
    +

    Methods

    +
    +
    +def test_getters(self) +
    +
    +
    +
    + +Expand source code + +
    def test_getters(self):
    +    model = StimFitModel(mode='selective', ukrin_vendor='ge')
    +    assert len(model.get_opt()) == 11
    +    assert len(model.get_lsq()) == 6
    +    assert len(model.get_rfe()) == 7
    +    assert len(model.get_rfr()) == 8
    +
    +
    +
    +def test_invalid_comp(self) +
    +
    +
    +
    + +Expand source code + +
    def test_invalid_comp(self):
    +    with pytest.raises(ValueError):
    +        model = StimFitModel(n_comp=0)
    +
    +    with pytest.raises(ValueError):
    +        model = StimFitModel(n_comp=4)
    +
    +    with pytest.raises(ValueError):
    +        model = StimFitModel(n_comp='one')
    +
    +
    +
    +def test_invalid_model(self) +
    +
    +
    +
    + +Expand source code + +
    def test_invalid_model(self):
    +    with pytest.raises(ValueError):
    +        model = StimFitModel(mode='invalid_model')
    +
    +
    +
    +def test_invalid_vendor(self) +
    +
    +
    +
    + +Expand source code + +
    def test_invalid_vendor(self):
    +    with pytest.warns(UserWarning):
    +        model = StimFitModel(ukrin_vendor='brucker')
    +
    +
    +
    +def test_mode_switching(self) +
    +
    +
    +
    + +Expand source code + +
    def test_mode_switching(self):
    +    model = StimFitModel(mode='selective')
    +    assert model.mode == 'selective'
    +    assert model.opt['RFe']['angle'] == 90
    +    assert model.opt['Dz'] == [-0.5, 0.5]
    +
    +    model = StimFitModel(mode='non_selective')
    +    assert model.mode == 'non_selective'
    +    assert model.opt['RFe']['angle'] == 90
    +    with pytest.raises(KeyError):
    +        model.opt['Dz']
    +
    +
    +
    +def test_n_comp_switching(self) +
    +
    +
    +
    + +Expand source code + +
    def test_n_comp_switching(self):
    +    model = StimFitModel(n_comp=1)
    +    assert model.n_comp == 1
    +    assert model.opt['lsq']['Ncomp'] == 1
    +    assert model.opt['lsq']['X0'][2] == 1
    +
    +    model = StimFitModel(n_comp=2)
    +    assert model.n_comp == 2
    +    assert model.opt['lsq']['Ncomp'] == 2
    +    assert model.opt['lsq']['X0'][2] == 0.331
    +
    +    model = StimFitModel(n_comp=3)
    +    assert model.n_comp == 3
    +    assert model.opt['lsq']['Ncomp'] == 3
    +    assert model.opt['lsq']['X0'][2] == 0.036
    +
    +
    +
    +def test_set_rf(self) +
    +
    +
    +
    + +Expand source code + +
    def test_set_rf(self):
    +    model = StimFitModel(mode='selective', ukrin_vendor='ge')
    +    npt.assert_almost_equal(model.opt['RFe']['RF'][-1], 2.58327955e-07)
    +    npt.assert_almost_equal(model.opt['RFe']['alpha'][-1], 0.04164681,
    +                            decimal=5)
    +    npt.assert_almost_equal(model.opt['RFr']['RF'][-1], -3.55622605e-05)
    +    npt.assert_almost_equal(model.opt['RFr']['alpha'][-1], 1.97107315)
    +
    +    model = StimFitModel(mode='selective', ukrin_vendor='philips')
    +    npt.assert_almost_equal(model.opt['RFe']['RF'][-1], 3.59081850e-04)
    +    npt.assert_almost_equal(model.opt['RFe']['alpha'][-1], 0.05002553,
    +                            decimal=5)
    +    npt.assert_almost_equal(model.opt['RFr']['RF'][-1], 0.00473865)
    +    npt.assert_almost_equal(model.opt['RFr']['alpha'][-1],  0.46764775,
    +                            decimal=5)
    +
    +    model = StimFitModel(mode='selective', ukrin_vendor='siemens')
    +    npt.assert_almost_equal(model.opt['RFe']['RF'][-1], 1.68182263e-07)
    +    npt.assert_almost_equal(model.opt['RFe']['alpha'][-1], 0.07221162,
    +                            decimal=5)
    +    npt.assert_almost_equal(model.opt['RFr']['RF'][-1], -3.71744163e-05)
    +    npt.assert_almost_equal(model.opt['RFr']['alpha'][-1], 1.31133498)
    +
    +
    +
    +def test_vendor_switching(self) +
    +
    +
    +
    + +Expand source code + +
    def test_vendor_switching(self):
    +    model = StimFitModel(mode='selective', ukrin_vendor='ge')
    +    assert model.vendor == 'ge'
    +    assert model.opt['RFe']['tau'] == 2000 / 1e6
    +
    +    model = StimFitModel(mode='selective', ukrin_vendor='philips')
    +    assert model.vendor == 'philips'
    +    assert model.opt['RFe']['tau'] == 3820 / 1e6
    +
    +    model = StimFitModel(mode='selective', ukrin_vendor='siemens')
    +    assert model.vendor == 'siemens'
    +    assert model.opt['RFe']['tau'] == 3072 / 1e6
    +
    +
    +
    +
    +
    +class TestT2StimFit +
    +
    +
    +
    + +Expand source code + +
    class TestT2StimFit:
    +    image_ge, affine_ge, te_ge = fetch.t2_ge(1)
    +    image_ge = image_ge[35:45, 50:65, 2:4, :]  # Crop to speed up tests
    +    image_philips, affine_philips, te_philips = fetch.t2_philips(2)
    +    image_philips = image_philips[35:45, 50:65, 2:4, :]
    +    image_siemens, affine_siemens, te_siemens = fetch.t2_siemens(1)
    +    image_siemens = image_siemens[35:45, 40:55, 2:4, :]
    +
    +    # selective
    +    def test_selectiveness(self):
    +        # Selective
    +        model = StimFitModel(mode='selective', ukrin_vendor='ge')
    +        mapper = T2StimFit(self.image_ge, self.affine_ge, model)
    +        stats = arraystats.ArrayStats(mapper.t2_map).calculate()
    +        npt.assert_allclose([stats["mean"]["3D"], stats["std"]["3D"],
    +                             stats["min"]["3D"], stats["max"]["3D"]],
    +                            [164.331581, 199.057747, 51.268116, 1455.551225],
    +                            rtol=1e-2, atol=0.25)
    +
    +        # Non-selective
    +        model = StimFitModel(mode='non_selective', ukrin_vendor='ge')
    +        mapper = T2StimFit(self.image_ge, self.affine_ge, model)
    +        stats = arraystats.ArrayStats(mapper.t2_map).calculate()
    +        npt.assert_allclose([stats["mean"]["3D"], stats["std"]["3D"],
    +                             stats["min"]["3D"], stats["max"]["3D"]],
    +                            [165.994692, 203.583211, 51.827107, 1497.168001],
    +                            rtol=1e-2, atol=0.25)
    +
    +    # n_comp
    +    def test_n_comp(self):
    +        # Two Components
    +        model = StimFitModel(mode='selective', ukrin_vendor='ge', n_comp=2)
    +        mapper = T2StimFit(self.image_ge[0, 14, :, :], self.affine_ge, model)
    +
    +        npt.assert_allclose([mapper.t2_map[0, 0]],
    +                            [117.991529],
    +                            rtol=5e-2, atol=0.1)
    +
    +        # Three Components
    +        # Cant get this to be stable across operating systems so commented out.
    +
    +        # model = StimFitModel(mode='selective', ukrin_vendor='ge', n_comp=3)
    +        # mapper = T2StimFit(self.image_ge[0, 14, :, :], self.affine_ge, model)
    +        # npt.assert_allclose([mapper.t2_map[0, 2]],
    +        #                     [1245.291925],
    +        #                     rtol=5e-2, atol=0.1)
    +
    +    # vendor
    +    def test_vendor(self):
    +        # Philips
    +        model = StimFitModel(mode='selective', ukrin_vendor='philips')
    +        mapper = T2StimFit(self.image_philips, self.affine_philips, model)
    +        stats = arraystats.ArrayStats(mapper.t2_map).calculate()
    +        npt.assert_allclose([stats["mean"]["3D"], stats["std"]["3D"],
    +                             stats["min"]["3D"], stats["max"]["3D"]],
    +                            [281.52594, 596.832203, 36.470879, 3000.0],
    +                            rtol=1e-6, atol=1e-4)
    +
    +        # Siemens
    +        model = StimFitModel(mode='selective', ukrin_vendor='siemens')
    +        mapper = T2StimFit(self.image_siemens, self.affine_siemens, model)
    +        stats = arraystats.ArrayStats(mapper.t2_map).calculate()
    +        npt.assert_allclose([stats["mean"]["3D"], stats["std"]["3D"],
    +                             stats["min"]["3D"], stats["max"]["3D"]],
    +                            [120.47096, 190.454984, 26.621704, 2999.999651],
    +                            rtol=1e-5, atol=1e-2)
    +
    +    # mask
    +    def test_mask(self):
    +        mask = self.image_ge[..., 0] > 3000
    +        model = StimFitModel(mode='non_selective', ukrin_vendor='ge')
    +        mapper = T2StimFit(self.image_ge, self.affine_ge, model, mask=mask)
    +        stats = arraystats.ArrayStats(mapper.t2_map).calculate()
    +        npt.assert_allclose([stats["mean"]["3D"], stats["std"]["3D"],
    +                             stats["min"]["3D"], stats["max"]["3D"]],
    +                            [156.693513, 207.797,  0.0, 1497.168001],
    +                            rtol=1e-2, atol=0.25)
    +
    +    # threading
    +    def test_st(self):
    +        model = StimFitModel(mode='non_selective', ukrin_vendor='ge')
    +        mapper = T2StimFit(self.image_ge, self.affine_ge, model,
    +                           multithread=False)
    +        stats = arraystats.ArrayStats(mapper.t2_map).calculate()
    +        npt.assert_allclose([stats["mean"]["3D"], stats["std"]["3D"],
    +                             stats["min"]["3D"], stats["max"]["3D"]],
    +                            [165.994692, 203.583211, 51.827107, 1497.168001],
    +                            rtol=1e-2, atol=0.25)
    +
    +    # normalisation
    +    def test_normalisation_warning(self):
    +        with pytest.warns(UserWarning):
    +            model = StimFitModel(mode='non_selective', ukrin_vendor='ge')
    +            mapper = T2StimFit(self.image_ge * 2, self.affine_ge, model,
    +                               norm=False)
    +
    +    def test_etl_signal_exception(self):
    +        with pytest.raises(Exception):
    +            model = StimFitModel(mode='non_selective', ukrin_vendor='ge')
    +            mapper = T2StimFit(self.image_ge[..., :-2], self.affine_ge, model)
    +
    +    # to_nifti
    +    def test_to_nifti(self):
    +        mask = self.image_ge[..., 0] > 3000
    +        model = StimFitModel(mode='non_selective', ukrin_vendor='ge')
    +        mapper = T2StimFit(self.image_ge, self.affine_ge, model, mask=mask)
    +
    +        if os.path.exists('test_output'):
    +            shutil.rmtree('test_output')
    +        os.makedirs('test_output', exist_ok=True)
    +
    +        # Check all is saved.
    +        mapper.to_nifti(output_directory='test_output',
    +                        base_file_name='t2stimfittest', maps='all')
    +        output_files = os.listdir('test_output')
    +        assert len(output_files) == 5
    +        assert 't2stimfittest_b1_map.nii.gz' in output_files
    +        assert 't2stimfittest_m0_map.nii.gz' in output_files
    +        assert 't2stimfittest_mask.nii.gz' in output_files
    +        assert 't2stimfittest_r2_map.nii.gz' in output_files
    +        assert 't2stimfittest_t2_map.nii.gz' in output_files
    +
    +        for f in os.listdir('test_output'):
    +            os.remove(os.path.join('test_output', f))
    +
    +        # Check that no files are saved.
    +        mapper.to_nifti(output_directory='test_output',
    +                        base_file_name='t2stimfittest', maps=[])
    +        output_files = os.listdir('test_output')
    +        assert len(output_files) == 0
    +
    +        # Check that only t2, mask and r2 are saved.
    +        mapper.to_nifti(output_directory='test_output',
    +                        base_file_name='t2stimfittest', maps=['mask', 't2',
    +                                                              'r2'])
    +        output_files = os.listdir('test_output')
    +        assert len(output_files) == 3
    +        assert 't2stimfittest_mask.nii.gz' in output_files
    +        assert 't2stimfittest_t2_map.nii.gz' in output_files
    +        assert 't2stimfittest_r2_map.nii.gz' in output_files
    +
    +        for f in os.listdir('test_output'):
    +            os.remove(os.path.join('test_output', f))
    +
    +        # Check that it fails when no maps are given
    +        with pytest.raises(ValueError):
    +            mapper.to_nifti(output_directory='test_output',
    +                            base_file_name='t2stimfittest', maps='')
    +
    +        # Delete 'test_output' folder
    +        shutil.rmtree('test_output')
    +
    +

    Class variables

    +
    +
    var affine_ge
    +
    +
    +
    +
    var affine_philips
    +
    +
    +
    +
    var affine_siemens
    +
    +
    +
    +
    var image_ge
    +
    +
    +
    +
    var image_philips
    +
    +
    +
    +
    var image_siemens
    +
    +
    +
    +
    var te_ge
    +
    +
    +
    +
    var te_philips
    +
    +
    +
    +
    var te_siemens
    +
    +
    +
    +
    +

    Methods

    +
    +
    +def test_etl_signal_exception(self) +
    +
    +
    +
    + +Expand source code + +
    def test_etl_signal_exception(self):
    +    with pytest.raises(Exception):
    +        model = StimFitModel(mode='non_selective', ukrin_vendor='ge')
    +        mapper = T2StimFit(self.image_ge[..., :-2], self.affine_ge, model)
    +
    +
    +
    +def test_mask(self) +
    +
    +
    +
    + +Expand source code + +
    def test_mask(self):
    +    mask = self.image_ge[..., 0] > 3000
    +    model = StimFitModel(mode='non_selective', ukrin_vendor='ge')
    +    mapper = T2StimFit(self.image_ge, self.affine_ge, model, mask=mask)
    +    stats = arraystats.ArrayStats(mapper.t2_map).calculate()
    +    npt.assert_allclose([stats["mean"]["3D"], stats["std"]["3D"],
    +                         stats["min"]["3D"], stats["max"]["3D"]],
    +                        [156.693513, 207.797,  0.0, 1497.168001],
    +                        rtol=1e-2, atol=0.25)
    +
    +
    +
    +def test_n_comp(self) +
    +
    +
    +
    + +Expand source code + +
    def test_n_comp(self):
    +    # Two Components
    +    model = StimFitModel(mode='selective', ukrin_vendor='ge', n_comp=2)
    +    mapper = T2StimFit(self.image_ge[0, 14, :, :], self.affine_ge, model)
    +
    +    npt.assert_allclose([mapper.t2_map[0, 0]],
    +                        [117.991529],
    +                        rtol=5e-2, atol=0.1)
    +
    +    # Three Components
    +    # Cant get this to be stable across operating systems so commented out.
    +
    +    # model = StimFitModel(mode='selective', ukrin_vendor='ge', n_comp=3)
    +    # mapper = T2StimFit(self.image_ge[0, 14, :, :], self.affine_ge, model)
    +    # npt.assert_allclose([mapper.t2_map[0, 2]],
    +    #                     [1245.291925],
    +    #                     rtol=5e-2, atol=0.1)
    +
    +
    +
    +def test_normalisation_warning(self) +
    +
    +
    +
    + +Expand source code + +
    def test_normalisation_warning(self):
    +    with pytest.warns(UserWarning):
    +        model = StimFitModel(mode='non_selective', ukrin_vendor='ge')
    +        mapper = T2StimFit(self.image_ge * 2, self.affine_ge, model,
    +                           norm=False)
    +
    +
    +
    +def test_selectiveness(self) +
    +
    +
    +
    + +Expand source code + +
    def test_selectiveness(self):
    +    # Selective
    +    model = StimFitModel(mode='selective', ukrin_vendor='ge')
    +    mapper = T2StimFit(self.image_ge, self.affine_ge, model)
    +    stats = arraystats.ArrayStats(mapper.t2_map).calculate()
    +    npt.assert_allclose([stats["mean"]["3D"], stats["std"]["3D"],
    +                         stats["min"]["3D"], stats["max"]["3D"]],
    +                        [164.331581, 199.057747, 51.268116, 1455.551225],
    +                        rtol=1e-2, atol=0.25)
    +
    +    # Non-selective
    +    model = StimFitModel(mode='non_selective', ukrin_vendor='ge')
    +    mapper = T2StimFit(self.image_ge, self.affine_ge, model)
    +    stats = arraystats.ArrayStats(mapper.t2_map).calculate()
    +    npt.assert_allclose([stats["mean"]["3D"], stats["std"]["3D"],
    +                         stats["min"]["3D"], stats["max"]["3D"]],
    +                        [165.994692, 203.583211, 51.827107, 1497.168001],
    +                        rtol=1e-2, atol=0.25)
    +
    +
    +
    +def test_st(self) +
    +
    +
    +
    + +Expand source code + +
    def test_st(self):
    +    model = StimFitModel(mode='non_selective', ukrin_vendor='ge')
    +    mapper = T2StimFit(self.image_ge, self.affine_ge, model,
    +                       multithread=False)
    +    stats = arraystats.ArrayStats(mapper.t2_map).calculate()
    +    npt.assert_allclose([stats["mean"]["3D"], stats["std"]["3D"],
    +                         stats["min"]["3D"], stats["max"]["3D"]],
    +                        [165.994692, 203.583211, 51.827107, 1497.168001],
    +                        rtol=1e-2, atol=0.25)
    +
    +
    +
    +def test_to_nifti(self) +
    +
    +
    +
    + +Expand source code + +
    def test_to_nifti(self):
    +    mask = self.image_ge[..., 0] > 3000
    +    model = StimFitModel(mode='non_selective', ukrin_vendor='ge')
    +    mapper = T2StimFit(self.image_ge, self.affine_ge, model, mask=mask)
    +
    +    if os.path.exists('test_output'):
    +        shutil.rmtree('test_output')
    +    os.makedirs('test_output', exist_ok=True)
    +
    +    # Check all is saved.
    +    mapper.to_nifti(output_directory='test_output',
    +                    base_file_name='t2stimfittest', maps='all')
    +    output_files = os.listdir('test_output')
    +    assert len(output_files) == 5
    +    assert 't2stimfittest_b1_map.nii.gz' in output_files
    +    assert 't2stimfittest_m0_map.nii.gz' in output_files
    +    assert 't2stimfittest_mask.nii.gz' in output_files
    +    assert 't2stimfittest_r2_map.nii.gz' in output_files
    +    assert 't2stimfittest_t2_map.nii.gz' in output_files
    +
    +    for f in os.listdir('test_output'):
    +        os.remove(os.path.join('test_output', f))
    +
    +    # Check that no files are saved.
    +    mapper.to_nifti(output_directory='test_output',
    +                    base_file_name='t2stimfittest', maps=[])
    +    output_files = os.listdir('test_output')
    +    assert len(output_files) == 0
    +
    +    # Check that only t2, mask and r2 are saved.
    +    mapper.to_nifti(output_directory='test_output',
    +                    base_file_name='t2stimfittest', maps=['mask', 't2',
    +                                                          'r2'])
    +    output_files = os.listdir('test_output')
    +    assert len(output_files) == 3
    +    assert 't2stimfittest_mask.nii.gz' in output_files
    +    assert 't2stimfittest_t2_map.nii.gz' in output_files
    +    assert 't2stimfittest_r2_map.nii.gz' in output_files
    +
    +    for f in os.listdir('test_output'):
    +        os.remove(os.path.join('test_output', f))
    +
    +    # Check that it fails when no maps are given
    +    with pytest.raises(ValueError):
    +        mapper.to_nifti(output_directory='test_output',
    +                        base_file_name='t2stimfittest', maps='')
    +
    +    # Delete 'test_output' folder
    +    shutil.rmtree('test_output')
    +
    +
    +
    +def test_vendor(self) +
    +
    +
    +
    + +Expand source code + +
    def test_vendor(self):
    +    # Philips
    +    model = StimFitModel(mode='selective', ukrin_vendor='philips')
    +    mapper = T2StimFit(self.image_philips, self.affine_philips, model)
    +    stats = arraystats.ArrayStats(mapper.t2_map).calculate()
    +    npt.assert_allclose([stats["mean"]["3D"], stats["std"]["3D"],
    +                         stats["min"]["3D"], stats["max"]["3D"]],
    +                        [281.52594, 596.832203, 36.470879, 3000.0],
    +                        rtol=1e-6, atol=1e-4)
    +
    +    # Siemens
    +    model = StimFitModel(mode='selective', ukrin_vendor='siemens')
    +    mapper = T2StimFit(self.image_siemens, self.affine_siemens, model)
    +    stats = arraystats.ArrayStats(mapper.t2_map).calculate()
    +    npt.assert_allclose([stats["mean"]["3D"], stats["std"]["3D"],
    +                         stats["min"]["3D"], stats["max"]["3D"]],
    +                        [120.47096, 190.454984, 26.621704, 2999.999651],
    +                        rtol=1e-5, atol=1e-2)
    +
    +
    +
    +
    +
    +
    +
    + +
    + + + \ No newline at end of file diff --git a/mapping/tests/test_t2star.html b/mapping/tests/test_t2star.html index aabd1a1e..239ab2f2 100644 --- a/mapping/tests/test_t2star.html +++ b/mapping/tests/test_t2star.html @@ -48,6 +48,7 @@

    Module ukat.mapping.tests.test_t2star

    1893.85093652, 1783.56164391, 1679.6950997, 1581.87727213, 1489.75591137, 1402.99928103]) affine = np.eye(4) + def test_two_param_eq(self): signal = two_param_eq(self.t, self.t2star, self.m0) npt.assert_allclose(signal, self.correct_signal, rtol=1e-6, atol=1e-8) @@ -64,6 +65,7 @@

    Module ukat.mapping.tests.test_t2star

    assert np.isnan(mapper.t2star_err.mean()) npt.assert_almost_equal(mapper.m0_map.mean(), self.m0) npt.assert_almost_equal(mapper.r2star_map().mean(), 1 / self.t2star) + npt.assert_almost_equal(mapper.r2.mean(), 1) # Single Threaded mapper = T2Star(signal_array, self.t, self.affine, method='loglin', @@ -73,6 +75,7 @@

    Module ukat.mapping.tests.test_t2star

    assert np.isnan(mapper.t2star_err.mean()) npt.assert_almost_equal(mapper.m0_map.mean(), self.m0) npt.assert_almost_equal(mapper.r2star_map().mean(), 1 / self.t2star) + npt.assert_almost_equal(mapper.r2.mean(), 1) # Auto Threaded mapper = T2Star(signal_array, self.t, self.affine, method='loglin', @@ -82,6 +85,7 @@

    Module ukat.mapping.tests.test_t2star

    assert np.isnan(mapper.t2star_err.mean()) npt.assert_almost_equal(mapper.m0_map.mean(), self.m0) npt.assert_almost_equal(mapper.r2star_map().mean(), 1 / self.t2star) + npt.assert_almost_equal(mapper.r2.mean(), 1) def test_2p_exp_fit(self): # Make the signal into a 4D array @@ -95,6 +99,7 @@

    Module ukat.mapping.tests.test_t2star

    npt.assert_almost_equal(mapper.t2star_err.mean(), 7.395706644238e-11) npt.assert_almost_equal(mapper.m0_map.mean(), self.m0) npt.assert_almost_equal(mapper.r2star_map().mean(), 1 / self.t2star) + npt.assert_almost_equal(mapper.r2.mean(), 1) # Single Threaded mapper = T2Star(signal_array, self.t, self.affine, method='2p_exp', @@ -104,6 +109,7 @@

    Module ukat.mapping.tests.test_t2star

    npt.assert_almost_equal(mapper.t2star_err.mean(), 7.395706644238e-11) npt.assert_almost_equal(mapper.m0_map.mean(), self.m0) npt.assert_almost_equal(mapper.r2star_map().mean(), 1 / self.t2star) + npt.assert_almost_equal(mapper.r2.mean(), 1) # Auto Threaded mapper = T2Star(signal_array, self.t, self.affine, method='2p_exp', @@ -113,14 +119,16 @@

    Module ukat.mapping.tests.test_t2star

    npt.assert_almost_equal(mapper.t2star_err.mean(), 7.395706644238e-11) npt.assert_almost_equal(mapper.m0_map.mean(), self.m0) npt.assert_almost_equal(mapper.r2star_map().mean(), 1 / self.t2star) + npt.assert_almost_equal(mapper.r2.mean(), 1) # Fail to fit mapper = T2Star(signal_array[..., ::-1], self.t, self.affine, method='2p_exp', multithread=True) assert mapper.shape == signal_array.shape[:-1] # Voxels that fail to fit are set to zero - npt.assert_almost_equal(mapper.t2star_map.mean(), 0.0) - npt.assert_almost_equal(mapper.t2star_err.mean(), 0.0) + npt.assert_almost_equal(mapper.t2star_map.mean(), 0) + npt.assert_almost_equal(mapper.t2star_err.mean(), 0) + npt.assert_almost_equal(mapper.r2.mean(), 0) def test_mask(self): signal_array = np.tile(self.correct_signal, (10, 10, 3, 1)) @@ -156,10 +164,11 @@

    Module ukat.mapping.tests.test_t2star

    mapper.to_nifti(output_directory='test_output', base_file_name='t2startest', maps='all') output_files = os.listdir('test_output') - assert len(output_files) == 6 + assert len(output_files) == 7 assert 't2startest_m0_err.nii.gz' in output_files assert 't2startest_m0_map.nii.gz' in output_files assert 't2startest_mask.nii.gz' in output_files + assert 't2startest_r2.nii.gz' in output_files assert 't2startest_r2star_map.nii.gz' in output_files assert 't2startest_t2star_err.nii.gz' in output_files assert 't2startest_t2star_map.nii.gz' in output_files @@ -198,9 +207,10 @@

    Module ukat.mapping.tests.test_t2star

    mapper.to_nifti(output_directory='test_output', base_file_name='t2startest', maps='all') output_files = os.listdir('test_output') - assert len(output_files) == 4 + assert len(output_files) == 5 assert 't2startest_m0_map.nii.gz' in output_files assert 't2startest_mask.nii.gz' in output_files + assert 't2startest_r2.nii.gz' in output_files assert 't2startest_r2star_map.nii.gz' in output_files assert 't2startest_t2star_map.nii.gz' in output_files @@ -343,6 +353,7 @@

    Classes

    1893.85093652, 1783.56164391, 1679.6950997, 1581.87727213, 1489.75591137, 1402.99928103]) affine = np.eye(4) + def test_two_param_eq(self): signal = two_param_eq(self.t, self.t2star, self.m0) npt.assert_allclose(signal, self.correct_signal, rtol=1e-6, atol=1e-8) @@ -359,6 +370,7 @@

    Classes

    assert np.isnan(mapper.t2star_err.mean()) npt.assert_almost_equal(mapper.m0_map.mean(), self.m0) npt.assert_almost_equal(mapper.r2star_map().mean(), 1 / self.t2star) + npt.assert_almost_equal(mapper.r2.mean(), 1) # Single Threaded mapper = T2Star(signal_array, self.t, self.affine, method='loglin', @@ -368,6 +380,7 @@

    Classes

    assert np.isnan(mapper.t2star_err.mean()) npt.assert_almost_equal(mapper.m0_map.mean(), self.m0) npt.assert_almost_equal(mapper.r2star_map().mean(), 1 / self.t2star) + npt.assert_almost_equal(mapper.r2.mean(), 1) # Auto Threaded mapper = T2Star(signal_array, self.t, self.affine, method='loglin', @@ -377,6 +390,7 @@

    Classes

    assert np.isnan(mapper.t2star_err.mean()) npt.assert_almost_equal(mapper.m0_map.mean(), self.m0) npt.assert_almost_equal(mapper.r2star_map().mean(), 1 / self.t2star) + npt.assert_almost_equal(mapper.r2.mean(), 1) def test_2p_exp_fit(self): # Make the signal into a 4D array @@ -390,6 +404,7 @@

    Classes

    npt.assert_almost_equal(mapper.t2star_err.mean(), 7.395706644238e-11) npt.assert_almost_equal(mapper.m0_map.mean(), self.m0) npt.assert_almost_equal(mapper.r2star_map().mean(), 1 / self.t2star) + npt.assert_almost_equal(mapper.r2.mean(), 1) # Single Threaded mapper = T2Star(signal_array, self.t, self.affine, method='2p_exp', @@ -399,6 +414,7 @@

    Classes

    npt.assert_almost_equal(mapper.t2star_err.mean(), 7.395706644238e-11) npt.assert_almost_equal(mapper.m0_map.mean(), self.m0) npt.assert_almost_equal(mapper.r2star_map().mean(), 1 / self.t2star) + npt.assert_almost_equal(mapper.r2.mean(), 1) # Auto Threaded mapper = T2Star(signal_array, self.t, self.affine, method='2p_exp', @@ -408,14 +424,16 @@

    Classes

    npt.assert_almost_equal(mapper.t2star_err.mean(), 7.395706644238e-11) npt.assert_almost_equal(mapper.m0_map.mean(), self.m0) npt.assert_almost_equal(mapper.r2star_map().mean(), 1 / self.t2star) + npt.assert_almost_equal(mapper.r2.mean(), 1) # Fail to fit mapper = T2Star(signal_array[..., ::-1], self.t, self.affine, method='2p_exp', multithread=True) assert mapper.shape == signal_array.shape[:-1] # Voxels that fail to fit are set to zero - npt.assert_almost_equal(mapper.t2star_map.mean(), 0.0) - npt.assert_almost_equal(mapper.t2star_err.mean(), 0.0) + npt.assert_almost_equal(mapper.t2star_map.mean(), 0) + npt.assert_almost_equal(mapper.t2star_err.mean(), 0) + npt.assert_almost_equal(mapper.r2.mean(), 0) def test_mask(self): signal_array = np.tile(self.correct_signal, (10, 10, 3, 1)) @@ -451,10 +469,11 @@

    Classes

    mapper.to_nifti(output_directory='test_output', base_file_name='t2startest', maps='all') output_files = os.listdir('test_output') - assert len(output_files) == 6 + assert len(output_files) == 7 assert 't2startest_m0_err.nii.gz' in output_files assert 't2startest_m0_map.nii.gz' in output_files assert 't2startest_mask.nii.gz' in output_files + assert 't2startest_r2.nii.gz' in output_files assert 't2startest_r2star_map.nii.gz' in output_files assert 't2startest_t2star_err.nii.gz' in output_files assert 't2startest_t2star_map.nii.gz' in output_files @@ -493,9 +512,10 @@

    Classes

    mapper.to_nifti(output_directory='test_output', base_file_name='t2startest', maps='all') output_files = os.listdir('test_output') - assert len(output_files) == 4 + assert len(output_files) == 5 assert 't2startest_m0_map.nii.gz' in output_files assert 't2startest_mask.nii.gz' in output_files + assert 't2startest_r2.nii.gz' in output_files assert 't2startest_r2star_map.nii.gz' in output_files assert 't2startest_t2star_map.nii.gz' in output_files @@ -648,6 +668,7 @@

    Methods

    npt.assert_almost_equal(mapper.t2star_err.mean(), 7.395706644238e-11) npt.assert_almost_equal(mapper.m0_map.mean(), self.m0) npt.assert_almost_equal(mapper.r2star_map().mean(), 1 / self.t2star) + npt.assert_almost_equal(mapper.r2.mean(), 1) # Single Threaded mapper = T2Star(signal_array, self.t, self.affine, method='2p_exp', @@ -657,6 +678,7 @@

    Methods

    npt.assert_almost_equal(mapper.t2star_err.mean(), 7.395706644238e-11) npt.assert_almost_equal(mapper.m0_map.mean(), self.m0) npt.assert_almost_equal(mapper.r2star_map().mean(), 1 / self.t2star) + npt.assert_almost_equal(mapper.r2.mean(), 1) # Auto Threaded mapper = T2Star(signal_array, self.t, self.affine, method='2p_exp', @@ -666,14 +688,16 @@

    Methods

    npt.assert_almost_equal(mapper.t2star_err.mean(), 7.395706644238e-11) npt.assert_almost_equal(mapper.m0_map.mean(), self.m0) npt.assert_almost_equal(mapper.r2star_map().mean(), 1 / self.t2star) + npt.assert_almost_equal(mapper.r2.mean(), 1) # Fail to fit mapper = T2Star(signal_array[..., ::-1], self.t, self.affine, method='2p_exp', multithread=True) assert mapper.shape == signal_array.shape[:-1] # Voxels that fail to fit are set to zero - npt.assert_almost_equal(mapper.t2star_map.mean(), 0.0) - npt.assert_almost_equal(mapper.t2star_err.mean(), 0.0)
    + npt.assert_almost_equal(mapper.t2star_map.mean(), 0) + npt.assert_almost_equal(mapper.t2star_err.mean(), 0) + npt.assert_almost_equal(mapper.r2.mean(), 0)
    @@ -697,6 +721,7 @@

    Methods

    assert np.isnan(mapper.t2star_err.mean()) npt.assert_almost_equal(mapper.m0_map.mean(), self.m0) npt.assert_almost_equal(mapper.r2star_map().mean(), 1 / self.t2star) + npt.assert_almost_equal(mapper.r2.mean(), 1) # Single Threaded mapper = T2Star(signal_array, self.t, self.affine, method='loglin', @@ -706,6 +731,7 @@

    Methods

    assert np.isnan(mapper.t2star_err.mean()) npt.assert_almost_equal(mapper.m0_map.mean(), self.m0) npt.assert_almost_equal(mapper.r2star_map().mean(), 1 / self.t2star) + npt.assert_almost_equal(mapper.r2.mean(), 1) # Auto Threaded mapper = T2Star(signal_array, self.t, self.affine, method='loglin', @@ -714,7 +740,8 @@

    Methods

    npt.assert_almost_equal(mapper.t2star_map.mean(), self.t2star) assert np.isnan(mapper.t2star_err.mean()) npt.assert_almost_equal(mapper.m0_map.mean(), self.m0) - npt.assert_almost_equal(mapper.r2star_map().mean(), 1 / self.t2star)
    + npt.assert_almost_equal(mapper.r2star_map().mean(), 1 / self.t2star) + npt.assert_almost_equal(mapper.r2.mean(), 1)
    @@ -892,10 +919,11 @@

    Methods

    mapper.to_nifti(output_directory='test_output', base_file_name='t2startest', maps='all') output_files = os.listdir('test_output') - assert len(output_files) == 6 + assert len(output_files) == 7 assert 't2startest_m0_err.nii.gz' in output_files assert 't2startest_m0_map.nii.gz' in output_files assert 't2startest_mask.nii.gz' in output_files + assert 't2startest_r2.nii.gz' in output_files assert 't2startest_r2star_map.nii.gz' in output_files assert 't2startest_t2star_err.nii.gz' in output_files assert 't2startest_t2star_map.nii.gz' in output_files @@ -934,9 +962,10 @@

    Methods

    mapper.to_nifti(output_directory='test_output', base_file_name='t2startest', maps='all') output_files = os.listdir('test_output') - assert len(output_files) == 4 + assert len(output_files) == 5 assert 't2startest_m0_map.nii.gz' in output_files assert 't2startest_mask.nii.gz' in output_files + assert 't2startest_r2.nii.gz' in output_files assert 't2startest_r2star_map.nii.gz' in output_files assert 't2startest_t2star_map.nii.gz' in output_files diff --git a/qa/tests/test_snr.html b/qa/tests/test_snr.html index a54aec8d..b813df19 100644 --- a/qa/tests/test_snr.html +++ b/qa/tests/test_snr.html @@ -51,7 +51,7 @@

    Module ukat.qa.tests.test_snr

    npt.assert_allclose([noise_mask_stats['mean']['3D'], noise_mask_stats['std']['3D'], noise_mask_stats['min']['3D'], - noise_mask_stats[ 'max']['3D']], + noise_mask_stats['max']['3D']], gold_standard_noise_mask, rtol=1e-6, atol=1e-4) npt.assert_allclose(isnr_obj.isnr, 45.968827) npt.assert_allclose([isnr_map_stats['mean']['3D'], @@ -299,7 +299,7 @@

    Classes

    npt.assert_allclose([noise_mask_stats['mean']['3D'], noise_mask_stats['std']['3D'], noise_mask_stats['min']['3D'], - noise_mask_stats[ 'max']['3D']], + noise_mask_stats['max']['3D']], gold_standard_noise_mask, rtol=1e-6, atol=1e-4) npt.assert_allclose(isnr_obj.isnr, 45.968827) npt.assert_allclose([isnr_map_stats['mean']['3D'], @@ -450,7 +450,7 @@

    Methods

    npt.assert_allclose([noise_mask_stats['mean']['3D'], noise_mask_stats['std']['3D'], noise_mask_stats['min']['3D'], - noise_mask_stats[ 'max']['3D']], + noise_mask_stats['max']['3D']], gold_standard_noise_mask, rtol=1e-6, atol=1e-4) npt.assert_allclose(isnr_obj.isnr, 45.968827) npt.assert_allclose([isnr_map_stats['mean']['3D'], diff --git a/segmentation/whole_kidney.html b/segmentation/whole_kidney.html index 29b93da8..389ff5c0 100644 --- a/segmentation/whole_kidney.html +++ b/segmentation/whole_kidney.html @@ -507,7 +507,7 @@

    Ancestors

    Class variables

    -
    var header_class : Type[Nifti1Header]
    +
    var header_class : type[Nifti1Header]

    Class for NIfTI1 header

    The NIfTI1 header has many more coded fields than the simpler Analyze @@ -521,6 +521,21 @@

    Class variables

    This class handles the header-preceding-data case.

    +

    Instance variables

    +
    +
    var header : nibabel.filebasedimages.FileBasedHeader
    +
    +
    +
    + +Expand source code + +
    @property
    +def header(self) -> FileBasedHeader:
    +    return self._header
    +
    +
    +

    Methods

    @@ -866,6 +881,7 @@

    get_rkv
  • get_tkv
  • get_volumes
  • +
  • header
  • header_class
  • save_volumes_csv
  • to_nifti
  • diff --git a/utils/arraystats.html b/utils/arraystats.html index 53783238..57e829d7 100644 --- a/utils/arraystats.html +++ b/utils/arraystats.html @@ -46,6 +46,7 @@

    Module ukat.utils.arraystats

    NOT_CALCULATED_MSG = 'Not calculated. See ArrayStats.calculate().' + class ArrayStats(): """Class to calculate array statistics (optionally within a mask) @@ -271,8 +272,8 @@

    Module ukat.utils.arraystats

    elif self.image_ndims == 3: n = { '2D': n2.transpose()[0], - '3D': n4, # n4 because {statistic}4 always returns the result - } # over the entire array, which here is 3D + '3D': n4, # n4 because {statistic}4 always returns the result + } # over the entire array, which here is 3D mean = { '2D': mean2.transpose()[0], '3D': mean4, @@ -711,8 +712,8 @@

    Attributes

    elif self.image_ndims == 3: n = { '2D': n2.transpose()[0], - '3D': n4, # n4 because {statistic}4 always returns the result - } # over the entire array, which here is 3D + '3D': n4, # n4 because {statistic}4 always returns the result + } # over the entire array, which here is 3D mean = { '2D': mean2.transpose()[0], '3D': mean4, @@ -992,8 +993,8 @@

    Returns

    elif self.image_ndims == 3: n = { '2D': n2.transpose()[0], - '3D': n4, # n4 because {statistic}4 always returns the result - } # over the entire array, which here is 3D + '3D': n4, # n4 because {statistic}4 always returns the result + } # over the entire array, which here is 3D mean = { '2D': mean2.transpose()[0], '3D': mean4, diff --git a/utils/tests/test_ge.html b/utils/tests/test_ge.html index 108da90d..b6c4867b 100644 --- a/utils/tests/test_ge.html +++ b/utils/tests/test_ge.html @@ -27,7 +27,6 @@

    Module ukat.utils.tests.test_ge

    Expand source code
    import numpy as np
    -import pytest
     from ukat.utils.ge import scale_b1
     
     
    diff --git a/utils/tests/test_tools.html b/utils/tests/test_tools.html
    index 98a417e7..7469aa06 100644
    --- a/utils/tests/test_tools.html
    +++ b/utils/tests/test_tools.html
    @@ -34,7 +34,6 @@ 

    Module ukat.utils.tests.test_tools

    class TestConvertToPiRange: - # Gold Standard = [mean, std, minimum, maximum] # Input: {np.arange(12).reshape((2, 2, 3)) - 6 * np.ones((2, 2, 3))} gold_standard = [-7.401486830834377e-17, 1.9718077939258474, @@ -50,8 +49,8 @@

    Module ukat.utils.tests.test_tools

    pi_range_calculated = tools.convert_to_pi_range(self.array) stats = arraystats.ArrayStats(pi_range_calculated).calculate() npt.assert_allclose([stats["mean"]["3D"], stats["std"]["3D"], - stats["min"]["3D"], stats["max"]["3D"]], - self.gold_standard, rtol=1e-6, atol=1e-4) + stats["min"]["3D"], stats["max"]["3D"]], + self.gold_standard, rtol=1e-6, atol=1e-4) def test_if_ranges(self): # Test for values > 3.2 @@ -82,7 +81,6 @@

    Module ukat.utils.tests.test_tools

    class TestResizeArray: - # Create arrays for testing array_2d = np.arange(100).reshape((10, 10)) array_3d = np.arange(500).reshape((10, 10, 5)) @@ -124,7 +122,6 @@

    Module ukat.utils.tests.test_tools

    class TestMaskSlices: - shape = (2, 2, 3) # Create mask where all pixels from all slices are True full_mask = np.full(shape, True) @@ -215,7 +212,6 @@

    Classes

    Expand source code
    class TestConvertToPiRange:
    -
         # Gold Standard = [mean, std, minimum, maximum]
         # Input: {np.arange(12).reshape((2, 2, 3)) - 6 * np.ones((2, 2, 3))}
         gold_standard = [-7.401486830834377e-17, 1.9718077939258474,
    @@ -231,8 +227,8 @@ 

    Classes

    pi_range_calculated = tools.convert_to_pi_range(self.array) stats = arraystats.ArrayStats(pi_range_calculated).calculate() npt.assert_allclose([stats["mean"]["3D"], stats["std"]["3D"], - stats["min"]["3D"], stats["max"]["3D"]], - self.gold_standard, rtol=1e-6, atol=1e-4) + stats["min"]["3D"], stats["max"]["3D"]], + self.gold_standard, rtol=1e-6, atol=1e-4) def test_if_ranges(self): # Test for values > 3.2 @@ -356,8 +352,8 @@

    Methods

    pi_range_calculated = tools.convert_to_pi_range(self.array) stats = arraystats.ArrayStats(pi_range_calculated).calculate() npt.assert_allclose([stats["mean"]["3D"], stats["std"]["3D"], - stats["min"]["3D"], stats["max"]["3D"]], - self.gold_standard, rtol=1e-6, atol=1e-4)
    + stats["min"]["3D"], stats["max"]["3D"]], + self.gold_standard, rtol=1e-6, atol=1e-4)
    @@ -372,7 +368,6 @@

    Methods

    Expand source code
    class TestMaskSlices:
    -
         shape = (2, 2, 3)
         # Create mask where all pixels from all slices are True
         full_mask = np.full(shape, True)
    @@ -625,7 +620,6 @@ 

    Methods

    Expand source code
    class TestResizeArray:
    -
         # Create arrays for testing
         array_2d = np.arange(100).reshape((10, 10))
         array_3d = np.arange(500).reshape((10, 10, 5))
    diff --git a/utils/tools.html b/utils/tools.html
    index aecb7817..9d8a2431 100644
    --- a/utils/tools.html
    +++ b/utils/tools.html
    @@ -145,7 +145,7 @@ 

    Module ukat.utils.tools

    s_min = min(slices) s_max = max(slices) - if not(s_min >= 0 and s_max+1 <= shape[2]): + if not(s_min >= 0 and s_max + 1 <= shape[2]): msg = f"The elements of `slices` must be > 0 and <= {shape[2]-1}" raise ValueError(msg) @@ -292,7 +292,7 @@

    Returns

    s_min = min(slices) s_max = max(slices) - if not(s_min >= 0 and s_max+1 <= shape[2]): + if not(s_min >= 0 and s_max + 1 <= shape[2]): msg = f"The elements of `slices` must be > 0 and <= {shape[2]-1}" raise ValueError(msg)