Coverage for /builds/alexhroom/ase/ase/calculators/siesta/siesta.py: 80.75%
374 statements
« prev ^ index » next coverage.py v7.5.3, created at 2024-08-05 14:37 +0000
« prev ^ index » next coverage.py v7.5.3, created at 2024-08-05 14:37 +0000
1"""
2This module defines the ASE interface to SIESTA.
4Written by Mads Engelund (see www.espeem.com)
6Home of the SIESTA package:
7http://www.uam.es/departamentos/ciencias/fismateriac/siesta
92017.04 - Pedro Brandimarte: changes for python 2-3 compatible
11"""
13import os
14import re
15import shutil
16import tempfile
17from dataclasses import dataclass
18from pathlib import Path
19from typing import Any, Dict, List
21import numpy as np
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
33meV = 0.001 * eV
36def parse_siesta_version(output: bytes) -> str:
37 match = re.search(rb'Siesta Version\s*:\s*(\S+)', output)
39 if match is None:
40 raise RuntimeError('Could not get Siesta version info from output '
41 '{!r}'.format(output))
43 string = match.group(1).decode('ascii')
44 return string
47def get_siesta_version(executable: str) -> str:
48 """ Return SIESTA version number.
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.
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)
73 return parse_siesta_version(output)
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)
85def bandpath2bandpoints(path):
86 return '\n'.join([
87 'BandLinesScale ReciprocalLatticeVectors',
88 format_block('BandPoints', path.kpts)])
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)
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
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']}
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']
144 # Dictionary of valid input vaiables.
145 default_parameters = SiestaParameters()
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
152 fileio_rules = FileIOCalculator.ruleset(
153 configspec=dict(pseudo_path=None),
154 stdin_name='{prefix}.fdf',
155 stdout_name='{prefix}.out')
157 def __init__(self, command=None, profile=None, directory='.', **kwargs):
158 """ASE interface to the SIESTA code.
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 """
214 # Put in the default arguments.
215 parameters = self.default_parameters.__class__(**kwargs)
217 # Call the base class.
218 FileIOCalculator.__init__(
219 self,
220 command=command,
221 profile=profile,
222 directory=directory,
223 **parameters)
225 def __getitem__(self, key):
226 """Convenience method to retrieve a parameter as
227 calculator[key] rather than calculator.parameters[key]
229 Parameters:
230 -key : str, the name of the parameters to get.
231 """
232 return self.parameters[key]
234 def species(self, atoms):
235 """Find all relevant species depending on the atoms object and
236 species input.
238 Parameters :
239 - atoms : An Atoms object.
240 """
241 return SiestaInput.get_species(
242 atoms, list(self['species']), self['basis_set'])
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.
253 Parameters:
254 -kwargs : Dictionary containing the keywords defined in
255 SiestaParameters.
257 .. deprecated:: 3.18.2
258 The keyword 'UNPOLARIZED' has been deprecated and replaced by
259 'non-polarized'
260 """
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
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))
275 # Use the default parameters.
276 parameters = self.__class__.default_parameters.copy()
277 parameters.update(kwargs)
278 kwargs = parameters
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)
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)
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))
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
315 if not found:
316 raise ValueError(f"Unrecognized 'xc' keyword: '{xc}'")
317 kwargs['xc'] = (functional, authors)
319 # Check fdf_arguments.
320 if kwargs['fdf_arguments'] is None:
321 kwargs['fdf_arguments'] = {}
323 if not isinstance(kwargs['fdf_arguments'], dict):
324 raise TypeError("fdf_arguments must be a dictionary.")
326 # Call baseclass.
327 FileIOCalculator.set(self, **kwargs)
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)
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
344 # Type checking.
345 if not isinstance(fdf_arguments, dict):
346 raise TypeError("fdf_arguments must be a dictionary.")
348 def write_input(self, atoms, properties=None, system_changes=None):
349 """Write input (fdf)-file.
350 See calculator.py for further details.
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 """
358 super().write_input(
359 atoms=atoms,
360 properties=properties,
361 system_changes=system_changes)
363 filename = self.getpath(ext='fdf')
365 more_fdf_args = {}
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)):
374 more_fdf_args['DM.UseSaveDM'] = True
376 if 'density' in properties:
377 more_fdf_args['SaveRho'] = True
379 species, species_numbers = self.species(atoms)
381 pseudo_path = (self['pseudo_path']
382 or self.profile.configvars.get('pseudo_path')
383 or self.cfg.get('SIESTA_PP_PATH'))
385 if not pseudo_path:
386 raise Exception(
387 'Please configure pseudo_path or SIESTA_PP_PATH envvar')
389 species_info = SpeciesInfo(
390 atoms=atoms,
391 pseudo_path=Path(pseudo_path),
392 pseudo_qualifier=self.pseudo_qualifier(),
393 species=species)
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 )
410 with open(filename, 'w') as fd:
411 writer.write(fd)
413 writer.link_pseudos_into_directory(
414 symlink_pseudos=self['symlink_pseudos'],
415 directory=Path(self.directory))
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 """
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()
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
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']
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)
458 self.results['ion'] = self.read_ion(self.atoms)
460 def read_ion(self, atoms):
461 """
462 Read the ion.xml file of each specie
463 """
464 species, species_numbers = self.species(atoms)
466 ion_results = {}
467 for species_number, spec in enumerate(species, start=1):
468 symbol = spec['symbol']
469 atomic_number = atomic_numbers[symbol]
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
481 name = f"{label}.{species_number}"
482 if spec['ghost']:
483 name = f"{name}.ghost"
484 atomic_number = -atomic_number
486 label = name.rsplit('.', 2)[0]
488 if label not in ion_results:
489 fname = self.getpath(label, 'ion.xml')
490 fname = Path(fname)
491 if fname.is_file():
492 ion_results[label] = get_ion(fname)
494 return ion_results
496 def band_structure(self):
497 return self.results['bandstructure']
499 def get_fermi_level(self):
500 return self.results['fermi_energy']
502 def get_k_point_weights(self):
503 return self.results['kpoint_weights']
505 def get_ibz_k_points(self):
506 return self.results['kpoints']
508 def get_eigenvalues(self, kpt=0, spin=0):
509 return self.results['eigenvalues'][spin, kpt]
511 def get_number_of_spins(self):
512 return self.results['eigenvalues'].shape[0]
515def generate_atomic_coordinates(atoms: Atoms, species_numbers,
516 atomic_coord_format: str):
517 """Write atomic coordinates.
519 Parameters
520 ----------
521 fd : IO
522 An open file object.
523 atoms : Atoms
524 An atoms object.
525 """
526 if atomic_coord_format == 'xyz':
527 return generate_atomic_coordinates_xyz(atoms, species_numbers)
528 elif atomic_coord_format == 'zmatrix':
529 return generate_atomic_coordinates_zmatrix(atoms, species_numbers)
530 else:
531 raise RuntimeError(
532 f'Unknown atomic_coord_format: {atomic_coord_format}')
535def generate_atomic_coordinates_zmatrix(atoms: Atoms, species_numbers):
536 """Write atomic coordinates in Z-matrix format.
538 Parameters
539 ----------
540 fd : IO
541 An open file object.
542 atoms : Atoms
543 An atoms object.
544 """
545 yield '\n'
546 yield var('ZM.UnitsLength', 'Ang')
547 yield '%block Zmatrix\n'
548 yield ' cartesian\n'
550 fstr = "{:5d}" + "{:20.10f}" * 3 + "{:3d}" * 3 + "{:7d} {:s}\n"
551 a2constr = SiestaInput.make_xyz_constraints(atoms)
552 a2p, a2s = atoms.get_positions(), atoms.symbols
553 for ia, (sp, xyz, ccc, sym) in enumerate(
554 zip(species_numbers, a2p, a2constr, a2s)):
555 yield fstr.format(
556 sp, xyz[0], xyz[1], xyz[2], ccc[0],
557 ccc[1], ccc[2], ia + 1, sym)
558 yield '%endblock Zmatrix\n'
560 # origin = tuple(-atoms.get_celldisp().flatten())
561 # yield block('AtomicCoordinatesOrigin', [origin])
564def generate_atomic_coordinates_xyz(atoms: Atoms, species_numbers):
565 """Write atomic coordinates.
567 Parameters
568 ----------
569 fd : IO
570 An open file object.
571 atoms : Atoms
572 An atoms object.
573 """
574 yield '\n'
575 yield var('AtomicCoordinatesFormat', 'Ang')
576 yield block('AtomicCoordinatesAndAtomicSpecies',
577 [[*atom.position, number]
578 for atom, number in zip(atoms, species_numbers)])
579 yield '\n'
581 # origin = tuple(-atoms.get_celldisp().flatten())
582 # yield block('AtomicCoordinatesOrigin', [origin])
585@dataclass
586class SpeciesInfo:
587 atoms: Atoms
588 pseudo_path: Path
589 pseudo_qualifier: str
590 species: dict # actually a kind of Parameters object, should refactor
592 def __post_init__(self):
593 pao_basis = []
594 chemical_labels = []
595 basis_sizes = []
596 file_instructions = []
598 for species_number, spec in enumerate(self.species, start=1):
599 symbol = spec['symbol']
600 atomic_number = atomic_numbers[symbol]
602 if spec['pseudopotential'] is None:
603 if self.pseudo_qualifier == '':
604 label = symbol
605 else:
606 label = f"{symbol}.{self.pseudo_qualifier}"
607 src_path = self.pseudo_path / f"{label}.psf"
608 else:
609 src_path = Path(spec['pseudopotential'])
610 label = src_path.stem
611 if not src_path.is_absolute():
612 src_path = self.pseudo_path / src_path
613 if not src_path.exists():
614 src_path = self.pseudo_path / f"{symbol}.psml"
616 name = src_path.name
617 name = name.split('.')
618 name.insert(-1, str(species_number))
619 if spec['ghost']:
620 name.insert(-1, 'ghost')
621 atomic_number = -atomic_number
623 name = '.'.join(name)
625 instr = FileInstruction(src_path, name)
626 file_instructions.append(instr)
628 label = '.'.join(np.array(name.split('.'))[:-1])
629 string = ' %d %d %s' % (species_number, atomic_number, label)
630 chemical_labels.append(string)
631 if isinstance(spec['basis_set'], PAOBasisBlock):
632 pao_basis.append(spec['basis_set'].script(label))
633 else:
634 basis_sizes.append((" " + label, spec['basis_set']))
636 self.file_instructions = file_instructions
637 self.chemical_labels = chemical_labels
638 self.pao_basis = pao_basis
639 self.basis_sizes = basis_sizes
641 def generate_text(self):
642 yield var('NumberOfSpecies', len(self.species))
643 yield var('NumberOfAtoms', len(self.atoms))
645 yield var('ChemicalSpecieslabel', self.chemical_labels)
646 yield '\n'
647 yield var('PAO.Basis', self.pao_basis)
648 yield var('PAO.BasisSizes', self.basis_sizes)
649 yield '\n'
652@dataclass
653class FileInstruction:
654 src_path: Path
655 targetname: str
657 def copy_to(self, directory):
658 self._link(shutil.copy, directory)
660 def symlink_to(self, directory):
661 self._link(os.symlink, directory)
663 def _link(self, file_operation, directory):
664 dst_path = directory / self.targetname
665 if self.src_path == dst_path:
666 return
668 dst_path.unlink(missing_ok=True)
669 file_operation(self.src_path, dst_path)
672@dataclass
673class FDFWriter:
674 name: str
675 xc: str
676 fdf_user_args: dict
677 more_fdf_args: dict
678 mesh_cutoff: float
679 energy_shift: float
680 spin: str
681 species_numbers: object # ?
682 atomic_coord_format: str
683 kpts: object # ?
684 bandpath: object # ?
685 species_info: object
687 def write(self, fd):
688 for chunk in self.generate_text():
689 fd.write(chunk)
691 def generate_text(self):
692 yield var('SystemName', self.name)
693 yield var('SystemLabel', self.name)
694 yield "\n"
696 # Write explicitly given options first to
697 # allow the user to override anything.
698 fdf_arguments = self.fdf_user_args
699 for key in sorted(fdf_arguments):
700 yield var(key, fdf_arguments[key])
702 # Force siesta to return error on no convergence.
703 # as default consistent with ASE expectations.
704 if 'SCFMustConverge' not in fdf_arguments:
705 yield var('SCFMustConverge', True)
706 yield '\n'
708 yield var('Spin', self.spin)
709 # Spin backwards compatibility.
710 if self.spin == 'collinear':
711 key = 'SpinPolarized'
712 elif self.spin == 'non-collinear':
713 key = 'NonCollinearSpin'
714 else:
715 key = None
717 if key is not None:
718 yield var(key, (True, '# Backwards compatibility.'))
720 # Write functional.
721 functional, authors = self.xc
722 yield var('XC.functional', functional)
723 yield var('XC.authors', authors)
724 yield '\n'
726 # Write mesh cutoff and energy shift.
727 yield var('MeshCutoff', (self.mesh_cutoff, 'eV'))
728 yield var('PAO.EnergyShift', (self.energy_shift, 'eV'))
729 yield '\n'
731 yield from self.species_info.generate_text()
732 yield from self.generate_atoms_text(self.species_info.atoms)
734 for key, value in self.more_fdf_args.items():
735 yield var(key, value)
737 if self.kpts is not None:
738 kpts = np.array(self.kpts)
739 yield from SiestaInput.generate_kpts(kpts)
741 if self.bandpath is not None:
742 lines = bandpath2bandpoints(self.bandpath)
743 assert isinstance(lines, str) # rename this variable?
744 yield lines
745 yield '\n'
747 def generate_atoms_text(self, atoms: Atoms):
748 """Translate the Atoms object to fdf-format."""
750 cell = atoms.cell
751 yield '\n'
753 if cell.rank in [1, 2]:
754 raise ValueError('Expected 3D unit cell or no unit cell. You may '
755 'wish to add vacuum along some directions.')
757 if np.any(cell):
758 yield var('LatticeConstant', '1.0 Ang')
759 yield block('LatticeVectors', cell)
761 yield from generate_atomic_coordinates(
762 atoms, self.species_numbers, self.atomic_coord_format)
764 # Write magnetic moments.
765 magmoms = atoms.get_initial_magnetic_moments()
767 # The DM.InitSpin block must be written to initialize to
768 # no spin. SIESTA default is FM initialization, if the
769 # block is not written, but we must conform to the
770 # atoms object.
771 if len(magmoms) == 0:
772 yield '#Empty block forces ASE initialization.\n'
774 yield '%block DM.InitSpin\n'
775 if len(magmoms) != 0 and isinstance(magmoms[0], np.ndarray):
776 for n, M in enumerate(magmoms):
777 if M[0] != 0:
778 yield (' %d %.14f %.14f %.14f \n'
779 % (n + 1, M[0], M[1], M[2]))
780 elif len(magmoms) != 0 and isinstance(magmoms[0], float):
781 for n, M in enumerate(magmoms):
782 if M != 0:
783 yield ' %d %.14f \n' % (n + 1, M)
784 yield '%endblock DM.InitSpin\n'
785 yield '\n'
787 def link_pseudos_into_directory(self, *, symlink_pseudos=None, directory):
788 if symlink_pseudos is None:
789 symlink_pseudos = os.name != 'nt'
791 for instruction in self.species_info.file_instructions:
792 if symlink_pseudos:
793 instruction.symlink_to(directory)
794 else:
795 instruction.copy_to(directory)
798# Utilities for generating bits of strings.
799#
800# We are re-aliasing format_fdf and format_block in the anticipation
801# that they may change, or we might move this onto a Formatter object
802# which applies consistent spacings etc.
803def var(key, value):
804 return format_fdf(key, value)
807def block(name, data):
808 return format_block(name, data)