Coverage for nilearn/glm/tests/test_utils.py: 0%

140 statements  

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

1#!/usr/bin/env python 

2import numpy as np 

3import pytest 

4import scipy.linalg as spl 

5import scipy.stats as sps 

6from numpy.testing import ( 

7 assert_almost_equal, 

8 assert_array_almost_equal, 

9 assert_array_equal, 

10) 

11from scipy.stats import norm 

12 

13from nilearn._utils.data_gen import generate_fake_fmri 

14from nilearn.glm._utils import ( 

15 full_rank, 

16 multiple_fast_inverse, 

17 multiple_mahalanobis, 

18 pad_contrast, 

19 positive_reciprocal, 

20 z_score, 

21) 

22from nilearn.glm.first_level import ( 

23 FirstLevelModel, 

24 make_first_level_design_matrix, 

25) 

26from nilearn.maskers import NiftiMasker 

27 

28 

29def test_full_rank(rng): 

30 n, p = 10, 5 

31 X = rng.standard_normal(size=(n, p)) 

32 X_, _ = full_rank(X) 

33 

34 assert_array_almost_equal(X, X_) 

35 

36 X[:, -1] = X[:, :-1].sum(1) 

37 X_, cond = full_rank(X) 

38 

39 assert cond > 1.0e10 

40 assert_array_almost_equal(X, X_) 

41 

42 

43def test_z_score_t_values(rng): 

44 # Randomly draw samples from the standard Student's t distribution 

45 t_val = rng.standard_t(10, size=10) 

46 # Estimate the p-values using the Survival Function (SF) 

47 p_val = sps.t.sf(t_val, 1e10) 

48 # Estimate the p-values using the Cumulative Distribution Function (CDF) 

49 cdf_val = sps.t.cdf(t_val, 1e10) 

50 # Set a minimum threshold for p-values to avoid infinite z-scores 

51 p_val = np.array(np.minimum(np.maximum(p_val, 1.0e-300), 1.0 - 1.0e-16)) 

52 cdf_val = np.array( 

53 np.minimum(np.maximum(cdf_val, 1.0e-300), 1.0 - 1.0e-16) 

54 ) 

55 # Compute z-score from the p-value estimated with the SF 

56 z_val_sf = norm.isf(p_val) 

57 # Compute z-score from the p-value estimated with the CDF 

58 z_val_cdf = norm.ppf(cdf_val) 

59 # Create the final array of z-scores, ... 

60 z_val = np.zeros(p_val.size) 

61 # ... in which z-scores < 0 estimated w/ SF are replaced by z-scores < 0 

62 # estimated w/ CDF 

63 z_val[np.atleast_1d(z_val_sf < 0)] = z_val_cdf[z_val_sf < 0] 

64 # ... and z-scores >=0 estimated from SF are kept. 

65 z_val[np.atleast_1d(z_val_sf >= 0)] = z_val_sf[z_val_sf >= 0] 

66 

67 # Test 'z_score' function in 'nilearn/glm/contrasts.py' 

68 assert_array_almost_equal(z_score(p_val, one_minus_pvalue=cdf_val), z_val) 

69 

70 # Test 'z_score' function in 'nilearn/glm/contrasts.py', 

71 # when one_minus_pvalue is None 

72 assert_array_almost_equal(norm.sf(z_score(p_val)), p_val) 

73 

74 

75def test_z_score_f_values(rng): 

76 # Randomly draw samples from the F distribution 

77 f_val = rng.f(1, 48, size=10) 

78 # Estimate the p-values using the Survival Function (SF) 

79 p_val = sps.f.sf(f_val, 42, 1e10) 

80 # Estimate the p-values using the Cumulative Distribution Function (CDF) 

81 cdf_val = sps.f.cdf(f_val, 42, 1e10) 

82 # Set a minimum threshold for p-values to avoid infinite z-scores 

83 p_val = np.array(np.minimum(np.maximum(p_val, 1.0e-300), 1.0 - 1.0e-16)) 

84 cdf_val = np.array( 

85 np.minimum(np.maximum(cdf_val, 1.0e-300), 1.0 - 1.0e-16) 

86 ) 

87 # Compute z-score from the p-value estimated with the SF 

88 z_val_sf = norm.isf(p_val) 

89 # Compute z-score from the p-value estimated with the CDF 

90 z_val_cdf = norm.ppf(cdf_val) 

91 # Create the final array of z-scores, ... 

92 z_val = np.zeros(p_val.size) 

93 # ... in which z-scores < 0 estimated w/ SF are replaced by z-scores < 0 

94 # estimated w/ CDF 

95 z_val[np.atleast_1d(z_val_sf < 0)] = z_val_cdf[z_val_sf < 0] 

96 # ... and z-scores >=0 estimated from SF are kept. 

97 z_val[np.atleast_1d(z_val_sf >= 0)] = z_val_sf[z_val_sf >= 0] 

98 

99 # Test 'z_score' function in 'nilearn/glm/contrasts.py' 

100 assert_array_almost_equal(z_score(p_val, one_minus_pvalue=cdf_val), z_val) 

101 

102 # Test 'z_score' function in 'nilearn/glm/contrasts.py', 

103 # when one_minus_pvalue is None 

104 assert_array_almost_equal(norm.sf(z_score(p_val)), p_val) 

105 

106 # ##################### Check the numerical precision ##################### 

107 for t in [33.75, -8.3]: 

108 p = sps.t.sf(t, 1e10) 

109 cdf = sps.t.cdf(t, 1e10) 

110 z_sf = norm.isf(p) 

111 z_cdf = norm.ppf(cdf) 

112 z = z_sf if p <= 0.5 else z_cdf 

113 

114 assert_array_almost_equal(z_score(p, one_minus_pvalue=cdf), z) 

115 

116 

117def test_z_score_opposite_contrast(rng): 

118 fmri, mask = generate_fake_fmri( 

119 shape=(50, 20, 50), length=96, random_state=rng 

120 ) 

121 

122 nifti_masker = NiftiMasker(mask_img=mask) 

123 data = nifti_masker.fit_transform(fmri) 

124 

125 frametimes = np.linspace(0, (96 - 1) * 2, 96) 

126 

127 for i in [0, 20]: 

128 design_matrix = make_first_level_design_matrix( 

129 frametimes, 

130 hrf_model="spm", 

131 add_regs=np.array(data[:, i]).reshape(-1, 1), 

132 ) 

133 c1 = np.array([1] + [0] * (design_matrix.shape[1] - 1)) 

134 c2 = np.array([0] + [1] + [0] * (design_matrix.shape[1] - 2)) 

135 contrasts = {"seed1 - seed2": c1 - c2, "seed2 - seed1": c2 - c1} 

136 fmri_glm = FirstLevelModel( 

137 t_r=2.0, 

138 noise_model="ar1", 

139 standardize=False, 

140 hrf_model="spm", 

141 drift_model="cosine", 

142 ) 

143 fmri_glm.fit(fmri, design_matrices=design_matrix) 

144 z_map_seed1_vs_seed2 = fmri_glm.compute_contrast( 

145 contrasts["seed1 - seed2"], output_type="z_score" 

146 ) 

147 z_map_seed2_vs_seed1 = fmri_glm.compute_contrast( 

148 contrasts["seed2 - seed1"], output_type="z_score" 

149 ) 

150 

151 assert_almost_equal( 

152 z_map_seed1_vs_seed2.get_fdata(dtype="float32").min(), 

153 -z_map_seed2_vs_seed1.get_fdata(dtype="float32").max(), 

154 decimal=10, 

155 ) 

156 assert_almost_equal( 

157 z_map_seed1_vs_seed2.get_fdata(dtype="float32").max(), 

158 -z_map_seed2_vs_seed1.get_fdata(dtype="float32").min(), 

159 decimal=10, 

160 ) 

161 

162 

163def test_mahalanobis(rng): 

164 n = 50 

165 x = rng.uniform(size=n) / n 

166 A = rng.uniform(size=(n, n)) / n 

167 A = np.dot(A.transpose(), A) + np.eye(n) 

168 mah = np.dot(x, np.dot(spl.inv(A), x)) 

169 

170 assert_almost_equal(mah, multiple_mahalanobis(x, A), decimal=1) 

171 

172 

173def test_mahalanobis2(rng): 

174 n = 50 

175 x = rng.standard_normal(size=(n, 3)) 

176 Aa = np.zeros([n, n, 3]) 

177 for i in range(3): 

178 A = rng.standard_normal(size=(120, n)) 

179 A = np.dot(A.T, A) 

180 Aa[:, :, i] = A 

181 i = rng.integers(3) 

182 mah = np.dot(x[:, i], np.dot(spl.inv(Aa[:, :, i]), x[:, i])) 

183 f_mah = (multiple_mahalanobis(x, Aa))[i] 

184 

185 assert np.allclose(mah, f_mah) 

186 

187 

188def test_mahalanobis_errors(): 

189 effect = np.zeros((1, 2, 3)) 

190 cov = np.zeros((3, 3, 3)) 

191 

192 with pytest.raises( 

193 ValueError, match="Inconsistent shape for effect and covariance" 

194 ): 

195 multiple_mahalanobis(effect, cov) 

196 

197 cov = np.zeros((1, 2, 3)) 

198 

199 with pytest.raises(ValueError, match="Inconsistent shape for covariance"): 

200 multiple_mahalanobis(effect, cov) 

201 

202 

203def test_multiple_fast_inv(rng): 

204 shape = (10, 20, 20) 

205 X = rng.standard_normal(size=shape) 

206 X_inv_ref = np.zeros(shape) 

207 for i in range(shape[0]): 

208 X[i] = np.dot(X[i], X[i].T) 

209 X_inv_ref[i] = spl.inv(X[i]) 

210 X_inv = multiple_fast_inverse(X) 

211 

212 assert_almost_equal(X_inv_ref, X_inv) 

213 

214 

215def test_multiple_fast_inverse_errors(): 

216 shape = (2, 2, 2) 

217 X = np.zeros(shape) 

218 

219 with pytest.raises(ValueError, match="Matrix LU decomposition failed"): 

220 multiple_fast_inverse(X) 

221 

222 shape = (10, 20, 20) 

223 X = np.zeros(shape) 

224 

225 with pytest.raises(ValueError, match="Matrix LU decomposition failed"): 

226 multiple_fast_inverse(X) 

227 

228 

229def test_pos_recipr(): 

230 X = np.array([2, 1, -1, 0], dtype=np.int8) 

231 eX = np.array([0.5, 1, 0, 0]) 

232 Y = positive_reciprocal(X) 

233 

234 assert_array_almost_equal(Y, eX) 

235 assert Y.dtype.type == np.float64 

236 

237 X2 = X.reshape((2, 2)) 

238 Y2 = positive_reciprocal(X2) 

239 

240 assert_array_almost_equal(Y2, eX.reshape((2, 2))) 

241 

242 # check that lists have arrived 

243 XL = [0, 1, -1] 

244 

245 assert_array_almost_equal(positive_reciprocal(XL), [0, 1, 0]) 

246 # scalars 

247 assert positive_reciprocal(-1) == 0 

248 assert positive_reciprocal(0) == 0 

249 assert positive_reciprocal(2) == 0.5 

250 

251 

252@pytest.mark.parametrize("val", [[1], [1, 0]]) 

253@pytest.mark.parametrize("stat_type", ["t", "F"]) 

254def test_pad_contrast(rng, val, stat_type): 

255 """Check padding of vector contrasts.""" 

256 theta = rng.random((4, 1)) 

257 con_val = np.array([val]) 

258 padded_contrast = pad_contrast(con_val, theta, stat_type=stat_type) 

259 assert_array_equal(padded_contrast, [[1, 0, 0, 0]]) 

260 

261 

262@pytest.mark.parametrize("val", [[[1, 0], [0, 1]], [[1, 0, 0], [0, 1, 0]]]) 

263def test_pad_f_contrast(rng, val): 

264 """Check padding of matrix contrasts.""" 

265 theta = rng.random((4, 1)) 

266 con_val = np.array(val) 

267 padded_contrast = pad_contrast(con_val, theta, stat_type="F") 

268 assert_array_equal(padded_contrast, [[1, 0, 0, 0], [0, 1, 0, 0]])