Skip to content

Commit ab07715

Browse files
authored
Merge pull request #1 from handwerkerd/EPTI
adding tests. simplifying getfbounds
2 parents 7a5df43 + 53c46d0 commit ab07715

9 files changed

+129
-51
lines changed

tedana/selection/selection_nodes.py

+14-8
Original file line numberDiff line numberDiff line change
@@ -714,10 +714,14 @@ def calc_kappa_elbow(
714714
This also means the kappa elbow should be calculated before those two other functions
715715
are called
716716
"""
717-
if "echo_dof" in selector.cross_component_metrics_.keys():
717+
if (
718+
"echo_dof" in selector.cross_component_metrics_.keys()
719+
and selector.cross_component_metrics_["echo_dof"]
720+
):
718721
echo_dof = selector.cross_component_metrics_["echo_dof"]
719722
else:
720-
echo_dof = None
723+
# DOF is number of echoes if not otherwise specified
724+
echo_dof = selector.cross_component_metrics_["n_echos"]
721725
outputs = {
722726
"decision_node_idx": selector.current_node_idx_,
723727
"node_label": None,
@@ -782,8 +786,7 @@ def calc_kappa_elbow(
782786
outputs["varex_upper_p"],
783787
) = kappa_elbow_kundu(
784788
selector.component_table_,
785-
selector.cross_component_metrics_["n_echos"],
786-
echo_dof=echo_dof,
789+
echo_dof,
787790
comps2use=comps2use,
788791
)
789792
selector.cross_component_metrics_["kappa_elbow_kundu"] = outputs["kappa_elbow_kundu"]
@@ -852,10 +855,14 @@ def calc_rho_elbow(
852855
f"It is {rho_elbow_type} "
853856
)
854857

855-
if "echo_dof" in selector.cross_component_metrics_.keys():
858+
if (
859+
"echo_dof" in selector.cross_component_metrics_.keys()
860+
and selector.cross_component_metrics_["echo_dof"]
861+
):
856862
echo_dof = selector.cross_component_metrics_["echo_dof"]
857863
else:
858-
echo_dof = None
864+
# DOF is number of echoes if not otherwise specified
865+
echo_dof = selector.cross_component_metrics_["n_echos"]
859866

860867
outputs = {
861868
"decision_node_idx": selector.current_node_idx_,
@@ -916,8 +923,7 @@ def calc_rho_elbow(
916923
outputs["elbow_f05"],
917924
) = rho_elbow_kundu_liberal(
918925
selector.component_table_,
919-
selector.cross_component_metrics_["n_echos"],
920-
echo_dof=echo_dof,
926+
echo_dof,
921927
rho_elbow_type=rho_elbow_type,
922928
comps2use=comps2use,
923929
subset_comps2use=subset_comps2use,

tedana/selection/selection_utils.py

+10-21
Original file line numberDiff line numberDiff line change
@@ -580,7 +580,7 @@ def getelbow(arr, return_val=False):
580580
return k_min_ind
581581

582582

583-
def kappa_elbow_kundu(component_table, n_echos, echo_dof=None, comps2use=None):
583+
def kappa_elbow_kundu(component_table, echo_dof, comps2use=None):
584584
"""
585585
Calculate an elbow for kappa.
586586
@@ -592,12 +592,10 @@ def kappa_elbow_kundu(component_table, n_echos, echo_dof=None, comps2use=None):
592592
Component metric table. One row for each component, with a column for
593593
each metric. The index should be the component number.
594594
Only the 'kappa' column is used in this function
595-
n_echos : :obj:`int`
596-
The number of echos in the multi-echo data
597-
echo_dof : :obj:`int`, optional
595+
echo_dof : :obj:`int`
598596
Degree of freedom to use in goodness of fit metrics (fstat).
599-
Primarily used for EPTI acquisitions.
600-
If None, number of echoes will be used. Default is None.
597+
Typically the number of echos in the multi-echo data
598+
May be a lower value for EPTI acquisitions.
601599
comps2use : :obj:`list[int]`
602600
A list of component indices used to calculate the elbow
603601
default=None which means use all components
@@ -637,10 +635,7 @@ def kappa_elbow_kundu(component_table, n_echos, echo_dof=None, comps2use=None):
637635
kappas2use = component_table.loc[comps2use, "kappa"].to_numpy()
638636

639637
# low kappa threshold
640-
if echo_dof is None:
641-
_, _, f01 = getfbounds(n_echos)
642-
else:
643-
_, _, f01 = getfbounds(echo_dof)
638+
_, _, f01 = getfbounds(echo_dof)
644639
# get kappa values for components below a significance threshold
645640
kappas_nonsig = kappas2use[kappas2use < f01]
646641

@@ -678,8 +673,7 @@ def kappa_elbow_kundu(component_table, n_echos, echo_dof=None, comps2use=None):
678673

679674
def rho_elbow_kundu_liberal(
680675
component_table,
681-
n_echos,
682-
echo_dof=None,
676+
echo_dof,
683677
rho_elbow_type="kundu",
684678
comps2use=None,
685679
subset_comps2use=-1,
@@ -696,12 +690,10 @@ def rho_elbow_kundu_liberal(
696690
Component metric table. One row for each component, with a column for
697691
each metric. The index should be the component number.
698692
Only the 'kappa' column is used in this function
699-
n_echos : :obj:`int`
700-
The number of echos in the multi-echo data
701-
echo_dof : :obj:`int`, optional
693+
echo_dof : :obj:`int`
702694
Degree of freedom to use in goodness of fit metrics (fstat).
703-
Primarily used for EPTI acquisitions.
704-
If None, number of echoes will be used. Default is None.
695+
Typically the number of echos in the multi-echo data
696+
May be a lower value for EPTI acquisitions.
705697
rho_elbow_type : :obj:`str`
706698
The algorithm used to calculate the rho elbow. Current options are
707699
'kundu' and 'liberal'.
@@ -769,10 +761,7 @@ def rho_elbow_kundu_liberal(
769761
].tolist()
770762

771763
# One rho elbow threshold set just on the number of echoes
772-
if echo_dof is None:
773-
elbow_f05, _, _ = getfbounds(n_echos)
774-
else:
775-
elbow_f05, _, _ = getfbounds(echo_dof)
764+
elbow_f05, _, _ = getfbounds(echo_dof)
776765
# One rho elbow threshold set using all componets in comps2use
777766
rhos_comps2use = component_table.loc[comps2use, "rho"].to_numpy()
778767
rho_allcomps_elbow = getelbow(rhos_comps2use, return_val=True)

tedana/stats.py

+8-6
Original file line numberDiff line numberDiff line change
@@ -11,24 +11,26 @@
1111
RepLGR = logging.getLogger("REPORT")
1212

1313

14-
def getfbounds(n_echos):
14+
def getfbounds(echo_dof):
1515
"""
1616
Get F-statistic boundaries based on number of echos.
1717
1818
Parameters
1919
----------
20-
n_echos : :obj:`int`
21-
Number of echoes
20+
echo_dof : :obj:`int`
21+
Degree of freedom to use in goodness of fit metrics (fstat).
22+
Typically the number of echos in the multi-echo data
23+
May be a lower value for EPTI acquisitions.
2224
2325
Returns
2426
-------
2527
fmin, fmid, fmax : :obj:`float`
2628
F-statistic thresholds for alphas of 0.05, 0.025, and 0.01,
2729
respectively.
2830
"""
29-
f05 = stats.f.ppf(q=(1 - 0.05), dfn=1, dfd=(n_echos - 1))
30-
f025 = stats.f.ppf(q=(1 - 0.025), dfn=1, dfd=(n_echos - 1))
31-
f01 = stats.f.ppf(q=(1 - 0.01), dfn=1, dfd=(n_echos - 1))
31+
f05 = stats.f.ppf(q=(1 - 0.05), dfn=1, dfd=(echo_dof - 1))
32+
f025 = stats.f.ppf(q=(1 - 0.025), dfn=1, dfd=(echo_dof - 1))
33+
f01 = stats.f.ppf(q=(1 - 0.01), dfn=1, dfd=(echo_dof - 1))
3234
return f05, f025, f01
3335

3436

tedana/tests/test_integration.py

+3
Original file line numberDiff line numberDiff line change
@@ -126,11 +126,13 @@ def test_integration_five_echo(skip_integration):
126126
suffix = ".sm.nii.gz"
127127
datalist = [prepend + str(i + 1) + suffix for i in range(5)]
128128
echo_times = [15.4, 29.7, 44.0, 58.3, 72.6]
129+
# also adding echo_dof=4 to make sure all workflow code using echo_dof is executed
129130
tedana_cli.tedana_workflow(
130131
data=datalist,
131132
tes=echo_times,
132133
ica_method="robustica",
133134
n_robust_runs=4,
135+
echo_dof=4,
134136
out_dir=out_dir,
135137
tedpca=0.95,
136138
fittype="curvefit",
@@ -631,6 +633,7 @@ def test_integration_t2smap(skip_integration):
631633
+ [str(te) for te in echo_times]
632634
+ ["--out-dir", out_dir, "--fittype", "curvefit"]
633635
+ ["--masktype", "dropout", "decay"]
636+
+ ["--n-independent-echos", "4"]
634637
)
635638
t2smap_cli._main(args)
636639

tedana/tests/test_metrics.py

+18
Original file line numberDiff line numberDiff line change
@@ -207,15 +207,33 @@ def test_smoke_calculate_f_maps():
207207
mixing = np.random.random((n_volumes, n_components))
208208
adaptive_mask = np.random.randint(1, n_echos + 1, size=n_voxels)
209209
tes = np.array([15, 25, 35, 45, 55])
210+
f_t2_maps_orig, f_s0_maps_orig, _, _ = dependence.calculate_f_maps(
211+
data_cat=data_cat,
212+
z_maps=z_maps,
213+
mixing=mixing,
214+
adaptive_mask=adaptive_mask,
215+
tes=tes,
216+
f_max=500,
217+
)
218+
assert f_t2_maps_orig.shape == f_s0_maps_orig.shape == (n_voxels, n_components)
219+
220+
# rerunning with echo_dof=3
210221
f_t2_maps, f_s0_maps, _, _ = dependence.calculate_f_maps(
211222
data_cat=data_cat,
212223
z_maps=z_maps,
213224
mixing=mixing,
214225
adaptive_mask=adaptive_mask,
215226
tes=tes,
227+
echo_dof=3,
216228
f_max=500,
217229
)
218230
assert f_t2_maps.shape == f_s0_maps.shape == (n_voxels, n_components)
231+
# When echo_dof < the number of echoes, then f_maps_orig should have the same or larger values
232+
assert np.min(f_t2_maps_orig - f_t2_maps) == 0
233+
assert np.min(f_s0_maps_orig - f_s0_maps) == 0
234+
# When echo_dof==3 and there are 5 good echoes, then f_maps_orig should always be larger than 0
235+
assert np.min(f_t2_maps_orig[adaptive_mask == 5] - f_t2_maps[adaptive_mask == 5]) > 0
236+
assert np.min(f_s0_maps_orig[adaptive_mask == 5] - f_s0_maps[adaptive_mask == 5]) > 0
219237

220238

221239
def test_smoke_calculate_varex():

tedana/tests/test_selection_utils.py

+8-8
Original file line numberDiff line numberDiff line change
@@ -388,7 +388,7 @@ def test_kappa_elbow_kundu_smoke():
388388
kappa_allcomps_elbow,
389389
kappa_nonsig_elbow,
390390
varex_upper_p,
391-
) = selection_utils.kappa_elbow_kundu(component_table, n_echos=5)
391+
) = selection_utils.kappa_elbow_kundu(component_table, echo_dof=5)
392392
assert isinstance(kappa_elbow_kundu, float)
393393
assert isinstance(kappa_allcomps_elbow, float)
394394
assert isinstance(kappa_nonsig_elbow, float)
@@ -401,7 +401,7 @@ def test_kappa_elbow_kundu_smoke():
401401
kappa_allcomps_elbow,
402402
kappa_nonsig_elbow,
403403
varex_upper_p,
404-
) = selection_utils.kappa_elbow_kundu(component_table, n_echos=6)
404+
) = selection_utils.kappa_elbow_kundu(component_table, echo_dof=6)
405405
assert isinstance(kappa_elbow_kundu, float)
406406
assert isinstance(kappa_allcomps_elbow, float)
407407
assert isinstance(kappa_nonsig_elbow, type(None))
@@ -415,7 +415,7 @@ def test_kappa_elbow_kundu_smoke():
415415
varex_upper_p,
416416
) = selection_utils.kappa_elbow_kundu(
417417
component_table,
418-
n_echos=5,
418+
echo_dof=5,
419419
comps2use=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 14, 15, 17, 18, 20],
420420
)
421421
assert isinstance(kappa_elbow_kundu, float)
@@ -434,7 +434,7 @@ def test_rho_elbow_kundu_liberal_smoke():
434434
rho_allcomps_elbow,
435435
rho_unclassified_elbow,
436436
elbow_f05,
437-
) = selection_utils.rho_elbow_kundu_liberal(component_table, n_echos=3)
437+
) = selection_utils.rho_elbow_kundu_liberal(component_table, echo_dof=3)
438438
assert isinstance(rho_elbow_kundu, float)
439439
assert isinstance(rho_allcomps_elbow, float)
440440
assert isinstance(rho_unclassified_elbow, float)
@@ -447,7 +447,7 @@ def test_rho_elbow_kundu_liberal_smoke():
447447
rho_unclassified_elbow,
448448
elbow_f05,
449449
) = selection_utils.rho_elbow_kundu_liberal(
450-
component_table, n_echos=3, rho_elbow_type="liberal"
450+
component_table, echo_dof=3, rho_elbow_type="liberal"
451451
)
452452
assert isinstance(rho_elbow_kundu, float)
453453
assert isinstance(rho_allcomps_elbow, float)
@@ -462,7 +462,7 @@ def test_rho_elbow_kundu_liberal_smoke():
462462
elbow_f05,
463463
) = selection_utils.rho_elbow_kundu_liberal(
464464
component_table,
465-
n_echos=3,
465+
echo_dof=3,
466466
rho_elbow_type="kundu",
467467
comps2use=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 14, 15, 17, 18, 20],
468468
subset_comps2use=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 14, 18, 20],
@@ -479,15 +479,15 @@ def test_rho_elbow_kundu_liberal_smoke():
479479
rho_allcomps_elbow,
480480
rho_unclassified_elbow,
481481
elbow_f05,
482-
) = selection_utils.rho_elbow_kundu_liberal(component_table, n_echos=3)
482+
) = selection_utils.rho_elbow_kundu_liberal(component_table, echo_dof=3)
483483
assert isinstance(rho_elbow_kundu, float)
484484
assert isinstance(rho_allcomps_elbow, float)
485485
assert isinstance(rho_unclassified_elbow, type(None))
486486
assert isinstance(elbow_f05, float)
487487

488488
with pytest.raises(ValueError):
489489
selection_utils.rho_elbow_kundu_liberal(
490-
component_table, n_echos=3, rho_elbow_type="perfect"
490+
component_table, echo_dof=3, rho_elbow_type="perfect"
491491
)
492492

493493

tedana/tests/test_utils.py

+49-1
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ def test_reshape_niimg():
7474
assert utils.reshape_niimg(fimg.get_fdata()).shape == exp_shape
7575

7676

77-
def test_make_adaptive_mask():
77+
def test_make_adaptive_mask(caplog):
7878
"""Test tedana.utils.make_adaptive_mask with different methods."""
7979
# load data make masks
8080
mask_file = pjoin(datadir, "mask.nii.gz")
@@ -102,6 +102,16 @@ def test_make_adaptive_mask():
102102
# Decay: good good good (3)
103103
data[idx + 5, :, :] = np.array([1, 0.9, -1])[:, None]
104104

105+
# Simulating 5 echo data to test the echo_dof parameter
106+
data5 = np.concat(
107+
(
108+
data,
109+
0.95 * np.expand_dims(data[:, 2, :], axis=1),
110+
0.9 * np.expand_dims(data[:, 2, :], axis=1),
111+
),
112+
axis=1,
113+
)
114+
105115
# Just dropout method
106116
mask, adaptive_mask = utils.make_adaptive_mask(
107117
data,
@@ -122,6 +132,7 @@ def test_make_adaptive_mask():
122132
vals, counts = np.unique(adaptive_mask, return_counts=True)
123133
assert np.allclose(vals, np.array([0, 1, 2, 3]))
124134
assert np.allclose(counts, np.array([14976, 1817, 4427, 43130]))
135+
assert "voxels in user-defined mask do not have good signal" in caplog.text
125136

126137
# Just decay method
127138
mask, adaptive_mask = utils.make_adaptive_mask(
@@ -206,6 +217,43 @@ def test_make_adaptive_mask():
206217
vals, counts = np.unique(adaptive_mask, return_counts=True)
207218
assert np.allclose(vals, np.array([0, 1, 2, 3]))
208219
assert np.allclose(counts, np.array([3365, 1412, 1195, 58378]))
220+
assert "No methods provided for adaptive mask generation." in caplog.text
221+
222+
# testing echo_dof
223+
# This should match "decay" from above, except all voxels with 3 good echoes should now have 5
224+
# since two echoes were added that should not have caused more decay
225+
mask, adaptive_mask = utils.make_adaptive_mask(
226+
data5, mask=mask_file, threshold=1, methods=["decay"], echo_dof=3
227+
)
228+
229+
assert mask.shape == adaptive_mask.shape == (64350,)
230+
assert np.allclose(mask, (adaptive_mask >= 1).astype(bool))
231+
assert adaptive_mask[idx] == 5
232+
assert adaptive_mask[idx + 1] == 2
233+
assert adaptive_mask[idx + 2] == 2
234+
assert adaptive_mask[idx + 3] == 1
235+
assert adaptive_mask[idx + 4] == 5
236+
assert adaptive_mask[idx + 5] == 2
237+
assert mask.sum() == 60985 # This method can't flag first echo as bad
238+
vals, counts = np.unique(adaptive_mask, return_counts=True)
239+
assert np.allclose(vals, np.array([0, 1, 2, 5]))
240+
assert np.allclose(counts, np.array([3365, 4366, 5973, 50646]))
241+
# 4366 + 5973 = 10399 (i.e. voxels with 1 or 2 good echoes are flagged here)
242+
assert (
243+
"10339 voxels (17.0%) have fewer than 3.0 good voxels. "
244+
"These voxels will be used in all analyses, "
245+
"but might not include 3 independant echo measurements."
246+
) in caplog.text
247+
248+
mask, adaptive_mask = utils.make_adaptive_mask(
249+
data5, mask=mask_file, threshold=1, methods=["decay"], echo_dof=4
250+
)
251+
252+
assert (
253+
"10339 voxels (17.0%) have fewer than 3.0 good voxels. "
254+
"The degrees of freedom for fits across echoes will remain 4 even if "
255+
"there might be fewer independant echo measurements."
256+
) in caplog.text
209257

210258

211259
# SMOKE TESTS

0 commit comments

Comments
 (0)