Coverage for nilearn/interfaces/fmriprep/tests/test_load_confounds_scrub.py: 0%

55 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-16 12:32 +0200

1import numpy as np 

2import pandas as pd 

3import pytest 

4from pandas.testing import assert_frame_equal 

5 

6from nilearn.interfaces.fmriprep.load_confounds_scrub import ( 

7 _get_outlier_cols, 

8 extract_outlier_regressors, 

9 optimize_scrub, 

10) 

11 

12 

13@pytest.mark.parametrize( 

14 "original_motion_outliers_index,expected_optimal", 

15 [ 

16 ( 

17 [3, 11, 13, 80, 97], 

18 np.array([0, 1, 2, 3, 11, 12, 13, 80, 97, 98, 99]), 

19 ), 

20 ( 

21 [11, 13, 16, 44, 50, 80], # middle volumes 

22 np.array([11, 12, 13, 14, 15, 16, 44, 50, 80]), 

23 ), 

24 ([4], np.array([0, 1, 2, 3, 4])), # head volumes 

25 ([96], np.array([96, 97, 98, 99])), # tail volumes 

26 ([5], np.array([5])), 

27 ], 

28) # no optimization needed 

29def test_optimize_scrub(original_motion_outliers_index, expected_optimal): 

30 """Check the segment removal is acting correctly.""" 

31 # simulated labels with 100 time frames and remove any segment under 

32 # 5 volumes 

33 optimised_index = optimize_scrub(original_motion_outliers_index, 100, 5) 

34 assert np.array_equal(optimised_index, expected_optimal) 

35 

36 

37def test_get_outlier_cols(): 

38 """Check the non-steady state columns are detached.""" 

39 col_names = ["confound_regressor"] 

40 non_steady_state = [f"non_steady_state_outlier{i:02d}" for i in range(3)] 

41 col_names += non_steady_state 

42 col_names = pd.Index(col_names) 

43 outlier_cols, confounds_cols = _get_outlier_cols(col_names) 

44 assert confounds_cols == ["confound_regressor"] 

45 assert outlier_cols == non_steady_state 

46 

47 

48def test_extract_outlier_regressors(rng): 

49 """Check outlier regressors of different types.""" 

50 # Create a fake confound dataframe 

51 n_scans = 50 

52 fake_confounds = pd.DataFrame( 

53 rng.random((n_scans, 1)), columns=["confound_regressor"] 

54 ) 

55 

56 # scrubbed volume one-hot, overlap with non-steady-state 

57 idx_scrubbed = [2, 4, 34, 44] 

58 scrub_vol = pd.DataFrame( 

59 np.eye(n_scans)[:, idx_scrubbed], 

60 columns=[f"motion_outlier{i:02d}" for i in range(len(idx_scrubbed))], 

61 ) 

62 # First three volumes are non-steady-state 

63 non_steady_vol = pd.DataFrame( 

64 np.eye(n_scans)[:, :3], 

65 columns=[f"non_steady_state_outlier{i:02d}" for i in range(3)], 

66 ) 

67 

68 # non-steady only 

69 non_steady_conf = pd.concat([fake_confounds, non_steady_vol], axis=1) 

70 sample_mask, confounds, outliers = extract_outlier_regressors( 

71 non_steady_conf 

72 ) 

73 assert np.array_equal(sample_mask, np.arange(n_scans)[3:]) is True 

74 assert_frame_equal(outliers, non_steady_vol) 

75 assert_frame_equal(confounds, fake_confounds) 

76 

77 # scrub only 

78 srub_conf = pd.concat([fake_confounds, scrub_vol], axis=1) 

79 make_mask = np.delete(np.arange(n_scans), idx_scrubbed) 

80 sample_mask, confounds, outliers = extract_outlier_regressors(srub_conf) 

81 assert np.array_equal(sample_mask, make_mask) is True 

82 assert_frame_equal(outliers, scrub_vol) 

83 assert_frame_equal(confounds, fake_confounds) 

84 

85 # scrub and non-steady state 

86 all_conf = pd.concat([fake_confounds, non_steady_vol, scrub_vol], axis=1) 

87 make_mask = np.delete(np.arange(n_scans), idx_scrubbed)[2:] 

88 make_outliers = pd.concat([non_steady_vol, scrub_vol], axis=1) 

89 make_outliers = make_outliers.reindex( 

90 sorted(make_outliers.columns), axis=1 

91 ) 

92 make_outliers = make_outliers.drop(columns="non_steady_state_outlier02") 

93 

94 sample_mask, confounds, outliers = extract_outlier_regressors(all_conf) 

95 assert len(sample_mask) == 44 

96 assert np.array_equal(sample_mask, make_mask) is True 

97 assert_frame_equal(outliers, make_outliers) 

98 assert_frame_equal(confounds, fake_confounds) 

99 

100 

101@pytest.mark.parametrize( 

102 "outlier_type", 

103 ["motion_outlier", "non_steady_state_outlier"], 

104) 

105def test_warning_no_volumes_left(outlier_type): 

106 """Check warning is thrown when all volumes in a run are scrubbed.""" 

107 rng = np.random.default_rng() 

108 n_scans = 10 

109 fake_confounds = pd.DataFrame( 

110 rng.random((n_scans, 1)), columns=["confound_regressor"] 

111 ) 

112 

113 # scrubbed volume one-hot, overlap with non-steady-state 

114 idx_scrubbed = np.arange(n_scans) 

115 scrub_vol = pd.DataFrame( 

116 np.eye(n_scans)[:, idx_scrubbed], 

117 columns=[f"{outlier_type}{i:02d}" for i in range(len(idx_scrubbed))], 

118 ) 

119 

120 srub_conf = pd.concat([fake_confounds, scrub_vol], axis=1) 

121 

122 with pytest.warns( 

123 RuntimeWarning, 

124 match="All volumes were marked as motion outliers.", 

125 ): 

126 sample_mask, _, _ = extract_outlier_regressors(srub_conf) 

127 assert sample_mask.size == 0