Coverage for /builds/debichem-team/python-ase/ase/calculators/siesta/siesta.py: 81.55%

374 statements  

« prev     ^ index     » next       coverage.py v7.5.3, created at 2025-03-06 04:00 +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'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 if name not in ion_results: 

487 fname = self.getpath(name, 'ion.xml') 

488 if fname.is_file(): 

489 ion_results[name] = get_ion(str(fname)) 

490 

491 return ion_results 

492 

493 def band_structure(self): 

494 return self.results['bandstructure'] 

495 

496 def get_fermi_level(self): 

497 return self.results['fermi_energy'] 

498 

499 def get_k_point_weights(self): 

500 return self.results['kpoint_weights'] 

501 

502 def get_ibz_k_points(self): 

503 return self.results['kpoints'] 

504 

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

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

507 

508 def get_number_of_spins(self): 

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

510 

511 

512def generate_atomic_coordinates(atoms: Atoms, species_numbers, 

513 atomic_coord_format: str): 

514 """Write atomic coordinates. 

515 

516 Parameters 

517 ---------- 

518 fd : IO 

519 An open file object. 

520 atoms : Atoms 

521 An atoms object. 

522 """ 

523 if atomic_coord_format == 'xyz': 

524 return generate_atomic_coordinates_xyz(atoms, species_numbers) 

525 elif atomic_coord_format == 'zmatrix': 

526 return generate_atomic_coordinates_zmatrix(atoms, species_numbers) 

527 else: 

528 raise RuntimeError( 

529 f'Unknown atomic_coord_format: {atomic_coord_format}') 

530 

531 

532def generate_atomic_coordinates_zmatrix(atoms: Atoms, species_numbers): 

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

534 

535 Parameters 

536 ---------- 

537 fd : IO 

538 An open file object. 

539 atoms : Atoms 

540 An atoms object. 

541 """ 

542 yield '\n' 

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

544 yield '%block Zmatrix\n' 

545 yield ' cartesian\n' 

546 

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

548 a2constr = SiestaInput.make_xyz_constraints(atoms) 

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

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

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

552 yield fstr.format( 

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

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

555 yield '%endblock Zmatrix\n' 

556 

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

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

559 

560 

561def generate_atomic_coordinates_xyz(atoms: Atoms, species_numbers): 

562 """Write atomic coordinates. 

563 

564 Parameters 

565 ---------- 

566 fd : IO 

567 An open file object. 

568 atoms : Atoms 

569 An atoms object. 

570 """ 

571 yield '\n' 

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

573 yield block('AtomicCoordinatesAndAtomicSpecies', 

574 [[*atom.position, number] 

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

576 yield '\n' 

577 

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

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

580 

581 

582@dataclass 

583class SpeciesInfo: 

584 atoms: Atoms 

585 pseudo_path: Path 

586 pseudo_qualifier: str 

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

588 

589 def __post_init__(self): 

590 pao_basis = [] 

591 chemical_labels = [] 

592 basis_sizes = [] 

593 file_instructions = [] 

594 

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

596 symbol = spec['symbol'] 

597 atomic_number = atomic_numbers[symbol] 

598 

599 if spec['pseudopotential'] is None: 

600 if self.pseudo_qualifier == '': 

601 label = symbol 

602 else: 

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

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

605 else: 

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

607 

608 if not src_path.is_absolute(): 

609 src_path = self.pseudo_path / src_path 

610 if not src_path.exists(): 

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

612 

613 name = src_path.name 

614 name = name.split('.') 

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

616 if spec['ghost']: 

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

618 atomic_number = -atomic_number 

619 

620 name = '.'.join(name) 

621 

622 instr = FileInstruction(src_path, name) 

623 file_instructions.append(instr) 

624 

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

626 pseudo_name = '' 

627 if src_path.suffix != '.psf': 

628 pseudo_name = f'{label}{src_path.suffix}' 

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

630 pseudo_name) 

631 chemical_labels.append(string) 

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

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

634 else: 

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

636 

637 self.file_instructions = file_instructions 

638 self.chemical_labels = chemical_labels 

639 self.pao_basis = pao_basis 

640 self.basis_sizes = basis_sizes 

641 

642 def generate_text(self): 

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

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

645 

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

647 yield '\n' 

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

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

650 yield '\n' 

651 

652 

653@dataclass 

654class FileInstruction: 

655 src_path: Path 

656 targetname: str 

657 

658 def copy_to(self, directory): 

659 self._link(shutil.copy, directory) 

660 

661 def symlink_to(self, directory): 

662 self._link(os.symlink, directory) 

663 

664 def _link(self, file_operation, directory): 

665 dst_path = directory / self.targetname 

666 if self.src_path == dst_path: 

667 return 

668 

669 dst_path.unlink(missing_ok=True) 

670 file_operation(self.src_path, dst_path) 

671 

672 

673@dataclass 

674class FDFWriter: 

675 name: str 

676 xc: str 

677 fdf_user_args: dict 

678 more_fdf_args: dict 

679 mesh_cutoff: float 

680 energy_shift: float 

681 spin: str 

682 species_numbers: object # ? 

683 atomic_coord_format: str 

684 kpts: object # ? 

685 bandpath: object # ? 

686 species_info: object 

687 

688 def write(self, fd): 

689 for chunk in self.generate_text(): 

690 fd.write(chunk) 

691 

692 def generate_text(self): 

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

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

695 yield "\n" 

696 

697 # Write explicitly given options first to 

698 # allow the user to override anything. 

699 fdf_arguments = self.fdf_user_args 

700 for key in sorted(fdf_arguments): 

701 yield var(key, fdf_arguments[key]) 

702 

703 # Force siesta to return error on no convergence. 

704 # as default consistent with ASE expectations. 

705 if 'SCFMustConverge' not in fdf_arguments: 

706 yield var('SCFMustConverge', True) 

707 yield '\n' 

708 

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

710 # Spin backwards compatibility. 

711 if self.spin == 'collinear': 

712 key = 'SpinPolarized' 

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

714 key = 'NonCollinearSpin' 

715 else: 

716 key = None 

717 

718 if key is not None: 

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

720 

721 # Write functional. 

722 functional, authors = self.xc 

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

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

725 yield '\n' 

726 

727 # Write mesh cutoff and energy shift. 

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

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

730 yield '\n' 

731 

732 yield from self.species_info.generate_text() 

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

734 

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

736 yield var(key, value) 

737 

738 if self.kpts is not None: 

739 kpts = np.array(self.kpts) 

740 yield from SiestaInput.generate_kpts(kpts) 

741 

742 if self.bandpath is not None: 

743 lines = bandpath2bandpoints(self.bandpath) 

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

745 yield lines 

746 yield '\n' 

747 

748 def generate_atoms_text(self, atoms: Atoms): 

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

750 

751 cell = atoms.cell 

752 yield '\n' 

753 

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

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

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

757 

758 if np.any(cell): 

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

760 yield block('LatticeVectors', cell) 

761 

762 yield from generate_atomic_coordinates( 

763 atoms, self.species_numbers, self.atomic_coord_format) 

764 

765 # Write magnetic moments. 

766 magmoms = atoms.get_initial_magnetic_moments() 

767 

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

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

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

771 # atoms object. 

772 if len(magmoms) == 0: 

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

774 

775 yield '%block DM.InitSpin\n' 

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

777 for n, M in enumerate(magmoms): 

778 if M[0] != 0: 

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

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

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

782 for n, M in enumerate(magmoms): 

783 if M != 0: 

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

785 yield '%endblock DM.InitSpin\n' 

786 yield '\n' 

787 

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

789 if symlink_pseudos is None: 

790 symlink_pseudos = os.name != 'nt' 

791 

792 for instruction in self.species_info.file_instructions: 

793 if symlink_pseudos: 

794 instruction.symlink_to(directory) 

795 else: 

796 instruction.copy_to(directory) 

797 

798 

799# Utilities for generating bits of strings. 

800# 

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

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

803# which applies consistent spacings etc. 

804def var(key, value): 

805 return format_fdf(key, value) 

806 

807 

808def block(name, data): 

809 return format_block(name, data)