Coverage for /builds/alexhroom/ase/ase/calculators/siesta/siesta.py: 80.75%

374 statements  

« prev     ^ index     » next       coverage.py v7.5.3, created at 2024-08-05 14:37 +0000

1""" 

2This module defines the ASE interface to SIESTA. 

3 

4Written by Mads Engelund (see www.espeem.com) 

5 

6Home of the SIESTA package: 

7http://www.uam.es/departamentos/ciencias/fismateriac/siesta 

8 

92017.04 - Pedro Brandimarte: changes for python 2-3 compatible 

10 

11""" 

12 

13import os 

14import re 

15import shutil 

16import tempfile 

17from dataclasses import dataclass 

18from pathlib import Path 

19from typing import Any, Dict, List 

20 

21import numpy as np 

22 

23from ase import Atoms 

24from ase.calculators.calculator import FileIOCalculator, Parameters, ReadError 

25from ase.calculators.siesta.import_ion_xml import get_ion 

26from ase.calculators.siesta.parameters import PAOBasisBlock, format_fdf 

27from ase.data import atomic_numbers 

28from ase.io.siesta import read_siesta_xv 

29from ase.io.siesta_input import SiestaInput 

30from ase.units import Ry, eV 

31from ase.utils import deprecated 

32 

33meV = 0.001 * eV 

34 

35 

36def parse_siesta_version(output: bytes) -> str: 

37 match = re.search(rb'Siesta Version\s*:\s*(\S+)', output) 

38 

39 if match is None: 

40 raise RuntimeError('Could not get Siesta version info from output ' 

41 '{!r}'.format(output)) 

42 

43 string = match.group(1).decode('ascii') 

44 return string 

45 

46 

47def get_siesta_version(executable: str) -> str: 

48 """ Return SIESTA version number. 

49 

50 Run the command, for instance 'siesta' and 

51 then parse the output in order find the 

52 version number. 

53 """ 

54 # XXX We need a test of this kind of function. But Siesta().command 

55 # is not enough to tell us how to run Siesta, because it could contain 

56 # all sorts of mpirun and other weird parts. 

57 

58 temp_dirname = tempfile.mkdtemp(prefix='siesta-version-check-') 

59 try: 

60 from subprocess import PIPE, Popen 

61 proc = Popen([executable], 

62 stdin=PIPE, 

63 stdout=PIPE, 

64 stderr=PIPE, 

65 cwd=temp_dirname) 

66 output, _ = proc.communicate() 

67 # We are not providing any input, so Siesta will give us a failure 

68 # saying that it has no Chemical_species_label and exit status 1 

69 # (as of siesta-4.1-b4) 

70 finally: 

71 shutil.rmtree(temp_dirname) 

72 

73 return parse_siesta_version(output) 

74 

75 

76def format_block(name, block): 

77 lines = [f'%block {name}'] 

78 for row in block: 

79 data = ' '.join(str(obj) for obj in row) 

80 lines.append(f' {data}') 

81 lines.append(f'%endblock {name}') 

82 return '\n'.join(lines) 

83 

84 

85def bandpath2bandpoints(path): 

86 return '\n'.join([ 

87 'BandLinesScale ReciprocalLatticeVectors', 

88 format_block('BandPoints', path.kpts)]) 

89 

90 

91class SiestaParameters(Parameters): 

92 def __init__( 

93 self, 

94 label='siesta', 

95 mesh_cutoff=200 * Ry, 

96 energy_shift=100 * meV, 

97 kpts=None, 

98 xc='LDA', 

99 basis_set='DZP', 

100 spin='non-polarized', 

101 species=(), 

102 pseudo_qualifier=None, 

103 pseudo_path=None, 

104 symlink_pseudos=None, 

105 atoms=None, 

106 restart=None, 

107 fdf_arguments=None, 

108 atomic_coord_format='xyz', 

109 bandpath=None): 

110 kwargs = locals() 

111 kwargs.pop('self') 

112 Parameters.__init__(self, **kwargs) 

113 

114 

115def _nonpolarized_alias(_: List, kwargs: Dict[str, Any]) -> bool: 

116 if kwargs.get("spin", None) == "UNPOLARIZED": 

117 kwargs["spin"] = "non-polarized" 

118 return True 

119 return False 

120 

121 

122class Siesta(FileIOCalculator): 

123 """Calculator interface to the SIESTA code. 

124 """ 

125 allowed_xc = { 

126 'LDA': ['PZ', 'CA', 'PW92'], 

127 'GGA': ['PW91', 'PBE', 'revPBE', 'RPBE', 

128 'WC', 'AM05', 'PBEsol', 'PBEJsJrLO', 

129 'PBEGcGxLO', 'PBEGcGxHEG', 'BLYP'], 

130 'VDW': ['DRSLL', 'LMKLL', 'KBM', 'C09', 'BH', 'VV']} 

131 

132 name = 'siesta' 

133 _legacy_default_command = 'siesta < PREFIX.fdf > PREFIX.out' 

134 implemented_properties = [ 

135 'energy', 

136 'free_energy', 

137 'forces', 

138 'stress', 

139 'dipole', 

140 'eigenvalues', 

141 'density', 

142 'fermi_energy'] 

143 

144 # Dictionary of valid input vaiables. 

145 default_parameters = SiestaParameters() 

146 

147 # XXX Not a ASE standard mechanism (yet). We need to communicate to 

148 # ase.spectrum.band_structure.calculate_band_structure() that we expect 

149 # it to use the bandpath keyword. 

150 accepts_bandpath_keyword = True 

151 

152 fileio_rules = FileIOCalculator.ruleset( 

153 configspec=dict(pseudo_path=None), 

154 stdin_name='{prefix}.fdf', 

155 stdout_name='{prefix}.out') 

156 

157 def __init__(self, command=None, profile=None, directory='.', **kwargs): 

158 """ASE interface to the SIESTA code. 

159 

160 Parameters: 

161 - label : The basename of all files created during 

162 calculation. 

163 - mesh_cutoff : Energy in eV. 

164 The mesh cutoff energy for determining number of 

165 grid points in the matrix-element calculation. 

166 - energy_shift : Energy in eV 

167 The confining energy of the basis set generation. 

168 - kpts : Tuple of 3 integers, the k-points in different 

169 directions. 

170 - xc : The exchange-correlation potential. Can be set to 

171 any allowed value for either the Siesta 

172 XC.funtional or XC.authors keyword. Default "LDA" 

173 - basis_set : "SZ"|"SZP"|"DZ"|"DZP"|"TZP", strings which specify 

174 the type of functions basis set. 

175 - spin : "non-polarized"|"collinear"| 

176 "non-collinear|spin-orbit". 

177 The level of spin description to be used. 

178 - species : None|list of Species objects. The species objects 

179 can be used to to specify the basis set, 

180 pseudopotential and whether the species is ghost. 

181 The tag on the atoms object and the element is used 

182 together to identify the species. 

183 - pseudo_path : None|path. This path is where 

184 pseudopotentials are taken from. 

185 If None is given, then then the path given 

186 in $SIESTA_PP_PATH will be used. 

187 - pseudo_qualifier: None|string. This string will be added to the 

188 pseudopotential path that will be retrieved. 

189 For hydrogen with qualifier "abc" the 

190 pseudopotential "H.abc.psf" will be retrieved. 

191 - symlink_pseudos: None|bool 

192 If true, symlink pseudopotentials 

193 into the calculation directory, else copy them. 

194 Defaults to true on Unix and false on Windows. 

195 - atoms : The Atoms object. 

196 - restart : str. Prefix for restart file. 

197 May contain a directory. 

198 Default is None, don't restart. 

199 - fdf_arguments: Explicitly given fdf arguments. Dictonary using 

200 Siesta keywords as given in the manual. List values 

201 are written as fdf blocks with each element on a 

202 separate line, while tuples will write each element 

203 in a single line. ASE units are assumed in the 

204 input. 

205 - atomic_coord_format: "xyz"|"zmatrix", strings to switch between 

206 the default way of entering the system's geometry 

207 (via the block AtomicCoordinatesAndAtomicSpecies) 

208 and a recent method via the block Zmatrix. The 

209 block Zmatrix allows to specify basic geometry 

210 constrains such as realized through the ASE classes 

211 FixAtom, FixedLine and FixedPlane. 

212 """ 

213 

214 # Put in the default arguments. 

215 parameters = self.default_parameters.__class__(**kwargs) 

216 

217 # Call the base class. 

218 FileIOCalculator.__init__( 

219 self, 

220 command=command, 

221 profile=profile, 

222 directory=directory, 

223 **parameters) 

224 

225 def __getitem__(self, key): 

226 """Convenience method to retrieve a parameter as 

227 calculator[key] rather than calculator.parameters[key] 

228 

229 Parameters: 

230 -key : str, the name of the parameters to get. 

231 """ 

232 return self.parameters[key] 

233 

234 def species(self, atoms): 

235 """Find all relevant species depending on the atoms object and 

236 species input. 

237 

238 Parameters : 

239 - atoms : An Atoms object. 

240 """ 

241 return SiestaInput.get_species( 

242 atoms, list(self['species']), self['basis_set']) 

243 

244 @deprecated( 

245 "The keyword 'UNPOLARIZED' has been deprecated," 

246 "and replaced by 'non-polarized'", 

247 category=FutureWarning, 

248 callback=_nonpolarized_alias, 

249 ) 

250 def set(self, **kwargs): 

251 """Set all parameters. 

252 

253 Parameters: 

254 -kwargs : Dictionary containing the keywords defined in 

255 SiestaParameters. 

256 

257 .. deprecated:: 3.18.2 

258 The keyword 'UNPOLARIZED' has been deprecated and replaced by 

259 'non-polarized' 

260 """ 

261 

262 # XXX Inserted these next few lines because set() would otherwise 

263 # discard all previously set keywords to their defaults! --askhl 

264 current = self.parameters.copy() 

265 current.update(kwargs) 

266 kwargs = current 

267 

268 # Find not allowed keys. 

269 default_keys = list(self.__class__.default_parameters) 

270 offending_keys = set(kwargs) - set(default_keys) 

271 if len(offending_keys) > 0: 

272 mess = "'set' does not take the keywords: %s " 

273 raise ValueError(mess % list(offending_keys)) 

274 

275 # Use the default parameters. 

276 parameters = self.__class__.default_parameters.copy() 

277 parameters.update(kwargs) 

278 kwargs = parameters 

279 

280 # Check energy inputs. 

281 for arg in ['mesh_cutoff', 'energy_shift']: 

282 value = kwargs.get(arg) 

283 if value is None: 

284 continue 

285 if not (isinstance(value, (float, int)) and value > 0): 

286 mess = "'{}' must be a positive number(in eV), \ 

287 got '{}'".format(arg, value) 

288 raise ValueError(mess) 

289 

290 # Check the functional input. 

291 xc = kwargs.get('xc', 'LDA') 

292 if isinstance(xc, (tuple, list)) and len(xc) == 2: 

293 functional, authors = xc 

294 if functional.lower() not in [k.lower() for k in self.allowed_xc]: 

295 mess = f"Unrecognized functional keyword: '{functional}'" 

296 raise ValueError(mess) 

297 

298 lsauthorslower = [a.lower() for a in self.allowed_xc[functional]] 

299 if authors.lower() not in lsauthorslower: 

300 mess = "Unrecognized authors keyword for %s: '%s'" 

301 raise ValueError(mess % (functional, authors)) 

302 

303 elif xc in self.allowed_xc: 

304 functional = xc 

305 authors = self.allowed_xc[xc][0] 

306 else: 

307 found = False 

308 for key, value in self.allowed_xc.items(): 

309 if xc in value: 

310 found = True 

311 functional = key 

312 authors = xc 

313 break 

314 

315 if not found: 

316 raise ValueError(f"Unrecognized 'xc' keyword: '{xc}'") 

317 kwargs['xc'] = (functional, authors) 

318 

319 # Check fdf_arguments. 

320 if kwargs['fdf_arguments'] is None: 

321 kwargs['fdf_arguments'] = {} 

322 

323 if not isinstance(kwargs['fdf_arguments'], dict): 

324 raise TypeError("fdf_arguments must be a dictionary.") 

325 

326 # Call baseclass. 

327 FileIOCalculator.set(self, **kwargs) 

328 

329 def set_fdf_arguments(self, fdf_arguments): 

330 """ Set the fdf_arguments after the initialization of the 

331 calculator. 

332 """ 

333 self.validate_fdf_arguments(fdf_arguments) 

334 FileIOCalculator.set(self, fdf_arguments=fdf_arguments) 

335 

336 def validate_fdf_arguments(self, fdf_arguments): 

337 """ Raises error if the fdf_argument input is not a 

338 dictionary of allowed keys. 

339 """ 

340 # None is valid 

341 if fdf_arguments is None: 

342 return 

343 

344 # Type checking. 

345 if not isinstance(fdf_arguments, dict): 

346 raise TypeError("fdf_arguments must be a dictionary.") 

347 

348 def write_input(self, atoms, properties=None, system_changes=None): 

349 """Write input (fdf)-file. 

350 See calculator.py for further details. 

351 

352 Parameters: 

353 - atoms : The Atoms object to write. 

354 - properties : The properties which should be calculated. 

355 - system_changes : List of properties changed since last run. 

356 """ 

357 

358 super().write_input( 

359 atoms=atoms, 

360 properties=properties, 

361 system_changes=system_changes) 

362 

363 filename = self.getpath(ext='fdf') 

364 

365 more_fdf_args = {} 

366 

367 # Use the saved density matrix if only 'cell' and 'positions' 

368 # have changed. 

369 if (system_changes is None or 

370 ('numbers' not in system_changes and 

371 'initial_magmoms' not in system_changes and 

372 'initial_charges' not in system_changes)): 

373 

374 more_fdf_args['DM.UseSaveDM'] = True 

375 

376 if 'density' in properties: 

377 more_fdf_args['SaveRho'] = True 

378 

379 species, species_numbers = self.species(atoms) 

380 

381 pseudo_path = (self['pseudo_path'] 

382 or self.profile.configvars.get('pseudo_path') 

383 or self.cfg.get('SIESTA_PP_PATH')) 

384 

385 if not pseudo_path: 

386 raise Exception( 

387 'Please configure pseudo_path or SIESTA_PP_PATH envvar') 

388 

389 species_info = SpeciesInfo( 

390 atoms=atoms, 

391 pseudo_path=Path(pseudo_path), 

392 pseudo_qualifier=self.pseudo_qualifier(), 

393 species=species) 

394 

395 writer = FDFWriter( 

396 name=self.prefix, 

397 xc=self['xc'], 

398 spin=self['spin'], 

399 mesh_cutoff=self['mesh_cutoff'], 

400 energy_shift=self['energy_shift'], 

401 fdf_user_args=self['fdf_arguments'], 

402 more_fdf_args=more_fdf_args, 

403 species_numbers=species_numbers, 

404 atomic_coord_format=self['atomic_coord_format'].lower(), 

405 kpts=self['kpts'], 

406 bandpath=self['bandpath'], 

407 species_info=species_info, 

408 ) 

409 

410 with open(filename, 'w') as fd: 

411 writer.write(fd) 

412 

413 writer.link_pseudos_into_directory( 

414 symlink_pseudos=self['symlink_pseudos'], 

415 directory=Path(self.directory)) 

416 

417 def read(self, filename): 

418 """Read structural parameters from file .XV file 

419 Read other results from other files 

420 filename : siesta.XV 

421 """ 

422 

423 fname = self.getpath(filename) 

424 if not fname.exists(): 

425 raise ReadError(f"The restart file '{fname}' does not exist") 

426 with fname.open() as fd: 

427 self.atoms = read_siesta_xv(fd) 

428 self.read_results() 

429 

430 def getpath(self, fname=None, ext=None): 

431 """ Returns the directory/fname string """ 

432 if fname is None: 

433 fname = self.prefix 

434 if ext is not None: 

435 fname = f'{fname}.{ext}' 

436 return Path(self.directory) / fname 

437 

438 def pseudo_qualifier(self): 

439 """Get the extra string used in the middle of the pseudopotential. 

440 The retrieved pseudopotential for a specific element will be 

441 'H.xxx.psf' for the element 'H' with qualifier 'xxx'. If qualifier 

442 is set to None then the qualifier is set to functional name. 

443 """ 

444 if self['pseudo_qualifier'] is None: 

445 return self['xc'][0].lower() 

446 else: 

447 return self['pseudo_qualifier'] 

448 

449 def read_results(self): 

450 """Read the results.""" 

451 from ase.io.siesta_output import OutputReader 

452 reader = OutputReader(prefix=self.prefix, 

453 directory=Path(self.directory), 

454 bandpath=self['bandpath']) 

455 results = reader.read_results() 

456 self.results.update(results) 

457 

458 self.results['ion'] = self.read_ion(self.atoms) 

459 

460 def read_ion(self, atoms): 

461 """ 

462 Read the ion.xml file of each specie 

463 """ 

464 species, species_numbers = self.species(atoms) 

465 

466 ion_results = {} 

467 for species_number, spec in enumerate(species, start=1): 

468 symbol = spec['symbol'] 

469 atomic_number = atomic_numbers[symbol] 

470 

471 if spec['pseudopotential'] is None: 

472 if self.pseudo_qualifier() == '': 

473 label = symbol 

474 else: 

475 label = f"{symbol}.{self.pseudo_qualifier()}" 

476 pseudopotential = self.getpath(label, 'psf') 

477 else: 

478 pseudopotential = Path(spec['pseudopotential']) 

479 label = pseudopotential.stem 

480 

481 name = f"{label}.{species_number}" 

482 if spec['ghost']: 

483 name = f"{name}.ghost" 

484 atomic_number = -atomic_number 

485 

486 label = name.rsplit('.', 2)[0] 

487 

488 if label not in ion_results: 

489 fname = self.getpath(label, 'ion.xml') 

490 fname = Path(fname) 

491 if fname.is_file(): 

492 ion_results[label] = get_ion(fname) 

493 

494 return ion_results 

495 

496 def band_structure(self): 

497 return self.results['bandstructure'] 

498 

499 def get_fermi_level(self): 

500 return self.results['fermi_energy'] 

501 

502 def get_k_point_weights(self): 

503 return self.results['kpoint_weights'] 

504 

505 def get_ibz_k_points(self): 

506 return self.results['kpoints'] 

507 

508 def get_eigenvalues(self, kpt=0, spin=0): 

509 return self.results['eigenvalues'][spin, kpt] 

510 

511 def get_number_of_spins(self): 

512 return self.results['eigenvalues'].shape[0] 

513 

514 

515def generate_atomic_coordinates(atoms: Atoms, species_numbers, 

516 atomic_coord_format: str): 

517 """Write atomic coordinates. 

518 

519 Parameters 

520 ---------- 

521 fd : IO 

522 An open file object. 

523 atoms : Atoms 

524 An atoms object. 

525 """ 

526 if atomic_coord_format == 'xyz': 

527 return generate_atomic_coordinates_xyz(atoms, species_numbers) 

528 elif atomic_coord_format == 'zmatrix': 

529 return generate_atomic_coordinates_zmatrix(atoms, species_numbers) 

530 else: 

531 raise RuntimeError( 

532 f'Unknown atomic_coord_format: {atomic_coord_format}') 

533 

534 

535def generate_atomic_coordinates_zmatrix(atoms: Atoms, species_numbers): 

536 """Write atomic coordinates in Z-matrix format. 

537 

538 Parameters 

539 ---------- 

540 fd : IO 

541 An open file object. 

542 atoms : Atoms 

543 An atoms object. 

544 """ 

545 yield '\n' 

546 yield var('ZM.UnitsLength', 'Ang') 

547 yield '%block Zmatrix\n' 

548 yield ' cartesian\n' 

549 

550 fstr = "{:5d}" + "{:20.10f}" * 3 + "{:3d}" * 3 + "{:7d} {:s}\n" 

551 a2constr = SiestaInput.make_xyz_constraints(atoms) 

552 a2p, a2s = atoms.get_positions(), atoms.symbols 

553 for ia, (sp, xyz, ccc, sym) in enumerate( 

554 zip(species_numbers, a2p, a2constr, a2s)): 

555 yield fstr.format( 

556 sp, xyz[0], xyz[1], xyz[2], ccc[0], 

557 ccc[1], ccc[2], ia + 1, sym) 

558 yield '%endblock Zmatrix\n' 

559 

560 # origin = tuple(-atoms.get_celldisp().flatten()) 

561 # yield block('AtomicCoordinatesOrigin', [origin]) 

562 

563 

564def generate_atomic_coordinates_xyz(atoms: Atoms, species_numbers): 

565 """Write atomic coordinates. 

566 

567 Parameters 

568 ---------- 

569 fd : IO 

570 An open file object. 

571 atoms : Atoms 

572 An atoms object. 

573 """ 

574 yield '\n' 

575 yield var('AtomicCoordinatesFormat', 'Ang') 

576 yield block('AtomicCoordinatesAndAtomicSpecies', 

577 [[*atom.position, number] 

578 for atom, number in zip(atoms, species_numbers)]) 

579 yield '\n' 

580 

581 # origin = tuple(-atoms.get_celldisp().flatten()) 

582 # yield block('AtomicCoordinatesOrigin', [origin]) 

583 

584 

585@dataclass 

586class SpeciesInfo: 

587 atoms: Atoms 

588 pseudo_path: Path 

589 pseudo_qualifier: str 

590 species: dict # actually a kind of Parameters object, should refactor 

591 

592 def __post_init__(self): 

593 pao_basis = [] 

594 chemical_labels = [] 

595 basis_sizes = [] 

596 file_instructions = [] 

597 

598 for species_number, spec in enumerate(self.species, start=1): 

599 symbol = spec['symbol'] 

600 atomic_number = atomic_numbers[symbol] 

601 

602 if spec['pseudopotential'] is None: 

603 if self.pseudo_qualifier == '': 

604 label = symbol 

605 else: 

606 label = f"{symbol}.{self.pseudo_qualifier}" 

607 src_path = self.pseudo_path / f"{label}.psf" 

608 else: 

609 src_path = Path(spec['pseudopotential']) 

610 label = src_path.stem 

611 if not src_path.is_absolute(): 

612 src_path = self.pseudo_path / src_path 

613 if not src_path.exists(): 

614 src_path = self.pseudo_path / f"{symbol}.psml" 

615 

616 name = src_path.name 

617 name = name.split('.') 

618 name.insert(-1, str(species_number)) 

619 if spec['ghost']: 

620 name.insert(-1, 'ghost') 

621 atomic_number = -atomic_number 

622 

623 name = '.'.join(name) 

624 

625 instr = FileInstruction(src_path, name) 

626 file_instructions.append(instr) 

627 

628 label = '.'.join(np.array(name.split('.'))[:-1]) 

629 string = ' %d %d %s' % (species_number, atomic_number, label) 

630 chemical_labels.append(string) 

631 if isinstance(spec['basis_set'], PAOBasisBlock): 

632 pao_basis.append(spec['basis_set'].script(label)) 

633 else: 

634 basis_sizes.append((" " + label, spec['basis_set'])) 

635 

636 self.file_instructions = file_instructions 

637 self.chemical_labels = chemical_labels 

638 self.pao_basis = pao_basis 

639 self.basis_sizes = basis_sizes 

640 

641 def generate_text(self): 

642 yield var('NumberOfSpecies', len(self.species)) 

643 yield var('NumberOfAtoms', len(self.atoms)) 

644 

645 yield var('ChemicalSpecieslabel', self.chemical_labels) 

646 yield '\n' 

647 yield var('PAO.Basis', self.pao_basis) 

648 yield var('PAO.BasisSizes', self.basis_sizes) 

649 yield '\n' 

650 

651 

652@dataclass 

653class FileInstruction: 

654 src_path: Path 

655 targetname: str 

656 

657 def copy_to(self, directory): 

658 self._link(shutil.copy, directory) 

659 

660 def symlink_to(self, directory): 

661 self._link(os.symlink, directory) 

662 

663 def _link(self, file_operation, directory): 

664 dst_path = directory / self.targetname 

665 if self.src_path == dst_path: 

666 return 

667 

668 dst_path.unlink(missing_ok=True) 

669 file_operation(self.src_path, dst_path) 

670 

671 

672@dataclass 

673class FDFWriter: 

674 name: str 

675 xc: str 

676 fdf_user_args: dict 

677 more_fdf_args: dict 

678 mesh_cutoff: float 

679 energy_shift: float 

680 spin: str 

681 species_numbers: object # ? 

682 atomic_coord_format: str 

683 kpts: object # ? 

684 bandpath: object # ? 

685 species_info: object 

686 

687 def write(self, fd): 

688 for chunk in self.generate_text(): 

689 fd.write(chunk) 

690 

691 def generate_text(self): 

692 yield var('SystemName', self.name) 

693 yield var('SystemLabel', self.name) 

694 yield "\n" 

695 

696 # Write explicitly given options first to 

697 # allow the user to override anything. 

698 fdf_arguments = self.fdf_user_args 

699 for key in sorted(fdf_arguments): 

700 yield var(key, fdf_arguments[key]) 

701 

702 # Force siesta to return error on no convergence. 

703 # as default consistent with ASE expectations. 

704 if 'SCFMustConverge' not in fdf_arguments: 

705 yield var('SCFMustConverge', True) 

706 yield '\n' 

707 

708 yield var('Spin', self.spin) 

709 # Spin backwards compatibility. 

710 if self.spin == 'collinear': 

711 key = 'SpinPolarized' 

712 elif self.spin == 'non-collinear': 

713 key = 'NonCollinearSpin' 

714 else: 

715 key = None 

716 

717 if key is not None: 

718 yield var(key, (True, '# Backwards compatibility.')) 

719 

720 # Write functional. 

721 functional, authors = self.xc 

722 yield var('XC.functional', functional) 

723 yield var('XC.authors', authors) 

724 yield '\n' 

725 

726 # Write mesh cutoff and energy shift. 

727 yield var('MeshCutoff', (self.mesh_cutoff, 'eV')) 

728 yield var('PAO.EnergyShift', (self.energy_shift, 'eV')) 

729 yield '\n' 

730 

731 yield from self.species_info.generate_text() 

732 yield from self.generate_atoms_text(self.species_info.atoms) 

733 

734 for key, value in self.more_fdf_args.items(): 

735 yield var(key, value) 

736 

737 if self.kpts is not None: 

738 kpts = np.array(self.kpts) 

739 yield from SiestaInput.generate_kpts(kpts) 

740 

741 if self.bandpath is not None: 

742 lines = bandpath2bandpoints(self.bandpath) 

743 assert isinstance(lines, str) # rename this variable? 

744 yield lines 

745 yield '\n' 

746 

747 def generate_atoms_text(self, atoms: Atoms): 

748 """Translate the Atoms object to fdf-format.""" 

749 

750 cell = atoms.cell 

751 yield '\n' 

752 

753 if cell.rank in [1, 2]: 

754 raise ValueError('Expected 3D unit cell or no unit cell. You may ' 

755 'wish to add vacuum along some directions.') 

756 

757 if np.any(cell): 

758 yield var('LatticeConstant', '1.0 Ang') 

759 yield block('LatticeVectors', cell) 

760 

761 yield from generate_atomic_coordinates( 

762 atoms, self.species_numbers, self.atomic_coord_format) 

763 

764 # Write magnetic moments. 

765 magmoms = atoms.get_initial_magnetic_moments() 

766 

767 # The DM.InitSpin block must be written to initialize to 

768 # no spin. SIESTA default is FM initialization, if the 

769 # block is not written, but we must conform to the 

770 # atoms object. 

771 if len(magmoms) == 0: 

772 yield '#Empty block forces ASE initialization.\n' 

773 

774 yield '%block DM.InitSpin\n' 

775 if len(magmoms) != 0 and isinstance(magmoms[0], np.ndarray): 

776 for n, M in enumerate(magmoms): 

777 if M[0] != 0: 

778 yield (' %d %.14f %.14f %.14f \n' 

779 % (n + 1, M[0], M[1], M[2])) 

780 elif len(magmoms) != 0 and isinstance(magmoms[0], float): 

781 for n, M in enumerate(magmoms): 

782 if M != 0: 

783 yield ' %d %.14f \n' % (n + 1, M) 

784 yield '%endblock DM.InitSpin\n' 

785 yield '\n' 

786 

787 def link_pseudos_into_directory(self, *, symlink_pseudos=None, directory): 

788 if symlink_pseudos is None: 

789 symlink_pseudos = os.name != 'nt' 

790 

791 for instruction in self.species_info.file_instructions: 

792 if symlink_pseudos: 

793 instruction.symlink_to(directory) 

794 else: 

795 instruction.copy_to(directory) 

796 

797 

798# Utilities for generating bits of strings. 

799# 

800# We are re-aliasing format_fdf and format_block in the anticipation 

801# that they may change, or we might move this onto a Formatter object 

802# which applies consistent spacings etc. 

803def var(key, value): 

804 return format_fdf(key, value) 

805 

806 

807def block(name, data): 

808 return format_block(name, data)