Coverage for gpaw/cdft/cdft.py: 71%

595 statements  

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

1"""Module for constrained DFT 

2 

3for review see Chem. Rev., 2012, 112 (1), pp 321-370 

4article on GPAW implementation: 

5J. Chem. Theory Comput., 2016, 12 (11), pp 5367-5378 

6""" 

7 

8import copy 

9import functools 

10from math import pi 

11 

12import numpy as np 

13from ase.calculators.calculator import Calculator 

14from ase.data import atomic_numbers, covalent_radii 

15from ase.units import Bohr, Hartree 

16from ase.utils import IOContext 

17from scipy.optimize import minimize 

18 

19from gpaw.external import ExternalPotential 

20 

21 

22class CDFT(Calculator): 

23 implemented_properties = ['energy', 'forces'] 

24 

25 def __init__(self, 

26 calc, 

27 atoms, 

28 charge_regions=[], 

29 charges=None, 

30 spin_regions=[], 

31 spins=None, 

32 charge_coefs=None, 

33 spin_coefs=None, 

34 promolecular_constraint=False, 

35 txt='-', 

36 minimizer_options={'gtol': 0.01}, 

37 Rc={}, 

38 mu={'Li': 0.5, 

39 'F': 0.7, 

40 'O': 0.7, 

41 'V': 0.5}, 

42 method='BFGS', 

43 forces='analytical', 

44 use_charge_difference=False, 

45 compute_forces=True, 

46 maxstep=100, 

47 tol=1e-3, 

48 bounds=None, 

49 restart=False, 

50 hess=None): 

51 """Constrained DFT calculator. 

52 

53 calc: GPAW instance 

54 DFT calculator object to be constrained. 

55 charge_regions: list of list of int 

56 Atom indices of atoms in the different charge_regions. 

57 spin_regions: list of list of int 

58 Atom indices of atoms in the different spin_regions. 

59 charges: list of float 

60 constrained charges in the different charge_regions. 

61 spins: list of float 

62 constrained spins in the different charge_regions. 

63 Value of 1 sets net magnetisation of one up/alpha electron 

64 charge_coefs: list of float 

65 Initial values for charge constraint coefficients (eV). 

66 spin_coefs: list of float 

67 Initial values for spin constraint coefficients (eV). 

68 promolecular_constraint: bool 

69 Define charge and/or spin constraints from promolecular 

70 densities, see: dx.doi.org/10.1021/cr200148b Eq. 29-31 

71 If true, user specified charges/spins are overwritten! 

72 The atoms (of Atoms object) specifying the charge/spin regions 

73 need to contain have correct charge/spin state! 

74 (atoms.set_initial_charges([atomic_charges]) and 

75 atoms.set_initial_magnetic_moments([moments])) 

76 txt: None or str or file descriptor 

77 Log file. Default id '-' meaning standard out. Use None for 

78 no output. 

79 minimizer_options: dict 

80 options for scipy optimizers, see:scipy.optimize.minimize 

81 method: str 

82 One of scipy optimizers, e.g., BFGS, CG 

83 forces: str 

84 cDFT weight function contribution to forces 

85 'fd' for finite difference or 'analytical' 

86 difference: bool 

87 If True, constrain charge difference between two regions 

88 Then charge_regions needs two regions and charges needs 

89 only one item which is the charge difference between 

90 the two regions, the first beign donor, the second acceptor 

91 

92 If False, each region is treated with the corresponding 

93 charge constraint 

94 compute_forces: bool 

95 Should the forces be computed? 

96 restart: bool 

97 starting from an old calculation from gpw 

98 hess: '2-point', '3-point', 'cs' 

99 scipy hessian approximation 

100 """ 

101 

102 Calculator.__init__(self) 

103 

104 self.calc = calc 

105 self.restart = restart 

106 self.iocontext = IOContext() 

107 self.log = self.iocontext.openfile(txt, calc.world) 

108 self.method = method 

109 self.forces = forces 

110 self.options = minimizer_options 

111 self.difference = use_charge_difference 

112 self.compute_forces = compute_forces 

113 self.Rc = Rc 

114 self.mu = mu 

115 # set charge constraints and lagrangians 

116 self.v_i = np.empty(shape=(0, 0)) 

117 

118 self.constraints = np.empty(shape=(0, 0)) 

119 

120 self.charge_regions = charge_regions 

121 self.spin_regions = spin_regions 

122 self._n_charge_regions = len(self.charge_regions) 

123 self._n_spin_regions = len(self.spin_regions) 

124 self._n_regions = self._n_charge_regions + self._n_spin_regions 

125 

126 self.max_step = maxstep 

127 self.tol = tol 

128 self.gtol = minimizer_options['gtol'] 

129 self.bounds = bounds 

130 

131 if self.bounds is not None: 

132 self.bounds = np.asarray(self.bounds) / Hartree 

133 self.hess = hess 

134 

135 if self.difference: 

136 # difference calculation only for 2 charge regions 

137 if self.n_spin_regions != 0 or self._n_charge_regions != 2: 

138 raise ValueError('No spin constraints ' 

139 'for charge difference calculations and' 

140 ' only two regions allowed') 

141 assert (self.n_charge_regions == 1) 

142 

143 if self.n_charge_regions == 0: 

144 self.regions = [] 

145 

146 else: 

147 self.charge_i = np.array(charges, dtype=float) 

148 

149 if charge_coefs is None: # to Hartree 

150 self.v_i = 0.1 * np.sign(self.charge_i) 

151 else: 

152 self.v_i = np.array(charge_coefs) / Hartree 

153 

154 if not self.difference: 

155 self.regions = copy.copy(self.charge_regions) 

156 

157 # The objective is to constrain the number of electrons (nel) 

158 # in a certain region --> convert charge to nel 

159 Zn = np.zeros(self.n_charge_regions) 

160 for j in range(len(Zn)): 

161 for atom in atoms[self.charge_regions[j]]: 

162 Zn[j] += atom.number 

163 

164 # combined spin and charge constraints 

165 self.constraints = Zn - self.charge_i 

166 

167 else: # constrain charge between two regions 

168 nD = 0. # neutral donor 

169 nA = 0. # neutral acceptor 

170 

171 for atom in atoms[charge_regions[0]]: 

172 nD += atom.number 

173 for atom in atoms[charge_regions[1]]: 

174 nA += atom.number 

175 

176 self.dn_core = nD - nA # difference of core 

177 self.constraints = [self.dn_core - charges[0]] 

178 self.regions = charge_regions 

179 

180 # set spin constraints 

181 if self.n_spin_regions != 0 and not self.difference: 

182 spin_i = np.array(spins, dtype=float) 

183 self.constraints = np.append(self.constraints, spin_i) 

184 

185 if spin_coefs is None: # to Hartree 

186 v_is = 0.1 * np.sign(spin_i) 

187 else: 

188 v_is = np.array(spin_coefs) / Hartree 

189 

190 self.v_i = np.append(self.v_i, v_is) 

191 # self.regions.append(spin_regions) 

192 for i in range(self.n_spin_regions): 

193 self.regions.append(self.spin_regions[i]) 

194 

195 # initialise without v_ext 

196 atoms.calc = self.calc 

197 if not self.restart: 

198 atoms.get_potential_energy() 

199 

200 assert atoms.calc.wfs.nspins == 2 

201 

202 self.cdft_initialised = False 

203 

204 self.atoms = atoms 

205 self.gd = self.calc.density.finegd 

206 

207 if promolecular_constraint: 

208 self.constraints = get_promolecular_constraints( 

209 calc=self.calc, 

210 atoms=self.atoms, 

211 charge_regions=self.charge_regions, 

212 spin_regions=self.spin_regions, 

213 charges=charges, 

214 spins=spins) 

215 

216 # get number of core electrons at each constrained region 

217 # used for pseudo free energy to neglect core contributions 

218 # in coupling calculation 

219 self.n_core_electrons = np.zeros(len(self.regions)) 

220 for a in self.atoms: 

221 for r in range(len(self.regions[:self.n_charge_regions])): 

222 if a.index in self.regions[r] and not self.difference: 

223 n_core = a.number - self.calc.wfs.setups[a.index].Nv 

224 self.n_core_electrons[r] += n_core 

225 elif a.index in self.regions[r] and self.difference: 

226 if r == 0: 

227 n_core = a.number - self.calc.wfs.setups[a.index].Nv 

228 self.n_core_electrons[r] += n_core 

229 else: 

230 n_core = a.number - self.calc.wfs.setups[a.index].Nv 

231 self.n_core_electrons[r] -= n_core 

232 

233 w = WeightFunc(self.gd, self.atoms, None, self.Rc, self.mu) 

234 self.Rc, self.mu = w.get_Rc_and_mu() 

235 # construct cdft potential 

236 self.ext = CDFTPotential(regions=self.regions, 

237 gd=self.gd, 

238 atoms=self.atoms, 

239 constraints=self.constraints, 

240 n_charge_regions=self.n_charge_regions, 

241 difference=self.difference, 

242 log=self.log, 

243 vi=self.v_i, 

244 Rc=self.Rc, 

245 mu=self.mu) 

246 

247 self.calc.set(external=self.ext) 

248 

249 self.w = self.ext.w_ig 

250 

251 def __del__(self): 

252 self.iocontext.close() 

253 

254 def calculate(self, atoms, properties, system_changes): 

255 # check we're dealing with same atoms 

256 if atoms != self.atoms: 

257 self.atoms = atoms 

258 if not self.restart: 

259 Calculator.calculate(self, self.atoms) 

260 

261 Calculator.calculate(self, self.atoms) 

262 

263 # update positions and weight functions 

264 if 'positions' in system_changes or not self.cdft_initialised: 

265 self.ext.set_positions_and_atomic_numbers( 

266 self.atoms.positions / Bohr, self.atoms.numbers) 

267 self.cdft_initialised = True 

268 

269 self.atoms.calc = self.calc 

270 

271 p = functools.partial(print, file=self.log) 

272 self.iteration = 0 

273 self.old_v_i = self.v_i.copy() 

274 

275 def f(v_i): 

276 # nonlocal iteration 

277 # very simple step size control 

278 diff = v_i - self.old_v_i 

279 

280 if np.any(np.abs(diff) >= self.max_step / Hartree): 

281 v_i = self.old_v_i + np.sign( 

282 v_i - self.old_v_i) * self.max_step / Hartree 

283 

284 self.ext.set_levels(v_i) 

285 self.v_i = v_i 

286 # cDFT free energy <A|H^KS + V_a w_a|A> = Edft + <A|Vw|A> 

287 self.Ecdft = self.atoms.get_potential_energy() # in eV 

288 

289 # cDFT corrections to energy 

290 self.get_atomic_density_correction() 

291 self.Ecdft += self.get_energy_correction() * Hartree 

292 

293 # get the cDFT gradient 

294 dn_i = [] 

295 Delta_n = self.get_energy_correction(return_density=True) 

296 

297 if self.calc.density.nt_sg is None: 

298 self.density.interpolate_pseudo_density() 

299 

300 self.nt_ag = self.calc.density.nt_sg[0] 

301 self.nt_bg = self.calc.density.nt_sg[1] 

302 total_electrons = [] 

303 

304 if self.n_charge_regions != 0: 

305 # total pseudo electron density 

306 n_gt = self.nt_ag + self.nt_bg 

307 

308 n_electrons = (self.gd.integrate( 

309 self.ext.w_ig[0:self.n_charge_regions] * n_gt, 

310 global_integral=True)) 

311 

312 # corrections 

313 n_electrons += Delta_n[0:self.n_charge_regions] 

314 

315 # constraint 

316 diff = n_electrons - self.constraints[0:self.n_charge_regions] 

317 total_electrons.append(n_electrons) 

318 dn_i.append(diff) 

319 

320 if self.n_spin_regions != 0: 

321 # difference of pseudo spin densities 

322 Dns_gt = (self.nt_ag - self.nt_bg) 

323 n_electrons = self.gd.integrate( 

324 self.ext.w_ig[self.n_charge_regions:] * Dns_gt, 

325 global_integral=True) 

326 # corrections 

327 n_electrons += Delta_n[self.n_charge_regions:] 

328 # constraint 

329 diff = n_electrons - self.constraints[self.n_charge_regions:] 

330 total_electrons.append(n_electrons) 

331 dn_i.append(diff) 

332 

333 self.dn_i = np.asarray(dn_i) 

334 self.dn_i = self.dn_i.flatten() 

335 total_electrons = np.asarray(total_electrons) 

336 self.w = self.ext.w_ig 

337 # Do not include external potential 

338 E_KS = get_ks_energy_wo_external(self.calc) 

339 

340 self.Edft = E_KS 

341 # pseudo free energy, neglecting core electrons as done for 

342 # coupling constant calculation 

343 if not self.difference: 

344 

345 self.Ecdft = E_KS + np.dot( 

346 self.v_i * Hartree, 

347 (total_electrons - self.n_core_electrons)[0]) 

348 

349 if self.iteration == 0: 

350 p(f'Optimizer: {self.method}') 

351 p(f'Optimizer setups:{self.options}') 

352 

353 header = '----------------------------------------\n' 

354 header += 'Iteration: {0}\n' 

355 header += 'Energy: {1:10.8f} eV\n' 

356 header += 'Coefficients [eV]: {2} \n' 

357 header += 'Errors [e]: {3} \n' 

358 

359 dn_intermediate = self.dn_i.tolist() 

360 p( 

361 header.format(self.iteration, 

362 self.Edft, 

363 ''.join(f'{v:4.3f} ' 

364 for v in self.v_i * Hartree), 

365 ''.join(f'{dn:6.4f} ' 

366 for dn in dn_intermediate), 

367 fill='left', 

368 align='left')) 

369 

370 self.log.flush() 

371 self.iteration += 1 

372 

373 self.old_v_i = self.v_i.copy() 

374 return np.max(np.abs(self.dn_i)) 

375 

376 m = minimize(f, 

377 self.v_i, 

378 jac=self.jacobian, 

379 hess=self.hess, 

380 bounds=self.bounds, 

381 tol=self.tol, 

382 method=self.method, 

383 options=self.options) 

384 

385 p(str(m.message) + '\n') 

386 

387 self.density = self.calc.density # TS09-vdw needs this 

388 self.results['energy'] = self.Edft 

389 

390 # print to log 

391 

392 p('Final DFT energy : ' + str(self.Edft) + ' eV \n') 

393 p('CDFT free energy <A|H+Vw|A> : ' + str(self.Ecdft) + ' eV \n') 

394 

395 if self.compute_forces: 

396 f = WeightFunc(self.gd, 

397 self.atoms, 

398 self.regions, 

399 self.Rc, 

400 self.mu, 

401 new=False) 

402 

403 f_cdft = f.get_cdft_forces2(dens=self.calc.density, 

404 v_i=self.v_i, 

405 n_charge_regions=self.n_charge_regions, 

406 n_spin_regions=self.n_spin_regions, 

407 w_ig=self.w, 

408 method=self.forces, 

409 difference=self.difference) 

410 

411 self.calc.wfs.world.broadcast(f_cdft, 0) 

412 self.ext.set_forces(f_cdft) 

413 self.results['forces'] = self.atoms.get_forces() 

414 

415 def get_weight(self, save=True, name='weight', pad=False): 

416 if not pad: 

417 w_g = self.w 

418 else: 

419 c = CDFTPotential(regions=self.regions, 

420 gd=self.gd, 

421 atoms=self.atoms, 

422 constraints=self.constraints, 

423 n_charge_regions=self.n_charge_regions, 

424 difference=self.difference, 

425 vi=self.v_i, 

426 Rc=self.Rc, 

427 mu=self.mu, 

428 log=self.log) 

429 

430 w_g = c.initialize_partitioning(self.gd, 

431 construct=True, 

432 pad=True, 

433 global_array=True) 

434 if save: 

435 w_s = self.gd.collect(w_g, broadcast=True) 

436 

437 if self.gd.comm.rank == 0: 

438 np.save('coarse_weight', w_s) 

439 return w_g 

440 

441 def jacobian(self, v_i): 

442 if np.all(np.abs(self.dn_i) < self.gtol): 

443 # forces scipy opt to converge when gtol is reached 

444 return np.zeros(len(self.v_i)) 

445 else: 

446 return -self.dn_i 

447 

448 def cdft_free_energy(self): 

449 return self.Ecdft 

450 

451 def dft_energy(self): 

452 return self.Edft 

453 

454 def get_lagrangians(self): 

455 return self.v_i * Hartree 

456 

457 def get_constraints(self): 

458 return self.constraints 

459 

460 def get_grid(self): 

461 return self.gd 

462 

463 def get_all_electron_density(self, gridrefinement=2, spin=None): 

464 return self.calc.get_all_electron_density( 

465 gridrefinement=gridrefinement, spin=spin) 

466 

467 def get_atomic_density_correction(self, return_els=False): 

468 # eq. 20 of the paper 

469 self.dn_s = np.zeros((2, len(self.atoms))) 

470 

471 for i in [0, 1]: 

472 for a, D_sp in self.calc.density.D_asp.items(): 

473 self.dn_s[i, a] += np.sqrt(4 * np.pi) * ( 

474 np.dot(D_sp[i], 

475 self.calc.wfs.setups[a].Delta_pL)[0] + 

476 self.calc.wfs.setups[a].Delta0 / 2) 

477 

478 self.gd.comm.sum(self.dn_s) 

479 for a in range(len(self.atoms)): 

480 self.dn_s[:, a] += self.atoms[a].number / 2. 

481 if return_els: 

482 return self.dn_s 

483 

484 def get_energy_correction(self, return_density=False): 

485 # Delta n^a part of eq 21 

486 

487 # for each region 

488 n_a = np.zeros(len(self.regions)) 

489 

490 # int w_i Dn_i for both spins 

491 # in spin constraints w_ib = -w_ia 

492 # inside augmentation spheres w_i = 1 

493 

494 for c in range(len(self.regions)): 

495 # sum all atoms in a region 

496 n_sa = self.dn_s[0, self.regions[c]].sum() 

497 n_sb = self.dn_s[1, self.regions[c]].sum() 

498 # total density correction 

499 n_a[c] = n_sa + n_sb 

500 

501 for s in range(self.n_spin_regions): 

502 n_sa = self.dn_s[0, self.regions[self.n_charge_regions + s]].sum() 

503 n_sb = self.dn_s[1, self.regions[self.n_charge_regions + s]].sum() 

504 n_a[self.n_charge_regions + s] = n_sa - n_sb 

505 

506 if return_density: 

507 if not self.difference: 

508 # Delta n^a, eq 20 

509 return n_a 

510 else: 

511 # the difference of corrections 

512 return [n_a[0] - n_a[1]] 

513 

514 else: 

515 if not self.difference: 

516 return (np.dot(self.v_i, n_a)) 

517 else: 

518 # negative for difference acceptor 

519 vi_temp = np.array([self.v_i[0], -self.v_i[0]]) 

520 return (np.dot(vi_temp, n_a)) 

521 

522 def get_number_of_electrons_on_atoms(self): 

523 """Return the number of electrons with each gaussian.""" 

524 

525 nelectrons = [] 

526 ae_dens_correction = self.get_atomic_density_correction( 

527 return_els=True) 

528 if self.calc.density.nt_sg is None: 

529 self.calc.density.interpolate_pseudo_density() 

530 

531 dens = self.calc.density.nt_sg[0] + self.calc.density.nt_sg[1] 

532 

533 for atom in self.atoms: 

534 # weight function with one atom 

535 f = WeightFunc(self.gd, 

536 self.atoms, [atom.index], 

537 self.Rc, 

538 self.mu, 

539 new=False) 

540 

541 w = f.construct_weight_function() 

542 n_el = (self.gd.integrate(w * dens, global_integral=True)) 

543 # corrections 

544 n_el += (ae_dens_correction[:, atom.index]).sum() 

545 nelectrons.append(n_el) 

546 

547 return nelectrons 

548 

549 def write(self, name, mode=None): 

550 self.calc.write(name, mode=mode) 

551 

552 def save_parameters(self, name='initial', save_weight=True, save_wfs=True): 

553 if self.gd.comm.rank == 0: 

554 file = open(name + '.txt', 'w') 

555 file.write('NA = %f ,\n' % (self.constraints)) 

556 file.write('FA = %f , \n' % (self.Ecdft)) 

557 file.write('EA = %f , \n' % (self.Edft)) 

558 file.write('Va = %f , \n' % (self.v_i * Hartree)) 

559 file.write('N_charge_regions_A = %d ,\n' % self.n_charge_regions) 

560 file.close() 

561 

562 def get_weight_function_on_coarse_grid(self, 

563 regions, 

564 gd, 

565 atoms, 

566 constraints, 

567 n_charge_regions, 

568 difference, 

569 save=True): 

570 

571 gd = self.calc.density.gd 

572 c = CDFTPotential(regions=self.regions, 

573 gd=gd, 

574 atoms=self.atoms, 

575 constraints=self.constraints, 

576 n_charge_regions=self.n_charge_regions, 

577 difference=self.difference, 

578 vi=self.v_i, 

579 log=self.log) 

580 

581 w_G = c.initialize_partitioning(gd, construct=True) 

582 

583 if save: 

584 w_s = gd.collect(w_G, broadcast=True) 

585 if gd.comm.rank == 0: 

586 np.save('coarse_weight', w_s) 

587 return w_G 

588 

589 def get_coarse_grid(self, save=True): 

590 

591 if save: 

592 w_s = self.calc.density.gd.collect(self.gd, broadcast=True) 

593 if self.calc.density.gd.comm.rank == 0: 

594 np.save('coarse_grid', w_s) 

595 

596 return self.calc.density.gd 

597 

598 @property 

599 def n_spin_regions(self): 

600 self._n_spin_regions = len(self.spin_regions) 

601 return self._n_spin_regions 

602 

603 @property 

604 def n_charge_regions(self): 

605 if not self.difference: 

606 self._n_charge_regions = len(self.charge_regions) 

607 else: 

608 self._n_charge_regions = 1 

609 return self._n_charge_regions 

610 

611 @property 

612 def n_regions(self): 

613 if not self.difference: 

614 self._n_regions = self._n_charge_regions + self._n_spin_regions 

615 else: 

616 self._n_regions = 2 

617 return self._n_regions 

618 

619 

620def gaussians(gd, positions, numbers): 

621 r_Rv = gd.get_grid_point_coordinates().transpose((1, 2, 3, 0)) 

622 radii = covalent_radii[numbers] 

623 cutoffs = radii + 3.0 

624 sigmas = radii * min(covalent_radii) + 0.5 

625 result_R = gd.zeros() 

626 for pos, Z, rc, sigma in zip(positions, numbers, cutoffs, sigmas): 

627 d2_R = ((r_Rv - pos)**2).sum(3) 

628 a_R = Z / (sigma**3 * (2 * np.pi)**1.5) * np.exp(-d2_R / 

629 (2 * sigma**2)) 

630 a_R[d2_R > rc] = 0.0 

631 result_R += a_R 

632 return result_R 

633 

634 

635class CDFTPotential(ExternalPotential): 

636 def __init__(self, 

637 regions, 

638 gd, 

639 atoms, 

640 constraints, 

641 n_charge_regions, 

642 difference, 

643 vi, 

644 log, 

645 Rc={}, 

646 mu={}): 

647 

648 self.indices_i = regions 

649 self.gd = gd 

650 self.iocontext = IOContext() 

651 self.log = log 

652 self.atoms = atoms 

653 self.pos_av = None 

654 self.Z_a = None 

655 self.w_ig = None 

656 self.n_charge_regions = n_charge_regions 

657 self.constraints = constraints 

658 self.difference = difference 

659 self.v_i = vi 

660 self.Rc = Rc 

661 self.mu = mu 

662 self.name = 'CDFTPotential' 

663 

664 def __str__(self): 

665 self.name = 'CDFTPotential' 

666 return 'CDFTPotential' 

667 

668 def get_name(self): 

669 return self.name 

670 

671 def update_ae_density(self, ae_dens): 

672 self.ae_dens = ae_dens 

673 

674 def get_atoms(self): 

675 return self.atoms 

676 

677 def get_ae_density(self): 

678 return self.ae_dens 

679 

680 def get_vi(self): 

681 return self.v_i 

682 

683 def get_constraints(self): 

684 return self.constraints 

685 

686 def set_levels(self, v_i): 

687 self.v_i = np.array(v_i, dtype=float) 

688 self.vext_g = None 

689 return self.v_i 

690 

691 def set_forces(self, cdft_forces): 

692 self.cdft_forces = cdft_forces 

693 

694 def get_cdft_forces(self): 

695 return self.cdft_forces 

696 

697 def spin_polarized_potential(self): 

698 return len(self.constraints) != self.n_charge_regions 

699 

700 def get_w(self): 

701 return self.w_ig 

702 

703 def set_positions_and_atomic_numbers(self, pos_av, Z_a): 

704 self.pos_av = pos_av 

705 self.Z_a = Z_a 

706 self.w_ig = None 

707 self.vext_g = None 

708 

709 def initialize_partitioning(self, 

710 gd, 

711 construct=False, 

712 pad=False, 

713 global_array=False): 

714 w = get_all_weight_functions(self.atoms, gd, self.indices_i, 

715 self.difference, self.Rc, self.mu) 

716 if construct: 

717 return np.array(w) 

718 

719 self.w_ig = np.array(w) 

720 p = functools.partial(print, file=self.log) 

721 p('Number of charge constrained regions: {n}'.format( 

722 n=self.n_charge_regions)) 

723 if not self.difference: 

724 p('Number of spin constrained regions: {n}'.format( 

725 n=len(self.indices_i) - self.n_charge_regions)) 

726 else: 

727 p('Number of spin constrained regions: 0') 

728 p(f'Charge difference: {self.difference}') 

729 p('Parameters') 

730 p('Atom Width[A] Rc[A]') 

731 for a in self.mu: 

732 p(' {atom} {width:.3f} {Rc:.3f}'.format( 

733 atom=a, width=self.mu[a] * Bohr, Rc=self.Rc[a] * Bohr)) 

734 print(file=self.log) 

735 self.log.flush() 

736 

737 def calculate_potential(self, gd): 

738 # return v_ext^{\sigma} = sum_i V_i*w_i^{\sigma} 

739 if self.w_ig is None: 

740 self.initialize_partitioning(self.gd) 

741 pot = np.empty(self.w_ig.shape) 

742 

743 for i in range(len(self.v_i)): 

744 pot[i] = self.v_i[i] * self.w_ig[i] 

745 

746 # first alpha spin 

747 vext_sga = np.sum(np.asarray(pot), axis=0) 

748 # then beta 

749 vext_sgb = np.asarray(pot) 

750 # spin constraints with beta spins 

751 vext_sgb[self.n_charge_regions:] *= -1. 

752 vext_sgb = np.sum(vext_sgb, axis=0) 

753 vext_sg = np.array([vext_sga, vext_sgb]) 

754 # spin-dependent cdft potential 

755 self.vext_g = vext_sg 

756 

757 def write(self, writer): 

758 writer.write(vext='CDFTPotential') 

759 

760 def read(self, reader): 

761 pass 

762 

763 def todict(self): 

764 return { 

765 'name': 'CDFTPotential', 

766 'regions': self.indices_i, 

767 'constraints': self.v_i * Hartree, 

768 'n_charge_regions': self.n_charge_regions, 

769 'difference': self.difference 

770 } 

771 

772 def get_atomic_hamiltonians(self, setups, atom): 

773 h_cdft_a = np.zeros(setups.shape) 

774 h_cdft_b = np.zeros(setups.shape) 

775 v_i = self.v_i 

776 if self.difference: 

777 v_i = [v_i, -v_i] 

778 

779 for i in range(len(self.indices_i)): 

780 if atom in self.indices_i[i]: 

781 h_cdft_a += v_i[i] * 2. * np.sqrt(np.pi) * setups 

782 h_cdft_b += v_i[i] * 2. * np.sqrt(np.pi) * setups 

783 if i >= self.n_charge_regions and not self.difference: 

784 h_cdft_b *= -1. 

785 

786 return h_cdft_a, h_cdft_b 

787 

788 def get_cdft_external_energy(self, dens, nspins, vext_g, vt_g, vbar_g, 

789 vt_sg): 

790 # cDFT works with all-electron (spin)density 

791 

792 # First the potential 

793 # spin dependent external potential, array of Vi*wi 

794 # vext_g = self.calculate_potential(self.gd).copy() 

795 vt_g += vext_g[0] 

796 vext_g[1:nspins] = vext_g[1] + vbar_g 

797 vt_sg[nspins:] = 0.0 

798 

799 # then energy 

800 w = self.get_w() # weight functions 

801 Vi = self.get_vi() 

802 constraints = self.get_constraints() 

803 

804 # pseudo electron density on fine grid 

805 if dens.nt_sg is None: 

806 dens.interpolate_pseudo_density() 

807 

808 diff = [] 

809 

810 if self.n_charge_regions != 0: 

811 # pseudo density 

812 nt_g = dens.nt_sg[0] + dens.nt_sg[1] 

813 charge_diff = ( 

814 self.gd.integrate(w[0:self.n_charge_regions], 

815 nt_g, global_integral=True) - 

816 constraints[0:self.n_charge_regions]) 

817 diff.append(charge_diff) 

818 

819 # constrained spins 

820 if len(constraints) - self.n_charge_regions != 0: 

821 Delta_nt_g = dens.nt_sg[0] - dens.nt_sg[ 

822 1] # pseudo spin difference density 

823 spin_diff = self.gd.integrate( 

824 w[self.n_charge_regions:], Delta_nt_g, 

825 global_integral=True) - constraints[self.n_charge_regions:] 

826 diff.append(spin_diff) 

827 

828 diff = np.asarray(diff) 

829 # number of domains 

830 size = self.gd.comm.size 

831 e = np.multiply(Vi, diff[:]).sum() 

832 e /= size 

833 

834 return e 

835 

836 

837class WeightFunc: 

838 """ Class which builds a weight function around atoms or molecules 

839 given the atom index - using normalized Gaussian with cut-off! 

840 

841 The weight function is constructed on the coarse or fine grid and 

842 can be used to do charge constraint DFT. 

843 

844 """ 

845 def __init__(self, gd, atoms, indices, Rc={}, mu={}, new=True): 

846 """ Given a grid-descriptor, atoms object and an index list 

847 construct a weight function defined by: 

848 n_i(r-R_i) 

849 w(r) = --------------- 

850 sum_a n_a(r-R_a) 

851 

852 where a runs over all atoms, and i can index 

853 an atom or a list of atoms comprising a molecule, etc. 

854 

855 The n_i are construced with atom centered gaussians 

856 using a pre-defined cut-off Rc_i. 

857 

858 """ 

859 self.gd = gd 

860 self.atoms = atoms 

861 self.indices_i = indices # Indices of constrained charge_regions 

862 

863 # Weight function parameters in Bohr 

864 # Cutoffs 

865 if new: 

866 new_Rc = {} 

867 for a in self.atoms: 

868 if a.symbol in Rc: 

869 new_Rc[a.symbol] = Rc[a.symbol] / Bohr 

870 else: 

871 element_number = atomic_numbers[a.symbol] 

872 cr = covalent_radii[element_number] 

873 # Rc to roughly between 3. and 5. 

874 new_Rc[a.symbol] = (cr + 2.5) / Bohr 

875 

876 self.Rc = new_Rc 

877 else: 

878 self.Rc = Rc 

879 

880 # Construct mu (width) dict 

881 # mu only sets the width and height so it's in angstrom 

882 if new: 

883 new_mu = {} 

884 for a in self.atoms: 

885 if a.symbol in mu: 

886 new_mu[a.symbol] = mu[a.symbol] / Bohr 

887 else: 

888 element_number = atomic_numbers[a.symbol] 

889 cr = covalent_radii[element_number] 

890 # mu to be roughly between 0.5 and 1.0 AA 

891 cr = (cr * min(covalent_radii) + 0.5) 

892 new_mu[a.symbol] = cr / Bohr 

893 

894 # "Larger" atoms may need a bit more width 

895 self.mu = new_mu 

896 else: 

897 self.mu = mu 

898 

899 def get_Rc_and_mu(self): 

900 return self.Rc, self.mu 

901 

902 def normalized_gaussian(self, dis, mu, Rc): 

903 # Given mu - width, and Rc 

904 # a normalized gaussian is constructed 

905 # around some atom. This is 

906 # placed on the gd (grid) - and truncated 

907 # at a given cut-off value Rc. dis 

908 # are the distances from atom to grid points. 

909 """ Normalized gaussian is: 

910 1 

911 g(r) = --------------- e^{-(r-Ra)^2/(2mu^2)} 

912 mu^3*(2pi)^1.5 

913 

914 for |r-Ra| <= Rc, 0 elsewhere 

915 

916 """ 

917 # Check function 

918 check = abs(dis) <= Rc 

919 

920 # Make gaussian 3D Guassian 

921 gauss = 1 / (mu * (2 * pi)**(1 / 2)) * np.exp(-dis**2 / (2.0 * mu**2)) 

922 

923 # apply cut-off and return 

924 return np.array(gauss * check) 

925 

926 def get_distance_vectors(self, pos, distance=True): 

927 xyz = self.gd.get_grid_point_distance_vectors(pos) 

928 if distance: 

929 # returns vector norm 

930 return np.sqrt((xyz**2).sum(axis=0)) 

931 else: 

932 # gives raw vector 

933 return xyz 

934 

935 def construct_total_density(self, atoms): 

936 # Add to empty grid 

937 dens = self.gd.zeros() 

938 

939 for atom in atoms: 

940 charge = atom.number 

941 symbol = atom.symbol 

942 pos = atom.position / Bohr 

943 

944 dis = self.get_distance_vectors(pos) 

945 

946 dens += charge * self.normalized_gaussian(dis, 

947 self.mu[symbol], 

948 self.Rc[symbol]) 

949 return dens 

950 

951 def construct_weight_function(self): 

952 # Grab atomic / molecular density 

953 dens_n = self.construct_total_density(self.atoms[self.indices_i]) 

954 # Grab total density 

955 dens = self.construct_total_density(self.atoms) 

956 # Check zero elements 

957 check = dens == 0 

958 # Add value to zeros ... 

959 dens += check * 1.0 

960 # make weight function 

961 return (dens_n / dens) 

962 

963 def get_cdft_forces2(self, dens, v_i, n_charge_regions, n_spin_regions, 

964 w_ig, method, difference): 

965 """ Calculate cDFT force as a sum 

966 dF/dRi = Fi(inside) + Fs(surf) 

967 due to cutoff (Rc) in gauss 

968 / dw(r) 

969 Fi_a = -V | ------ n(r) dr 

970 / dR_a 

971 dw(r) 

972 ---- = sum of Gaussian functions... 

973 dR_a 

974 

975 this is computed in get_dw_dRa 

976 dens = density 

977 Vc = cDFT constraint value 

978 method = 'fd' or 'analytical' for 

979 finite difference or analytical 

980 dw/dR 

981 """ 

982 

983 cdft_forces = np.zeros((len(self.atoms), 3)) 

984 

985 if dens.nt_sg is None: 

986 dens.interpolate_pseudo_density() 

987 

988 rho_kd = self.construct_total_density(self.atoms) # sum_k n_k 

989 # Check zero elements 

990 check = rho_kd == 0. 

991 # Add value to zeros for denominator... 

992 rho_kd += check * 1.0 

993 

994 for a, atom in enumerate(self.atoms): 

995 wn_sg = self.gd.zeros() 

996 prefactor = self.get_derivative_prefactor(n_charge_regions, 

997 n_spin_regions, w_ig, 

998 v_i, 

999 difference, 

1000 atom, rho_kd) 

1001 

1002 # make extended array 

1003 for c in range(n_charge_regions): 

1004 wn_sg += (dens.nt_sg[0] + dens.nt_sg[1]) * prefactor[0] 

1005 

1006 for s in range(n_spin_regions): 

1007 wn_sg += (dens.nt_sg[0] - dens.nt_sg[1]) * prefactor[1] 

1008 

1009 for i in [0, 1, 2]: 

1010 if method == 'analytical': 

1011 dG_dRav = self.get_analytical_derivates(atom, 

1012 direction=i) 

1013 elif method == 'fd': 

1014 dG_dRav = self.get_fd_derivatives(atom, 

1015 direction=i) 

1016 

1017 cdft_forces[a][i] -= self.gd.integrate(wn_sg * dG_dRav, 

1018 global_integral=True) 

1019 

1020 return cdft_forces 

1021 

1022 def get_fd_derivatives(self, atom, direction, dx=1.e-4): 

1023 

1024 dirs = [[dx, 0., 0.], [0., dx, 0.], [0., 0., dx]] 

1025 charge = atom.number 

1026 symbol = atom.symbol 

1027 mu = self.mu[symbol] 

1028 Rc = self.Rc[symbol] 

1029 

1030 a_posx = atom.position / Bohr + dirs[direction] 

1031 a_dis = self.get_distance_vectors(a_posx) 

1032 Ga_posx = charge * self.normalized_gaussian(a_dis, mu, Rc) 

1033 # move to -dx 

1034 a_negx = atom.position / Bohr - dirs[direction] 

1035 a_dis = self.get_distance_vectors(a_negx) 

1036 Ga_negx = charge * self.normalized_gaussian(a_dis, mu, Rc) 

1037 # dG/dx 

1038 dGav = (Ga_posx - Ga_negx) / (2 * dx) 

1039 

1040 return dGav 

1041 

1042 def get_derivative_prefactor(self, n_charge_regions, n_spin_regions, w_ig, 

1043 v_i, difference, atom, rho_kd): 

1044 """Computes the dw/dRa array needed for derivatives/forces 

1045 eq 31 

1046 needed for lfc-derivative/integrals 

1047 """ 

1048 wc = self.gd.zeros() 

1049 ws = self.gd.zeros() 

1050 

1051 for i in range(n_charge_regions): 

1052 # build V_i [sum_k rho_k + sum_{j in i}rho_i] 

1053 wi = -w_ig[i] 

1054 

1055 if not difference: 

1056 if atom.index in self.indices_i[i]: 

1057 wi += 1. 

1058 else: 

1059 if atom.index in self.indices_i[0]: 

1060 wi += 1. 

1061 elif atom.index in self.indices_i[1]: 

1062 wi -= 1. 

1063 

1064 wi *= v_i[i] 

1065 wc += wi / rho_kd 

1066 

1067 for i in range(n_spin_regions): 

1068 # build V_i [sum_k rho_k + sum_{j in i}rho_i] 

1069 wi = -w_ig[n_charge_regions + i] 

1070 if atom.index in self.indices_i[n_charge_regions + i]: 

1071 wi += 1. 

1072 wi *= v_i[n_charge_regions + i] 

1073 ws += wi / rho_kd 

1074 

1075 return [wc, ws] 

1076 

1077 def get_analytical_derivates(self, atom, direction): 

1078 # equations 32,33,34 

1079 

1080 a_pos = atom.position / Bohr 

1081 a_symbol = atom.symbol 

1082 a_charge = atom.number 

1083 

1084 a_dis = self.get_distance_vectors(a_pos) 

1085 rRa = -self.get_distance_vectors(a_pos, distance=False) 

1086 dist_rRa = self.get_distance_vectors(a_pos, distance=True) 

1087 check = dist_rRa == 0 

1088 

1089 # Add value to zeros ... 

1090 dist_rRa += check * 1.0 

1091 # eq 33 

1092 drRa_di = rRa[direction] / dist_rRa 

1093 

1094 # Gaussian derivative eq 34 

1095 

1096 G_a = a_charge * self.normalized_gaussian(a_dis, 

1097 self.mu[a_symbol], 

1098 self.Rc[a_symbol]) 

1099 

1100 # within cutoff or at surface ? --> heaviside 

1101 # inside 

1102 check_i = abs(a_dis) <= self.Rc[a_symbol] 

1103 rRc = check_i * a_dis 

1104 dGa_drRa = -rRc * G_a / ( 

1105 self.mu[a_symbol])**2 # (\Theta * (r-R_a) n_A) / \sigma^2 

1106 

1107 # at surface 

1108 surf = abs(abs(a_dis) - self.Rc[a_symbol]) 

1109 check_s = surf <= max(self.gd.get_grid_spacings()) 

1110 dGa_drRa += check_s * G_a # \ sigma_{A\in i} n_A 

1111 

1112 # eq 32 

1113 return dGa_drRa * drRa_di 

1114 

1115 

1116def get_ks_energy_wo_external(calc): 

1117 return (calc.hamiltonian.e_kinetic + calc.hamiltonian.e_coulomb + 

1118 calc.hamiltonian.e_zero + calc.hamiltonian.e_xc - 

1119 calc.hamiltonian.e_entropy) * Hartree 

1120 

1121 

1122def get_all_weight_functions(atoms, 

1123 gd, 

1124 indices_i, 

1125 difference, 

1126 Rc, 

1127 mu, 

1128 new=False, 

1129 return_Rc_mu=False): 

1130 w = [] 

1131 for i in range(len(indices_i)): 

1132 wf = WeightFunc(gd, atoms, indices_i[i], Rc, mu, new) 

1133 weig = wf.construct_weight_function() 

1134 

1135 if not difference: 

1136 w.append(weig) 

1137 else: # for charge difference constraint 

1138 if i == 0: 

1139 w.append(weig) 

1140 else: 

1141 w[0] -= weig # negative for acceptor 

1142 if return_Rc_mu: 

1143 Rc, mu = wf.get_Rc_and_mu() 

1144 return w, Rc, mu 

1145 else: 

1146 return w 

1147 

1148 

1149def get_promolecular_constraints(calc_a, 

1150 atoms_a, 

1151 calc_b, 

1152 atoms_b, 

1153 charge_constraint=True, 

1154 spin_constraint=False, 

1155 restart=False, 

1156 Rc={}, 

1157 mu={}): 

1158 """ 

1159 - calc_a is for the region you're interested in. Its charge 

1160 should correspond to the "free charge" of the promolecule 

1161 - atoms_a: atoms object for the region of interest 

1162 - calc_b is for the other region 

1163 - atoms_b: atoms object for the other region 

1164 - restart: bool. If true, atoms_a have used calc_a and atoms_b calc_b 

1165 """ 

1166 

1167 constraints = [] 

1168 atoms = atoms_a + atoms_b 

1169 

1170 if not restart: 

1171 atoms_a.calc = calc_a 

1172 atoms_b.calc = calc_b 

1173 atoms_a.get_potential_energy() 

1174 atoms_b.get_potential_energy() 

1175 

1176 gd = calc_a.density.finegd 

1177 

1178 if charge_constraint: 

1179 # can charge and spin constraints be treated at the same time? 

1180 d_a = calc_a.get_all_electron_density(gridrefinement=2, pad=False) 

1181 d_b = calc_b.get_all_electron_density(gridrefinement=2, pad=False) 

1182 

1183 charge_region = [atom.index for atom in atoms_a] 

1184 weight = WeightFunc(gd=gd, 

1185 atoms=atoms, 

1186 indices=charge_region, 

1187 Rc=Rc, 

1188 mu=mu) 

1189 w = weight.construct_weight_function() 

1190 

1191 dv = atoms.get_volume() / calc_a.get_number_of_grid_points().prod() 

1192 gridrefinement = 2. 

1193 Nel = (w * (d_a + d_b)).sum() * dv / gridrefinement**3 

1194 

1195 Zn = 0 

1196 for atom in atoms_a: 

1197 Zn += atom.number 

1198 

1199 constraints.append(Zn - Nel) 

1200 

1201 if spin_constraint: 

1202 # can charge and spin constraints be treated at the same time? 

1203 d_a = calc_a.get_all_electron_density(gridrefinement=2, 

1204 pad=False, 

1205 spin=0) 

1206 d_b = calc_b.get_all_electron_density(gridrefinement=2, 

1207 pad=False, 

1208 spin=1) 

1209 

1210 charge_region = [atom.index for atom in atoms_a] 

1211 weight = WeightFunc(gd=gd, 

1212 atoms=atoms, 

1213 indices=charge_region, 

1214 Rc=Rc, 

1215 mu=mu) 

1216 w = weight.construct_weight_function() 

1217 

1218 dv = atoms.get_volume() / calc_a.get_number_of_grid_points().prod() 

1219 gridrefinement = 2. 

1220 Ns = (w * (d_a - d_b)).sum() * dv / gridrefinement**3 

1221 

1222 constraints.append(Ns) 

1223 

1224 return constraints