Coverage for gpaw/kpt_refine.py: 72%

236 statements  

« prev     ^ index     » next       coverage.py v7.7.1, created at 2025-07-09 00:21 +0000

1# Copyright (C) 2016 R. Warmbier Materials for Energy Research Group, 

2# Wits University 

3 

4import copy 

5import numpy as np 

6from ase.dft.kpoints import monkhorst_pack, get_monkhorst_pack_size_and_offset 

7from gpaw import KPointError 

8from gpaw.kpt_descriptor import KPointDescriptor 

9 

10""" 

11This file provides routines to create non-uniform k-point grids. We use 

12locally uniform grids of different densities, practically grid refinement. 

13 

14One starts from a standard Monkhorst-Pack grid and chooses a number of grid 

15points, which shall be replaced with a finer grid of a given size. 

16 

17The user can further choose, whether the original symmetry of the system is 

18enforced (careful!) or whether a reduced symmetry should be used. 

19 

20Optionally, the user can ask to add all k+q points, if not already included. 

21 

22Please cite https://doi.org/10.1016/j.cpc.2018.03.001 

23 

24Example (Graphene): 

25 

26calc = GPAW(mode=PW(ecut=450), 

27 kpts={"size":[15,15,1], "gamma":True}, 

28 kpt_refine={"center":[1./3,1./3,0.], "size":[3,3,1], 

29 "reduce_symmetry":False, 

30 "q":[1./15,1./15,0.]}, 

31 ) 

32 

33""" 

34 

35 

36def create_kpoint_descriptor_with_refinement(refine, bzkpts_kc, nspins, atoms, 

37 symmetry, comm, timer): 

38 """Main routine to build refined k-point grids.""" 

39 if 'center' not in refine: 

40 raise RuntimeError('Center for refinement not given!') 

41 if 'size' not in refine: 

42 raise RuntimeError('Grid size for refinement not given!') 

43 

44 center_ic = np.array(refine.get('center'), dtype=float, ndmin=2) 

45 size = np.array(refine.get('size'), ndmin=2) 

46 reduce_symmetry = refine.get('reduce_symmetry', True) 

47 

48 # Check that all sizes are odd. That's not so much an issue really. 

49 # But even Monkhorst-Pack grids have points on the boundary, 

50 # which just would require more special casing, which I want to avoid. 

51 if (np.array(size) % 2 == 0).any(): 

52 raise RuntimeError('Grid size for refinement must be odd! Is: {}'. 

53 format(size)) 

54 

55 # Arguments needed for k-point descriptor construction 

56 kwargs = {'nspins': nspins, 'atoms': atoms, 'symmetry': symmetry, 

57 'comm': comm} 

58 

59 # Define coarse grid points 

60 bzk_coarse_kc = bzkpts_kc 

61 

62 # Define fine grid points 

63 centers_i, bzk_fine_kc, weight_fine_k = get_fine_bzkpts(center_ic, size, 

64 bzk_coarse_kc, 

65 kwargs) 

66 

67 if reduce_symmetry: 

68 # Define new symmetry object ignoring symmetries violated by the 

69 # refined kpoints 

70 kd_fine = create_kpoint_descriptor(bzk_fine_kc, **kwargs) 

71 symm = prune_symmetries_kpoints(kd_fine, symmetry) 

72 del kd_fine 

73 else: 

74 symm = copy.copy(symmetry) 

75 kwargs['symmetry'] = symm 

76 

77 # Create new descriptor with both sets of points 

78 

79 with timer('Create mixed descriptor'): 

80 kd = create_mixed_kpoint_descriptor(bzk_coarse_kc, 

81 bzk_fine_kc, 

82 centers_i, 

83 weight_fine_k, 

84 kwargs) 

85 

86 # Add missing k-points to fulfill group properties with 

87 # zero-weighted points 

88 with timer('Add_missing_points'): 

89 kd = add_missing_points(kd, kwargs) 

90 

91 # Add additional +q k-points, if necessary 

92 if 'q' in refine: 

93 timer.start("+q") 

94 N_coarse_c = get_monkhorst_pack_size_and_offset(bzk_coarse_kc)[0] 

95 bla = N_coarse_c * refine['q'] 

96 if not max(abs(bla - np.rint(bla))) < 1e-8: 

97 kd.refine_info.almostoptical = True 

98 kd = add_plusq_points(kd, refine['q'], kwargs) 

99 symm = kd.symmetry 

100 kwargs['symmetry'] = symm 

101 kd = add_missing_points(kd, kwargs) 

102 timer.stop("+q") 

103 

104 return kd 

105 

106 

107def create_kpoint_descriptor(bzkpts_kc, nspins, atoms, symmetry, comm): 

108 kd = KPointDescriptor(bzkpts_kc, nspins) 

109 # self.timer.start('Set symmetry') 

110 kd.set_symmetry(atoms, symmetry, comm=comm) 

111 # self.timer.stop('Set symmetry') 

112 return kd 

113 

114 

115def create_mixed_kpoint_descriptor(bzk_coarse_kc, bzk_fine_kc, centers_i, 

116 weight_fine_k, kwargs): 

117 assert len(weight_fine_k) == bzk_fine_kc.shape[0] 

118 

119 # Get old Monkhorst-Pack grid points and delete the centers from them 

120 nbzkpts_coarse = bzk_coarse_kc.shape[0] 

121 bzk_new_kc = np.delete(bzk_coarse_kc, centers_i, axis=0) 

122 nbzkpts_coarse_new = bzk_new_kc.shape[0] 

123 # Add refined points 

124 nbzkpts_fine = bzk_fine_kc.shape[0] 

125 bzk_new_kc = np.append(bzk_new_kc, bzk_fine_kc, axis=0) 

126 

127 # Construct the new KPointDescriptor 

128 kd_new = create_kpoint_descriptor(bzk_new_kc, **kwargs) 

129 refine_info = KRefinement() 

130 refine_info.set_unrefined_nbzkpts(nbzkpts_coarse) 

131 label_k = np.array(nbzkpts_coarse_new * ['mh'] + nbzkpts_fine * ['refine']) 

132 refine_info.set_label_k(label_k) 

133 weight_k = np.append(np.ones(nbzkpts_coarse_new), weight_fine_k) 

134 refine_info.set_weight_k(weight_k) 

135 kd_new.refine_info = refine_info 

136 assert len(weight_k) == bzk_new_kc.shape[0] 

137 

138 # This new Descriptor is good, except the weights are completely wrong now. 

139 kd_new.weight_k = np.bincount(kd_new.bz2ibz_k, weight_k) / nbzkpts_coarse 

140 assert np.abs(np.sum(kd_new.weight_k) - 1) < 1e-6 

141 

142 # return kd_new.copy() 

143 return kd_new 

144 

145 

146def get_fine_bzkpts(center_ic, size, bzk_coarse_kc, kwargs): 

147 """Return list of reducible refined kpoints and the indexes for the points 

148 on the coarse kpoint grid. """ 

149 

150 # Define Monkhorst-Pack grid as starting point 

151 kd_coarse = create_kpoint_descriptor(bzk_coarse_kc, **kwargs) 

152 

153 # Determine how many (cubic) nearest neighbour shells to replace 

154 nshells = size.shape[0] 

155 

156 # Get coordinates of neighbour k-points. First index is the 'shell' aka 

157 # order of neighbour. Coordinates always centred around zero. 

158 neighbours_kc = construct_neighbours_by_shells(nshells, kd_coarse.N_c) 

159 

160 # loop over all the different shells 

161 centers_i = np.empty((0), dtype=int) 

162 bzk_fine_kc = [] 

163 weight_k = [] 

164 for shell in range(nshells): 

165 # Determine all k-points in shell, which will be refinement centers 

166 shell_kc = [] 

167 for i, center_c in enumerate(center_ic): 

168 shell_c = neighbours_kc[shell] + center_c 

169 shell_kc.append(shell_c) 

170 shell_kc = np.concatenate(shell_kc) 

171 

172 # Find all equivalent centers using full symmetry 

173 center_i = [] 

174 for k, shell_c in enumerate(shell_kc): 

175 equiv_i = find_equivalent_kpoints(shell_c, kd_coarse) 

176 center_i.append(equiv_i) 

177 center_i = np.concatenate(center_i) 

178 

179 # Remove redundant entries, which come from symmetry 

180 center_i, index_map = np.unique(center_i, return_index=True) 

181 

182 # Append to list for all shells 

183 # Remove also points which occur also in a previous shell. This can 

184 # happen due to symmetry or overlap of centers' shells 

185 center_i = np.setdiff1d(center_i, centers_i) 

186 centers_i = np.append(centers_i, center_i) 

187 

188 # Assign the kpoint vectors corresponding to the indexes 

189 centers_kc = bzk_coarse_kc[center_i] 

190 

191 # Get scaled Monkhorst-Pack for refinement 

192 mh_kc = get_reduced_monkhorst(size[shell], kd_coarse.N_c) 

193 

194 # Gather the refined points of the grid for all centers 

195 for k, centers_c in enumerate(centers_kc): 

196 this_kc = mh_kc + centers_c 

197 bzk_fine_kc.append(this_kc) 

198 

199 # Determine weight of refined points 

200 weight = 1. / np.prod(size[shell]) 

201 nkpts = len(center_i) * np.prod(size[shell]) 

202 weight *= np.ones(nkpts) 

203 weight_k.append(weight) 

204 

205 weight_k = np.concatenate(weight_k) 

206 bzk_fine_kc = np.concatenate(bzk_fine_kc) 

207 

208 assert np.abs(len(centers_i) - sum(weight_k)) < 1e-6 

209 assert len(weight_k) == bzk_fine_kc.shape[0] 

210 

211 return centers_i, bzk_fine_kc, weight_k 

212 

213 

214def find_equivalent_kpoints(point, kd): 

215 """Find coordinates and BZ index for all refinement centers.""" 

216 

217 # check whether point in bzkpts_kc 

218 min_dist, k = minimal_point_distance(point, kd.bzk_kc) 

219 if min_dist > 1e-8: 

220 raise RuntimeError('Could not find k-point in kd list!') 

221 

222 equiv_k = list(set(kd.bz2bz_ks[k])) 

223 # equiv_kc = kd.bzk_kc[equiv_k] 

224 

225 return np.array(equiv_k) # , equiv_kc 

226 

227 

228def get_reduced_monkhorst(size, N_c): 

229 """Find monkhorst_pack according to size of refined grid and shrink it into 

230 the volume of one original kpoint.""" 

231 

232 mh_kc = monkhorst_pack(size) 

233 return mh_kc / N_c 

234 

235 

236def construct_neighbours_by_shells(nshells, N_c): 

237 """Construct a list of neighbours (translations from center for each 

238 shell around the center.""" 

239 

240 neighbours = [] 

241 

242 # The innermost shell is always the center itself 

243 neighbours.append(np.array([0, 0, 0], dtype=float, ndmin=2)) 

244 

245 # Loop through the other shells 

246 for shell in range(1, nshells): 

247 # Construct the displacements/elements for each dimension. 

248 elements = [] 

249 for i in range(3): 

250 if N_c[i] == 1: 

251 elements.append([0, ]) 

252 else: 

253 elements.append(list(range(-shell, shell + 1))) 

254 

255 # Construct the vectors 

256 # For each valid point, at least one component must have the value 

257 # of the shell index (+ or -). 

258 this_list = [] 

259 for cx in elements[0]: 

260 for cy in elements[1]: 

261 for cz in elements[2]: 

262 candidate = [cx, cy, cz] 

263 if (np.abs(np.array(candidate)) == shell).any(): 

264 this_list.append(candidate) 

265 

266 this_list = np.array(this_list, dtype=float, ndmin=2) / N_c 

267 neighbours.append(this_list) 

268 

269 return np.array(neighbours) 

270 

271 

272def prune_symmetries_kpoints(kd, symmetry): 

273 """Remove the symmetries which are violated by a kpoint set.""" 

274 

275 new_symmetry = copy.copy(symmetry) 

276 

277 nsyms = len(symmetry.op_scc) 

278 

279 where = np.where(~np.any(kd.bz2bz_ks[:, 0:nsyms] == -1, 0)) 

280 

281 new_symmetry.op_scc = new_symmetry.op_scc[where] 

282 new_symmetry.ft_sc = new_symmetry.ft_sc[where] 

283 new_symmetry.a_sa = new_symmetry.a_sa[where] 

284 

285 inv_cc = -np.eye(3, dtype=int) 

286 new_symmetry.has_inversion = (new_symmetry.op_scc == 

287 inv_cc).all(2).all(1).any() 

288 

289 return new_symmetry 

290 

291 

292def find_missing_points(kd): 

293 """Find points in k-point descriptor, which miss in the set to fill the 

294 group.""" 

295 if -1 not in kd.bz2bz_ks: 

296 return None 

297 

298 # Find unaccounted points 

299 buf = np.empty((0, 3)) 

300 for k in range(kd.bz2bz_ks.shape[0]): 

301 for s in range(kd.bz2bz_ks.shape[1]): 

302 if kd.bz2bz_ks[k, s] == -1: 

303 sign = 1.0 

304 i = s 

305 if kd.symmetry.time_reversal: 

306 if s / kd.bz2bz_ks.shape[1] >= 0.5: 

307 i = s - int(kd.bz2bz_ks.shape[1] / 2) 

308 sign = -1. 

309 k_c = sign * np.dot(kd.symmetry.op_scc[i], kd.bzk_kc[k]) 

310 k_c -= np.rint(k_c) 

311 # Check that k_c hasn't been added yet 

312 if buf.shape[0] > 0: 

313 min_dist, index = minimal_point_distance(k_c, buf) 

314 if min_dist < 1e-6: 

315 continue 

316 buf = np.concatenate((buf, np.array(k_c, ndmin=2))) 

317 

318 return buf 

319 

320 

321def add_missing_points(kd, kwargs): 

322 """Add points to k-point descriptor, which miss to fill the group.""" 

323 add_points_kc = find_missing_points(kd) 

324 if add_points_kc is None: 

325 return kd 

326 

327 kd_new = create_new_descriptor_with_zero_points(kd, add_points_kc, kwargs) 

328 assert -1 not in kd_new.bz2bz_ks 

329 

330 return kd_new 

331 

332 

333def add_plusq_points(kd, q_c, kwargs): 

334 """Add +q points to k-point descriptor, if missing. Also, reduce the 

335 symmetry of the system as necessary.""" 

336 

337 # Add missing points to retrieve full symmetry. Might be redundant. 

338 _kd = add_missing_points(kd, kwargs) 

339 

340 # Find missing q 

341 add_points_kc = [] 

342 for k in range(_kd.nbzkpts): 

343 # if q_c is small, use q_c = 0.0 for mh points, else they don't need 

344 # extra points anyway 

345 if _kd.refine_info.label_k[k] == 'mh': 

346 continue 

347 try: 

348 _kd.find_k_plus_q(q_c, [k]) 

349 except KPointError: 

350 k_c = _kd.bzk_kc[k] + q_c 

351 add_points_kc.append(np.array(k_c, ndmin=2)) 

352 add_points_kc = np.concatenate(add_points_kc) 

353 

354 if add_points_kc.shape[0] == 0: 

355 return _kd 

356 

357 # Find reduced +q symmetry 

358 bzk_kc = np.append(_kd.bzk_kc, add_points_kc, axis=0) 

359 _kd_new = create_kpoint_descriptor(bzk_kc, **kwargs) 

360 symm_new = prune_symmetries_kpoints(_kd_new, kwargs.get('symmetry')) 

361 kwargs['symmetry'] = symm_new 

362 del _kd_new, _kd 

363 

364 kd_new = create_new_descriptor_with_zero_points(kd, add_points_kc, kwargs) 

365 return kd_new 

366 

367 

368def create_new_descriptor_with_zero_points(kd, add_points_kc, kwargs): 

369 """Create a new k-point descriptor including some additional zero-weighted 

370 points.""" 

371 bzk_kc = np.append(kd.bzk_kc, add_points_kc, axis=0) 

372 nbzkpts_add = add_points_kc.shape[0] 

373 

374 # Make sure all points are unique 

375 assert bzk_kc.shape[0] == np.vstack([tuple(row) 

376 for row in bzk_kc]).shape[0] 

377 

378 # Construct the new KPointDescriptor 

379 kd_new = create_kpoint_descriptor(bzk_kc, **kwargs) 

380 

381 # Update refine_info 

382 kd_new.refine_info = kd.refine_info.copy() 

383 label_k = np.append(kd.refine_info.label_k, 

384 np.array(nbzkpts_add * ['zero'])) 

385 kd_new.refine_info.set_label_k(label_k) 

386 # Avoid exact zero, as that would screw up the occupations calculation 

387 weight_k = np.append(kd.refine_info.weight_k, 1e-10 * np.ones(nbzkpts_add)) 

388 kd_new.refine_info.set_weight_k(weight_k) 

389 

390 # Correct ibz weights 

391 kd_new.weight_k = np.bincount(kd_new.bz2ibz_k, weight_k) 

392 kd_new.weight_k *= 1.0 / kd_new.refine_info.get_unrefined_nbzkpts() 

393 assert np.abs(np.sum(kd_new.weight_k) - 1) < 1e-6 

394 

395 return kd_new 

396 

397 

398def minimal_point_distance(point_c, bzk_kc): 

399 d_kc = point_c - bzk_kc 

400 d_k = abs(d_kc - d_kc.round()).sum(1) 

401 k = d_k.argmin() 

402 return d_k[k], k 

403 

404 

405class KRefinement: 

406 """Additional information for refined kpoint grids. 

407 """ 

408 def __init__(self): 

409 self.mhnbzkpts = None 

410 self.label_k = None 

411 self.weight_k = None 

412 self.almostoptical = None 

413 

414 def __str__(self): 

415 s = "Using k-point grid refinement" 

416 if self.almostoptical: 

417 s += " with almostoptical approximation" 

418 s += "\n" 

419 return s 

420 

421 def set_unrefined_nbzkpts(self, mhnbzkpts): 

422 self.mhnbzkpts = mhnbzkpts 

423 

424 def get_unrefined_nbzkpts(self): 

425 return self.mhnbzkpts 

426 

427 def set_weight_k(self, weight_k): 

428 self.weight_k = weight_k 

429 

430 def get_weight_k(self): 

431 return self.weight_k 

432 

433 def set_label_k(self, label_k): 

434 self.label_k = label_k 

435 

436 def get_label_k(self): 

437 return self.label_k 

438 

439 def copy(self): 

440 refine_info = KRefinement() 

441 refine_info.mhnbzkpts = self.mhnbzkpts 

442 refine_info.label_k = self.label_k 

443 refine_info.weight_k = self.weight_k 

444 refine_info.almostoptical = self.almostoptical 

445 return refine_info