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
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-30 08:22 +0000
1# fmt: off
3"""
4This module defines the ASE interface to SIESTA.
6Written by Mads Engelund (see www.espeem.com)
8Home of the SIESTA package:
9https://siesta-project.org/
112017.04 - Pedro Brandimarte: changes for python 2-3 compatible
13"""
15import os
16import re
17import shutil
18import tempfile
19from dataclasses import dataclass
20from pathlib import Path
21from typing import Any
23import numpy as np
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
35meV = 0.001 * eV
38def parse_siesta_version(output: bytes) -> str:
39 match = re.search(rb'Version\s*:\s*(\S+)', output)
41 if match is None:
42 raise RuntimeError('Could not get Siesta version info from output '
43 '{!r}'.format(output))
45 string = match.group(1).decode('ascii')
46 return string
49def get_siesta_version(executable: str) -> str:
50 """ Return SIESTA version number.
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.
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)
75 return parse_siesta_version(output)
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)
87def bandpath2bandpoints(path):
88 return '\n'.join([
89 'BandLinesScale ReciprocalLatticeVectors',
90 format_block('BandPoints', path.kpts)])
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)
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
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']}
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']
146 # Dictionary of valid input vaiables.
147 default_parameters = SiestaParameters()
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
154 fileio_rules = FileIOCalculator.ruleset(
155 configspec=dict(pseudo_path=None),
156 stdin_name='{prefix}.fdf',
157 stdout_name='{prefix}.out')
159 def __init__(self, command=None, profile=None, directory='.', **kwargs):
160 """ASE interface to the SIESTA code.
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 """
217 # Put in the default arguments.
218 parameters = self.default_parameters.__class__(**kwargs)
220 # Call the base class.
221 FileIOCalculator.__init__(
222 self,
223 command=command,
224 profile=profile,
225 directory=directory,
226 **parameters)
228 def __getitem__(self, key):
229 """Convenience method to retrieve a parameter as
230 calculator[key] rather than calculator.parameters[key]
232 Parameters
233 ----------
234 -key : str, the name of the parameters to get.
235 """
236 return self.parameters[key]
238 def species(self, atoms):
239 """Find all relevant species depending on the atoms object and
240 species input.
242 Parameters :
243 - atoms : An Atoms object.
244 """
245 return SiestaInput.get_species(
246 atoms, list(self['species']), self['basis_set'])
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.
257 Parameters
258 ----------
259 -kwargs : Dictionary containing the keywords defined in
260 SiestaParameters.
262 .. deprecated:: 3.18.2
263 The keyword 'UNPOLARIZED' has been deprecated and replaced by
264 'non-polarized'
265 """
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
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))
280 # Use the default parameters.
281 parameters = self.__class__.default_parameters.copy()
282 parameters.update(kwargs)
283 kwargs = parameters
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)
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)
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))
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
320 if not found:
321 raise ValueError(f"Unrecognized 'xc' keyword: '{xc}'")
322 kwargs['xc'] = (functional, authors)
324 # Check fdf_arguments.
325 if kwargs['fdf_arguments'] is None:
326 kwargs['fdf_arguments'] = {}
328 if not isinstance(kwargs['fdf_arguments'], dict):
329 raise TypeError("fdf_arguments must be a dictionary.")
331 # Call baseclass.
332 FileIOCalculator.set(self, **kwargs)
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)
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
349 # Type checking.
350 if not isinstance(fdf_arguments, dict):
351 raise TypeError("fdf_arguments must be a dictionary.")
353 def write_input(self, atoms, properties=None, system_changes=None):
354 """Write input (fdf)-file.
355 See calculator.py for further details.
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 """
364 super().write_input(
365 atoms=atoms,
366 properties=properties,
367 system_changes=system_changes)
369 filename = self.getpath(ext='fdf')
371 more_fdf_args = {}
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)):
380 more_fdf_args['DM.UseSaveDM'] = True
382 if 'density' in properties:
383 more_fdf_args['SaveRho'] = True
385 species, species_numbers = self.species(atoms)
387 pseudo_path = (self['pseudo_path']
388 or self.profile.configvars.get('pseudo_path')
389 or self.cfg.get('SIESTA_PP_PATH'))
391 if not pseudo_path:
392 raise Exception(
393 'Please configure pseudo_path or SIESTA_PP_PATH envvar')
395 species_info = SpeciesInfo(
396 atoms=atoms,
397 pseudo_path=Path(pseudo_path),
398 pseudo_qualifier=self.pseudo_qualifier(),
399 species=species)
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 )
416 with open(filename, 'w') as fd:
417 writer.write(fd)
419 writer.link_pseudos_into_directory(
420 symlink_pseudos=self['symlink_pseudos'],
421 directory=Path(self.directory))
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 """
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()
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
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']
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)
464 self.results['ion'] = self.read_ion(self.atoms)
466 def read_ion(self, atoms):
467 """
468 Read the ion.xml file of each specie
469 """
470 species, _species_numbers = self.species(atoms)
472 ion_results = {}
473 for species_number, spec in enumerate(species, start=1):
474 symbol = spec['symbol']
475 atomic_number = atomic_numbers[symbol]
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
487 name = f"{label}.{species_number}"
488 if spec['ghost']:
489 name = f"{name}.ghost"
490 atomic_number = -atomic_number
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))
497 return ion_results
499 def band_structure(self):
500 return self.results['bandstructure']
502 def get_fermi_level(self):
503 return self.results['fermi_energy']
505 def get_k_point_weights(self):
506 return self.results['kpoint_weights']
508 def get_ibz_k_points(self):
509 return self.results['kpoints']
511 def get_eigenvalues(self, kpt=0, spin=0):
512 return self.results['eigenvalues'][spin, kpt]
514 def get_number_of_spins(self):
515 return self.results['eigenvalues'].shape[0]
518def generate_atomic_coordinates(atoms: Atoms, species_numbers,
519 atomic_coord_format: str):
520 """Write atomic coordinates.
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}')
538def generate_atomic_coordinates_zmatrix(atoms: Atoms, species_numbers):
539 """Write atomic coordinates in Z-matrix format.
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'
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'
563 # origin = tuple(-atoms.get_celldisp().flatten())
564 # yield block('AtomicCoordinatesOrigin', [origin])
567def generate_atomic_coordinates_xyz(atoms: Atoms, species_numbers):
568 """Write atomic coordinates.
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'
584 # origin = tuple(-atoms.get_celldisp().flatten())
585 # yield block('AtomicCoordinatesOrigin', [origin])
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
595 def __post_init__(self):
596 pao_basis = []
597 chemical_labels = []
598 basis_sizes = []
599 file_instructions = []
601 for species_number, spec in enumerate(self.species, start=1):
602 symbol = spec['symbol']
603 atomic_number = atomic_numbers[symbol]
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'])
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"
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
626 name = '.'.join(name)
628 instr = FileInstruction(src_path, name)
629 file_instructions.append(instr)
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']))
643 self.file_instructions = file_instructions
644 self.chemical_labels = chemical_labels
645 self.pao_basis = pao_basis
646 self.basis_sizes = basis_sizes
648 def generate_text(self):
649 yield var('NumberOfSpecies', len(self.species))
650 yield var('NumberOfAtoms', len(self.atoms))
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'
659@dataclass
660class FileInstruction:
661 src_path: Path
662 targetname: str
664 def copy_to(self, directory):
665 self._link(shutil.copy, directory)
667 def symlink_to(self, directory):
668 self._link(os.symlink, directory)
670 def _link(self, file_operation, directory):
671 dst_path = directory / self.targetname
672 if self.src_path == dst_path:
673 return
675 dst_path.unlink(missing_ok=True)
676 file_operation(self.src_path, dst_path)
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
694 def write(self, fd):
695 for chunk in self.generate_text():
696 fd.write(chunk)
698 def generate_text(self):
699 yield var('SystemName', self.name)
700 yield var('SystemLabel', self.name)
701 yield "\n"
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])
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'
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
724 if key is not None:
725 yield var(key, (True, '# Backwards compatibility.'))
727 # Write functional.
728 functional, authors = self.xc
729 yield var('XC.functional', functional)
730 yield var('XC.authors', authors)
731 yield '\n'
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'
738 yield from self.species_info.generate_text()
739 yield from self.generate_atoms_text(self.species_info.atoms)
741 for key, value in self.more_fdf_args.items():
742 yield var(key, value)
744 if self.kpts is not None:
745 kpts = np.array(self.kpts)
746 yield from SiestaInput.generate_kpts(kpts)
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'
754 def generate_atoms_text(self, atoms: Atoms):
755 """Translate the Atoms object to fdf-format."""
757 cell = atoms.cell
758 yield '\n'
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.')
764 if np.any(cell):
765 yield var('LatticeConstant', '1.0 Ang')
766 yield block('LatticeVectors', cell)
768 yield from generate_atomic_coordinates(
769 atoms, self.species_numbers, self.atomic_coord_format)
771 # Write magnetic moments.
772 magmoms = atoms.get_initial_magnetic_moments()
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'
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'
794 def link_pseudos_into_directory(self, *, symlink_pseudos=None, directory):
795 if symlink_pseudos is None:
796 symlink_pseudos = os.name != 'nt'
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)
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)
814def block(name, data):
815 return format_block(name, data)