1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

256

257

258

259

260

261

262

263

264

265

266

267

268

269

270

271

272

273

274

275

276

277

278

279

280

281

282

283

284

285

286

287

288

289

290

291

292

293

294

295

296

297

298

299

300

301

302

303

304

305

306

307

308

309

310

311

312

313

314

315

316

317

318

319

320

321

322

323

324

325

326

327

328

329

330

331

332

333

334

335

336

337

338

339

340

341

342

343

344

345

346

347

348

349

350

351

352

353

354

355

356

357

358

359

360

361

362

363

364

365

366

367

368

369

370

371

372

373

374

375

376

377

378

379

380

381

382

383

384

385

386

387

388

389

390

391

392

393

394

395

396

397

398

399

400

401

402

403

404

405

406

407

408

409

410

411

412

413

414

415

416

417

418

419

420

421

422

423

424

425

426

427

428

429

430

431

432

433

434

435

436

437

438

439

440

441

442

443

444

445

446

447

448

449

450

451

452

453

454

455

456

457

458

459

460

461

462

463

464

465

466

467

468

"""Sparse block 1-norm estimator. 

""" 

 

from __future__ import division, print_function, absolute_import 

 

import numpy as np 

from scipy.sparse.linalg import aslinearoperator 

 

 

__all__ = ['onenormest'] 

 

 

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

""" 

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

 

Parameters 

---------- 

A : ndarray or other linear operator 

A linear operator that can be transposed and that can 

produce matrix products. 

t : int, optional 

A positive parameter controlling the tradeoff between 

accuracy versus time and memory usage. 

Larger values take longer and use more memory 

but give more accurate output. 

itmax : int, optional 

Use at most this many iterations. 

compute_v : bool, optional 

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

compute_w : bool, optional 

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

 

Returns 

------- 

est : float 

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

v : ndarray, optional 

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

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

that gives an output with particularly large norm. 

w : ndarray, optional 

The vector Av which has relatively large 1-norm. 

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

that is relatively large in norm compared to the input. 

 

Notes 

----- 

This is algorithm 2.4 of [1]. 

 

In [2] it is described as follows. 

"This algorithm typically requires the evaluation of 

about 4t matrix-vector products and almost invariably 

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

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

 

.. versionadded:: 0.13.0 

 

References 

---------- 

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

"A Block Algorithm for Matrix 1-Norm Estimation, 

with an Application to 1-Norm Pseudospectra." 

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

 

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

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

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

 

Examples 

-------- 

>>> from scipy.sparse import csc_matrix 

>>> from scipy.sparse.linalg import onenormest 

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

>>> A.todense() 

matrix([[ 1., 0., 0.], 

[ 5., 8., 2.], 

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

>>> onenormest(A) 

9.0 

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

9.0 

""" 

 

# Check the input. 

A = aslinearoperator(A) 

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

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

 

# If the operator size is small compared to t, 

# then it is easier to compute the exact norm. 

# Otherwise estimate the norm. 

n = A.shape[1] 

if t >= n: 

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

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

raise Exception('internal error: ', 

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

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

if col_abs_sums.shape != (n, ): 

raise Exception('internal error: ', 

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

argmax_j = np.argmax(col_abs_sums) 

v = elementary_vector(n, argmax_j) 

w = A_explicit[:, argmax_j] 

est = col_abs_sums[argmax_j] 

else: 

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

 

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

if compute_v or compute_w: 

result = (est,) 

if compute_v: 

result += (v,) 

if compute_w: 

result += (w,) 

return result 

else: 

return est 

 

 

def _blocked_elementwise(func): 

""" 

Decorator for an elementwise function, to apply it blockwise along 

first dimension, to avoid excessive memory usage in temporaries. 

""" 

block_size = 2**20 

 

def wrapper(x): 

if x.shape[0] < block_size: 

return func(x) 

else: 

y0 = func(x[:block_size]) 

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

y[:block_size] = y0 

del y0 

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

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

return y 

return wrapper 

 

 

@_blocked_elementwise 

def sign_round_up(X): 

""" 

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

 

From Higham and Tisseur: 

"Everything in this section remains valid for complex matrices 

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

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

 

""" 

Y = X.copy() 

Y[Y == 0] = 1 

Y /= np.abs(Y) 

return Y 

 

 

@_blocked_elementwise 

def _max_abs_axis1(X): 

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

 

 

def _sum_abs_axis0(X): 

block_size = 2**20 

r = None 

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

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

if r is None: 

r = y 

else: 

r += y 

return r 

 

 

def elementary_vector(n, i): 

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

v[i] = 1 

return v 

 

 

def vectors_are_parallel(v, w): 

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

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

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

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

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

n = v.shape[0] 

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

 

 

def every_col_of_X_is_parallel_to_a_col_of_Y(X, Y): 

for v in X.T: 

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

return False 

return True 

 

 

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

# column i of X needs resampling if either 

# it is parallel to a previous column of X or 

# it is parallel to a column of Y 

n, t = X.shape 

v = X[:, i] 

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

return True 

if Y is not None: 

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

return True 

return False 

 

 

def resample_column(i, X): 

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

 

 

def less_than_or_close(a, b): 

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

 

 

def _algorithm_2_2(A, AT, t): 

""" 

This is Algorithm 2.2. 

 

Parameters 

---------- 

A : ndarray or other linear operator 

A linear operator that can produce matrix products. 

AT : ndarray or other linear operator 

The transpose of A. 

t : int, optional 

A positive parameter controlling the tradeoff between 

accuracy versus time and memory usage. 

 

Returns 

------- 

g : sequence 

A non-negative decreasing vector 

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

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

The first entry of this vector is therefore a lower bound 

on the 1-norm of the linear operator A. 

This sequence has length t. 

ind : sequence 

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

is given by g[i]. 

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

chosen from range(n), possibly with repetition, 

where n is the order of the operator A. 

 

Notes 

----- 

This algorithm is mainly for testing. 

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

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

so it gives a chance of uncovering bugs related to indexing 

which could have propagated less noticeably to algorithm 2.4. 

 

""" 

A_linear_operator = aslinearoperator(A) 

AT_linear_operator = aslinearoperator(AT) 

n = A_linear_operator.shape[0] 

 

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

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

if t > 1: 

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

X /= float(n) 

 

# Iteratively improve the lower bounds. 

# Track extra things, to assert invariants for debugging. 

g_prev = None 

h_prev = None 

k = 1 

ind = range(t) 

while True: 

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

g = _sum_abs_axis0(Y) 

best_j = np.argmax(g) 

g.sort() 

g = g[::-1] 

S = sign_round_up(Y) 

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

h = _max_abs_axis1(Z) 

 

# If this algorithm runs for fewer than two iterations, 

# then its return values do not have the properties indicated 

# in the description of the algorithm. 

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

# column of A until the second iteration. 

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

# two iterations, even though this requirement is not stated 

# in the description of the algorithm. 

if k >= 2: 

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

break 

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

h = h[ind] 

for j in range(t): 

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

 

# Check invariant (2.2). 

if k >= 2: 

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

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

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

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

 

# Check invariant (2.3). 

if k >= 3: 

for j in range(t): 

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

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

 

# Update for the next iteration. 

g_prev = g 

h_prev = h 

k += 1 

 

# Return the lower bounds and the corresponding column indices. 

return g, ind 

 

 

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

""" 

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

 

Parameters 

---------- 

A : ndarray or other linear operator 

A linear operator that can produce matrix products. 

AT : ndarray or other linear operator 

The transpose of A. 

t : int, optional 

A positive parameter controlling the tradeoff between 

accuracy versus time and memory usage. 

itmax : int, optional 

Use at most this many iterations. 

 

Returns 

------- 

est : float 

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

v : ndarray, optional 

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

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

that gives an output with particularly large norm. 

w : ndarray, optional 

The vector Av which has relatively large 1-norm. 

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

that is relatively large in norm compared to the input. 

nmults : int, optional 

The number of matrix products that were computed. 

nresamples : int, optional 

The number of times a parallel column was observed, 

necessitating a re-randomization of the column. 

 

Notes 

----- 

This is algorithm 2.4. 

 

""" 

# This function is a more or less direct translation 

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

A_linear_operator = aslinearoperator(A) 

AT_linear_operator = aslinearoperator(AT) 

if itmax < 2: 

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

if t < 1: 

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

n = A.shape[0] 

if t >= n: 

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

# Track the number of big*small matrix multiplications 

# and the number of resamplings. 

nmults = 0 

nresamples = 0 

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

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

# for a matrix with nonnegative elements the algorithm converges 

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

# arise in applications [...]" 

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

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

# with a check for and correction of parallel columns, 

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

if t > 1: 

for i in range(1, t): 

# These are technically initial samples, not resamples, 

# so the resampling count is not incremented. 

resample_column(i, X) 

for i in range(t): 

while column_needs_resampling(i, X): 

resample_column(i, X) 

nresamples += 1 

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

X /= float(n) 

# "indices of used unit vectors e_j" 

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

est_old = 0 

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

k = 1 

ind = None 

while True: 

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

nmults += 1 

mags = _sum_abs_axis0(Y) 

est = np.max(mags) 

best_j = np.argmax(mags) 

if est > est_old or k == 2: 

if k >= 2: 

ind_best = ind[best_j] 

w = Y[:, best_j] 

# (1) 

if k >= 2 and est <= est_old: 

est = est_old 

break 

est_old = est 

S_old = S 

if k > itmax: 

break 

S = sign_round_up(Y) 

del Y 

# (2) 

if every_col_of_X_is_parallel_to_a_col_of_Y(S, S_old): 

break 

if t > 1: 

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

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

for i in range(t): 

while column_needs_resampling(i, S, S_old): 

resample_column(i, S) 

nresamples += 1 

del S_old 

# (3) 

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

nmults += 1 

h = _max_abs_axis1(Z) 

del Z 

# (4) 

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

break 

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

# and re-order ind correspondingly." 

# 

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

# entries, so drop the rest 

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

del h 

if t > 1: 

# (5) 

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

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

break 

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

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

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

seen = np.in1d(ind, ind_hist) 

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

for j in range(t): 

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

 

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

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

k += 1 

v = elementary_vector(n, ind_best) 

return est, v, w, nmults, nresamples