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

374 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-30 08:22 +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: 

9https://siesta-project.org/ 

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 

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 ---------- 

164 - label : The basename of all files created during 

165 calculation. 

166 - mesh_cutoff : Energy in eV. 

167 The mesh cutoff energy for determining number of 

168 grid points in the matrix-element calculation. 

169 - energy_shift : Energy in eV 

170 The confining energy of the basis set generation. 

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

172 directions. 

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

174 any allowed value for either the Siesta 

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

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

177 the type of functions basis set. 

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

179 "non-collinear|spin-orbit". 

180 The level of spin description to be used. 

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

182 can be used to to specify the basis set, 

183 pseudopotential and whether the species is ghost. 

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

185 together to identify the species. 

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

187 pseudopotentials are taken from. 

188 If None is given, then then the path given 

189 in $SIESTA_PP_PATH will be used. 

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

191 pseudopotential path that will be retrieved. 

192 For hydrogen with qualifier "abc" the 

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

194 - symlink_pseudos: None|bool 

195 If true, symlink pseudopotentials 

196 into the calculation directory, else copy them. 

197 Defaults to true on Unix and false on Windows. 

198 - atoms : The Atoms object. 

199 - restart : str. Prefix for restart file. 

200 May contain a directory. 

201 Default is None, don't restart. 

202 - fdf_arguments: Explicitly given fdf arguments. Dictonary using 

203 Siesta keywords as given in the manual. List values 

204 are written as fdf blocks with each element on a 

205 separate line, while tuples will write each element 

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

207 input. 

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

209 the default way of entering the system's geometry 

210 (via the block AtomicCoordinatesAndAtomicSpecies) 

211 and a recent method via the block Zmatrix. The 

212 block Zmatrix allows to specify basic geometry 

213 constrains such as realized through the ASE classes 

214 FixAtom, FixedLine and FixedPlane. 

215 """ 

216 

217 # Put in the default arguments. 

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

219 

220 # Call the base class. 

221 FileIOCalculator.__init__( 

222 self, 

223 command=command, 

224 profile=profile, 

225 directory=directory, 

226 **parameters) 

227 

228 def __getitem__(self, key): 

229 """Convenience method to retrieve a parameter as 

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

231 

232 Parameters 

233 ---------- 

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

235 """ 

236 return self.parameters[key] 

237 

238 def species(self, atoms): 

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

240 species input. 

241 

242 Parameters : 

243 - atoms : An Atoms object. 

244 """ 

245 return SiestaInput.get_species( 

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

247 

248 @deprecated( 

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

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

251 category=FutureWarning, 

252 callback=_nonpolarized_alias, 

253 ) 

254 def set(self, **kwargs): 

255 """Set all parameters. 

256 

257 Parameters 

258 ---------- 

259 -kwargs : Dictionary containing the keywords defined in 

260 SiestaParameters. 

261 

262 .. deprecated:: 3.18.2 

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

264 'non-polarized' 

265 """ 

266 

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

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

269 current = self.parameters.copy() 

270 current.update(kwargs) 

271 kwargs = current 

272 

273 # Find not allowed keys. 

274 default_keys = list(self.__class__.default_parameters) 

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

276 if len(offending_keys) > 0: 

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

278 raise ValueError(mess % list(offending_keys)) 

279 

280 # Use the default parameters. 

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

282 parameters.update(kwargs) 

283 kwargs = parameters 

284 

285 # Check energy inputs. 

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

287 value = kwargs.get(arg) 

288 if value is None: 

289 continue 

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

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

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

293 raise ValueError(mess) 

294 

295 # Check the functional input. 

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

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

298 functional, authors = xc 

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

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

301 raise ValueError(mess) 

302 

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

304 if authors.lower() not in lsauthorslower: 

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

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

307 

308 elif xc in self.allowed_xc: 

309 functional = xc 

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

311 else: 

312 found = False 

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

314 if xc in value: 

315 found = True 

316 functional = key 

317 authors = xc 

318 break 

319 

320 if not found: 

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

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

323 

324 # Check fdf_arguments. 

325 if kwargs['fdf_arguments'] is None: 

326 kwargs['fdf_arguments'] = {} 

327 

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

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

330 

331 # Call baseclass. 

332 FileIOCalculator.set(self, **kwargs) 

333 

334 def set_fdf_arguments(self, fdf_arguments): 

335 """ Set the fdf_arguments after the initialization of the 

336 calculator. 

337 """ 

338 self.validate_fdf_arguments(fdf_arguments) 

339 FileIOCalculator.set(self, fdf_arguments=fdf_arguments) 

340 

341 def validate_fdf_arguments(self, fdf_arguments): 

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

343 dictionary of allowed keys. 

344 """ 

345 # None is valid 

346 if fdf_arguments is None: 

347 return 

348 

349 # Type checking. 

350 if not isinstance(fdf_arguments, dict): 

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

352 

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

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

355 See calculator.py for further details. 

356 

357 Parameters 

358 ---------- 

359 - atoms : The Atoms object to write. 

360 - properties : The properties which should be calculated. 

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

362 """ 

363 

364 super().write_input( 

365 atoms=atoms, 

366 properties=properties, 

367 system_changes=system_changes) 

368 

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

370 

371 more_fdf_args = {} 

372 

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

374 # have changed. 

375 if (system_changes is None or 

376 ('numbers' not in system_changes and 

377 'initial_magmoms' not in system_changes and 

378 'initial_charges' not in system_changes)): 

379 

380 more_fdf_args['DM.UseSaveDM'] = True 

381 

382 if 'density' in properties: 

383 more_fdf_args['SaveRho'] = True 

384 

385 species, species_numbers = self.species(atoms) 

386 

387 pseudo_path = (self['pseudo_path'] 

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

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

390 

391 if not pseudo_path: 

392 raise Exception( 

393 'Please configure pseudo_path or SIESTA_PP_PATH envvar') 

394 

395 species_info = SpeciesInfo( 

396 atoms=atoms, 

397 pseudo_path=Path(pseudo_path), 

398 pseudo_qualifier=self.pseudo_qualifier(), 

399 species=species) 

400 

401 writer = FDFWriter( 

402 name=self.prefix, 

403 xc=self['xc'], 

404 spin=self['spin'], 

405 mesh_cutoff=self['mesh_cutoff'], 

406 energy_shift=self['energy_shift'], 

407 fdf_user_args=self['fdf_arguments'], 

408 more_fdf_args=more_fdf_args, 

409 species_numbers=species_numbers, 

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

411 kpts=self['kpts'], 

412 bandpath=self['bandpath'], 

413 species_info=species_info, 

414 ) 

415 

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

417 writer.write(fd) 

418 

419 writer.link_pseudos_into_directory( 

420 symlink_pseudos=self['symlink_pseudos'], 

421 directory=Path(self.directory)) 

422 

423 def read(self, filename): 

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

425 Read other results from other files 

426 filename : siesta.XV 

427 """ 

428 

429 fname = self.getpath(filename) 

430 if not fname.exists(): 

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

432 with fname.open() as fd: 

433 self.atoms = read_siesta_xv(fd) 

434 self.read_results() 

435 

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

437 """ Returns the directory/fname string """ 

438 if fname is None: 

439 fname = self.prefix 

440 if ext is not None: 

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

442 return Path(self.directory) / fname 

443 

444 def pseudo_qualifier(self): 

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

446 The retrieved pseudopotential for a specific element will be 

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

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

449 """ 

450 if self['pseudo_qualifier'] is None: 

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

452 else: 

453 return self['pseudo_qualifier'] 

454 

455 def read_results(self): 

456 """Read the results.""" 

457 from ase.io.siesta_output import OutputReader 

458 reader = OutputReader(prefix=self.prefix, 

459 directory=Path(self.directory), 

460 bandpath=self['bandpath']) 

461 results = reader.read_results() 

462 self.results.update(results) 

463 

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

465 

466 def read_ion(self, atoms): 

467 """ 

468 Read the ion.xml file of each specie 

469 """ 

470 species, _species_numbers = self.species(atoms) 

471 

472 ion_results = {} 

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

474 symbol = spec['symbol'] 

475 atomic_number = atomic_numbers[symbol] 

476 

477 if spec['pseudopotential'] is None: 

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

479 label = symbol 

480 else: 

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

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

483 else: 

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

485 label = pseudopotential.stem 

486 

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

488 if spec['ghost']: 

489 name = f"{name}.ghost" 

490 atomic_number = -atomic_number 

491 

492 if name not in ion_results: 

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

494 if fname.is_file(): 

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

496 

497 return ion_results 

498 

499 def band_structure(self): 

500 return self.results['bandstructure'] 

501 

502 def get_fermi_level(self): 

503 return self.results['fermi_energy'] 

504 

505 def get_k_point_weights(self): 

506 return self.results['kpoint_weights'] 

507 

508 def get_ibz_k_points(self): 

509 return self.results['kpoints'] 

510 

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

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

513 

514 def get_number_of_spins(self): 

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

516 

517 

518def generate_atomic_coordinates(atoms: Atoms, species_numbers, 

519 atomic_coord_format: str): 

520 """Write atomic coordinates. 

521 

522 Parameters 

523 ---------- 

524 fd : IO 

525 An open file object. 

526 atoms : Atoms 

527 An atoms object. 

528 """ 

529 if atomic_coord_format == 'xyz': 

530 return generate_atomic_coordinates_xyz(atoms, species_numbers) 

531 elif atomic_coord_format == 'zmatrix': 

532 return generate_atomic_coordinates_zmatrix(atoms, species_numbers) 

533 else: 

534 raise RuntimeError( 

535 f'Unknown atomic_coord_format: {atomic_coord_format}') 

536 

537 

538def generate_atomic_coordinates_zmatrix(atoms: Atoms, species_numbers): 

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

540 

541 Parameters 

542 ---------- 

543 fd : IO 

544 An open file object. 

545 atoms : Atoms 

546 An atoms object. 

547 """ 

548 yield '\n' 

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

550 yield '%block Zmatrix\n' 

551 yield ' cartesian\n' 

552 

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

554 a2constr = SiestaInput.make_xyz_constraints(atoms) 

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

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

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

558 yield fstr.format( 

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

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

561 yield '%endblock Zmatrix\n' 

562 

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

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

565 

566 

567def generate_atomic_coordinates_xyz(atoms: Atoms, species_numbers): 

568 """Write atomic coordinates. 

569 

570 Parameters 

571 ---------- 

572 fd : IO 

573 An open file object. 

574 atoms : Atoms 

575 An atoms object. 

576 """ 

577 yield '\n' 

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

579 yield block('AtomicCoordinatesAndAtomicSpecies', 

580 [[*atom.position, number] 

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

582 yield '\n' 

583 

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

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

586 

587 

588@dataclass 

589class SpeciesInfo: 

590 atoms: Atoms 

591 pseudo_path: Path 

592 pseudo_qualifier: str 

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

594 

595 def __post_init__(self): 

596 pao_basis = [] 

597 chemical_labels = [] 

598 basis_sizes = [] 

599 file_instructions = [] 

600 

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

602 symbol = spec['symbol'] 

603 atomic_number = atomic_numbers[symbol] 

604 

605 if spec['pseudopotential'] is None: 

606 if self.pseudo_qualifier == '': 

607 label = symbol 

608 else: 

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

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

611 else: 

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

613 

614 if not src_path.is_absolute(): 

615 src_path = self.pseudo_path / src_path 

616 if not src_path.exists(): 

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

618 

619 name = src_path.name 

620 name = name.split('.') 

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

622 if spec['ghost']: 

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

624 atomic_number = -atomic_number 

625 

626 name = '.'.join(name) 

627 

628 instr = FileInstruction(src_path, name) 

629 file_instructions.append(instr) 

630 

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

632 pseudo_name = '' 

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

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

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

636 pseudo_name) 

637 chemical_labels.append(string) 

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

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

640 else: 

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

642 

643 self.file_instructions = file_instructions 

644 self.chemical_labels = chemical_labels 

645 self.pao_basis = pao_basis 

646 self.basis_sizes = basis_sizes 

647 

648 def generate_text(self): 

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

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

651 

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

653 yield '\n' 

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

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

656 yield '\n' 

657 

658 

659@dataclass 

660class FileInstruction: 

661 src_path: Path 

662 targetname: str 

663 

664 def copy_to(self, directory): 

665 self._link(shutil.copy, directory) 

666 

667 def symlink_to(self, directory): 

668 self._link(os.symlink, directory) 

669 

670 def _link(self, file_operation, directory): 

671 dst_path = directory / self.targetname 

672 if self.src_path == dst_path: 

673 return 

674 

675 dst_path.unlink(missing_ok=True) 

676 file_operation(self.src_path, dst_path) 

677 

678 

679@dataclass 

680class FDFWriter: 

681 name: str 

682 xc: str 

683 fdf_user_args: dict 

684 more_fdf_args: dict 

685 mesh_cutoff: float 

686 energy_shift: float 

687 spin: str 

688 species_numbers: object # ? 

689 atomic_coord_format: str 

690 kpts: object # ? 

691 bandpath: object # ? 

692 species_info: object 

693 

694 def write(self, fd): 

695 for chunk in self.generate_text(): 

696 fd.write(chunk) 

697 

698 def generate_text(self): 

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

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

701 yield "\n" 

702 

703 # Write explicitly given options first to 

704 # allow the user to override anything. 

705 fdf_arguments = self.fdf_user_args 

706 for key in sorted(fdf_arguments): 

707 yield var(key, fdf_arguments[key]) 

708 

709 # Force siesta to return error on no convergence. 

710 # as default consistent with ASE expectations. 

711 if 'SCFMustConverge' not in fdf_arguments: 

712 yield var('SCFMustConverge', True) 

713 yield '\n' 

714 

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

716 # Spin backwards compatibility. 

717 if self.spin == 'collinear': 

718 key = 'SpinPolarized' 

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

720 key = 'NonCollinearSpin' 

721 else: 

722 key = None 

723 

724 if key is not None: 

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

726 

727 # Write functional. 

728 functional, authors = self.xc 

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

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

731 yield '\n' 

732 

733 # Write mesh cutoff and energy shift. 

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

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

736 yield '\n' 

737 

738 yield from self.species_info.generate_text() 

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

740 

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

742 yield var(key, value) 

743 

744 if self.kpts is not None: 

745 kpts = np.array(self.kpts) 

746 yield from SiestaInput.generate_kpts(kpts) 

747 

748 if self.bandpath is not None: 

749 lines = bandpath2bandpoints(self.bandpath) 

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

751 yield lines 

752 yield '\n' 

753 

754 def generate_atoms_text(self, atoms: Atoms): 

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

756 

757 cell = atoms.cell 

758 yield '\n' 

759 

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

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

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

763 

764 if np.any(cell): 

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

766 yield block('LatticeVectors', cell) 

767 

768 yield from generate_atomic_coordinates( 

769 atoms, self.species_numbers, self.atomic_coord_format) 

770 

771 # Write magnetic moments. 

772 magmoms = atoms.get_initial_magnetic_moments() 

773 

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

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

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

777 # atoms object. 

778 if len(magmoms) == 0: 

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

780 

781 yield '%block DM.InitSpin\n' 

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

783 for n, M in enumerate(magmoms): 

784 if M[0] != 0: 

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

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

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

788 for n, M in enumerate(magmoms): 

789 if M != 0: 

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

791 yield '%endblock DM.InitSpin\n' 

792 yield '\n' 

793 

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

795 if symlink_pseudos is None: 

796 symlink_pseudos = os.name != 'nt' 

797 

798 for instruction in self.species_info.file_instructions: 

799 if symlink_pseudos: 

800 instruction.symlink_to(directory) 

801 else: 

802 instruction.copy_to(directory) 

803 

804 

805# Utilities for generating bits of strings. 

806# 

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

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

809# which applies consistent spacings etc. 

810def var(key, value): 

811 return format_fdf(key, value) 

812 

813 

814def block(name, data): 

815 return format_block(name, data)