Coverage for tbkit/kpm.py: 100%

142 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-15 06:13 +0000

1r""" 

2The kernel polynomial method (KPM; Silver and Roder 1994, Weisse, 

3Wellein, Alvermann and Fehske, Rev. Mod. Phys. 78, 275 (2006)): spectral 

4quantities of a large sparse Hamiltonian from its Chebyshev moments, 

5without diagonalizing it. The spectrum is first mapped into 

6:math:`(-1, 1)`, :math:`\tilde H = (H - b)/a`; then 

7 

8.. math:: 

9 

10 \delta(E - H) = \sum_{m=0}^{M-1} c_m(E)\, T_m(\tilde H)\, ,\qquad 

11 c_m(E) = \frac{(2-\delta_{m0})\,g_m\,T_m(\epsilon)}{a\pi\sqrt{1-\epsilon^2}}\, , 

12 

13with :math:`\epsilon = (E-b)/a`, :math:`T_m` the Chebyshev polynomials 

14(computed by the recursion :math:`T_{m+1}(\tilde H)|r\rangle = 

152\tilde H\,T_m(\tilde H)|r\rangle - T_{m-1}(\tilde H)|r\rangle`, one sparse 

16matrix-vector product each), and :math:`g_m` a kernel that damps the Gibbs 

17oscillations of the truncated series (Jackson: an almost Gaussian 

18broadening of width about :math:`\pi a/M`). Traces are estimated with a 

19few random-phase vectors, :math:`\mathrm{Tr}\,A \approx 

20\frac1R\sum_r\langle r|A|r\rangle`, whose error falls as :math:`1/\sqrt{RN}`: 

21the cost is :math:`O(MRN)` for :math:`N` sites, so millions of sites are 

22within reach. 

23 

24The Hamiltonian must be Hermitian (*System.ham*, a sparse matrix). 

25""" 

26from __future__ import annotations 

27 

28import numpy as np 

29from numpy.typing import ArrayLike, NDArray 

30import scipy.sparse as sparse 

31from scipy.integrate import trapezoid 

32 

33import tbkit.error_handling as error_handling 

34import tbkit.occupation as occupation 

35 

36 

37PI = np.pi 

38 

39 

40def spectral_bounds(ham) -> tuple[float, float]: 

41 r''' 

42 Get bounds on the spectrum of a Hermitian matrix from Gershgorin's 

43 circle theorem: every eigenvalue lies within 

44 :math:`H_{ii} \pm \sum_{j\neq i}|H_{ij}|` for some :math:`i`. 

45 

46 :param ham: Square sparse (or dense) Hermitian matrix. 

47 

48 :returns: 

49 * **e_min**, **e_max** -- Real numbers. 

50 ''' 

51 ham = sparse.csr_matrix(ham) 

52 diag = ham.diagonal().real 

53 radii = np.asarray(abs(ham).sum(axis=1)).ravel() - np.abs(ham.diagonal()) 

54 return float(np.min(diag - radii)), float(np.max(diag + radii)) 

55 

56 

57def jackson_kernel(n_moments: int) -> NDArray[np.float64]: 

58 r''' 

59 Get the Jackson kernel 

60 

61 .. math:: 

62 

63 g_m = \frac{(M-m+1)\cos\frac{\pi m}{M+1} 

64 + \sin\frac{\pi m}{M+1}\cot\frac{\pi}{M+1}}{M+1}\, , 

65 

66 the optimal choice for spectral densities (positive, and resolution 

67 about :math:`\pi/M` in the scaled energy). 

68 

69 :param n_moments: Positive integer :math:`M`. 

70 

71 :returns: 

72 * **g** -- Real ndarray, shape (n_moments,). 

73 ''' 

74 error_handling.positive_int(n_moments, 'n_moments') 

75 m = np.arange(n_moments) 

76 q = PI / (n_moments + 1) 

77 return ((n_moments - m + 1) * np.cos(q * m) + np.sin(q * m) / np.tan(q)) / (n_moments + 1) 

78 

79 

80def lorentz_kernel(n_moments: int, lam: float = 4.) -> NDArray[np.float64]: 

81 r''' 

82 Get the Lorentz kernel :math:`g_m = \sinh[\lambda(1 - m/M)]/\sinh\lambda`, 

83 which gives Lorentzian broadening (the better choice for Green's 

84 functions). 

85 

86 :param n_moments: Positive integer :math:`M`. 

87 :param lam: Positive real. Default value 4. 

88 

89 :returns: 

90 * **g** -- Real ndarray, shape (n_moments,). 

91 ''' 

92 error_handling.positive_int(n_moments, 'n_moments') 

93 error_handling.positive_real(lam, 'lam') 

94 return np.sinh(lam * (1. - np.arange(n_moments) / n_moments)) / np.sinh(lam) 

95 

96 

97def _setup( 

98 ham, n_moments, kernel, bounds, e_grid, n_grid=1001, 

99) -> tuple[sparse.csr_matrix, NDArray[np.float64], NDArray[np.float64], float, float]: 

100 ''' 

101 Private function. Validate, rescale the Hamiltonian, and build the 

102 coefficients c_m(E) (shape (n_moments, len(e_grid))). 

103 ''' 

104 ham = sparse.csr_matrix(ham, dtype='c16') 

105 error_handling.square_matrix(ham, 'ham') 

106 error_handling.hermitian(ham) 

107 error_handling.positive_int(n_moments, 'n_moments') 

108 error_handling.kpm_kernel(kernel) 

109 if bounds is None: 

110 bounds = spectral_bounds(ham) 

111 error_handling.lims(bounds) 

112 e_min, e_max = bounds 

113 a = (e_max - e_min) / (2. - 0.02) # keep the scaled spectrum inside (-0.99, 0.99) 

114 b = (e_max + e_min) / 2. 

115 if e_grid is None: 

116 e_grid = b + a * np.cos(PI * (np.arange(n_grid) + 0.5) / n_grid)[::-1] 

117 e_grid = np.atleast_1d(np.asarray(e_grid, dtype='f8')) 

118 error_handling.ndarray_empty(e_grid, 'e_grid') 

119 eps = (e_grid - b) / a 

120 error_handling.kpm_grid(eps) 

121 g = jackson_kernel(n_moments) if kernel == 'jackson' else lorentz_kernel(n_moments) 

122 tm = np.cos(np.arange(n_moments)[:, None] * np.arccos(eps)[None, :]) 

123 weight = np.where(np.arange(n_moments) == 0, 1., 2.) 

124 coef = (weight * g)[:, None] * tm / (a * PI * np.sqrt(1. - eps ** 2))[None, :] 

125 ham_s = (ham - b * sparse.identity(ham.shape[0], format='csr')) / a 

126 return ham_s, e_grid, coef, a, b 

127 

128 

129def _vectors(n: int, n_random: int | None, seed) -> NDArray[np.complex128]: 

130 ''' 

131 Private function. Random-phase vectors (columns, |r_i| = 1), or, if 

132 n_random is None, the full basis (the exact trace). 

133 ''' 

134 if n_random is None: 

135 return np.eye(n, dtype='c16') 

136 error_handling.positive_int(n_random, 'n_random') 

137 rng = np.random.default_rng(seed) 

138 return np.exp(2j * PI * rng.random((n, n_random))) 

139 

140 

141def _chebyshev(ham_s, vec: NDArray[np.complex128], n_moments: int): 

142 ''' 

143 Private function. Yield T_m(H~) vec for m < n_moments, keeping only two 

144 vectors in memory. 

145 ''' 

146 prev, cur = None, vec 

147 for m in range(n_moments): 

148 yield cur 

149 prev, cur = cur, (ham_s @ cur if m == 0 else 2 * (ham_s @ cur) - prev) 

150 

151 

152def dos( 

153 ham, n_moments: int = 256, n_random: int | None = 10, e_grid: ArrayLike | None = None, 

154 kernel: str = 'jackson', seed=None, bounds: tuple[float, float] | None = None, 

155) -> tuple[NDArray[np.float64], NDArray[np.float64]]: 

156 r''' 

157 Get the density of states :math:`\rho(E) = \mathrm{Tr}\,\delta(E - H)` 

158 (normalized, like *tbkit.dos.density_of_states*, to integrate to the 

159 number of states). 

160 

161 :param ham: Square sparse Hermitian matrix (e.g. *System.ham*). 

162 :param n_moments: Positive integer. Default value 256. Number of 

163 Chebyshev moments (the energy resolution is about 

164 :math:`\pi a/M`, :math:`2a` the spectral width). 

165 :param n_random: Positive integer, or None. Default value 10. Number of 

166 random vectors for the trace; None for the exact trace (N vectors: 

167 small systems only). 

168 :param e_grid: Real array. Default value None: 1001 Chebyshev nodes over 

169 the spectrum. Energies, inside the spectral bounds. 

170 :param kernel: String. Default value 'jackson'. 'jackson' or 'lorentz'. 

171 :param seed: Default value None. Seed of the random vectors. 

172 :param bounds: Pair of reals. Default value None (Gershgorin bounds, see 

173 *spectral_bounds*). Bounds enclosing the whole spectrum. 

174 

175 :returns: 

176 * **e_grid** -- Real ndarray. 

177 * **rho** -- Real ndarray, same shape. 

178 ''' 

179 ham_s, e_grid, coef, _, _ = _setup(ham, n_moments, kernel, bounds, e_grid) 

180 vec = _vectors(ham_s.shape[0], n_random, seed) 

181 mu = np.array([np.vdot(vec, t).real for t in _chebyshev(ham_s, vec, n_moments)]) 

182 if n_random is not None: 

183 mu = mu / n_random # E<r|A|r> = Tr A for random phases 

184 return e_grid, mu @ coef 

185 

186 

187def dos_from_levels( 

188 levels: ArrayLike, n_moments: int, bounds: tuple[float, float], e_grid: ArrayLike, 

189 kernel: str = 'jackson', 

190) -> NDArray[np.float64]: 

191 r''' 

192 Get the KPM density of states of a *known* spectrum: every level 

193 broadened by exactly the truncated, kernel-damped Chebyshev series that 

194 *dos* uses. It is the reference *dos* converges to (up to the random 

195 trace), e.g. for the bands of a periodic model on a k-mesh. 

196 

197 :param levels: Real array. Energy levels. 

198 :param n_moments: Positive integer. Number of Chebyshev moments. 

199 :param bounds: Pair of reals. Spectral bounds (those of *dos*, e.g. 

200 *spectral_bounds* of the Hamiltonian it is compared with). 

201 :param e_grid: Real array. Energies. 

202 :param kernel: String. Default value 'jackson'. 

203 

204 :returns: 

205 * **rho** -- Real ndarray, same shape as *e_grid* (integrating to the number of levels). 

206 ''' 

207 levels = np.asarray(levels, dtype='f8').ravel() 

208 error_handling.ndarray_empty(levels, 'levels') 

209 _, e_grid, coef, a, b = _setup(sparse.identity(1, format='csr'), n_moments, kernel, bounds, e_grid) 

210 error_handling.kpm_grid((levels - b) / a) 

211 tm = np.cos(np.arange(n_moments)[:, None] * np.arccos((levels - b) / a)[None, :]) 

212 return coef.T @ tm.sum(axis=1) 

213 

214 

215def ldos( 

216 ham, site: int, n_moments: int = 256, e_grid: ArrayLike | None = None, 

217 kernel: str = 'jackson', bounds: tuple[float, float] | None = None, 

218) -> tuple[NDArray[np.float64], NDArray[np.float64]]: 

219 r''' 

220 Get the local density of states on one site (or orbital), 

221 :math:`\rho_i(E) = \langle i|\delta(E-H)|i\rangle`, exactly (no random vectors). 

222 

223 :param ham: Square sparse Hermitian matrix. 

224 :param site: Integer. Site (row) index. 

225 :param n_moments: Positive integer. Default value 256. 

226 :param e_grid: See *dos*. 

227 :param kernel: See *dos*. 

228 :param bounds: See *dos*. 

229 

230 :returns: 

231 * **e_grid** -- Real ndarray. 

232 * **rho** -- Real ndarray, same shape. 

233 ''' 

234 ham_s, e_grid, coef, _, _ = _setup(ham, n_moments, kernel, bounds, e_grid) 

235 error_handling.site_index(site, ham_s.shape[0]) 

236 vec = np.zeros(ham_s.shape[0], 'c16') 

237 vec[site] = 1. 

238 mu = np.array([t[site].real for t in _chebyshev(ham_s, vec, n_moments)]) 

239 return e_grid, mu @ coef 

240 

241 

242def conductivity( 

243 ham, x: ArrayLike, n_moments: int = 128, n_random: int | None = 10, 

244 e_grid: ArrayLike | None = None, kernel: str = 'jackson', seed=None, 

245 bounds: tuple[float, float] | None = None, area: float | None = None, 

246) -> tuple[NDArray[np.float64], NDArray[np.float64]]: 

247 r''' 

248 Get the zero-temperature DC conductivity along :math:`x` from the 

249 Kubo-Greenwood formula, 

250 

251 .. math:: 

252 

253 \sigma_{xx}(E) = \frac{\pi}{\Omega}\, 

254 \mathrm{Tr}\left[v_x\,\delta(E-H)\,v_x\,\delta(E-H)\right]\, ,\qquad 

255 v_x = i[H, x]\, , 

256 

257 in units of :math:`e^2/\hbar` (:math:`\hbar = e = 1`), by the 

258 two-dimensional Chebyshev expansion of Weisse et al. (the moments 

259 :math:`\mu_{mn} = \mathrm{Tr}[v_x T_m(\tilde H) v_x T_n(\tilde H)]`, 

260 :math:`O(M N)` memory, the DOS needing only :math:`O(N)`). 

261 

262 :param ham: Square sparse Hermitian matrix. 

263 :param x: Real array, one coordinate per site (e.g. *lat.coor['x']*). 

264 :param n_moments: Positive integer. Default value 128. 

265 :param n_random: Positive integer, or None. Default value 10. See *dos*. 

266 :param e_grid: See *dos*. 

267 :param kernel: See *dos*. 

268 :param seed: See *dos*. 

269 :param bounds: See *dos*. 

270 :param area: Positive real. Default value None (the number of sites): 

271 the area (volume) :math:`\Omega` of the sample. 

272 

273 :returns: 

274 * **e_grid** -- Real ndarray. 

275 * **sigma** -- Real ndarray, same shape. 

276 ''' 

277 ham_s, e_grid, coef, a, b = _setup(ham, n_moments, kernel, bounds, e_grid) 

278 n = ham_s.shape[0] 

279 x = np.asarray(x, dtype='f8') 

280 error_handling.ndarray(x, 'x', n) 

281 if area is None: 

282 area = float(n) 

283 error_handling.positive_real(area, 'area') 

284 # v = i [H, x]: v_ij = i H_ij (x_j - x_i); the scale a comes back below 

285 ham_c = sparse.csr_matrix(ham, dtype='c16') 

286 vel = 1j * (ham_c @ sparse.diags(x) - sparse.diags(x) @ ham_c) 

287 vec = _vectors(n, n_random, seed) 

288 mu = np.zeros((n_moments, n_moments), 'c16') 

289 for r in vec.T: 

290 left = np.array(list(_chebyshev(ham_s, vel @ r, n_moments))) # T_m v |r> 

291 right = np.array([vel @ t for t in _chebyshev(ham_s, r, n_moments)]) # v T_n |r> 

292 mu += left.conj() @ right.T 

293 if n_random is not None: 

294 mu = mu / n_random 

295 sigma = PI / area * np.einsum('me,mn,ne->e', coef, mu, coef).real 

296 return e_grid, sigma 

297 

298 

299def hall_conductivity( 

300 ham, vx, vy, n_moments: int = 256, n_random: int | None = 10, 

301 e_grid: ArrayLike | None = None, kernel: str = 'jackson', seed=None, 

302 bounds: tuple[float, float] | None = None, area: float | None = None, 

303 temperature: float = 0., 

304) -> tuple[NDArray[np.float64], NDArray[np.float64]]: 

305 r''' 

306 Get the Hall conductivity :math:`\sigma_{xy}(\mu)` of a large sample, at 

307 any Fermi level and temperature, from the Kubo-Bastin formula expanded 

308 in Chebyshev polynomials (Garcia, Covaci and Rappoport, Phys. Rev. Lett. 

309 114, 116602 (2015), Eqs. (1)-(5)). Bastin's formula, 

310 

311 .. math:: 

312 

313 \sigma^{B}_{\alpha\beta}(\mu) = \frac{ie^2\hbar}{\Omega}\int d\varepsilon\,f(\varepsilon)\, 

314 \mathrm{Tr}\left[v_\alpha\,\delta(\varepsilon-H)\,v_\beta\,\frac{dG^+}{d\varepsilon} 

315 - v_\alpha\,\frac{dG^-}{d\varepsilon}\,v_\beta\,\delta(\varepsilon-H)\right]\, , 

316 

317 :math:`G^\pm = (\varepsilon - H \pm i0)^{-1}`, becomes, with the spectrum 

318 rescaled into :math:`(-1, 1)`, :math:`\tilde H = (H-b)/a`, 

319 

320 .. math:: 

321 

322 \sigma^{B}_{\alpha\beta}(\mu) = \frac{4e^2\hbar}{\pi\Omega a^2}\int_{-1}^{1} 

323 d\tilde\varepsilon\,\frac{f(\tilde\varepsilon)}{(1-\tilde\varepsilon^2)^2} 

324 \sum_{m,n}\Gamma_{nm}(\tilde\varepsilon)\,\mu^{\alpha\beta}_{nm}\, , 

325 

326 .. math:: 

327 

328 \mu^{\alpha\beta}_{mn} = \frac{g_mg_n}{(1+\delta_{m0})(1+\delta_{n0})} 

329 \mathrm{Tr}\left[v_\alpha T_m(\tilde H)\,v_\beta T_n(\tilde H)\right]\, , 

330 

331 .. math:: 

332 

333 \Gamma_{mn}(\tilde\varepsilon) = (\tilde\varepsilon - in\sqrt{1-\tilde\varepsilon^2})\, 

334 e^{in\theta}\,T_m(\tilde\varepsilon) 

335 + (\tilde\varepsilon + im\sqrt{1-\tilde\varepsilon^2})\,e^{-im\theta}\,T_n(\tilde\varepsilon)\, , 

336 

337 :math:`\theta = \arccos\tilde\varepsilon`, :math:`g_m` the kernel. The 

338 moments :math:`\mu_{mn}` (the expensive part: :math:`5MR` sparse 

339 products, :math:`O(MN)` memory) serve every Fermi level and temperature. 

340 The energy integral runs over :math:`|\tilde\varepsilon|\le 0.995` 

341 (the spectrum lies within :math:`\pm0.99`): at the ends, 

342 :math:`(1-\tilde\varepsilon^2)^{-2}` would only amplify rounding errors. 

343 

344 **Hall part.** The value is computed from the antisymmetric moments 

345 :math:`(\mu^{xy}_{mn} - \mu^{yx}_{mn})/2`, i.e. it is the Hall 

346 conductivity proper, :math:`(\sigma^B_{xy} - \sigma^B_{yx})/2`, the part 

347 that the Berry curvature gives (*KSpace.hall_conductivity*). The 

348 symmetric part :math:`(\sigma^B_{xy} + \sigma^B_{yx})/2` is a 

349 Fermi-surface term, like :math:`\sigma_{xx}` (see *conductivity*): it 

350 vanishes by symmetry in many lattices (e.g. with a :math:`C_3` axis), 

351 but in a clean metal of lower symmetry it grows as the kernel's 

352 broadening :math:`\pi a/M` shrinks (the Drude weight), and with random 

353 vectors it is the main source of noise. Leaving it out removes that 

354 noise too. In a clean metal the random-vector estimate is still noisy 

355 inside the bands (increase *n_random*, or the size of the sample); 

356 disorder, which makes the Fermi-surface terms finite, and gaps, where 

357 they vanish, are much quieter. 

358 

359 **Sign and units.** The value returned is :math:`-(\sigma^B_{xy}-\sigma^B_{yx})/2` in units 

360 of :math:`e^2/h` (:math:`\hbar = e = 1`), the sign of 

361 *KSpace.hall_conductivity*: :math:`C` in the gap of a Chern insulator 

362 of Chern number :math:`C` (*KSpace.chern_number*). Bastin's 

363 :math:`\sigma^B_{xy}` is the Ohm's-law tensor, :math:`j_x = \sigma^B_{xy}E_y` 

364 (see *KSpace.hall_conductivity*). 

365 

366 **Sample.** The trace must run over a sample without edges -- a torus 

367 (*KSpace.finite_ham* and *KSpace.finite_velocity* with 

368 ``periodic=True``): over a whole open flake, the edge currents cancel 

369 the bulk and the trace vanishes in a gap. 

370 

371 :param ham: Square sparse Hermitian matrix. 

372 :param vx: Square sparse (or dense) Hermitian matrix, like *ham*. Velocity 

373 :math:`v_x = i[H, x]` (see *KSpace.finite_velocity*). For a spin 

374 Hall conductivity, pass the spin current :math:`\{s, v_x\}/2` 

375 instead (units :math:`e/2\pi`, see *KSpace.spin_hall_conductivity*). 

376 :param vy: Square sparse (or dense) Hermitian matrix. Velocity :math:`v_y`. 

377 :param n_moments: Positive integer. Default value 256. Number of 

378 Chebyshev moments :math:`M` (energy resolution about :math:`\pi a/M`). 

379 :param n_random: Positive integer, or None. Default value 10. See *dos*. 

380 :param e_grid: Real array. Default value None: 1001 Chebyshev nodes over 

381 the spectrum. Fermi energies :math:`\mu`. 

382 :param kernel: String. Default value 'jackson'. See *dos*. 

383 :param seed: See *dos*. 

384 :param bounds: See *dos*. 

385 :param area: Positive real. Default value None (the number of orbitals). 

386 Area :math:`\Omega` of the sample (e.g. number of cells times the 

387 cell area). 

388 :param temperature: Positive real or zero. Default value 0. In energy units. 

389 

390 :returns: 

391 * **e_grid** -- Real ndarray. 

392 * **sigma** -- Real ndarray, same shape, in units of :math:`e^2/h`. 

393 ''' 

394 ham_s, e_grid, _, a, b = _setup(ham, n_moments, kernel, bounds, e_grid) 

395 n = ham_s.shape[0] 

396 error_handling.velocity(vx, n, 'vx') 

397 error_handling.velocity(vy, n, 'vy') 

398 vx = sparse.csr_matrix(vx, dtype='c16') 

399 vy = sparse.csr_matrix(vy, dtype='c16') 

400 error_handling.hermitian(vx) 

401 error_handling.hermitian(vy) 

402 if area is None: 

403 area = float(n) 

404 error_handling.positive_real(area, 'area') 

405 error_handling.positive_real_zero(temperature, 'temperature') 

406 g = jackson_kernel(n_moments) if kernel == 'jackson' else lorentz_kernel(n_moments) 

407 w = g / np.where(np.arange(n_moments) == 0, 2., 1.) 

408 mu = np.zeros((n_moments, n_moments), 'c16') 

409 for r in _vectors(n, n_random, seed).T: 

410 # <r| v_x T_m = (T_m v_x |r>)^dagger and v_y T_n |r>, for mu^xy; 

411 # the same with x and y swapped, for mu^yx 

412 t_r = list(_chebyshev(ham_s, r, n_moments)) 

413 left_x = np.array(list(_chebyshev(ham_s, vx @ r, n_moments))) 

414 left_y = np.array(list(_chebyshev(ham_s, vy @ r, n_moments))) 

415 right_y = np.array([vy @ t for t in t_r]) 

416 right_x = np.array([vx @ t for t in t_r]) 

417 mu += (left_x.conj() @ right_y.T - left_y.conj() @ right_x.T) / 2 

418 if n_random is not None: 

419 mu = mu / n_random 

420 mu = w[:, None] * mu * w[None, :] 

421 # the energy integrand on a grid uniform in theta = arccos(eps) 

422 n_int = max(2001, 8 * n_moments + 1) 

423 theta = np.linspace(np.arccos(-0.995), np.arccos(0.995), n_int) 

424 eps = np.cos(theta) 

425 root = np.sin(theta) 

426 m = np.arange(n_moments)[:, None] 

427 cheb = np.cos(m * theta[None, :]) 

428 plus = (eps[None, :] - 1j * m * root[None, :]) * np.exp(1j * m * theta[None, :]) 

429 minus = (eps[None, :] + 1j * m * root[None, :]) * np.exp(-1j * m * theta[None, :]) 

430 # sum_{n,m} Gamma_nm mu_nm, Gamma_nm = plus_m T_n + minus_n T_m 

431 gamma_mu = np.sum(cheb * (mu @ plus), axis=0) + np.sum(minus * (mu @ cheb), axis=0) 

432 integrand = (gamma_mu / (1. - eps ** 2) ** 2).real 

433 prefactor = -2 * PI * 4. / (PI * area * a ** 2) # e^2/hbar = 2 pi e^2/h, TKNN sign 

434 energies = a * eps + b 

435 if temperature == 0: 

436 cum = np.concatenate([[0.], np.cumsum(0.5 * (integrand[1:] + integrand[:-1]) * np.diff(eps))]) 

437 sigma = prefactor * np.interp(e_grid, energies, cum) 

438 else: 

439 f = np.array([occupation.fermi_dirac(energies, float(e), temperature) for e in e_grid]) 

440 sigma = prefactor * trapezoid(f * integrand[None, :], eps, axis=1) 

441 return e_grid, sigma 

442