Coverage for /builds/kinetik161/ase/ase/io/nwchem/nwwriter.py: 78.87%

284 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-12-10 11:04 +0000

1import os 

2import random 

3import string 

4import warnings 

5from copy import deepcopy 

6from typing import List, Optional, Tuple 

7 

8import numpy as np 

9 

10from ase.calculators.calculator import KPoints, kpts2kpts 

11 

12_special_kws = ['center', 'autosym', 'autoz', 'theory', 'basis', 'xc', 'task', 

13 'set', 'symmetry', 'label', 'geompar', 'basispar', 'kpts', 

14 'bandpath', 'restart_kw', 'pretasks', 'charge'] 

15 

16_system_type = {1: 'polymer', 2: 'surface', 3: 'crystal'} 

17 

18 

19def _render_geom(atoms, params: dict) -> List[str]: 

20 """Generate the geometry block 

21 

22 Parameters 

23 ---------- 

24 atoms : Atoms 

25 Geometry for the computation 

26 params : dict 

27 Parameter set for the computation 

28 

29 Returns 

30 ------- 

31 geom : [str] 

32 Geometry block to use in the computation 

33 """ 

34 geom_header = ['geometry units angstrom'] 

35 for geomkw in ['center', 'autosym', 'autoz']: 

36 geom_header.append(geomkw if params.get(geomkw) else 'no' + geomkw) 

37 if 'geompar' in params: 

38 geom_header.append(params['geompar']) 

39 geom = [' '.join(geom_header)] 

40 

41 outpos = atoms.get_positions() 

42 pbc = atoms.pbc 

43 if np.any(pbc): 

44 scpos = atoms.get_scaled_positions() 

45 for i, pbci in enumerate(pbc): 

46 if pbci: 

47 outpos[:, i] = scpos[:, i] 

48 npbc = pbc.sum() 

49 cellpars = atoms.cell.cellpar() 

50 geom.append(f' system {_system_type[npbc]} units angstrom') 

51 if npbc == 3: 

52 geom.append(' lattice_vectors') 

53 for row in atoms.cell: 

54 geom.append(' {:20.16e} {:20.16e} {:20.16e}'.format(*row)) 

55 else: 

56 if pbc[0]: 

57 geom.append(f' lat_a {cellpars[0]:20.16e}') 

58 if pbc[1]: 

59 geom.append(f' lat_b {cellpars[1]:20.16e}') 

60 if pbc[2]: 

61 geom.append(f' lat_c {cellpars[2]:20.16e}') 

62 if pbc[1] and pbc[2]: 

63 geom.append(f' alpha {cellpars[3]:20.16e}') 

64 if pbc[0] and pbc[2]: 

65 geom.append(f' beta {cellpars[4]:20.16e}') 

66 if pbc[1] and pbc[0]: 

67 geom.append(f' gamma {cellpars[5]:20.16e}') 

68 geom.append(' end') 

69 

70 for i, atom in enumerate(atoms): 

71 geom.append(' {:<2} {:20.16e} {:20.16e} {:20.16e}' 

72 ''.format(atom.symbol, *outpos[i])) 

73 symm = params.get('symmetry') 

74 if symm is not None: 

75 geom.append(f' symmetry {symm}') 

76 geom.append('end') 

77 return geom 

78 

79 

80def _render_basis(theory, params: dict) -> List[str]: 

81 """Infer the basis set block 

82 

83 Arguments 

84 --------- 

85 theory : str 

86 Name of the theory used for the calculation 

87 params : dict 

88 Parameters for the computation 

89 

90 Returns 

91 ------- 

92 [str] 

93 List of input file lines for the basis block 

94 """ 

95 

96 # Break if no basis set is provided and non is applicable 

97 if 'basis' not in params: 

98 if theory in ['pspw', 'band', 'paw']: 

99 return [] 

100 

101 # Write the header section 

102 if 'basispar' in params: 

103 header = 'basis {} noprint'.format(params['basispar']) 

104 else: 

105 header = 'basis noprint' 

106 basis_out = [header] 

107 

108 # Write the basis set for each atom type 

109 basis_in = params.get('basis', '3-21G') 

110 if isinstance(basis_in, str): 

111 basis_out.append(f' * library {basis_in}') 

112 else: 

113 for symbol, ibasis in basis_in.items(): 

114 basis_out.append(f'{symbol:>4} library {ibasis}') 

115 basis_out.append('end') 

116 return basis_out 

117 

118 

119_special_keypairs = [('nwpw', 'simulation_cell'), 

120 ('nwpw', 'carr-parinello'), 

121 ('nwpw', 'brillouin_zone'), 

122 ('tddft', 'grad'), 

123 ] 

124 

125 

126def _render_brillouin_zone(array, name=None) -> List[str]: 

127 out = [' brillouin_zone'] 

128 if name is not None: 

129 out += [f' zone_name {name}'] 

130 template = ' kvector' + ' {:20.16e}' * array.shape[1] 

131 for row in array: 

132 out.append(template.format(*row)) 

133 out.append(' end') 

134 return out 

135 

136 

137def _render_bandpath(bp) -> List[str]: 

138 if bp is None: 

139 return [] 

140 out = ['nwpw'] 

141 out += _render_brillouin_zone(bp.kpts, name=bp.path) 

142 out += [f' zone_structure_name {bp.path}', 

143 'end', 

144 'task band structure'] 

145 return out 

146 

147 

148def _format_line(key, val) -> str: 

149 if val is None: 

150 return key 

151 if isinstance(val, bool): 

152 return f'{key} .{str(val).lower()}.' 

153 else: 

154 return ' '.join([key, str(val)]) 

155 

156 

157def _format_block(key, val, nindent=0) -> List[str]: 

158 prefix = ' ' * nindent 

159 prefix2 = ' ' * (nindent + 1) 

160 if val is None: 

161 return [prefix + key] 

162 

163 if not isinstance(val, dict): 

164 return [prefix + _format_line(key, val)] 

165 

166 out = [prefix + key] 

167 for subkey, subval in val.items(): 

168 if (key, subkey) in _special_keypairs: 

169 if (key, subkey) == ('nwpw', 'brillouin_zone'): 

170 out += _render_brillouin_zone(subval) 

171 else: 

172 out += _format_block(subkey, subval, nindent + 1) 

173 else: 

174 if isinstance(subval, dict): 

175 subval = ' '.join([_format_line(a, b) 

176 for a, b in subval.items()]) 

177 out.append(prefix2 + ' '.join([_format_line(subkey, subval)])) 

178 out.append(prefix + 'end') 

179 return out 

180 

181 

182def _render_other(params) -> List[str]: 

183 """Render other commands 

184 

185 Parameters 

186 ---------- 

187 params : dict 

188 Parameter set to be rendered 

189 

190 Returns 

191 ------- 

192 out : [str] 

193 Block defining other commands 

194 """ 

195 out = [] 

196 for kw, block in params.items(): 

197 if kw in _special_kws: 

198 continue 

199 out += _format_block(kw, block) 

200 return out 

201 

202 

203def _render_set(set_params) -> List[str]: 

204 """Render the commands for the set parameters 

205 

206 Parameters 

207 ---------- 

208 set_params : dict 

209 Parameters being set 

210 

211 Returns 

212 ------- 

213 out : [str] 

214 Block defining set commands 

215 """ 

216 return ['set ' + _format_line(key, val) for key, val in set_params.items()] 

217 

218 

219_gto_theories = ['tce', 'ccsd', 'tddft', 'scf', 'dft', 

220 'direct_mp2', 'mp2', 'rimp2'] 

221_pw_theories = ['band', 'pspw', 'paw'] 

222_all_theories = _gto_theories + _pw_theories 

223 

224 

225def _get_theory(params: dict) -> str: 

226 """Infer the theory given the user-provided settings 

227 

228 Parameters 

229 ---------- 

230 params : dict 

231 Parameters for the computation 

232 

233 Returns 

234 ------- 

235 theory : str 

236 Theory directive to use 

237 """ 

238 # Default: user-provided theory 

239 theory = params.get('theory') 

240 if theory is not None: 

241 return theory 

242 

243 # Check if the user passed a theory to xc 

244 xc = params.get('xc') 

245 if xc in _all_theories: 

246 return xc 

247 

248 # Check for input blocks that correspond to a particular level of 

249 # theory. Correlated theories (e.g. CCSD) are checked first. 

250 for kw in _gto_theories: 

251 if kw in params: 

252 return kw 

253 

254 # If the user passed an 'nwpw' block, then they want a plane-wave 

255 # calculation, but what kind? If they request k-points, then 

256 # they want 'band', otherwise assume 'pspw' (if the user wants 

257 # to use 'paw', they will have to ask for it specifically). 

258 nwpw = params.get('nwpw') 

259 if nwpw is not None: 

260 if 'monkhorst-pack' in nwpw or 'brillouin_zone' in nwpw: 

261 return 'band' 

262 return 'pspw' 

263 

264 # When all else fails, default to dft. 

265 return 'dft' 

266 

267 

268_xc_conv = dict(lda='slater pw91lda', 

269 pbe='xpbe96 cpbe96', 

270 revpbe='revpbe cpbe96', 

271 rpbe='rpbe cpbe96', 

272 pw91='xperdew91 perdew91', 

273 ) 

274 

275 

276def _update_mult(magmom_tot: int, params: dict) -> None: 

277 """Update parameters for multiplicity given the magnetic moment 

278 

279 For example, sets the number of open shells for SCF calculations 

280 and the multiplicity for DFT calculations. 

281 

282 Parameters 

283 ---------- 

284 magmom_tot : int 

285 Total magnetic moment of the system 

286 params : dict 

287 Current set of parameters, will be modified 

288 """ 

289 # Determine theory and multiplicity 

290 theory = params['theory'] 

291 if magmom_tot == 0: 

292 magmom_mult = 1 

293 else: 

294 magmom_mult = np.sign(magmom_tot) * (abs(magmom_tot) + 1) 

295 

296 # Adjust the kwargs for each type of theory 

297 if 'scf' in params: 

298 for kw in ['nopen', 'singlet', 'doublet', 'triplet', 'quartet', 

299 'quintet', 'sextet', 'septet', 'octet']: 

300 if kw in params['scf']: 

301 break 

302 else: 

303 params['scf']['nopen'] = magmom_tot 

304 elif theory in ['scf', 'mp2', 'direct_mp2', 'rimp2', 'ccsd', 'tce']: 

305 params['scf'] = dict(nopen=magmom_tot) 

306 

307 if 'dft' in params: 

308 if 'mult' not in params['dft']: 

309 params['dft']['mult'] = magmom_mult 

310 elif theory in ['dft', 'tddft']: 

311 params['dft'] = dict(mult=magmom_mult) 

312 

313 if 'nwpw' in params: 

314 if 'mult' not in params['nwpw']: 

315 params['nwpw']['mult'] = magmom_mult 

316 elif theory in ['pspw', 'band', 'paw']: 

317 params['nwpw'] = dict(mult=magmom_mult) 

318 

319 

320def _update_kpts(atoms, params) -> None: 

321 """Converts top-level 'kpts' argument to native keywords 

322 

323 Parameters 

324 ---------- 

325 atoms : Atoms 

326 Input structure 

327 params : dict 

328 Current parameter set, will be updated 

329 """ 

330 kpts = params.get('kpts') 

331 if kpts is None: 

332 return 

333 

334 nwpw = params.get('nwpw', {}) 

335 

336 if 'monkhorst-pack' in nwpw or 'brillouin_zone' in nwpw: 

337 raise ValueError("Redundant k-points specified!") 

338 

339 if isinstance(kpts, KPoints): 

340 nwpw['brillouin_zone'] = kpts.kpts 

341 elif isinstance(kpts, dict): 

342 if kpts.get('gamma', False) or 'size' not in kpts: 

343 nwpw['brillouin_zone'] = kpts2kpts(kpts, atoms).kpts 

344 else: 

345 nwpw['monkhorst-pack'] = ' '.join(map(str, kpts['size'])) 

346 elif isinstance(kpts, np.ndarray): 

347 nwpw['brillouin_zone'] = kpts 

348 else: 

349 nwpw['monkhorst-pack'] = ' '.join(map(str, kpts)) 

350 

351 params['nwpw'] = nwpw 

352 

353 

354def _render_pretask( 

355 this_step: dict, 

356 previous_basis: Optional[List[str]], 

357 wfc_path: str, 

358 next_steps: List[dict], 

359) -> Tuple[List[str], List[str]]: 

360 """Generate input file lines that perform a cheaper method first 

361 

362 Parameters 

363 ---------- 

364 this_step: dict 

365 Input parameters used to define the computation 

366 previous_basis: [str], optional 

367 Basis set block used in the previous step 

368 wfc_path: str 

369 Name of the wavefunction path 

370 next_steps: [dict] 

371 Parameters for the next steps in the calculation. 

372 This function will adjust the next steps to read 

373 and project from the wave functions written to disk by this step 

374 if the basis set changes between this step and the next. 

375 

376 Returns 

377 ------- 

378 output: [str] 

379 Output lines for this task 

380 this_basis: [str] 

381 Basis set block used for this task 

382 """ 

383 

384 # Get the theory for the next step 

385 next_step = next_steps[0] 

386 next_theory = _get_theory(next_step) 

387 next_step['theory'] = next_theory 

388 out = [] 

389 if next_theory not in ['dft', 'mp2', 'direct_mp2', 'rimp2', 'scf']: 

390 raise ValueError(f'Initial guesses not supported for {next_theory}') 

391 

392 # Determine the theory for this step 

393 this_theory = _get_theory(this_step) 

394 this_step['theory'] = this_theory 

395 if this_theory not in ['dft', 'scf']: 

396 raise ValueError('Initial guesses must use either dft or scf') 

397 

398 # Determine which basis set to use for this step. Our priorities 

399 # 1. Basis defined explicitly in this step 

400 # 2. Basis set of the previous step 

401 # 3. Basis set of the target computation level 

402 if 'basis' in this_step: 

403 this_basis = _render_basis(this_theory, this_step) 

404 elif previous_basis is not None: 

405 this_basis = previous_basis.copy() 

406 else: 

407 # Use the basis for the last step 

408 this_basis = _render_basis(next_theory, next_steps[-1]) 

409 

410 # Determine the basis for the next step 

411 # If not defined, it'll be the same as this step 

412 if 'basis' in next_step: 

413 next_basis = _render_basis(next_theory, next_step) 

414 else: 

415 next_basis = this_basis 

416 

417 # Set up projections if needed 

418 if this_basis == next_basis: 

419 out.append('\n'.join(this_basis)) 

420 proj_from = None # We do not need to project basis 

421 else: 

422 # Check for known limitations of NWChem 

423 if this_theory != next_theory: 

424 msg = 'Theories must be the same if basis are different. ' \ 

425 f'This step: {this_theory}//{this_basis} ' \ 

426 f'Next step: {next_theory}//{next_basis}' 

427 if 'basis' not in this_step: 

428 msg += f". Consider specifying basis in {this_step}" 

429 raise ValueError(msg) 

430 if not any('* library' in x for x in this_basis): 

431 raise ValueError('We can only support projecting from systems ' 

432 'where all atoms share the same basis') 

433 

434 # Append a new name to this basis function by 

435 # appending it as the first argument of the basis block 

436 proj_from = f"smb_{len(next_steps)}" 

437 this_basis[0] = f'basis {proj_from} {this_basis[0][6:]}' 

438 out.append('\n'.join(this_basis)) 

439 

440 # Point ao basis (the active basis set) to this new basis set 

441 out.append(f'set "ao basis" {proj_from}') 

442 

443 # Insert a command to write wfcs from this computation to disk 

444 if this_theory not in this_step: 

445 this_step[this_theory] = {} 

446 if 'vectors' not in this_step[this_theory]: 

447 this_step[this_theory]['vectors'] = {} 

448 this_step[this_theory]['vectors']['output'] = wfc_path 

449 

450 # Check if the initial theory changes 

451 if this_theory != next_theory and \ 

452 'lindep:n_dep' not in this_step.get('set', {}): 

453 warnings.warn('Loading initial guess may fail if you do not specify' 

454 ' the number of linearly-dependent basis functions.' 

455 ' Consider adding {"set": {"lindep:n_dep": 0}} ' 

456 f' to the step: {this_step}.') 

457 

458 # Add this to the input file along with a "task * ignore" command 

459 out.extend(['\n'.join(_render_other(this_step)), 

460 '\n'.join(_render_set(this_step.get('set', {}))), 

461 f'task {this_theory} ignore']) 

462 

463 # Command to read the wavefunctions in the next step 

464 # Theory used to get the wavefunctions may be different (mp2 uses SCF) 

465 wfc_theory = 'scf' if 'mp2' in next_theory else next_theory 

466 next_step = next_step 

467 if wfc_theory not in next_step: 

468 next_step[wfc_theory] = {} 

469 if 'vectors' not in next_step[wfc_theory]: 

470 next_step[wfc_theory]['vectors'] = {} 

471 

472 if proj_from is None: 

473 # No need for projection 

474 next_step[wfc_theory]['vectors']['input'] = wfc_path 

475 else: 

476 # Define that we should project from our basis set 

477 next_step[wfc_theory]['vectors']['input'] \ 

478 = f'project {proj_from} {wfc_path}' 

479 

480 # Replace the name of the basis set to the default 

481 out.append('set "ao basis" "ao basis"') 

482 

483 return out, this_basis 

484 

485 

486def write_nwchem_in(fd, atoms, properties=None, echo=False, **params): 

487 """Writes NWChem input file. 

488 

489 See :class:`~ase.calculators.nwchem.NWChem` for available params. 

490 

491 Parameters 

492 ---------- 

493 fd 

494 file descriptor 

495 atoms 

496 atomic configuration 

497 properties 

498 list of properties to compute; by default only the 

499 calculation of the energy is requested 

500 echo 

501 if True include the `echo` keyword at the top of the file, 

502 which causes the content of the input file to be included 

503 in the output file 

504 params 

505 dict of instructions blocks to be included 

506 """ 

507 # Copy so we can alter params w/o changing it for the function caller 

508 params = deepcopy(params) 

509 

510 if properties is None: 

511 properties = ['energy'] 

512 

513 if 'stress' in properties: 

514 if 'set' not in params: 

515 params['set'] = {} 

516 params['set']['includestress'] = True 

517 

518 task = params.get('task') 

519 if task is None: 

520 if 'stress' in properties or 'forces' in properties: 

521 task = 'gradient' 

522 else: 

523 task = 'energy' 

524 

525 _update_kpts(atoms, params) 

526 

527 # Determine the theory for each step 

528 # We determine the theory ahead of time because it is 

529 # used when generating other parts of the input file (e.g., _get_mult) 

530 theory = _get_theory(params) 

531 params['theory'] = theory 

532 

533 for pretask in params.get('pretasks', []): 

534 pretask['theory'] = _get_theory(pretask) 

535 

536 if 'xc' in params: 

537 xc = _xc_conv.get(params['xc'].lower(), params['xc']) 

538 if theory in ['dft', 'tddft']: 

539 if 'dft' not in params: 

540 params['dft'] = {} 

541 params['dft']['xc'] = xc 

542 elif theory in ['pspw', 'band', 'paw']: 

543 if 'nwpw' not in params: 

544 params['nwpw'] = {} 

545 params['nwpw']['xc'] = xc 

546 

547 # Update the multiplicity given the charge of the system 

548 magmom_tot = int(atoms.get_initial_magnetic_moments().sum()) 

549 _update_mult(magmom_tot, params) 

550 for pretask in params.get('pretasks', []): 

551 _update_mult(magmom_tot, pretask) 

552 

553 # Determine the header options 

554 label = params.get('label', 'nwchem') 

555 perm = os.path.abspath(params.pop('perm', label)) 

556 scratch = os.path.abspath(params.pop('scratch', label)) 

557 restart_kw = params.get('restart_kw', 'start') 

558 if restart_kw not in ('start', 'restart'): 

559 raise ValueError("Unrecognised restart keyword: {}!" 

560 .format(restart_kw)) 

561 short_label = label.rsplit('/', 1)[-1] 

562 if echo: 

563 out = ['echo'] 

564 else: 

565 out = [] 

566 

567 # Defines the geometry and global options 

568 out.extend([f'title "{short_label}"', 

569 f'permanent_dir {perm}', 

570 f'scratch_dir {scratch}', 

571 f'{restart_kw} {short_label}', 

572 '\n'.join(_render_geom(atoms, params))]) 

573 

574 # Add the charge if provided 

575 if 'charge' in params: 

576 out.append(f'charge {params["charge"]}') 

577 

578 # Define the memory separately from the other options so that it 

579 # is defined before any initial wfc guesses are performed 

580 memory_opts = params.pop('memory', None) 

581 if memory_opts is not None: 

582 out.extend(_render_other(dict(memory=memory_opts))) 

583 

584 # If desired, output commands to generate the initial wavefunctions 

585 if 'pretasks' in params: 

586 wfc_path = f'tmp-{"".join(random.choices(string.hexdigits, k=8))}.wfc' 

587 pretasks = params['pretasks'] 

588 previous_basis = None 

589 for this_ind, this_step in enumerate(pretasks): 

590 new_out, previous_basis = _render_pretask( 

591 this_step, 

592 previous_basis, 

593 wfc_path, 

594 pretasks[this_ind + 1:] + [params] 

595 ) 

596 out.extend(new_out) 

597 

598 # Finish output file with the commands to perform the desired computation 

599 out.extend(['\n'.join(_render_basis(theory, params)), 

600 '\n'.join(_render_other(params)), 

601 '\n'.join(_render_set(params.get('set', {}))), 

602 f'task {theory} {task}', 

603 '\n'.join(_render_bandpath(params.get('bandpath', None)))]) 

604 

605 fd.write('\n\n'.join(out))