Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Identify the last good echo in adaptive mask instead of sum of good echoes #1061

Merged
merged 47 commits into from
Apr 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
89f4bb6
Limit adaptive mask calculation to brain mask.
tsalo Mar 10, 2024
28bc80f
Use `compute_epi_mask` in t2smap workflow.
tsalo Mar 15, 2024
c5d7d91
Try fixing the tests.
tsalo Mar 15, 2024
3fa7593
Fix make_adaptive_mask.
tsalo Mar 15, 2024
4cc86c6
Update test_utils.py
tsalo Mar 15, 2024
5b379e1
Update test_utils.py
tsalo Mar 15, 2024
ccff6dc
Improve docstring.
tsalo Mar 15, 2024
a2dc300
Identify the last good echo instead of sum.
tsalo Mar 10, 2024
beeae89
Fix.
tsalo Mar 16, 2024
7097a75
Update utils.py
tsalo Mar 16, 2024
ea5c364
Update utils.py
tsalo Mar 16, 2024
32d1cb7
Try fixing.
tsalo Mar 16, 2024
bb0dbdc
Update utils.py
tsalo Mar 16, 2024
d096d08
Update utils.py
tsalo Mar 16, 2024
1f72638
add checks
tsalo Mar 16, 2024
18b66ac
Just loop over voxels.
tsalo Mar 16, 2024
aea9fe2
Update utils.py
tsalo Mar 16, 2024
28267f7
Update utils.py
tsalo Mar 16, 2024
259b002
Update test_utils.py
tsalo Mar 16, 2024
55a2694
Revert "Update test_utils.py"
tsalo Mar 16, 2024
d34c65a
Update test_utils.py
tsalo Mar 16, 2024
b3bfbbd
Update test_utils.py
tsalo Mar 16, 2024
1014000
Remove checks.
tsalo Mar 16, 2024
b80524b
Don't take absolute value of echo means.
tsalo Mar 18, 2024
2bfa240
Log echo-wise thresholds in adaptive mask.
tsalo Mar 18, 2024
7888c4e
Add comment about non-zero voxels.
tsalo Mar 18, 2024
20de578
Update utils.py
tsalo Mar 30, 2024
def2770
Update utils.py
tsalo Mar 30, 2024
15b80f7
Merge remote-tracking branch 'upstream/main' into fix-old-adaptive-mask
tsalo Apr 8, 2024
e44878b
Update test_utils.py
tsalo Apr 8, 2024
f762573
Update test_utils.py
tsalo Apr 8, 2024
d91c016
Update test_utils.py
tsalo Apr 8, 2024
097d3a7
Log the thresholds again.
tsalo Apr 8, 2024
3a3f115
Merge branch 'fix-old-adaptive-mask' into fix-old-adaptive-mask-2
tsalo Apr 11, 2024
a99cca2
Update test_utils.py
tsalo Apr 11, 2024
b0192c4
Update test_utils.py
tsalo Apr 11, 2024
508d9c2
Update test_utils.py
tsalo Apr 11, 2024
189cb8e
Merge remote-tracking branch 'upstream/main' into fix-old-adaptive-ma…
tsalo Apr 12, 2024
30f5e41
Merge remote-tracking branch 'upstream/main' into fix-old-adaptive-ma…
tsalo Apr 16, 2024
e76c1d1
Merge remote-tracking branch 'upstream/main' into fix-old-adaptive-ma…
tsalo Apr 16, 2024
fcb6104
Add simulated data to adaptive mask test.
tsalo Apr 17, 2024
2ccc8aa
Clean up the tests.
tsalo Apr 17, 2024
60482db
Merge remote-tracking branch 'upstream/main' into fix-old-adaptive-ma…
tsalo Apr 17, 2024
2d34d98
Add value that tests the base mask.
tsalo Apr 17, 2024
682f8a6
Remove print in test.
tsalo Apr 17, 2024
ef85ca1
Update tedana/utils.py
tsalo Apr 18, 2024
2d3712a
Update tedana/utils.py
tsalo Apr 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 84 additions & 27 deletions tedana/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,73 +80,130 @@ def test_make_adaptive_mask():
mask_file = pjoin(datadir, "mask.nii.gz")
data = io.load_data(fnames, n_echos=len(tes))[0]

# Add in simulated values
base_val = np.mean(data[:, 0, :]) # mean value of first echo
idx = 5457
# Three good echoes (3)
data[idx, :, :] = np.array([base_val, base_val - 1, base_val - 2])[:, None]
# Dropout: good bad good (3)
# Decay: good good bad (2)
data[idx + 1, :, :] = np.array([base_val, 1, base_val])[:, None]
# Dropout: good bad bad (1)
# Decay: good good bad (2)
data[idx + 2, :, :] = np.array([base_val, 1, 1])[:, None]
# Dropout: good good good (3)
# Decay: good bad bad (1)
data[idx + 3, :, :] = np.array([base_val, base_val, base_val])[:, None]
# Dropout: bad bad bad (0)
# Decay: good good good (3)
data[idx + 4, :, :] = np.array([1, 0.9, 0.8])[:, None]
# Base: good good bad (2)
# Dropout: bad bad bad (0)
# Decay: good good good (3)
data[idx + 5, :, :] = np.array([1, 0.9, -1])[:, None]

# Just dropout method
mask, masksum = utils.make_adaptive_mask(
mask, adaptive_mask = utils.make_adaptive_mask(
data,
mask=mask_file,
threshold=1,
methods=["dropout"],
)

assert mask.shape == masksum.shape == (64350,)
assert np.allclose(mask, (masksum >= 1).astype(bool))
assert mask.sum() == 49376
vals, counts = np.unique(masksum, return_counts=True)
assert mask.shape == adaptive_mask.shape == (64350,)
assert np.allclose(mask, (adaptive_mask >= 1).astype(bool))
assert adaptive_mask[idx] == 3
assert adaptive_mask[idx + 1] == 3
assert adaptive_mask[idx + 2] == 1
assert adaptive_mask[idx + 3] == 3
assert adaptive_mask[idx + 4] == 0
assert adaptive_mask[idx + 5] == 0
assert mask.sum() == 49374
vals, counts = np.unique(adaptive_mask, return_counts=True)
assert np.allclose(vals, np.array([0, 1, 2, 3]))
assert np.allclose(counts, np.array([14974, 3682, 5128, 40566]))
assert np.allclose(counts, np.array([14976, 1817, 4427, 43130]))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was trying to get a better handle with what was happening within this PR. In make_adaptive_mask I added older_mask =(np.abs(echo_means) > lthrs).sum(axis=-1) right after dropout_adaptive_mask was calculated and compared results in my debugger. Calls like ((older_mask==2) * (dropout_adaptive_mask==2)).sum() show voxels that used to be 1 in the adaptive mask and are now 2.
Voxels that were 0 are still 0. (This interacts with other masking steps, but seems matched at this point in the code)
Of the 3590 voxels that were 1, 1061 are now 2, and 913 are now 3.
Of the voxels 5276 voxels that were 2, 1900 are now 3.
As expected, none of the voxels that had a higher values are now lower.

That means this change will substantively expand the number of voxels used in ICA and will balance out some of the drop caused by raising the threshold from masking. The one thing that concerns me is it turns how this test data had 913 voxels where the first and second echos were both below threshold, but the third was above threshold. That doesn't seem great, but the proposed enhancement would give this voxels a 0 in the adaptive mask, which would mean they'd be even be excluded from the optimally combined data. There's nothing to change here, but this is a discussion that will be relevant if a future enhancement is considered.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've opened #1083 about this. Can you follow up in that issue with your ratio idea?


# Just decay method
mask, masksum = utils.make_adaptive_mask(
mask, adaptive_mask = utils.make_adaptive_mask(
data,
mask=mask_file,
threshold=1,
methods=["decay"],
)

assert mask.shape == masksum.shape == (64350,)
assert np.allclose(mask, (masksum >= 1).astype(bool))
assert mask.shape == adaptive_mask.shape == (64350,)
assert np.allclose(mask, (adaptive_mask >= 1).astype(bool))
assert adaptive_mask[idx] == 3
assert adaptive_mask[idx + 1] == 2
assert adaptive_mask[idx + 2] == 2
assert adaptive_mask[idx + 3] == 1
assert adaptive_mask[idx + 4] == 3
assert adaptive_mask[idx + 5] == 2
assert mask.sum() == 60985 # This method can't flag first echo as bad
vals, counts = np.unique(masksum, return_counts=True)
vals, counts = np.unique(adaptive_mask, return_counts=True)
assert np.allclose(vals, np.array([0, 1, 2, 3]))
assert np.allclose(counts, np.array([3365, 4365, 5971, 50649]))
assert np.allclose(counts, np.array([3365, 4366, 5973, 50646]))

# Dropout and decay methods combined
mask, masksum = utils.make_adaptive_mask(
mask, adaptive_mask = utils.make_adaptive_mask(
data,
mask=mask_file,
threshold=1,
methods=["dropout", "decay"],
)

assert mask.shape == masksum.shape == (64350,)
assert np.allclose(mask, (masksum >= 1).astype(bool))
assert mask.sum() == 49376
vals, counts = np.unique(masksum, return_counts=True)
assert mask.shape == adaptive_mask.shape == (64350,)
assert np.allclose(mask, (adaptive_mask >= 1).astype(bool))
assert adaptive_mask[idx] == 3
assert adaptive_mask[idx + 1] == 2
assert adaptive_mask[idx + 2] == 1
assert adaptive_mask[idx + 3] == 1
assert adaptive_mask[idx + 4] == 0
assert adaptive_mask[idx + 5] == 0
assert mask.sum() == 49374
vals, counts = np.unique(adaptive_mask, return_counts=True)
assert np.allclose(vals, np.array([0, 1, 2, 3]))
assert np.allclose(counts, np.array([14974, 4386, 5604, 39386]))
assert np.allclose(counts, np.array([14976, 3111, 6248, 40015]))

# Adding "none" should have no effect
mask, masksum = utils.make_adaptive_mask(
mask, adaptive_mask = utils.make_adaptive_mask(
data,
mask=mask_file,
threshold=1,
methods=["dropout", "decay", "none"],
)

assert mask.shape == masksum.shape == (64350,)
assert np.allclose(mask, (masksum >= 1).astype(bool))
assert mask.sum() == 49376
vals, counts = np.unique(masksum, return_counts=True)
assert mask.shape == adaptive_mask.shape == (64350,)
assert np.allclose(mask, (adaptive_mask >= 1).astype(bool))
assert adaptive_mask[idx] == 3
assert adaptive_mask[idx + 1] == 2
assert adaptive_mask[idx + 2] == 1
assert adaptive_mask[idx + 3] == 1
assert adaptive_mask[idx + 4] == 0
assert adaptive_mask[idx + 5] == 0
assert mask.sum() == 49374
vals, counts = np.unique(adaptive_mask, return_counts=True)
assert np.allclose(vals, np.array([0, 1, 2, 3]))
assert np.allclose(counts, np.array([14974, 4386, 5604, 39386]))
assert np.allclose(counts, np.array([14976, 3111, 6248, 40015]))

# Just "none"
mask, masksum = utils.make_adaptive_mask(data, mask=mask_file, threshold=1, methods=["none"])
mask, adaptive_mask = utils.make_adaptive_mask(
data,
mask=mask_file,
threshold=1,
methods=["none"],
)

assert mask.shape == masksum.shape == (64350,)
assert np.allclose(mask, (masksum >= 1).astype(bool))
assert mask.shape == adaptive_mask.shape == (64350,)
assert np.allclose(mask, (adaptive_mask >= 1).astype(bool))
assert adaptive_mask[idx] == 3
assert adaptive_mask[idx + 1] == 3
assert adaptive_mask[idx + 2] == 3
assert adaptive_mask[idx + 3] == 3
assert adaptive_mask[idx + 4] == 3
assert adaptive_mask[idx + 5] == 2
assert mask.sum() == 60985
vals, counts = np.unique(masksum, return_counts=True)
vals, counts = np.unique(adaptive_mask, return_counts=True)
assert np.allclose(vals, np.array([0, 1, 2, 3]))
assert np.allclose(counts, np.array([3365, 1412, 1195, 58378]))

Expand Down
25 changes: 19 additions & 6 deletions tedana/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ def make_adaptive_mask(data, mask, threshold=1, methods=["dropout"]):
Without a mask limiting the voxels to consider,
the adaptive mask will generally select voxels outside the brain as exemplars.
threshold : :obj:`int`, optional
Minimum echo count to retain in the mask. Default is 1, which is
equivalent not thresholding.
Minimum echo count to retain in the mask.
Default is 1, which is equivalent to not thresholding.
methods : :obj:`list`, optional
List of methods to use for adaptive mask generation. Default is ["dropout"].
Valid methods are "decay", "dropout", and "none".
Expand Down Expand Up @@ -103,9 +103,14 @@ def make_adaptive_mask(data, mask, threshold=1, methods=["dropout"]):
- This is the threshold for "good" data.
- The 1/3 value is arbitrary.
- If there was more than one exemplar voxel, retain the the highest value for each echo.
d. For each voxel, count the number of echoes that have a mean value greater than the
d. For each voxel, identify the last echo with a mean value greater than the
corresponding echo's threshold.

- Preceding echoes (including ones with mean values less than the threshold)
are considered "good" data. That means, if echoes 1-3 in a voxel are
[good, good, bad] the adaptive mask will assign a 2, and if they are
[good, bad, good], the adaptive mask will assign a 3.

Decay

Determine the echo at which the signal stops decreasing for each voxel.
Expand Down Expand Up @@ -187,9 +192,17 @@ def make_adaptive_mask(data, mask, threshold=1, methods=["dropout"]):

LGR.info("Echo-wise intensity thresholds for adaptive mask: %s", lthrs)

# determine samples where absolute value is greater than echo-specific thresholds
# and count # of echos that pass criterion
dropout_adaptive_mask = (np.abs(echo_means) > lthrs).sum(axis=-1)
# Find the last good echo for each voxel
# Start with every voxel's value==0, increment up the echoes, and
# change to a new value every time a later good echo is found
dropout_adaptive_mask = np.zeros(n_samples, dtype=np.int16)
for echo_idx in range(n_echos):
dropout_adaptive_mask[(np.abs(echo_means[:, echo_idx]) > lthrs[echo_idx])] = (
echo_idx + 1
)

adaptive_masks.append(dropout_adaptive_mask)

adaptive_masks.append(dropout_adaptive_mask)

if "decay" in methods:
Expand Down