Coverage for /builds/ase/ase/ase/calculators/siesta/siesta.py: 81.55%

374 statements  

« prev     ^ index     » next       coverage.py v7.5.3, created at 2025-08-02 00:12 +0000

1# fmt: off 

2 

3""" 

4This module defines the ASE interface to SIESTA. 

5 

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

7 

8Home of the SIESTA package: 

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

10 

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

12 

13""" 

14 

15import os 

16import re 

17import shutil 

18import tempfile 

19from dataclasses import dataclass 

20from pathlib import Path 

21from typing import Any, Dict, List 

22 

23import numpy as np 

24 

25from ase import Atoms 

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

27from ase.calculators.siesta.import_ion_xml import get_ion 

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

29from ase.data import atomic_numbers 

30from ase.io.siesta import read_siesta_xv 

31from ase.io.siesta_input import SiestaInput 

32from ase.units import Ry, eV 

33from ase.utils import deprecated 

34 

35meV = 0.001 * eV 

36 

37 

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

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

40 

41 if match is None: 

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

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

44 

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

46 return string 

47 

48 

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

50 """ Return SIESTA version number. 

51 

52 Run the command, for instance 'siesta' and 

53 then parse the output in order find the 

54 version number. 

55 """ 

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

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

58 # all sorts of mpirun and other weird parts. 

59 

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

61 try: 

62 from subprocess import PIPE, Popen 

63 proc = Popen([executable], 

64 stdin=PIPE, 

65 stdout=PIPE, 

66 stderr=PIPE, 

67 cwd=temp_dirname) 

68 output, _ = proc.communicate() 

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

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

71 # (as of siesta-4.1-b4) 

72 finally: 

73 shutil.rmtree(temp_dirname) 

74 

75 return parse_siesta_version(output) 

76 

77 

78def format_block(name, block): 

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

80 for row in block: 

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

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

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

84 return '\n'.join(lines) 

85 

86 

87def bandpath2bandpoints(path): 

88 return '\n'.join([ 

89 'BandLinesScale ReciprocalLatticeVectors', 

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

91 

92 

93class SiestaParameters(Parameters): 

94 def __init__( 

95 self, 

96 label='siesta', 

97 mesh_cutoff=200 * Ry, 

98 energy_shift=100 * meV, 

99 kpts=None, 

100 xc='LDA', 

101 basis_set='DZP', 

102 spin='non-polarized', 

103 species=(), 

104 pseudo_qualifier=None, 

105 pseudo_path=None, 

106 symlink_pseudos=None, 

107 atoms=None, 

108 restart=None, 

109 fdf_arguments=None, 

110 atomic_coord_format='xyz', 

111 bandpath=None): 

112 kwargs = locals() 

113 kwargs.pop('self') 

114 Parameters.__init__(self, **kwargs) 

115 

116 

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

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

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

120 return True 

121 return False 

122 

123 

124class Siesta(FileIOCalculator): 

125 """Calculator interface to the SIESTA code. 

126 """ 

127 allowed_xc = { 

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

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

130 'WC', 'AM05', 'PBEsol', 'PBEJsJrLO', 

131 'PBEGcGxLO', 'PBEGcGxHEG', 'BLYP'], 

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

133 

134 name = 'siesta' 

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

136 implemented_properties = [ 

137 'energy', 

138 'free_energy', 

139 'forces', 

140 'stress', 

141 'dipole', 

142 'eigenvalues', 

143 'density', 

144 'fermi_energy'] 

145 

146 # Dictionary of valid input vaiables. 

147 default_parameters = SiestaParameters() 

148 

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

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

151 # it to use the bandpath keyword. 

152 accepts_bandpath_keyword = True 

153 

154 fileio_rules = FileIOCalculator.ruleset( 

155 configspec=dict(pseudo_path=None), 

156 stdin_name='{prefix}.fdf', 

157 stdout_name='{prefix}.out') 

158 

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

160 """ASE interface to the SIESTA code. 

161 

162 Parameters: 

163 - label : The basename of all files created during 

164 calculation. 

165 - mesh_cutoff : Energy in eV. 

166 The mesh cutoff energy for determining number of 

167 grid points in the matrix-element calculation. 

168 - energy_shift : Energy in eV 

169 The confining energy of the basis set generation. 

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

171 directions. 

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

173 any allowed value for either the Siesta 

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

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

176 the type of functions basis set. 

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

178 "non-collinear|spin-orbit". 

179 The level of spin description to be used. 

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

181 can be used to to specify the basis set, 

182 pseudopotential and whether the species is ghost. 

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

184 together to identify the species. 

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

186 pseudopotentials are taken from. 

187 If None is given, then then the path given 

188 in $SIESTA_PP_PATH will be used. 

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

190 pseudopotential path that will be retrieved. 

191 For hydrogen with qualifier "abc" the 

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

193 - symlink_pseudos: None|bool 

194 If true, symlink pseudopotentials 

195 into the calculation directory, else copy them. 

196 Defaults to true on Unix and false on Windows. 

197 - atoms : The Atoms object. 

198 - restart : str. Prefix for restart file. 

199 May contain a directory. 

200 Default is None, don't restart. 

201 - fdf_arguments: Explicitly given fdf arguments. Dictonary using 

202 Siesta keywords as given in the manual. List values 

203 are written as fdf blocks with each element on a 

204 separate line, while tuples will write each element 

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

206 input. 

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

208 the default way of entering the system's geometry 

209 (via the block AtomicCoordinatesAndAtomicSpecies) 

210 and a recent method via the block Zmatrix. The 

211 block Zmatrix allows to specify basic geometry 

212 constrains such as realized through the ASE classes 

213 FixAtom, FixedLine and FixedPlane. 

214 """ 

215 

216 # Put in the default arguments. 

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

218 

219 # Call the base class. 

220 FileIOCalculator.__init__( 

221 self, 

222 command=command, 

223 profile=profile, 

224 directory=directory, 

225 **parameters) 

226 

227 def __getitem__(self, key): 

228 """Convenience method to retrieve a parameter as 

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

230 

231 Parameters: 

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

233 """ 

234 return self.parameters[key] 

235 

236 def species(self, atoms): 

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

238 species input. 

239 

240 Parameters : 

241 - atoms : An Atoms object. 

242 """ 

243 return SiestaInput.get_species( 

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

245 

246 @deprecated( 

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

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

249 category=FutureWarning, 

250 callback=_nonpolarized_alias, 

251 ) 

252 def set(self, **kwargs): 

253 """Set all parameters. 

254 

255 Parameters: 

256 -kwargs : Dictionary containing the keywords defined in 

257 SiestaParameters. 

258 

259 .. deprecated:: 3.18.2 

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

261 'non-polarized' 

262 """ 

263 

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

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

266 current = self.parameters.copy() 

267 current.update(kwargs) 

268 kwargs = current 

269 

270 # Find not allowed keys. 

271 default_keys = list(self.__class__.default_parameters) 

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

273 if len(offending_keys) > 0: 

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

275 raise ValueError(mess % list(offending_keys)) 

276 

277 # Use the default parameters. 

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

279 parameters.update(kwargs) 

280 kwargs = parameters 

281 

282 # Check energy inputs. 

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

284 value = kwargs.get(arg) 

285 if value is None: 

286 continue 

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

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

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

290 raise ValueError(mess) 

291 

292 # Check the functional input. 

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

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

295 functional, authors = xc 

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

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

298 raise ValueError(mess) 

299 

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

301 if authors.lower() not in lsauthorslower: 

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

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

304 

305 elif xc in self.allowed_xc: 

306 functional = xc 

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

308 else: 

309 found = False 

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

311 if xc in value: 

312 found = True 

313 functional = key 

314 authors = xc 

315 break 

316 

317 if not found: 

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

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

320 

321 # Check fdf_arguments. 

322 if kwargs['fdf_arguments'] is None: 

323 kwargs['fdf_arguments'] = {} 

324 

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

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

327 

328 # Call baseclass. 

329 FileIOCalculator.set(self, **kwargs) 

330 

331 def set_fdf_arguments(self, fdf_arguments): 

332 """ Set the fdf_arguments after the initialization of the 

333 calculator. 

334 """ 

335 self.validate_fdf_arguments(fdf_arguments) 

336 FileIOCalculator.set(self, fdf_arguments=fdf_arguments) 

337 

338 def validate_fdf_arguments(self, fdf_arguments): 

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

340 dictionary of allowed keys. 

341 """ 

342 # None is valid 

343 if fdf_arguments is None: 

344 return 

345 

346 # Type checking. 

347 if not isinstance(fdf_arguments, dict): 

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

349 

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

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

352 See calculator.py for further details. 

353 

354 Parameters: 

355 - atoms : The Atoms object to write. 

356 - properties : The properties which should be calculated. 

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

358 """ 

359 

360 super().write_input( 

361 atoms=atoms, 

362 properties=properties, 

363 system_changes=system_changes) 

364 

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

366 

367 more_fdf_args = {} 

368 

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

370 # have changed. 

371 if (system_changes is None or 

372 ('numbers' not in system_changes and 

373 'initial_magmoms' not in system_changes and 

374 'initial_charges' not in system_changes)): 

375 

376 more_fdf_args['DM.UseSaveDM'] = True 

377 

378 if 'density' in properties: 

379 more_fdf_args['SaveRho'] = True 

380 

381 species, species_numbers = self.species(atoms) 

382 

383 pseudo_path = (self['pseudo_path'] 

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

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

386 

387 if not pseudo_path: 

388 raise Exception( 

389 'Please configure pseudo_path or SIESTA_PP_PATH envvar') 

390 

391 species_info = SpeciesInfo( 

392 atoms=atoms, 

393 pseudo_path=Path(pseudo_path), 

394 pseudo_qualifier=self.pseudo_qualifier(), 

395 species=species) 

396 

397 writer = FDFWriter( 

398 name=self.prefix, 

399 xc=self['xc'], 

400 spin=self['spin'], 

401 mesh_cutoff=self['mesh_cutoff'], 

402 energy_shift=self['energy_shift'], 

403 fdf_user_args=self['fdf_arguments'], 

404 more_fdf_args=more_fdf_args, 

405 species_numbers=species_numbers, 

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

407 kpts=self['kpts'], 

408 bandpath=self['bandpath'], 

409 species_info=species_info, 

410 ) 

411 

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

413 writer.write(fd) 

414 

415 writer.link_pseudos_into_directory( 

416 symlink_pseudos=self['symlink_pseudos'], 

417 directory=Path(self.directory)) 

418 

419 def read(self, filename): 

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

421 Read other results from other files 

422 filename : siesta.XV 

423 """ 

424 

425 fname = self.getpath(filename) 

426 if not fname.exists(): 

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

428 with fname.open() as fd: 

429 self.atoms = read_siesta_xv(fd) 

430 self.read_results() 

431 

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

433 """ Returns the directory/fname string """ 

434 if fname is None: 

435 fname = self.prefix 

436 if ext is not None: 

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

438 return Path(self.directory) / fname 

439 

440 def pseudo_qualifier(self): 

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

442 The retrieved pseudopotential for a specific element will be 

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

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

445 """ 

446 if self['pseudo_qualifier'] is None: 

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

448 else: 

449 return self['pseudo_qualifier'] 

450 

451 def read_results(self): 

452 """Read the results.""" 

453 from ase.io.siesta_output import OutputReader 

454 reader = OutputReader(prefix=self.prefix, 

455 directory=Path(self.directory), 

456 bandpath=self['bandpath']) 

457 results = reader.read_results() 

458 self.results.update(results) 

459 

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

461 

462 def read_ion(self, atoms): 

463 """ 

464 Read the ion.xml file of each specie 

465 """ 

466 species, _species_numbers = self.species(atoms) 

467 

468 ion_results = {} 

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

470 symbol = spec['symbol'] 

471 atomic_number = atomic_numbers[symbol] 

472 

473 if spec['pseudopotential'] is None: 

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

475 label = symbol 

476 else: 

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

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

479 else: 

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

481 label = pseudopotential.stem 

482 

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

484 if spec['ghost']: 

485 name = f"{name}.ghost" 

486 atomic_number = -atomic_number 

487 

488 if name not in ion_results: 

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

490 if fname.is_file(): 

491 ion_results[name] = get_ion(str(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 

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 pseudo_name = '' 

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

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

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

632 pseudo_name) 

633 chemical_labels.append(string) 

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

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

636 else: 

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

638 

639 self.file_instructions = file_instructions 

640 self.chemical_labels = chemical_labels 

641 self.pao_basis = pao_basis 

642 self.basis_sizes = basis_sizes 

643 

644 def generate_text(self): 

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

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

647 

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

649 yield '\n' 

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

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

652 yield '\n' 

653 

654 

655@dataclass 

656class FileInstruction: 

657 src_path: Path 

658 targetname: str 

659 

660 def copy_to(self, directory): 

661 self._link(shutil.copy, directory) 

662 

663 def symlink_to(self, directory): 

664 self._link(os.symlink, directory) 

665 

666 def _link(self, file_operation, directory): 

667 dst_path = directory / self.targetname 

668 if self.src_path == dst_path: 

669 return 

670 

671 dst_path.unlink(missing_ok=True) 

672 file_operation(self.src_path, dst_path) 

673 

674 

675@dataclass 

676class FDFWriter: 

677 name: str 

678 xc: str 

679 fdf_user_args: dict 

680 more_fdf_args: dict 

681 mesh_cutoff: float 

682 energy_shift: float 

683 spin: str 

684 species_numbers: object # ? 

685 atomic_coord_format: str 

686 kpts: object # ? 

687 bandpath: object # ? 

688 species_info: object 

689 

690 def write(self, fd): 

691 for chunk in self.generate_text(): 

692 fd.write(chunk) 

693 

694 def generate_text(self): 

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

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

697 yield "\n" 

698 

699 # Write explicitly given options first to 

700 # allow the user to override anything. 

701 fdf_arguments = self.fdf_user_args 

702 for key in sorted(fdf_arguments): 

703 yield var(key, fdf_arguments[key]) 

704 

705 # Force siesta to return error on no convergence. 

706 # as default consistent with ASE expectations. 

707 if 'SCFMustConverge' not in fdf_arguments: 

708 yield var('SCFMustConverge', True) 

709 yield '\n' 

710 

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

712 # Spin backwards compatibility. 

713 if self.spin == 'collinear': 

714 key = 'SpinPolarized' 

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

716 key = 'NonCollinearSpin' 

717 else: 

718 key = None 

719 

720 if key is not None: 

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

722 

723 # Write functional. 

724 functional, authors = self.xc 

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

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

727 yield '\n' 

728 

729 # Write mesh cutoff and energy shift. 

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

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

732 yield '\n' 

733 

734 yield from self.species_info.generate_text() 

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

736 

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

738 yield var(key, value) 

739 

740 if self.kpts is not None: 

741 kpts = np.array(self.kpts) 

742 yield from SiestaInput.generate_kpts(kpts) 

743 

744 if self.bandpath is not None: 

745 lines = bandpath2bandpoints(self.bandpath) 

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

747 yield lines 

748 yield '\n' 

749 

750 def generate_atoms_text(self, atoms: Atoms): 

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

752 

753 cell = atoms.cell 

754 yield '\n' 

755 

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

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

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

759 

760 if np.any(cell): 

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

762 yield block('LatticeVectors', cell) 

763 

764 yield from generate_atomic_coordinates( 

765 atoms, self.species_numbers, self.atomic_coord_format) 

766 

767 # Write magnetic moments. 

768 magmoms = atoms.get_initial_magnetic_moments() 

769 

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

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

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

773 # atoms object. 

774 if len(magmoms) == 0: 

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

776 

777 yield '%block DM.InitSpin\n' 

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

779 for n, M in enumerate(magmoms): 

780 if M[0] != 0: 

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

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

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

784 for n, M in enumerate(magmoms): 

785 if M != 0: 

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

787 yield '%endblock DM.InitSpin\n' 

788 yield '\n' 

789 

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

791 if symlink_pseudos is None: 

792 symlink_pseudos = os.name != 'nt' 

793 

794 for instruction in self.species_info.file_instructions: 

795 if symlink_pseudos: 

796 instruction.symlink_to(directory) 

797 else: 

798 instruction.copy_to(directory) 

799 

800 

801# Utilities for generating bits of strings. 

802# 

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

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

805# which applies consistent spacings etc. 

806def var(key, value): 

807 return format_fdf(key, value) 

808 

809 

810def block(name, data): 

811 return format_block(name, data)