Coverage for /builds/hweiske/ase/ase/calculators/siesta/siesta.py: 80.64%

377 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2024-04-22 11:22 +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 stdin_name='{prefix}.fdf', 

154 stdout_name='{prefix}.out') 

155 

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

157 """ASE interface to the SIESTA code. 

158 

159 Parameters: 

160 - label : The basename of all files created during 

161 calculation. 

162 - mesh_cutoff : Energy in eV. 

163 The mesh cutoff energy for determining number of 

164 grid points in the matrix-element calculation. 

165 - energy_shift : Energy in eV 

166 The confining energy of the basis set generation. 

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

168 directions. 

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

170 any allowed value for either the Siesta 

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

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

173 the type of functions basis set. 

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

175 "non-collinear|spin-orbit". 

176 The level of spin description to be used. 

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

178 can be used to to specify the basis set, 

179 pseudopotential and whether the species is ghost. 

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

181 together to identify the species. 

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

183 pseudopotentials are taken from. 

184 If None is given, then then the path given 

185 in $SIESTA_PP_PATH will be used. 

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

187 pseudopotential path that will be retrieved. 

188 For hydrogen with qualifier "abc" the 

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

190 - symlink_pseudos: None|bool 

191 If true, symlink pseudopotentials 

192 into the calculation directory, else copy them. 

193 Defaults to true on Unix and false on Windows. 

194 - atoms : The Atoms object. 

195 - restart : str. Prefix for restart file. 

196 May contain a directory. 

197 Default is None, don't restart. 

198 - fdf_arguments: Explicitly given fdf arguments. Dictonary using 

199 Siesta keywords as given in the manual. List values 

200 are written as fdf blocks with each element on a 

201 separate line, while tuples will write each element 

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

203 input. 

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

205 the default way of entering the system's geometry 

206 (via the block AtomicCoordinatesAndAtomicSpecies) 

207 and a recent method via the block Zmatrix. The 

208 block Zmatrix allows to specify basic geometry 

209 constrains such as realized through the ASE classes 

210 FixAtom, FixedLine and FixedPlane. 

211 """ 

212 

213 # Put in the default arguments. 

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

215 

216 # Call the base class. 

217 FileIOCalculator.__init__( 

218 self, 

219 command=command, 

220 profile=profile, 

221 directory=directory, 

222 **parameters) 

223 

224 def __getitem__(self, key): 

225 """Convenience method to retrieve a parameter as 

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

227 

228 Parameters: 

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

230 """ 

231 return self.parameters[key] 

232 

233 def species(self, atoms): 

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

235 species input. 

236 

237 Parameters : 

238 - atoms : An Atoms object. 

239 """ 

240 return SiestaInput.get_species( 

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

242 

243 @deprecated( 

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

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

246 category=FutureWarning, 

247 callback=_nonpolarized_alias, 

248 ) 

249 def set(self, **kwargs): 

250 """Set all parameters. 

251 

252 Parameters: 

253 -kwargs : Dictionary containing the keywords defined in 

254 SiestaParameters. 

255 

256 .. deprecated:: 3.18.2 

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

258 'non-polarized' 

259 """ 

260 

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

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

263 current = self.parameters.copy() 

264 current.update(kwargs) 

265 kwargs = current 

266 

267 # Find not allowed keys. 

268 default_keys = list(self.__class__.default_parameters) 

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

270 if len(offending_keys) > 0: 

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

272 raise ValueError(mess % list(offending_keys)) 

273 

274 # Use the default parameters. 

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

276 parameters.update(kwargs) 

277 kwargs = parameters 

278 

279 # Check energy inputs. 

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

281 value = kwargs.get(arg) 

282 if value is None: 

283 continue 

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

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

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

287 raise ValueError(mess) 

288 

289 # Check the functional input. 

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

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

292 functional, authors = xc 

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

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

295 raise ValueError(mess) 

296 

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

298 if authors.lower() not in lsauthorslower: 

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

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

301 

302 elif xc in self.allowed_xc: 

303 functional = xc 

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

305 else: 

306 found = False 

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

308 if xc in value: 

309 found = True 

310 functional = key 

311 authors = xc 

312 break 

313 

314 if not found: 

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

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

317 

318 # Check fdf_arguments. 

319 if kwargs['fdf_arguments'] is None: 

320 kwargs['fdf_arguments'] = {} 

321 

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

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

324 

325 # Call baseclass. 

326 FileIOCalculator.set(self, **kwargs) 

327 

328 def set_fdf_arguments(self, fdf_arguments): 

329 """ Set the fdf_arguments after the initialization of the 

330 calculator. 

331 """ 

332 self.validate_fdf_arguments(fdf_arguments) 

333 FileIOCalculator.set(self, fdf_arguments=fdf_arguments) 

334 

335 def validate_fdf_arguments(self, fdf_arguments): 

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

337 dictionary of allowed keys. 

338 """ 

339 # None is valid 

340 if fdf_arguments is None: 

341 return 

342 

343 # Type checking. 

344 if not isinstance(fdf_arguments, dict): 

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

346 

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

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

349 See calculator.py for further details. 

350 

351 Parameters: 

352 - atoms : The Atoms object to write. 

353 - properties : The properties which should be calculated. 

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

355 """ 

356 

357 super().write_input( 

358 atoms=atoms, 

359 properties=properties, 

360 system_changes=system_changes) 

361 

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

363 

364 more_fdf_args = {} 

365 

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

367 # have changed. 

368 if (system_changes is None or 

369 ('numbers' not in system_changes and 

370 'initial_magmoms' not in system_changes and 

371 'initial_charges' not in system_changes)): 

372 

373 more_fdf_args['DM.UseSaveDM'] = True 

374 

375 if 'density' in properties: 

376 more_fdf_args['SaveRho'] = True 

377 

378 species, species_numbers = self.species(atoms) 

379 

380 if self['pseudo_path'] is not None: 

381 pseudo_path = self['pseudo_path'] 

382 elif 'SIESTA_PP_PATH' in self.cfg: 

383 pseudo_path = self.cfg['SIESTA_PP_PATH'] 

384 else: 

385 mess = "Please set the environment variable 'SIESTA_PP_PATH'" 

386 raise Exception(mess) 

387 

388 species_info = SpeciesInfo( 

389 atoms=atoms, 

390 pseudo_path=Path(pseudo_path), 

391 pseudo_qualifier=self.pseudo_qualifier(), 

392 species=species) 

393 

394 writer = FDFWriter( 

395 name=self.prefix, 

396 xc=self['xc'], 

397 spin=self['spin'], 

398 mesh_cutoff=self['mesh_cutoff'], 

399 energy_shift=self['energy_shift'], 

400 fdf_user_args=self['fdf_arguments'], 

401 more_fdf_args=more_fdf_args, 

402 species_numbers=species_numbers, 

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

404 kpts=self['kpts'], 

405 bandpath=self['bandpath'], 

406 species_info=species_info, 

407 ) 

408 

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

410 writer.write(fd) 

411 

412 writer.link_pseudos_into_directory( 

413 symlink_pseudos=self['symlink_pseudos'], 

414 directory=Path(self.directory)) 

415 

416 def read(self, filename): 

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

418 Read other results from other files 

419 filename : siesta.XV 

420 """ 

421 

422 fname = self.getpath(filename) 

423 if not fname.exists(): 

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

425 with fname.open() as fd: 

426 self.atoms = read_siesta_xv(fd) 

427 self.read_results() 

428 

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

430 """ Returns the directory/fname string """ 

431 if fname is None: 

432 fname = self.prefix 

433 if ext is not None: 

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

435 return Path(self.directory) / fname 

436 

437 def pseudo_qualifier(self): 

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

439 The retrieved pseudopotential for a specific element will be 

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

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

442 """ 

443 if self['pseudo_qualifier'] is None: 

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

445 else: 

446 return self['pseudo_qualifier'] 

447 

448 def read_results(self): 

449 """Read the results.""" 

450 from ase.io.siesta_output import OutputReader 

451 reader = OutputReader(prefix=self.prefix, 

452 directory=Path(self.directory), 

453 bandpath=self['bandpath']) 

454 results = reader.read_results() 

455 self.results.update(results) 

456 

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

458 

459 def read_ion(self, atoms): 

460 """ 

461 Read the ion.xml file of each specie 

462 """ 

463 species, species_numbers = self.species(atoms) 

464 

465 ion_results = {} 

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

467 symbol = spec['symbol'] 

468 atomic_number = atomic_numbers[symbol] 

469 

470 if spec['pseudopotential'] is None: 

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

472 label = symbol 

473 else: 

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

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

476 else: 

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

478 label = pseudopotential.stem 

479 

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

481 if spec['ghost']: 

482 name = f"{name}.ghost" 

483 atomic_number = -atomic_number 

484 

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

486 

487 if label not in ion_results: 

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

489 fname = Path(fname) 

490 if fname.is_file(): 

491 ion_results[label] = get_ion(fname) 

492 

493 return ion_results 

494 

495 def band_structure(self): 

496 return self.results['bandstructure'] 

497 

498 def get_fermi_level(self): 

499 return self.results['fermi_energy'] 

500 

501 def get_k_point_weights(self): 

502 return self.results['kpoint_weights'] 

503 

504 def get_ibz_k_points(self): 

505 return self.results['kpoints'] 

506 

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

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

509 

510 def get_number_of_spins(self): 

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

512 

513 

514def generate_atomic_coordinates(atoms: Atoms, species_numbers, 

515 atomic_coord_format: str): 

516 """Write atomic coordinates. 

517 

518 Parameters 

519 ---------- 

520 fd : IO 

521 An open file object. 

522 atoms : Atoms 

523 An atoms object. 

524 """ 

525 if atomic_coord_format == 'xyz': 

526 return generate_atomic_coordinates_xyz(atoms, species_numbers) 

527 elif atomic_coord_format == 'zmatrix': 

528 return generate_atomic_coordinates_zmatrix(atoms, species_numbers) 

529 else: 

530 raise RuntimeError( 

531 f'Unknown atomic_coord_format: {atomic_coord_format}') 

532 

533 

534def generate_atomic_coordinates_zmatrix(atoms: Atoms, species_numbers): 

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

536 

537 Parameters 

538 ---------- 

539 fd : IO 

540 An open file object. 

541 atoms : Atoms 

542 An atoms object. 

543 """ 

544 yield '\n' 

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

546 yield '%block Zmatrix\n' 

547 yield ' cartesian\n' 

548 

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

550 a2constr = SiestaInput.make_xyz_constraints(atoms) 

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

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

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

554 yield fstr.format( 

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

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

557 yield '%endblock Zmatrix\n' 

558 

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

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

561 

562 

563def generate_atomic_coordinates_xyz(atoms: Atoms, species_numbers): 

564 """Write atomic coordinates. 

565 

566 Parameters 

567 ---------- 

568 fd : IO 

569 An open file object. 

570 atoms : Atoms 

571 An atoms object. 

572 """ 

573 yield '\n' 

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

575 yield block('AtomicCoordinatesAndAtomicSpecies', 

576 [[*atom.position, number] 

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

578 yield '\n' 

579 

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

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

582 

583 

584@dataclass 

585class SpeciesInfo: 

586 atoms: Atoms 

587 pseudo_path: Path 

588 pseudo_qualifier: str 

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

590 

591 def __post_init__(self): 

592 pao_basis = [] 

593 chemical_labels = [] 

594 basis_sizes = [] 

595 file_instructions = [] 

596 

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

598 symbol = spec['symbol'] 

599 atomic_number = atomic_numbers[symbol] 

600 

601 if spec['pseudopotential'] is None: 

602 if self.pseudo_qualifier == '': 

603 label = symbol 

604 else: 

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

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

607 else: 

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

609 label = src_path.stem 

610 if not src_path.is_absolute(): 

611 src_path = self.pseudo_path / src_path 

612 if not src_path.exists(): 

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

614 

615 name = src_path.name 

616 name = name.split('.') 

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

618 if spec['ghost']: 

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

620 atomic_number = -atomic_number 

621 

622 name = '.'.join(name) 

623 

624 instr = FileInstruction(src_path, name) 

625 file_instructions.append(instr) 

626 

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

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

629 chemical_labels.append(string) 

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

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

632 else: 

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

634 

635 self.file_instructions = file_instructions 

636 self.chemical_labels = chemical_labels 

637 self.pao_basis = pao_basis 

638 self.basis_sizes = basis_sizes 

639 

640 def generate_text(self): 

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

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

643 

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

645 yield '\n' 

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

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

648 yield '\n' 

649 

650 

651@dataclass 

652class FileInstruction: 

653 src_path: Path 

654 targetname: str 

655 

656 def copy_to(self, directory): 

657 self._link(shutil.copy, directory) 

658 

659 def symlink_to(self, directory): 

660 self._link(os.symlink, directory) 

661 

662 def _link(self, file_operation, directory): 

663 dst_path = directory / self.targetname 

664 if self.src_path == dst_path: 

665 return 

666 

667 dst_path.unlink(missing_ok=True) 

668 file_operation(self.src_path, dst_path) 

669 

670 

671@dataclass 

672class FDFWriter: 

673 name: str 

674 xc: str 

675 fdf_user_args: dict 

676 more_fdf_args: dict 

677 mesh_cutoff: float 

678 energy_shift: float 

679 spin: str 

680 species_numbers: object # ? 

681 atomic_coord_format: str 

682 kpts: object # ? 

683 bandpath: object # ? 

684 species_info: object 

685 

686 def write(self, fd): 

687 for chunk in self.generate_text(): 

688 fd.write(chunk) 

689 

690 def generate_text(self): 

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

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

693 yield "\n" 

694 

695 # Write explicitly given options first to 

696 # allow the user to override anything. 

697 fdf_arguments = self.fdf_user_args 

698 for key in sorted(fdf_arguments): 

699 yield var(key, fdf_arguments[key]) 

700 

701 # Force siesta to return error on no convergence. 

702 # as default consistent with ASE expectations. 

703 if 'SCFMustConverge' not in fdf_arguments: 

704 yield var('SCFMustConverge', True) 

705 yield '\n' 

706 

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

708 # Spin backwards compatibility. 

709 if self.spin == 'collinear': 

710 key = 'SpinPolarized' 

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

712 key = 'NonCollinearSpin' 

713 else: 

714 key = None 

715 

716 if key is not None: 

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

718 

719 # Write functional. 

720 functional, authors = self.xc 

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

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

723 yield '\n' 

724 

725 # Write mesh cutoff and energy shift. 

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

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

728 yield '\n' 

729 

730 yield from self.species_info.generate_text() 

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

732 

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

734 yield var(key, value) 

735 

736 if self.kpts is not None: 

737 kpts = np.array(self.kpts) 

738 yield from SiestaInput.generate_kpts(kpts) 

739 

740 if self.bandpath is not None: 

741 lines = bandpath2bandpoints(self.bandpath) 

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

743 yield lines 

744 yield '\n' 

745 

746 def generate_atoms_text(self, atoms: Atoms): 

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

748 

749 cell = atoms.cell 

750 yield '\n' 

751 

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

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

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

755 

756 if np.any(cell): 

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

758 yield block('LatticeVectors', cell) 

759 

760 yield from generate_atomic_coordinates( 

761 atoms, self.species_numbers, self.atomic_coord_format) 

762 

763 # Write magnetic moments. 

764 magmoms = atoms.get_initial_magnetic_moments() 

765 

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

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

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

769 # atoms object. 

770 if len(magmoms) == 0: 

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

772 

773 yield '%block DM.InitSpin\n' 

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

775 for n, M in enumerate(magmoms): 

776 if M[0] != 0: 

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

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

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

780 for n, M in enumerate(magmoms): 

781 if M != 0: 

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

783 yield '%endblock DM.InitSpin\n' 

784 yield '\n' 

785 

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

787 if symlink_pseudos is None: 

788 symlink_pseudos = os.name != 'nt' 

789 

790 for instruction in self.species_info.file_instructions: 

791 if symlink_pseudos: 

792 instruction.symlink_to(directory) 

793 else: 

794 instruction.copy_to(directory) 

795 

796 

797# Utilities for generating bits of strings. 

798# 

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

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

801# which applies consistent spacings etc. 

802def var(key, value): 

803 return format_fdf(key, value) 

804 

805 

806def block(name, data): 

807 return format_block(name, data)