Coverage for gpaw/fdtd/poisson_fdtd.py: 90%

458 statements  

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

1"""Part of the module for electrodynamic simulations.""" 

2 

3import numpy as np 

4from ase import Atoms 

5from ase.io.trajectory import read_atoms 

6from ase.units import Bohr 

7 

8import gpaw.mpi as mpi 

9from gpaw import GPAW 

10from gpaw.fdtd.polarizable_material import PolarizableMaterial 

11from gpaw.fdtd.potential_couplers import (RefinerPotentialCoupler, 

12 MultipolesPotentialCoupler) 

13from gpaw.grid_descriptor import GridDescriptor 

14from gpaw.mpi import world, serial_comm 

15from gpaw.poisson import PoissonSolver 

16from gpaw.poisson_moment import MomentCorrectionPoissonSolver 

17from gpaw.tddft import TDDFT, DipoleMomentWriter, RestartFileWriter 

18from gpaw.transformers import Transformer 

19from gpaw.utilities import h2gpts 

20 

21# QSFDTD is a wrapper class to make calculations 

22# easier, something like this: 

23# qsfdtd = QSFDTD(cl, at, c, h) 

24# energy = qsfdtd.ground_state('gs.gpw') 

25# qsfdtd.time_propagation('gs.gpw', kick) 

26# photoabsorption_spectrum('dm.dat', 'spec.dat') 

27 

28 

29class QSFDTD: 

30 def __init__(self, 

31 classical_material, 

32 atoms, 

33 cells, 

34 spacings, 

35 communicator=serial_comm, 

36 remove_moments=(1, 1)): 

37 

38 self.td_calc = None 

39 

40 # Define classical cell in one of these ways: 

41 # 1. [cx, cy, cz] -> vacuum for the quantum grid = 4.0 

42 # 2. ([cx, cy, cz]) -> vacuum = 4.0 

43 # 3. ([cx, cy, cz], vacuum) 

44 # 4. ([cx, cy, cz], ([p1x, p1y, p1z], [p2x, p2y, p2z])) 

45 # where p1 and p2 are corners of the quantum grid 

46 if len(cells) == 1: # (cell) 

47 assert (len(cells[0]) == 3) 

48 cell = np.array(cells[0]) 

49 vacuum = 4.0 

50 elif len(cells) == 3: # [cell.x, cell.y, cell.z] 

51 cell = np.array(cells) 

52 vacuum = 4.0 

53 elif len(cells) == 2: # cell + vacuum/corners 

54 cell = np.array(cells[0]) 

55 if np.array(cells[1]).size == 1: 

56 vacuum = cells[1] 

57 corners = None 

58 else: 

59 vacuum = None 

60 corners = cells[1] 

61 else: 

62 raise Exception('QSFDTD: cells defined incorrectly') 

63 

64 # Define spacings in in one of these ways: 

65 # 1. (clh, qmh) 

66 # 2. clh -> qmh=clh/4 

67 if np.array(spacings).size == 1: # clh 

68 cl_spacing = spacings 

69 qm_spacing = 0.25 * cl_spacing 

70 elif len(spacings) == 2: # (clh, qmh) 

71 cl_spacing = spacings[0] 

72 qm_spacing = spacings[1] 

73 else: 

74 raise Exception('QSFDTD: spacings defined incorrectly') 

75 

76 self.poissonsolver = FDTDPoissonSolver( 

77 classical_material=classical_material, 

78 cl_spacing=cl_spacing, 

79 qm_spacing=qm_spacing, 

80 remove_moments=remove_moments, 

81 communicator=communicator, 

82 cell=cell) 

83 self.poissonsolver.set_calculation_mode('iterate') 

84 

85 # Quantum system 

86 if atoms is not None: 

87 assert (len(cells) == 2) 

88 assert (len(spacings) == 2) 

89 assert (len(remove_moments) == 2) 

90 self.atoms = atoms 

91 self.atoms.set_cell(cell) 

92 if vacuum is not None: # vacuum 

93 self.atoms, self.qm_spacing, self.gpts = \ 

94 self.poissonsolver.cut_cell(self.atoms, vacuum=vacuum) 

95 else: # corners 

96 self.atoms, self.qm_spacing, self.gpts = \ 

97 self.poissonsolver.cut_cell(self.atoms, corners=corners) 

98 else: # Dummy quantum system 

99 self.atoms = Atoms('H', [0.5 * cell], cell=cell) 

100 if vacuum is not None: # vacuum 

101 self.atoms, self.qm_spacing, self.gpts = \ 

102 self.poissonsolver.cut_cell(self.atoms, vacuum=vacuum) 

103 else: # corners 

104 self.atoms, self.qm_spacing, self.gpts = \ 

105 self.poissonsolver.cut_cell(self.atoms, corners=corners) 

106 del self.atoms[:] 

107 

108 def ground_state(self, filename, **kwargs): 

109 # GPAW calculator for the ground state 

110 self.gs_calc = GPAW(gpts=self.gpts, 

111 poissonsolver=self.poissonsolver, 

112 **kwargs) 

113 self.atoms.calc = self.gs_calc 

114 self.energy = self.atoms.get_potential_energy() 

115 self.write(filename, mode='all') 

116 

117 def write(self, filename, **kwargs): 

118 if self.td_calc is None: 

119 self.gs_calc.write(filename, **kwargs) 

120 else: 

121 self.td_calc.write(filename, **kwargs) 

122 

123 def time_propagation(self, 

124 filename, 

125 kick_strength, 

126 time_step, 

127 iterations, 

128 dipole_moment_file=None, 

129 restart_file=None, 

130 dump_interval=100, 

131 **kwargs): 

132 self.td_calc = TDDFT(filename, **kwargs) 

133 if dipole_moment_file is not None: 

134 DipoleMomentWriter(self.td_calc, dipole_moment_file) 

135 if restart_file is not None: 

136 RestartFileWriter(self.td_calc, restart_file, dump_interval) 

137 if kick_strength is not None: 

138 self.td_calc.absorption_kick(kick_strength) 

139 self.td_calc.propagate(time_step, iterations) 

140 if restart_file is not None: 

141 self.td_calc.write(restart_file, 'all') 

142 

143 

144# This helps in telling the classical quantities from the quantum ones 

145class PoissonOrganizer: 

146 def __init__(self): 

147 self.poisson_solver = None 

148 self.gd = None 

149 self.density = None 

150 self.cell = None 

151 self.spacing_def = None 

152 self.spacing = None 

153 

154 

155# For mixing the potential 

156class SimpleMixer(): 

157 def __init__(self, alpha, data): 

158 self.alpha = alpha 

159 self.data = np.copy(data) 

160 

161 def mix(self, data): 

162 self.data = self.alpha * data + (1.0 - self.alpha) * self.data 

163 return np.copy(self.data) 

164 

165 

166# Contains one PoissonSolver for the classical 

167# and one for the quantum subsystem 

168class FDTDPoissonSolver: 

169 def __init__(self, 

170 nn=3, 

171 relax='J', 

172 eps=1e-24, 

173 classical_material=None, 

174 cell=None, 

175 qm_spacing=0.30, 

176 cl_spacing=1.20, 

177 remove_moments=(1, 1), 

178 potential_coupler='Refiner', 

179 communicator=serial_comm): 

180 

181 self.rank = mpi.rank 

182 

183 self.messages = [] 

184 

185 assert (potential_coupler in ['Multipoles', 'Refiner']) 

186 self.potential_coupling_scheme = potential_coupler 

187 

188 if classical_material is None: 

189 self.classical_material = PolarizableMaterial() 

190 else: 

191 self.classical_material = classical_material 

192 

193 self.set_calculation_mode('solve') 

194 

195 self.has_subsystems = False 

196 self.remove_moment_cl = remove_moments[0] 

197 self.remove_moment_qm = remove_moments[1] 

198 self.time = 0.0 

199 self.time_step = 0.0 

200 self.kick = np.array([0.0, 0.0, 0.0], dtype=float) 

201 self.maxiter = 2000 

202 self.eps = eps 

203 self.relax = relax 

204 self.nn = nn 

205 

206 # Only handle the quantities via self.qm or self.cl 

207 self.cl = PoissonOrganizer() 

208 self.cl.spacing_def = cl_spacing * np.ones(3) / Bohr 

209 self.cl.extrapolated_qm_phi = None 

210 self.cl.dcomm = communicator 

211 self.cl.dparsize = None 

212 self.qm = PoissonOrganizer() 

213 self.qm.spacing_def = qm_spacing * np.ones(3) / Bohr 

214 self.qm.cell = np.array(cell) / Bohr 

215 

216 # Classical spacing and simulation cell 

217 _cell = np.array(cell) / Bohr 

218 self.cl.spacing = self.cl.spacing_def 

219 if np.size(_cell) == 3: 

220 self.cl.cell = np.diag(_cell) 

221 else: 

222 self.cl.cell = _cell 

223 

224 # Generate classical grid descriptor 

225 self.initialize_clgd() 

226 

227 def todict(self): 

228 dct = dict( 

229 name='fdtd', 

230 nn=self.nn, 

231 relax=self.relax, 

232 eps=self.eps, 

233 # classical_material missing 

234 cell=self.cl.cell * Bohr, 

235 qm_spacing=self.qm.spacing_def[0] * Bohr, 

236 cl_spacing=self.cl.spacing_def[0] * Bohr, 

237 remove_moments=(self.remove_moment_cl, self.remove_moment_qm), 

238 potential_coupler=self.potential_coupling_scheme) 

239 

240 return dct 

241 

242 def get_description(self): 

243 return 'FDTD+TDDFT' 

244 

245 # Generated classical GridDescriptors, after spacing and cell are known 

246 def initialize_clgd(self): 

247 N_c = h2gpts(self.cl.spacing, self.cl.cell, 4) 

248 self.cl.spacing = np.diag(self.cl.cell) / N_c 

249 self.cl.gd = GridDescriptor(N_c, self.cl.cell, False, self.cl.dcomm, 

250 self.cl.dparsize) 

251 self.cl.gd_global = GridDescriptor(N_c, self.cl.cell, False, 

252 serial_comm, None) 

253 self.cl.extrapolated_qm_phi = self.cl.gd.empty() 

254 

255 def estimate_memory(self, mem): 

256 # self.cl.poisson_solver.estimate_memory(mem) 

257 # print(self.qm.poisson_solver.estimate_memory.__code__.co_varnames) 

258 # WTF? How can this shabby method suddenly be unbound? 

259 # It needs both self and mem. 

260 # Ferchrissakes! 

261 # self.qm.poisson_solver.estimate_memory(mem=mem) 

262 pass 

263 

264 # Return the TDDFT stencil by default 

265 def get_stencil(self, mode='qm'): 

266 if mode == 'qm': 

267 return self.qm.poisson_solver.get_stencil() 

268 else: 

269 return self.cl.poisson_solver.get_stencil() 

270 

271 # Initialize both PoissonSolvers 

272 # def initialize(self): 

273 # self.qm.poisson_solver._init() 

274 # self.cl.poisson_solver._init() 

275 

276 def set_grid_descriptor(self, qmgd): 

277 if not self.has_subsystems: 

278 return 

279 

280 self.qm.gd = qmgd 

281 

282 # Create quantum Poisson solver 

283 poisson_qm_kwargs = dict(name='fd', nn=self.nn, 

284 eps=self.eps, relax=self.relax) 

285 self.qm.poisson_solver = MomentCorrectionPoissonSolver( 

286 poissonsolver=PoissonSolver(**poisson_qm_kwargs), 

287 moment_corrections=self.remove_moment_qm) 

288 self.qm.poisson_solver.set_grid_descriptor(self.qm.gd) 

289 # self.qm.poisson_solver.initialize() 

290 self.qm.phi = self.qm.gd.zeros() 

291 self.qm.rho = self.qm.gd.zeros() 

292 

293 # Set quantum grid descriptor 

294 self.qm.poisson_solver.set_grid_descriptor(qmgd) 

295 

296 # Create classical PoissonSolver 

297 poisson_cl_kwargs = dict(name='fd', nn=self.nn, 

298 eps=self.eps, relax=self.relax) 

299 self.cl.poisson_solver = MomentCorrectionPoissonSolver( 

300 poissonsolver=PoissonSolver(**poisson_cl_kwargs), 

301 moment_corrections=self.remove_moment_cl) 

302 self.cl.poisson_solver.set_grid_descriptor(self.cl.gd) 

303 # self.cl.poisson_solver.initialize() 

304 

305 # Initialize classical material, 

306 # its Poisson solver was generated already 

307 self.cl.poisson_solver.set_grid_descriptor(self.cl.gd) 

308 self.classical_material.initialize(self.cl.gd) 

309 self.cl.extrapolated_qm_phi = self.cl.gd.zeros() 

310 self.cl.phi = self.cl.gd.zeros() 

311 self.cl.extrapolated_qm_phi = self.cl.gd.empty() 

312 

313 msg = self.messages.append 

314 msg('\nFDTDPoissonSolver/grid descriptors and coupler:') 

315 msg(' Domain parallelization with %i processes.' % 

316 self.cl.gd.comm.size) 

317 if self.cl.gd.comm == serial_comm: 

318 msg(' Communicator for domain parallelization: serial_comm') 

319 elif self.cl.gd.comm == world: 

320 msg(' Communicator for domain parallelization: world') 

321 elif self.cl.gd.comm == self.qm.gd.comm: 

322 msg(' Communicator for domain parallelization: dft_domain_comm') 

323 else: 

324 msg(' Communicator for domain parallelization: %s' 

325 % self.cl.gd.comm) 

326 

327 # Initialize potential coupler 

328 if self.potential_coupling_scheme == 'Multipoles': 

329 msg('Classical-quantum coupling by multipole expansion ' + 

330 'with maxL: %i' % (self.remove_moment_qm)) 

331 self.potential_coupler = MultipolesPotentialCoupler( 

332 qm=self.qm, 

333 cl=self.cl, 

334 index_offset_1=self.shift_indices_1, 

335 index_offset_2=self.shift_indices_2, 

336 extended_index_offset_1=self.extended_shift_indices_1, 

337 extended_index_offset_2=self.extended_shift_indices_2, 

338 extended_delta_index=self.extended_deltaIndex, 

339 num_refinements=self.num_refinements, 

340 remove_moment_qm=self.remove_moment_qm, 

341 remove_moment_cl=self.remove_moment_cl, 

342 rank=self.rank) 

343 else: 

344 msg('Classical-quantum coupling by coarsening/refining') 

345 self.potential_coupler = RefinerPotentialCoupler( 

346 qm=self.qm, 

347 cl=self.cl, 

348 index_offset_1=self.shift_indices_1, 

349 index_offset_2=self.shift_indices_2, 

350 extended_index_offset_1=self.extended_shift_indices_1, 

351 extended_index_offset_2=self.extended_shift_indices_2, 

352 extended_delta_index=self.extended_deltaIndex, 

353 num_refinements=self.num_refinements, 

354 remove_moment_qm=self.remove_moment_qm, 

355 remove_moment_cl=self.remove_moment_cl, 

356 rank=self.rank) 

357 

358 self.phi_tot_clgd = self.cl.gd.empty() 

359 self.phi_tot_qmgd = self.qm.gd.empty() 

360 

361 def cut_cell(self, 

362 atoms_in, 

363 vacuum=5.0, 

364 corners=None, 

365 create_subsystems=True): 

366 qmh = self.qm.spacing_def 

367 if corners is not None: 

368 v1 = np.array(corners[0]).ravel() / Bohr 

369 v2 = np.array(corners[1]).ravel() / Bohr 

370 else: # Use vacuum 

371 pos_old = atoms_in.get_positions()[0] 

372 dmy_atoms = atoms_in.copy() 

373 dmy_atoms.center(vacuum=vacuum) 

374 pos_new = dmy_atoms.get_positions()[0] 

375 v1 = (pos_old - pos_new) / Bohr 

376 v2 = v1 + np.diag(dmy_atoms.get_cell()) / Bohr 

377 

378 # Needed for restarting 

379 self.given_corner_v1 = v1 * Bohr 

380 self.given_corner_v2 = v2 * Bohr 

381 self.given_cell = atoms_in.get_cell() 

382 

383 # Sanity check: quantum box must be inside the classical one 

384 assert (all([v1[w] <= v2[w] and v1[w] >= 0 and 

385 v2[w] <= np.diag(self.cl.cell)[w] for w in range(3)])) 

386 

387 # Ratios of the user-given spacings 

388 self.hratios = self.cl.spacing_def / qmh 

389 self.num_refinements = \ 

390 1 + int(round(np.log(self.hratios[0]) / np.log(2.0))) 

391 assert ([int(round(np.log(self.hratios[w]) / np.log(2.0))) == 

392 self.num_refinements for w in range(3)]) 

393 

394 # Create quantum grid 

395 self.qm.cell = np.zeros((3, 3)) 

396 for w in range(3): 

397 self.qm.cell[w, w] = v2[w] - v1[w] 

398 

399 N_c = h2gpts(qmh, self.qm.cell, 4) 

400 # N_c = get_number_of_grid_points(self.qm.cell, qmh) 

401 self.qm.spacing = np.diag(self.qm.cell) / N_c 

402 

403 # Classical corner indices must be divisible with numb 

404 if any(self.cl.spacing / self.qm.spacing >= 3): 

405 numb = 1 

406 elif any(self.cl.spacing / self.qm.spacing >= 2): 

407 numb = 2 

408 else: 

409 numb = 4 

410 

411 # The index mismatch of the two simulation cells 

412 # Round before taking floor/ceil to avoid floating point 

413 # arithmetic errors 

414 num_indices_1 = numb * np.floor( 

415 np.round(np.array(v1) / self.cl.spacing / numb, 2)).astype(int) 

416 num_indices_2 = numb * np.ceil( 

417 np.round(np.array(v2) / self.cl.spacing / numb, 2)).astype(int) 

418 self.num_indices = num_indices_2 - num_indices_1 

419 

420 # Center, left, and right points of the suggested quantum grid 

421 cp = 0.5 * (np.array(v1) + np.array(v2)) 

422 lp = cp - 0.5 * self.num_indices * self.cl.spacing 

423 # rp = cp + 0.5 * self.num_indices * self.cl.spacing 

424 

425 # Indices in the classical grid restricting the quantum grid 

426 self.shift_indices_1 = np.round(lp / self.cl.spacing).astype(int) 

427 self.shift_indices_2 = self.shift_indices_1 + self.num_indices 

428 

429 # Sanity checks 

430 assert (all([self.shift_indices_1[w] >= 0 and 

431 self.shift_indices_2[w] <= self.cl.gd.N_c[w] 

432 for w in range(3)])), ( 

433 'Could not find appropriate quantum grid. ' 

434 'Move it further away from the boundary.') 

435 

436 # Corner coordinates 

437 self.qm.corner1 = self.shift_indices_1 * self.cl.spacing 

438 self.qm.corner2 = self.shift_indices_2 * self.cl.spacing 

439 

440 # Now the information for creating the subsystems is ready 

441 if create_subsystems: 

442 return self.create_subsystems(atoms_in) 

443 

444 def create_subsystems(self, atoms_in): 

445 

446 # Create new Atoms object 

447 atoms_out = atoms_in.copy() 

448 

449 # New quantum grid 

450 self.qm.cell = \ 

451 np.diag([(self.shift_indices_2[w] - self.shift_indices_1[w]) * 

452 self.cl.spacing[w] for w in range(3)]) 

453 self.qm.spacing = self.cl.spacing / self.hratios 

454 N_c = h2gpts(self.qm.spacing, self.qm.cell, 4) 

455 

456 atoms_out.set_cell(np.diag(self.qm.cell) * Bohr) 

457 atoms_out.positions = atoms_in.get_positions() - self.qm.corner1 * Bohr 

458 

459 msg = self.messages.append 

460 msg("Quantum box readjustment:") 

461 msg(" Given cell: [%10.5f %10.5f %10.5f]" % 

462 tuple(np.diag(atoms_in.get_cell()))) 

463 msg(" Given atomic coordinates:") 

464 for s, c in zip(atoms_in.get_chemical_symbols(), 

465 atoms_in.get_positions()): 

466 msg(" %s %10.5f %10.5f %10.5f" % 

467 (s, c[0], c[1], c[2])) 

468 msg(" Readjusted cell: [%10.5f %10.5f %10.5f]" % 

469 tuple(np.diag(atoms_out.get_cell()))) 

470 msg(" Readjusted atomic coordinates:") 

471 for s, c in zip(atoms_out.get_chemical_symbols(), 

472 atoms_out.get_positions()): 

473 msg(" %s %10.5f %10.5f %10.5f" % 

474 (s, c[0], c[1], c[2])) 

475 

476 msg(" Given corner points: " + 

477 "(%10.5f %10.5f %10.5f) - (%10.5f %10.5f %10.5f)" 

478 % (tuple(np.concatenate((self.given_corner_v1, 

479 self.given_corner_v2))))) 

480 msg(" Readjusted corner points: " + 

481 "(%10.5f %10.5f %10.5f) - (%10.5f %10.5f %10.5f)" 

482 % (tuple(np.concatenate((self.qm.corner1, 

483 self.qm.corner2)) * Bohr))) 

484 msg(" Indices in classical grid: " + 

485 "(%10i %10i %10i) - (%10i %10i %10i)" 

486 % (tuple(np.concatenate((self.shift_indices_1, 

487 self.shift_indices_2))))) 

488 msg(" Grid points in classical grid: " + 

489 "(%10i %10i %10i)" 

490 % (tuple(self.cl.gd.N_c))) 

491 msg(" Grid points in quantum grid: " + 

492 "(%10i %10i %10i)" 

493 % (tuple(N_c))) 

494 msg(" Spacings in quantum grid: " + 

495 "(%10.5f %10.5f %10.5f)" 

496 % (tuple(np.diag(self.qm.cell) * Bohr / N_c))) 

497 msg(" Spacings in classical grid: " + 

498 "(%10.5f %10.5f %10.5f)" 

499 % (tuple(np.diag(self.cl.cell) * 

500 Bohr / h2gpts(self.cl.spacing, self.cl.cell, 4)))) 

501 # msg(" Ratios of cl/qm spacings: " + 

502 # "(%10i %10i %10i)" % (tuple(self.hratios))) 

503 # msg(" = (%10.2f %10.2f %10.2f)" % 

504 # (tuple((np.diag(self.cl.cell) * Bohr / \ 

505 # get_number_of_grid_points(self.cl.cell, 

506 # self.cl.spacing)) / \ 

507 # (np.diag(self.qm.cell) * Bohr / N_c)))) 

508 msg(" Needed number of refinements: %10i" 

509 % self.num_refinements) 

510 

511 # First, create the quantum grid equivalent GridDescriptor 

512 # self.cl.subgd. Then coarsen it until its h_cv equals 

513 # that of self.cl.gd. Finally, map the points between 

514 # clgd and coarsened subgrid. 

515 subcell_cv = np.diag(self.qm.corner2 - self.qm.corner1) 

516 N_c = h2gpts(self.cl.spacing, subcell_cv, 4) 

517 N_c = self.shift_indices_2 - self.shift_indices_1 

518 self.cl.subgds = [] 

519 self.cl.subgds.append(GridDescriptor(N_c, subcell_cv, False, 

520 serial_comm, self.cl.dparsize)) 

521 

522 # msg(" N_c/spacing of the subgrid: " + 

523 # "%3i %3i %3i / %.4f %.4f %.4f" % 

524 # (self.cl.subgds[0].N_c[0], 

525 # self.cl.subgds[0].N_c[1], 

526 # self.cl.subgds[0].N_c[2], 

527 # self.cl.subgds[0].h_cv[0][0] * Bohr, 

528 # self.cl.subgds[0].h_cv[1][1] * Bohr, 

529 # self.cl.subgds[0].h_cv[2][2] * Bohr)) 

530 # msg(" shape from the subgrid: " + 

531 # "%3i %3i %3i" % (tuple(self.cl.subgds[0].empty().shape))) 

532 

533 self.cl.coarseners = [] 

534 self.cl.refiners = [] 

535 for n in range(self.num_refinements): 

536 self.cl.subgds.append(self.cl.subgds[n].refine()) 

537 self.cl.refiners.append(Transformer(self.cl.subgds[n], 

538 self.cl.subgds[n + 1])) 

539 

540 # msg(" refiners[%i] can perform the transformation " + 

541 # "(%3i %3i %3i) -> (%3i %3i %3i)" % (\ 

542 # n, 

543 # self.cl.subgds[n].empty().shape[0], 

544 # self.cl.subgds[n].empty().shape[1], 

545 # self.cl.subgds[n].empty().shape[2], 

546 # self.cl.subgds[n + 1].empty().shape[0], 

547 # self.cl.subgds[n + 1].empty().shape[1], 

548 # self.cl.subgds[n + 1].empty().shape[2])) 

549 self.cl.coarseners.append(Transformer(self.cl.subgds[n + 1], 

550 self.cl.subgds[n])) 

551 self.cl.coarseners[:] = self.cl.coarseners[::-1] 

552 

553 # Now extend the grid in order to handle 

554 # the zero boundary conditions that the refiner assumes 

555 # The default interpolation order 

556 self.extend_nn = \ 

557 Transformer(GridDescriptor([8, 8, 8], [1, 1, 1], 

558 False, serial_comm, None), 

559 GridDescriptor([8, 8, 8], [1, 1, 1], 

560 False, serial_comm, None).coarsen()).nn 

561 

562 self.extended_num_indices = self.num_indices + [2, 2, 2] 

563 

564 # Center, left, and right points of the suggested quantum grid 

565 extended_cp = 0.5 * (np.array(self.given_corner_v1 / Bohr) + 

566 np.array(self.given_corner_v2 / Bohr)) 

567 extended_lp = extended_cp - 0.5 * (self.extended_num_indices * 

568 self.cl.spacing) 

569 # extended_rp = extended_cp + 0.5 * (self.extended_num_indices * 

570 # self.cl.spacing) 

571 

572 # Indices in the classical grid restricting the quantum grid 

573 self.extended_shift_indices_1 = \ 

574 np.round(extended_lp / self.cl.spacing).astype(int) 

575 self.extended_shift_indices_2 = \ 

576 self.extended_shift_indices_1 + self.extended_num_indices 

577 

578 # msg(' extended_shift_indices_1: %i %i %i' 

579 # % (self.extended_shift_indices_1[0], 

580 # self.extended_shift_indices_1[1], 

581 # self.extended_shift_indices_1[2])) 

582 # msg(' extended_shift_indices_2: %i %i %i' 

583 # % (self.extended_shift_indices_2[0], 

584 # self.extended_shift_indices_2[1], 

585 # self.extended_shift_indices_2[2])) 

586 # msg(' cl.gd.N_c: %i %i %i' 

587 # % (self.cl.gd.N_c[0], self.cl.gd.N_c[1], self.cl.gd.N_c[2])) 

588 

589 # Sanity checks 

590 assert (all([self.extended_shift_indices_1[w] >= 0 and 

591 self.extended_shift_indices_2[w] <= self.cl.gd.N_c[w] 

592 for w in range(3)])), ( 

593 'Could not find appropriate quantum grid. ' 

594 'Move it further away from the boundary.') 

595 

596 # Corner coordinates 

597 self.qm.extended_corner1 = \ 

598 self.extended_shift_indices_1 * self.cl.spacing 

599 self.qm.extended_corner2 = \ 

600 self.extended_shift_indices_2 * self.cl.spacing 

601 N_c = self.extended_shift_indices_2 - self.extended_shift_indices_1 

602 

603 self.cl.extended_subgds = [] 

604 self.cl.extended_refiners = [] 

605 extended_subcell_cv = np.diag(self.qm.extended_corner2 - 

606 self.qm.extended_corner1) 

607 

608 self.cl.extended_subgds.append(GridDescriptor( 

609 N_c, extended_subcell_cv, False, serial_comm, None)) 

610 

611 for n in range(self.num_refinements): 

612 self.cl.extended_subgds.append(self.cl.extended_subgds[n].refine()) 

613 self.cl.extended_refiners.append(Transformer( 

614 self.cl.extended_subgds[n], self.cl.extended_subgds[n + 1])) 

615 

616 # msg(" extended_refiners[%i] can perform the transformation " + 

617 # "(%3i %3i %3i) -> (%3i %3i %3i)" 

618 # % (n, 

619 # self.cl.extended_subgds[n].empty().shape[0], 

620 # self.cl.extended_subgds[n].empty().shape[1], 

621 # self.cl.extended_subgds[n].empty().shape[2], 

622 # self.cl.extended_subgds[n + 1].empty().shape[0], 

623 # self.cl.extended_subgds[n + 1].empty().shape[1], 

624 # self.cl.extended_subgds[n + 1].empty().shape[2])) 

625 

626 # msg(" N_c/spacing of the refined subgrid: " + 

627 # "%3i %3i %3i / %.4f %.4f %.4f" 

628 # % (self.cl.subgds[-1].N_c[0], 

629 # self.cl.subgds[-1].N_c[1], 

630 # self.cl.subgds[-1].N_c[2], 

631 # self.cl.subgds[-1].h_cv[0][0] * Bohr, 

632 # self.cl.subgds[-1].h_cv[1][1] * Bohr, 

633 # self.cl.subgds[-1].h_cv[2][2] * Bohr)) 

634 # msg(" shape from the refined subgrid: %3i %3i %3i" 

635 # % (tuple(self.cl.subgds[-1].empty().shape))) 

636 

637 self.extended_deltaIndex = 2**(self.num_refinements) * self.extend_nn 

638 # msg(" self.extended_deltaIndex = %i" % self.extended_deltaIndex) 

639 

640 qgpts = self.cl.subgds[-1].coarsen().N_c 

641 

642 # Assure that one returns to the original shape 

643 dmygd = self.cl.subgds[-1].coarsen() 

644 for n in range(self.num_refinements - 1): 

645 dmygd = dmygd.coarsen() 

646 

647 # msg(" N_c/spacing of the coarsened subgrid: " + 

648 # "%3i %3i %3i / %.4f %.4f %.4f" 

649 # % (dmygd.N_c[0], dmygd.N_c[1], dmygd.N_c[2], 

650 # dmygd.h_cv[0][0] * Bohr, 

651 # dmygd.h_cv[1][1] * Bohr, 

652 # dmygd.h_cv[2][2] * Bohr)) 

653 

654 self.has_subsystems = True 

655 return atoms_out, self.qm.spacing[0] * Bohr, qgpts 

656 

657 def print_messages(self, printer_function): 

658 printer_function("\n *** QSFDTD ***\n") 

659 for msg in self.messages: 

660 printer_function(msg) 

661 

662 for msg in self.classical_material.messages: 

663 printer_function(msg) 

664 printer_function("\n *********************\n") 

665 

666 # Set the time step 

667 def set_time_step(self, time_step): 

668 self.time_step = time_step 

669 

670 # Set the time 

671 def set_time(self, time): 

672 self.time = time 

673 

674 # Setup kick 

675 def set_kick(self, kick): 

676 self.kick = np.array(kick) 

677 

678 def finalize_propagation(self): 

679 pass 

680 

681 def set_calculation_mode(self, calculation_mode): 

682 # Three calculation modes are available: 

683 # 1) solve: just solve the Poisson equation with 

684 # given quantum+classical rho 

685 # 2) iterate: iterate classical density so that the Poisson 

686 # equation for quantum+classical rho is satisfied 

687 # 3) propagate: propagate classical density in time, and solve 

688 # the new Poisson equation 

689 assert (calculation_mode == 'solve' or calculation_mode == 'iterate' or 

690 calculation_mode == 'propagate') 

691 self.calculation_mode = calculation_mode 

692 

693 # The density object must be attached, so that the electric field 

694 # from all-electron density can be calculated 

695 def set_density(self, density): 

696 self.density = density 

697 

698 # Returns the classical density and the grid descriptor 

699 def get_density(self, global_array=False): 

700 if global_array: 

701 return \ 

702 self.cl.gd.collect(self.classical_material.charge_density) * \ 

703 self.classical_material.sign, \ 

704 self.cl.gd 

705 else: 

706 return \ 

707 self.classical_material.charge_density * \ 

708 self.classical_material.sign, \ 

709 self.cl.gd 

710 

711 # Returns the quantum + classical density in the large classical box, 

712 # so that the classical charge is refined into it and the quantum 

713 # charge is coarsened there 

714 def get_combined_data(self, qmdata=None, cldata=None, spacing=None, 

715 qmgd=None, clgd=None): 

716 

717 if qmdata is None: 

718 qmdata = self.density.rhot_g 

719 

720 if cldata is None: 

721 cldata = (self.classical_material.charge_density * 

722 self.classical_material.sign) 

723 

724 if qmgd is None: 

725 qmgd = self.qm.gd 

726 

727 if clgd is None: 

728 clgd = self.cl.gd 

729 

730 if spacing is None: 

731 spacing = clgd.h_cv[0, 0] 

732 

733 spacing_au = spacing / Bohr # from Angstroms to a.u. 

734 

735 # Refine classical part 

736 clgd = clgd.new_descriptor() 

737 cl_g = cldata.copy() 

738 while clgd.h_cv[0, 0] > spacing_au * 1.50: # 45: 

739 clgd2 = clgd.refine() 

740 cl_g = Transformer(clgd, clgd2).apply(cl_g) 

741 clgd = clgd2 

742 

743 # Coarse quantum part 

744 qmgd = qmgd.new_descriptor() 

745 qm_g = qmdata.copy() 

746 while qmgd.h_cv[0, 0] < clgd.h_cv[0, 0] * 0.95: 

747 qmgd2 = qmgd.coarsen() 

748 qm_g = Transformer(qmgd, qmgd2).apply(qm_g) 

749 qmgd = qmgd2 

750 

751 assert np.all(np.absolute(qmgd.h_cv - clgd.h_cv) < 1e-12), \ 

752 " Spacings %.8f (qm) and %.8f (cl) Angstroms" \ 

753 % (qmgd.h_cv[0][0] * Bohr, clgd.h_cv[0][0] * Bohr) 

754 

755 # Do distribution on master 

756 big_cl_g = clgd.collect(cl_g) 

757 big_qm_g = qmgd.collect(qm_g, broadcast=True) 

758 

759 if clgd.comm.rank == 0: 

760 # now find the corners 

761 # r_gv_cl = \ 

762 # clgd.get_grid_point_coordinates().transpose((1, 2, 3, 0)) 

763 cind = (self.qm.corner1 / np.diag(clgd.h_cv) - 1).astype(int) 

764 

765 n = big_qm_g.shape 

766 

767 # print 'Corner points: ', self.qm.corner1*Bohr, \ 

768 # ' - ', self.qm.corner2*Bohr 

769 # print 'Calculated points: ', r_gv_cl[tuple(cind)]*Bohr, \ 

770 # ' - ', r_gv_cl[tuple(cind+n+1)]*Bohr 

771 

772 big_cl_g[cind[0] + 1:cind[0] + n[0] + 1, 

773 cind[1] + 1:cind[1] + n[1] + 1, 

774 cind[2] + 1:cind[2] + n[2] + 1] += big_qm_g 

775 

776 clgd.distribute(big_cl_g, cl_g) 

777 return cl_g, clgd 

778 

779 # Solve quantum and classical potentials, and add them up 

780 def solve_solve(self, **kwargs): 

781 self.phi_tot_qmgd, self.phi_tot_clgd, niter = \ 

782 self.potential_coupler.getPotential( 

783 local_rho_qm_qmgd=self.qm.rho, 

784 local_rho_cl_clgd=self.classical_material.sign * 

785 self.classical_material.charge_density, 

786 **kwargs) 

787 self.qm.phi[:] = self.phi_tot_qmgd[:] 

788 self.cl.phi[:] = self.phi_tot_clgd[:] 

789 return niter 

790 

791 # Iterate classical and quantum potentials until convergence 

792 def solve_iterate(self, **kwargs): 

793 # Initial value (unefficient?) 

794 self.solve_solve(**kwargs) 

795 old_rho_qm = self.qm.rho.copy() 

796 old_rho_cl = self.classical_material.charge_density.copy() 

797 

798 niter_cl = 0 

799 

800 while True: 

801 # field from the potential 

802 # E = -Div[Vh] 

803 self.classical_material.solve_electric_field(self.cl.phi) 

804 

805 # Polarizations P0_j and Ptot 

806 # P = (eps - eps0)E 

807 self.classical_material.solve_polarizations() 

808 

809 # Classical charge density 

810 # n = -Grad[P] 

811 self.classical_material.solve_rho() 

812 

813 # Update electrostatic potential 

814 # nabla^2 Vh = -4*pi*n 

815 niter = self.solve_solve(**kwargs) 

816 

817 # Mix potential 

818 try: 

819 self.mix_phi 

820 except AttributeError: 

821 self.mix_phi = SimpleMixer(0.10, self.qm.phi) 

822 

823 self.qm.phi = self.mix_phi.mix(self.qm.phi) 

824 

825 # Check convergence 

826 niter_cl += 1 

827 

828 dRho = self.qm.gd.integrate(abs(self.qm.rho - old_rho_qm)) + \ 

829 self.cl.gd.integrate( 

830 abs(self.classical_material.charge_density - old_rho_cl)) 

831 

832 if (abs(dRho) < 1e-3): 

833 break 

834 old_rho_qm = self.qm.rho.copy() 

835 old_rho_cl = (self.classical_material.sign * 

836 self.classical_material.charge_density).copy() 

837 

838 return (niter, niter_cl) 

839 

840 def solve_propagate(self, **kwargs): 

841 

842 # 1) P(t) from P(t-dt) and J(t-dt/2) 

843 self.classical_material.propagate_polarizations(self.time_step) 

844 

845 # 2) n(t) from P(t) 

846 self.classical_material.solve_rho() 

847 

848 # 3a) V(t) from n(t) 

849 niter = self.solve_solve(**kwargs) 

850 

851 # 4a) E(r) from V(t): E = -Div[Vh] 

852 self.classical_material.solve_electric_field(self.cl.phi) 

853 

854 # 4b) Apply the kick by changing the electric field 

855 if self.time == 0: 

856 self.cl.rho_gs = self.classical_material.charge_density.copy() 

857 self.qm.rho_gs = self.qm.rho.copy() 

858 self.cl.phi_gs = self.cl.phi.copy() 

859 self.cl.extrapolated_qm_phi_gs = self.cl.gd.zeros() 

860 self.qm.phi_gs = self.qm.phi.copy() 

861 self.classical_material.kick_electric_field(self.time_step, 

862 self.kick) 

863 

864 # 5) J(t+dt/2) from J(t-dt/2) and P(t) 

865 self.classical_material.propagate_currents(self.time_step) 

866 

867 # Update timer 

868 self.time = self.time + self.time_step 

869 

870 # Do not propagate before the next time step 

871 self.set_calculation_mode('solve') 

872 

873 return niter 

874 

875 def solve(self, 

876 phi, 

877 rho, 

878 charge=None, 

879 maxcharge=1e-6, 

880 zero_initial_phi=False, 

881 calculation_mode=None, 

882 timer=None): 

883 

884 if self.density is None: 

885 raise RuntimeError('FDTDPoissonSolver requires a density object.' 

886 ' Use set_density routine to initialize it.') 

887 

888 # Update local variables (which may 

889 # have changed in SCF cycle or propagator) 

890 self.qm.phi = phi 

891 self.qm.rho = rho 

892 

893 if (self.calculation_mode == 'solve'): 

894 # do not modify the polarizable material 

895 niter = self.solve_solve(charge=None, 

896 maxcharge=maxcharge, 

897 zero_initial_phi=False) 

898 

899 elif (self.calculation_mode == 'iterate'): 

900 # find self-consistent density 

901 niter = self.solve_iterate(charge=None, 

902 maxcharge=maxcharge, 

903 zero_initial_phi=False) 

904 

905 elif (self.calculation_mode == 'propagate'): 

906 # propagate one time step 

907 niter = self.solve_propagate(charge=None, 

908 maxcharge=maxcharge, 

909 zero_initial_phi=False) 

910 

911 phi = self.qm.phi 

912 rho = self.qm.rho 

913 

914 return niter 

915 

916 # Classical contribution. Note the different origin. 

917 def get_classical_dipole_moment(self): 

918 r_gv = self.cl.gd.get_grid_point_coordinates().transpose(( 

919 1, 2, 3, 0)) - self.qm.corner1 

920 return -1.0 * self.classical_material.sign * np.array( 

921 [self.cl.gd.integrate(np.multiply( 

922 r_gv[:, :, :, w] + self.qm.corner1[w], 

923 self.classical_material.charge_density)) for w in range(3)]) 

924 

925 # Quantum contribution 

926 def get_quantum_dipole_moment(self): 

927 return self.density.finegd.calculate_dipole_moment(self.density.rhot_g) 

928 

929 # Read restart data 

930 def read(self, reader): 

931 r = reader.hamiltonian.poisson 

932 

933 # FDTDPoissonSolver related data 

934 self.description = r.description 

935 self.time = r.time 

936 self.time_step = r.time_step 

937 

938 # Try to read time-dependent information 

939 self.kick = r.kick 

940 self.maxiter = r.maxiter 

941 

942 # PoissonOrganizer: classical 

943 self.cl = PoissonOrganizer() 

944 self.cl.spacing_def = r.cl_spacing_def 

945 self.cl.spacing = r.cl_spacing 

946 self.cl.cell = np.diag(r.cl_cell) 

947 self.cl.dparsize = None 

948 

949 # TODO: it should be possible to use different 

950 # communicator after restart 

951 if r.cl_world_comm: 

952 self.cl.dcomm = world 

953 else: 

954 self.cl.dcomm = mpi.serial_comm 

955 

956 # Generate classical grid descriptor 

957 self.initialize_clgd() 

958 

959 # Classical materials data 

960 self.classical_material = PolarizableMaterial() 

961 self.classical_material.read(r) 

962 self.classical_material.initialize(self.cl.gd) 

963 

964 # PoissonOrganizer: quantum 

965 self.qm = PoissonOrganizer() 

966 self.qm.corner1 = r.qm_corner1 

967 self.qm.corner2 = r.qm_corner2 

968 self.given_corner_v1 = r.given_corner_1 

969 self.given_corner_v2 = r.given_corner_2 

970 self.given_cell = np.diag(r.given_cell) 

971 self.hratios = r.hratios 

972 self.shift_indices_1 = r.shift_indices_1.astype(int) 

973 self.shift_indices_2 = r.shift_indices_2.astype(int) 

974 self.num_indices = r.num_indices.astype(int) 

975 self.num_refinements = int(r.num_refinements) 

976 

977 # Redefine atoms to suit the cut_cell routine 

978 newatoms = read_atoms(reader.atoms) 

979 newatoms.positions = newatoms.positions + self.qm.corner1 * Bohr 

980 newatoms.set_cell(np.diag(self.given_cell)) 

981 self.create_subsystems(newatoms) 

982 

983 # Read self.classical_material.charge_density 

984 if self.cl.gd.comm.rank == 0: 

985 big_charge_density = \ 

986 np.array(r.get('classical_material_rho'), dtype=float) 

987 else: 

988 big_charge_density = None 

989 self.cl.gd.distribute(big_charge_density, 

990 self.classical_material.charge_density) 

991 

992 # Read self.classical_material.polarization_total 

993 if self.cl.gd.comm.rank == 0: 

994 big_polarization_total = \ 

995 np.array(r.get('polarization_total'), dtype=float) 

996 else: 

997 big_polarization_total = None 

998 self.cl.gd.distribute(big_polarization_total, 

999 self.classical_material.polarization_total) 

1000 

1001 # Read self.classical_material.polarizations 

1002 if self.cl.gd.comm.rank == 0: 

1003 big_polarizations = np.array(r.get('polarizations'), dtype=float) 

1004 else: 

1005 big_polarizations = None 

1006 self.cl.gd.distribute(big_polarizations, 

1007 self.classical_material.polarizations) 

1008 

1009 # Read self.classical_material.currents 

1010 if self.cl.gd.comm.rank == 0: 

1011 big_currents = np.array(r.get('currents'), dtype=float) 

1012 else: 

1013 big_currents = None 

1014 self.cl.gd.distribute(big_currents, self.classical_material.currents) 

1015 

1016 def write(self, writer): 

1017 # Classical materials data 

1018 self.classical_material.write(writer.child('classmat')) 

1019 

1020 # FDTDPoissonSolver related data 

1021 writer.write( 

1022 description=self.get_description(), 

1023 time=self.time, 

1024 time_step=self.time_step, 

1025 kick=self.kick, 

1026 maxiter=self.maxiter, 

1027 # PoissonOrganizer 

1028 cl_cell=np.diag(self.cl.cell), 

1029 cl_spacing_def=self.cl.spacing_def, 

1030 cl_spacing=self.cl.spacing, 

1031 cl_world_comm=self.cl.dcomm == world, 

1032 qm_corner1=self.qm.corner1, 

1033 qm_corner2=self.qm.corner2, 

1034 given_corner_1=self.given_corner_v1, 

1035 given_corner_2=self.given_corner_v2, 

1036 given_cell=np.diag(self.given_cell), 

1037 hratios=self.hratios, 

1038 shift_indices_1=self.shift_indices_1, 

1039 shift_indices_2=self.shift_indices_2, 

1040 num_refinements=self.num_refinements, 

1041 num_indices=self.num_indices) 

1042 

1043 # Write the classical charge density 

1044 charge_density = self.cl.gd.collect( 

1045 self.classical_material.charge_density) 

1046 writer.write(classical_material_rho=charge_density) 

1047 

1048 # Write the total polarization 

1049 polarization_total = self.cl.gd.collect( 

1050 self.classical_material.polarization_total) 

1051 writer.write(polarization_total=polarization_total) 

1052 

1053 # Write the partial polarizations 

1054 polarizations = self.cl.gd.collect( 

1055 self.classical_material.polarizations) 

1056 writer.write(polarizations=polarizations) 

1057 

1058 # Write the partial currents 

1059 currents = self.cl.gd.collect(self.classical_material.currents) 

1060 writer.write(currents=currents)