Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/scipy/sparse/linalg/_onenormest.py: 11%

198 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-12-12 06:31 +0000

1"""Sparse block 1-norm estimator. 

2""" 

3 

4import numpy as np 

5from scipy.sparse.linalg import aslinearoperator 

6 

7 

8__all__ = ['onenormest'] 

9 

10 

11def onenormest(A, t=2, itmax=5, compute_v=False, compute_w=False): 

12 """ 

13 Compute a lower bound of the 1-norm of a sparse matrix. 

14 

15 Parameters 

16 ---------- 

17 A : ndarray or other linear operator 

18 A linear operator that can be transposed and that can 

19 produce matrix products. 

20 t : int, optional 

21 A positive parameter controlling the tradeoff between 

22 accuracy versus time and memory usage. 

23 Larger values take longer and use more memory 

24 but give more accurate output. 

25 itmax : int, optional 

26 Use at most this many iterations. 

27 compute_v : bool, optional 

28 Request a norm-maximizing linear operator input vector if True. 

29 compute_w : bool, optional 

30 Request a norm-maximizing linear operator output vector if True. 

31 

32 Returns 

33 ------- 

34 est : float 

35 An underestimate of the 1-norm of the sparse matrix. 

36 v : ndarray, optional 

37 The vector such that ||Av||_1 == est*||v||_1. 

38 It can be thought of as an input to the linear operator 

39 that gives an output with particularly large norm. 

40 w : ndarray, optional 

41 The vector Av which has relatively large 1-norm. 

42 It can be thought of as an output of the linear operator 

43 that is relatively large in norm compared to the input. 

44 

45 Notes 

46 ----- 

47 This is algorithm 2.4 of [1]. 

48 

49 In [2] it is described as follows. 

50 "This algorithm typically requires the evaluation of 

51 about 4t matrix-vector products and almost invariably 

52 produces a norm estimate (which is, in fact, a lower 

53 bound on the norm) correct to within a factor 3." 

54 

55 .. versionadded:: 0.13.0 

56 

57 References 

58 ---------- 

59 .. [1] Nicholas J. Higham and Francoise Tisseur (2000), 

60 "A Block Algorithm for Matrix 1-Norm Estimation, 

61 with an Application to 1-Norm Pseudospectra." 

62 SIAM J. Matrix Anal. Appl. Vol. 21, No. 4, pp. 1185-1201. 

63 

64 .. [2] Awad H. Al-Mohy and Nicholas J. Higham (2009), 

65 "A new scaling and squaring algorithm for the matrix exponential." 

66 SIAM J. Matrix Anal. Appl. Vol. 31, No. 3, pp. 970-989. 

67 

68 Examples 

69 -------- 

70 >>> import numpy as np 

71 >>> from scipy.sparse import csc_matrix 

72 >>> from scipy.sparse.linalg import onenormest 

73 >>> A = csc_matrix([[1., 0., 0.], [5., 8., 2.], [0., -1., 0.]], dtype=float) 

74 >>> A.toarray() 

75 array([[ 1., 0., 0.], 

76 [ 5., 8., 2.], 

77 [ 0., -1., 0.]]) 

78 >>> onenormest(A) 

79 9.0 

80 >>> np.linalg.norm(A.toarray(), ord=1) 

81 9.0 

82 """ 

83 

84 # Check the input. 

85 A = aslinearoperator(A) 

86 if A.shape[0] != A.shape[1]: 

87 raise ValueError('expected the operator to act like a square matrix') 

88 

89 # If the operator size is small compared to t, 

90 # then it is easier to compute the exact norm. 

91 # Otherwise estimate the norm. 

92 n = A.shape[1] 

93 if t >= n: 

94 A_explicit = np.asarray(aslinearoperator(A).matmat(np.identity(n))) 

95 if A_explicit.shape != (n, n): 

96 raise Exception('internal error: ', 

97 'unexpected shape ' + str(A_explicit.shape)) 

98 col_abs_sums = abs(A_explicit).sum(axis=0) 

99 if col_abs_sums.shape != (n, ): 

100 raise Exception('internal error: ', 

101 'unexpected shape ' + str(col_abs_sums.shape)) 

102 argmax_j = np.argmax(col_abs_sums) 

103 v = elementary_vector(n, argmax_j) 

104 w = A_explicit[:, argmax_j] 

105 est = col_abs_sums[argmax_j] 

106 else: 

107 est, v, w, nmults, nresamples = _onenormest_core(A, A.H, t, itmax) 

108 

109 # Report the norm estimate along with some certificates of the estimate. 

110 if compute_v or compute_w: 

111 result = (est,) 

112 if compute_v: 

113 result += (v,) 

114 if compute_w: 

115 result += (w,) 

116 return result 

117 else: 

118 return est 

119 

120 

121def _blocked_elementwise(func): 

122 """ 

123 Decorator for an elementwise function, to apply it blockwise along 

124 first dimension, to avoid excessive memory usage in temporaries. 

125 """ 

126 block_size = 2**20 

127 

128 def wrapper(x): 

129 if x.shape[0] < block_size: 

130 return func(x) 

131 else: 

132 y0 = func(x[:block_size]) 

133 y = np.zeros((x.shape[0],) + y0.shape[1:], dtype=y0.dtype) 

134 y[:block_size] = y0 

135 del y0 

136 for j in range(block_size, x.shape[0], block_size): 

137 y[j:j+block_size] = func(x[j:j+block_size]) 

138 return y 

139 return wrapper 

140 

141 

142@_blocked_elementwise 

143def sign_round_up(X): 

144 """ 

145 This should do the right thing for both real and complex matrices. 

146 

147 From Higham and Tisseur: 

148 "Everything in this section remains valid for complex matrices 

149 provided that sign(A) is redefined as the matrix (aij / |aij|) 

150 (and sign(0) = 1) transposes are replaced by conjugate transposes." 

151 

152 """ 

153 Y = X.copy() 

154 Y[Y == 0] = 1 

155 Y /= np.abs(Y) 

156 return Y 

157 

158 

159@_blocked_elementwise 

160def _max_abs_axis1(X): 

161 return np.max(np.abs(X), axis=1) 

162 

163 

164def _sum_abs_axis0(X): 

165 block_size = 2**20 

166 r = None 

167 for j in range(0, X.shape[0], block_size): 

168 y = np.sum(np.abs(X[j:j+block_size]), axis=0) 

169 if r is None: 

170 r = y 

171 else: 

172 r += y 

173 return r 

174 

175 

176def elementary_vector(n, i): 

177 v = np.zeros(n, dtype=float) 

178 v[i] = 1 

179 return v 

180 

181 

182def vectors_are_parallel(v, w): 

183 # Columns are considered parallel when they are equal or negative. 

184 # Entries are required to be in {-1, 1}, 

185 # which guarantees that the magnitudes of the vectors are identical. 

186 if v.ndim != 1 or v.shape != w.shape: 

187 raise ValueError('expected conformant vectors with entries in {-1,1}') 

188 n = v.shape[0] 

189 return np.dot(v, w) == n 

190 

191 

192def every_col_of_X_is_parallel_to_a_col_of_Y(X, Y): 

193 for v in X.T: 

194 if not any(vectors_are_parallel(v, w) for w in Y.T): 

195 return False 

196 return True 

197 

198 

199def column_needs_resampling(i, X, Y=None): 

200 # column i of X needs resampling if either 

201 # it is parallel to a previous column of X or 

202 # it is parallel to a column of Y 

203 n, t = X.shape 

204 v = X[:, i] 

205 if any(vectors_are_parallel(v, X[:, j]) for j in range(i)): 

206 return True 

207 if Y is not None: 

208 if any(vectors_are_parallel(v, w) for w in Y.T): 

209 return True 

210 return False 

211 

212 

213def resample_column(i, X): 

214 X[:, i] = np.random.randint(0, 2, size=X.shape[0])*2 - 1 

215 

216 

217def less_than_or_close(a, b): 

218 return np.allclose(a, b) or (a < b) 

219 

220 

221def _algorithm_2_2(A, AT, t): 

222 """ 

223 This is Algorithm 2.2. 

224 

225 Parameters 

226 ---------- 

227 A : ndarray or other linear operator 

228 A linear operator that can produce matrix products. 

229 AT : ndarray or other linear operator 

230 The transpose of A. 

231 t : int, optional 

232 A positive parameter controlling the tradeoff between 

233 accuracy versus time and memory usage. 

234 

235 Returns 

236 ------- 

237 g : sequence 

238 A non-negative decreasing vector 

239 such that g[j] is a lower bound for the 1-norm 

240 of the column of A of jth largest 1-norm. 

241 The first entry of this vector is therefore a lower bound 

242 on the 1-norm of the linear operator A. 

243 This sequence has length t. 

244 ind : sequence 

245 The ith entry of ind is the index of the column A whose 1-norm 

246 is given by g[i]. 

247 This sequence of indices has length t, and its entries are 

248 chosen from range(n), possibly with repetition, 

249 where n is the order of the operator A. 

250 

251 Notes 

252 ----- 

253 This algorithm is mainly for testing. 

254 It uses the 'ind' array in a way that is similar to 

255 its usage in algorithm 2.4. This algorithm 2.2 may be easier to test, 

256 so it gives a chance of uncovering bugs related to indexing 

257 which could have propagated less noticeably to algorithm 2.4. 

258 

259 """ 

260 A_linear_operator = aslinearoperator(A) 

261 AT_linear_operator = aslinearoperator(AT) 

262 n = A_linear_operator.shape[0] 

263 

264 # Initialize the X block with columns of unit 1-norm. 

265 X = np.ones((n, t)) 

266 if t > 1: 

267 X[:, 1:] = np.random.randint(0, 2, size=(n, t-1))*2 - 1 

268 X /= float(n) 

269 

270 # Iteratively improve the lower bounds. 

271 # Track extra things, to assert invariants for debugging. 

272 g_prev = None 

273 h_prev = None 

274 k = 1 

275 ind = range(t) 

276 while True: 

277 Y = np.asarray(A_linear_operator.matmat(X)) 

278 g = _sum_abs_axis0(Y) 

279 best_j = np.argmax(g) 

280 g.sort() 

281 g = g[::-1] 

282 S = sign_round_up(Y) 

283 Z = np.asarray(AT_linear_operator.matmat(S)) 

284 h = _max_abs_axis1(Z) 

285 

286 # If this algorithm runs for fewer than two iterations, 

287 # then its return values do not have the properties indicated 

288 # in the description of the algorithm. 

289 # In particular, the entries of g are not 1-norms of any 

290 # column of A until the second iteration. 

291 # Therefore we will require the algorithm to run for at least 

292 # two iterations, even though this requirement is not stated 

293 # in the description of the algorithm. 

294 if k >= 2: 

295 if less_than_or_close(max(h), np.dot(Z[:, best_j], X[:, best_j])): 

296 break 

297 ind = np.argsort(h)[::-1][:t] 

298 h = h[ind] 

299 for j in range(t): 

300 X[:, j] = elementary_vector(n, ind[j]) 

301 

302 # Check invariant (2.2). 

303 if k >= 2: 

304 if not less_than_or_close(g_prev[0], h_prev[0]): 

305 raise Exception('invariant (2.2) is violated') 

306 if not less_than_or_close(h_prev[0], g[0]): 

307 raise Exception('invariant (2.2) is violated') 

308 

309 # Check invariant (2.3). 

310 if k >= 3: 

311 for j in range(t): 

312 if not less_than_or_close(g[j], g_prev[j]): 

313 raise Exception('invariant (2.3) is violated') 

314 

315 # Update for the next iteration. 

316 g_prev = g 

317 h_prev = h 

318 k += 1 

319 

320 # Return the lower bounds and the corresponding column indices. 

321 return g, ind 

322 

323 

324def _onenormest_core(A, AT, t, itmax): 

325 """ 

326 Compute a lower bound of the 1-norm of a sparse matrix. 

327 

328 Parameters 

329 ---------- 

330 A : ndarray or other linear operator 

331 A linear operator that can produce matrix products. 

332 AT : ndarray or other linear operator 

333 The transpose of A. 

334 t : int, optional 

335 A positive parameter controlling the tradeoff between 

336 accuracy versus time and memory usage. 

337 itmax : int, optional 

338 Use at most this many iterations. 

339 

340 Returns 

341 ------- 

342 est : float 

343 An underestimate of the 1-norm of the sparse matrix. 

344 v : ndarray, optional 

345 The vector such that ||Av||_1 == est*||v||_1. 

346 It can be thought of as an input to the linear operator 

347 that gives an output with particularly large norm. 

348 w : ndarray, optional 

349 The vector Av which has relatively large 1-norm. 

350 It can be thought of as an output of the linear operator 

351 that is relatively large in norm compared to the input. 

352 nmults : int, optional 

353 The number of matrix products that were computed. 

354 nresamples : int, optional 

355 The number of times a parallel column was observed, 

356 necessitating a re-randomization of the column. 

357 

358 Notes 

359 ----- 

360 This is algorithm 2.4. 

361 

362 """ 

363 # This function is a more or less direct translation 

364 # of Algorithm 2.4 from the Higham and Tisseur (2000) paper. 

365 A_linear_operator = aslinearoperator(A) 

366 AT_linear_operator = aslinearoperator(AT) 

367 if itmax < 2: 

368 raise ValueError('at least two iterations are required') 

369 if t < 1: 

370 raise ValueError('at least one column is required') 

371 n = A.shape[0] 

372 if t >= n: 

373 raise ValueError('t should be smaller than the order of A') 

374 # Track the number of big*small matrix multiplications 

375 # and the number of resamplings. 

376 nmults = 0 

377 nresamples = 0 

378 # "We now explain our choice of starting matrix. We take the first 

379 # column of X to be the vector of 1s [...] This has the advantage that 

380 # for a matrix with nonnegative elements the algorithm converges 

381 # with an exact estimate on the second iteration, and such matrices 

382 # arise in applications [...]" 

383 X = np.ones((n, t), dtype=float) 

384 # "The remaining columns are chosen as rand{-1,1}, 

385 # with a check for and correction of parallel columns, 

386 # exactly as for S in the body of the algorithm." 

387 if t > 1: 

388 for i in range(1, t): 

389 # These are technically initial samples, not resamples, 

390 # so the resampling count is not incremented. 

391 resample_column(i, X) 

392 for i in range(t): 

393 while column_needs_resampling(i, X): 

394 resample_column(i, X) 

395 nresamples += 1 

396 # "Choose starting matrix X with columns of unit 1-norm." 

397 X /= float(n) 

398 # "indices of used unit vectors e_j" 

399 ind_hist = np.zeros(0, dtype=np.intp) 

400 est_old = 0 

401 S = np.zeros((n, t), dtype=float) 

402 k = 1 

403 ind = None 

404 while True: 

405 Y = np.asarray(A_linear_operator.matmat(X)) 

406 nmults += 1 

407 mags = _sum_abs_axis0(Y) 

408 est = np.max(mags) 

409 best_j = np.argmax(mags) 

410 if est > est_old or k == 2: 

411 if k >= 2: 

412 ind_best = ind[best_j] 

413 w = Y[:, best_j] 

414 # (1) 

415 if k >= 2 and est <= est_old: 

416 est = est_old 

417 break 

418 est_old = est 

419 S_old = S 

420 if k > itmax: 

421 break 

422 S = sign_round_up(Y) 

423 del Y 

424 # (2) 

425 if every_col_of_X_is_parallel_to_a_col_of_Y(S, S_old): 

426 break 

427 if t > 1: 

428 # "Ensure that no column of S is parallel to another column of S 

429 # or to a column of S_old by replacing columns of S by rand{-1,1}." 

430 for i in range(t): 

431 while column_needs_resampling(i, S, S_old): 

432 resample_column(i, S) 

433 nresamples += 1 

434 del S_old 

435 # (3) 

436 Z = np.asarray(AT_linear_operator.matmat(S)) 

437 nmults += 1 

438 h = _max_abs_axis1(Z) 

439 del Z 

440 # (4) 

441 if k >= 2 and max(h) == h[ind_best]: 

442 break 

443 # "Sort h so that h_first >= ... >= h_last 

444 # and re-order ind correspondingly." 

445 # 

446 # Later on, we will need at most t+len(ind_hist) largest 

447 # entries, so drop the rest 

448 ind = np.argsort(h)[::-1][:t+len(ind_hist)].copy() 

449 del h 

450 if t > 1: 

451 # (5) 

452 # Break if the most promising t vectors have been visited already. 

453 if np.in1d(ind[:t], ind_hist).all(): 

454 break 

455 # Put the most promising unvisited vectors at the front of the list 

456 # and put the visited vectors at the end of the list. 

457 # Preserve the order of the indices induced by the ordering of h. 

458 seen = np.in1d(ind, ind_hist) 

459 ind = np.concatenate((ind[~seen], ind[seen])) 

460 for j in range(t): 

461 X[:, j] = elementary_vector(n, ind[j]) 

462 

463 new_ind = ind[:t][~np.in1d(ind[:t], ind_hist)] 

464 ind_hist = np.concatenate((ind_hist, new_ind)) 

465 k += 1 

466 v = elementary_vector(n, ind_best) 

467 return est, v, w, nmults, nresamples