Coverage for /builds/hweiske/ase/ase/calculators/siesta/siesta.py: 80.64%
377 statements
« prev ^ index » next coverage.py v7.2.7, created at 2024-04-22 11:22 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2024-04-22 11:22 +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 stdin_name='{prefix}.fdf',
154 stdout_name='{prefix}.out')
156 def __init__(self, command=None, profile=None, directory='.', **kwargs):
157 """ASE interface to the SIESTA code.
159 Parameters:
160 - label : The basename of all files created during
161 calculation.
162 - mesh_cutoff : Energy in eV.
163 The mesh cutoff energy for determining number of
164 grid points in the matrix-element calculation.
165 - energy_shift : Energy in eV
166 The confining energy of the basis set generation.
167 - kpts : Tuple of 3 integers, the k-points in different
168 directions.
169 - xc : The exchange-correlation potential. Can be set to
170 any allowed value for either the Siesta
171 XC.funtional or XC.authors keyword. Default "LDA"
172 - basis_set : "SZ"|"SZP"|"DZ"|"DZP"|"TZP", strings which specify
173 the type of functions basis set.
174 - spin : "non-polarized"|"collinear"|
175 "non-collinear|spin-orbit".
176 The level of spin description to be used.
177 - species : None|list of Species objects. The species objects
178 can be used to to specify the basis set,
179 pseudopotential and whether the species is ghost.
180 The tag on the atoms object and the element is used
181 together to identify the species.
182 - pseudo_path : None|path. This path is where
183 pseudopotentials are taken from.
184 If None is given, then then the path given
185 in $SIESTA_PP_PATH will be used.
186 - pseudo_qualifier: None|string. This string will be added to the
187 pseudopotential path that will be retrieved.
188 For hydrogen with qualifier "abc" the
189 pseudopotential "H.abc.psf" will be retrieved.
190 - symlink_pseudos: None|bool
191 If true, symlink pseudopotentials
192 into the calculation directory, else copy them.
193 Defaults to true on Unix and false on Windows.
194 - atoms : The Atoms object.
195 - restart : str. Prefix for restart file.
196 May contain a directory.
197 Default is None, don't restart.
198 - fdf_arguments: Explicitly given fdf arguments. Dictonary using
199 Siesta keywords as given in the manual. List values
200 are written as fdf blocks with each element on a
201 separate line, while tuples will write each element
202 in a single line. ASE units are assumed in the
203 input.
204 - atomic_coord_format: "xyz"|"zmatrix", strings to switch between
205 the default way of entering the system's geometry
206 (via the block AtomicCoordinatesAndAtomicSpecies)
207 and a recent method via the block Zmatrix. The
208 block Zmatrix allows to specify basic geometry
209 constrains such as realized through the ASE classes
210 FixAtom, FixedLine and FixedPlane.
211 """
213 # Put in the default arguments.
214 parameters = self.default_parameters.__class__(**kwargs)
216 # Call the base class.
217 FileIOCalculator.__init__(
218 self,
219 command=command,
220 profile=profile,
221 directory=directory,
222 **parameters)
224 def __getitem__(self, key):
225 """Convenience method to retrieve a parameter as
226 calculator[key] rather than calculator.parameters[key]
228 Parameters:
229 -key : str, the name of the parameters to get.
230 """
231 return self.parameters[key]
233 def species(self, atoms):
234 """Find all relevant species depending on the atoms object and
235 species input.
237 Parameters :
238 - atoms : An Atoms object.
239 """
240 return SiestaInput.get_species(
241 atoms, list(self['species']), self['basis_set'])
243 @deprecated(
244 "The keyword 'UNPOLARIZED' has been deprecated,"
245 "and replaced by 'non-polarized'",
246 category=FutureWarning,
247 callback=_nonpolarized_alias,
248 )
249 def set(self, **kwargs):
250 """Set all parameters.
252 Parameters:
253 -kwargs : Dictionary containing the keywords defined in
254 SiestaParameters.
256 .. deprecated:: 3.18.2
257 The keyword 'UNPOLARIZED' has been deprecated and replaced by
258 'non-polarized'
259 """
261 # XXX Inserted these next few lines because set() would otherwise
262 # discard all previously set keywords to their defaults! --askhl
263 current = self.parameters.copy()
264 current.update(kwargs)
265 kwargs = current
267 # Find not allowed keys.
268 default_keys = list(self.__class__.default_parameters)
269 offending_keys = set(kwargs) - set(default_keys)
270 if len(offending_keys) > 0:
271 mess = "'set' does not take the keywords: %s "
272 raise ValueError(mess % list(offending_keys))
274 # Use the default parameters.
275 parameters = self.__class__.default_parameters.copy()
276 parameters.update(kwargs)
277 kwargs = parameters
279 # Check energy inputs.
280 for arg in ['mesh_cutoff', 'energy_shift']:
281 value = kwargs.get(arg)
282 if value is None:
283 continue
284 if not (isinstance(value, (float, int)) and value > 0):
285 mess = "'{}' must be a positive number(in eV), \
286 got '{}'".format(arg, value)
287 raise ValueError(mess)
289 # Check the functional input.
290 xc = kwargs.get('xc', 'LDA')
291 if isinstance(xc, (tuple, list)) and len(xc) == 2:
292 functional, authors = xc
293 if functional.lower() not in [k.lower() for k in self.allowed_xc]:
294 mess = f"Unrecognized functional keyword: '{functional}'"
295 raise ValueError(mess)
297 lsauthorslower = [a.lower() for a in self.allowed_xc[functional]]
298 if authors.lower() not in lsauthorslower:
299 mess = "Unrecognized authors keyword for %s: '%s'"
300 raise ValueError(mess % (functional, authors))
302 elif xc in self.allowed_xc:
303 functional = xc
304 authors = self.allowed_xc[xc][0]
305 else:
306 found = False
307 for key, value in self.allowed_xc.items():
308 if xc in value:
309 found = True
310 functional = key
311 authors = xc
312 break
314 if not found:
315 raise ValueError(f"Unrecognized 'xc' keyword: '{xc}'")
316 kwargs['xc'] = (functional, authors)
318 # Check fdf_arguments.
319 if kwargs['fdf_arguments'] is None:
320 kwargs['fdf_arguments'] = {}
322 if not isinstance(kwargs['fdf_arguments'], dict):
323 raise TypeError("fdf_arguments must be a dictionary.")
325 # Call baseclass.
326 FileIOCalculator.set(self, **kwargs)
328 def set_fdf_arguments(self, fdf_arguments):
329 """ Set the fdf_arguments after the initialization of the
330 calculator.
331 """
332 self.validate_fdf_arguments(fdf_arguments)
333 FileIOCalculator.set(self, fdf_arguments=fdf_arguments)
335 def validate_fdf_arguments(self, fdf_arguments):
336 """ Raises error if the fdf_argument input is not a
337 dictionary of allowed keys.
338 """
339 # None is valid
340 if fdf_arguments is None:
341 return
343 # Type checking.
344 if not isinstance(fdf_arguments, dict):
345 raise TypeError("fdf_arguments must be a dictionary.")
347 def write_input(self, atoms, properties=None, system_changes=None):
348 """Write input (fdf)-file.
349 See calculator.py for further details.
351 Parameters:
352 - atoms : The Atoms object to write.
353 - properties : The properties which should be calculated.
354 - system_changes : List of properties changed since last run.
355 """
357 super().write_input(
358 atoms=atoms,
359 properties=properties,
360 system_changes=system_changes)
362 filename = self.getpath(ext='fdf')
364 more_fdf_args = {}
366 # Use the saved density matrix if only 'cell' and 'positions'
367 # have changed.
368 if (system_changes is None or
369 ('numbers' not in system_changes and
370 'initial_magmoms' not in system_changes and
371 'initial_charges' not in system_changes)):
373 more_fdf_args['DM.UseSaveDM'] = True
375 if 'density' in properties:
376 more_fdf_args['SaveRho'] = True
378 species, species_numbers = self.species(atoms)
380 if self['pseudo_path'] is not None:
381 pseudo_path = self['pseudo_path']
382 elif 'SIESTA_PP_PATH' in self.cfg:
383 pseudo_path = self.cfg['SIESTA_PP_PATH']
384 else:
385 mess = "Please set the environment variable 'SIESTA_PP_PATH'"
386 raise Exception(mess)
388 species_info = SpeciesInfo(
389 atoms=atoms,
390 pseudo_path=Path(pseudo_path),
391 pseudo_qualifier=self.pseudo_qualifier(),
392 species=species)
394 writer = FDFWriter(
395 name=self.prefix,
396 xc=self['xc'],
397 spin=self['spin'],
398 mesh_cutoff=self['mesh_cutoff'],
399 energy_shift=self['energy_shift'],
400 fdf_user_args=self['fdf_arguments'],
401 more_fdf_args=more_fdf_args,
402 species_numbers=species_numbers,
403 atomic_coord_format=self['atomic_coord_format'].lower(),
404 kpts=self['kpts'],
405 bandpath=self['bandpath'],
406 species_info=species_info,
407 )
409 with open(filename, 'w') as fd:
410 writer.write(fd)
412 writer.link_pseudos_into_directory(
413 symlink_pseudos=self['symlink_pseudos'],
414 directory=Path(self.directory))
416 def read(self, filename):
417 """Read structural parameters from file .XV file
418 Read other results from other files
419 filename : siesta.XV
420 """
422 fname = self.getpath(filename)
423 if not fname.exists():
424 raise ReadError(f"The restart file '{fname}' does not exist")
425 with fname.open() as fd:
426 self.atoms = read_siesta_xv(fd)
427 self.read_results()
429 def getpath(self, fname=None, ext=None):
430 """ Returns the directory/fname string """
431 if fname is None:
432 fname = self.prefix
433 if ext is not None:
434 fname = f'{fname}.{ext}'
435 return Path(self.directory) / fname
437 def pseudo_qualifier(self):
438 """Get the extra string used in the middle of the pseudopotential.
439 The retrieved pseudopotential for a specific element will be
440 'H.xxx.psf' for the element 'H' with qualifier 'xxx'. If qualifier
441 is set to None then the qualifier is set to functional name.
442 """
443 if self['pseudo_qualifier'] is None:
444 return self['xc'][0].lower()
445 else:
446 return self['pseudo_qualifier']
448 def read_results(self):
449 """Read the results."""
450 from ase.io.siesta_output import OutputReader
451 reader = OutputReader(prefix=self.prefix,
452 directory=Path(self.directory),
453 bandpath=self['bandpath'])
454 results = reader.read_results()
455 self.results.update(results)
457 self.results['ion'] = self.read_ion(self.atoms)
459 def read_ion(self, atoms):
460 """
461 Read the ion.xml file of each specie
462 """
463 species, species_numbers = self.species(atoms)
465 ion_results = {}
466 for species_number, spec in enumerate(species, start=1):
467 symbol = spec['symbol']
468 atomic_number = atomic_numbers[symbol]
470 if spec['pseudopotential'] is None:
471 if self.pseudo_qualifier() == '':
472 label = symbol
473 else:
474 label = f"{symbol}.{self.pseudo_qualifier()}"
475 pseudopotential = self.getpath(label, 'psf')
476 else:
477 pseudopotential = Path(spec['pseudopotential'])
478 label = pseudopotential.stem
480 name = f"{label}.{species_number}"
481 if spec['ghost']:
482 name = f"{name}.ghost"
483 atomic_number = -atomic_number
485 label = name.rsplit('.', 2)[0]
487 if label not in ion_results:
488 fname = self.getpath(label, 'ion.xml')
489 fname = Path(fname)
490 if fname.is_file():
491 ion_results[label] = get_ion(fname)
493 return ion_results
495 def band_structure(self):
496 return self.results['bandstructure']
498 def get_fermi_level(self):
499 return self.results['fermi_energy']
501 def get_k_point_weights(self):
502 return self.results['kpoint_weights']
504 def get_ibz_k_points(self):
505 return self.results['kpoints']
507 def get_eigenvalues(self, kpt=0, spin=0):
508 return self.results['eigenvalues'][spin, kpt]
510 def get_number_of_spins(self):
511 return self.results['eigenvalues'].shape[0]
514def generate_atomic_coordinates(atoms: Atoms, species_numbers,
515 atomic_coord_format: str):
516 """Write atomic coordinates.
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}')
534def generate_atomic_coordinates_zmatrix(atoms: Atoms, species_numbers):
535 """Write atomic coordinates in Z-matrix format.
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'
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'
559 # origin = tuple(-atoms.get_celldisp().flatten())
560 # yield block('AtomicCoordinatesOrigin', [origin])
563def generate_atomic_coordinates_xyz(atoms: Atoms, species_numbers):
564 """Write atomic coordinates.
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'
580 # origin = tuple(-atoms.get_celldisp().flatten())
581 # yield block('AtomicCoordinatesOrigin', [origin])
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
591 def __post_init__(self):
592 pao_basis = []
593 chemical_labels = []
594 basis_sizes = []
595 file_instructions = []
597 for species_number, spec in enumerate(self.species, start=1):
598 symbol = spec['symbol']
599 atomic_number = atomic_numbers[symbol]
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 label = src_path.stem
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"
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
622 name = '.'.join(name)
624 instr = FileInstruction(src_path, name)
625 file_instructions.append(instr)
627 label = '.'.join(np.array(name.split('.'))[:-1])
628 string = ' %d %d %s' % (species_number, atomic_number, label)
629 chemical_labels.append(string)
630 if isinstance(spec['basis_set'], PAOBasisBlock):
631 pao_basis.append(spec['basis_set'].script(label))
632 else:
633 basis_sizes.append((" " + label, spec['basis_set']))
635 self.file_instructions = file_instructions
636 self.chemical_labels = chemical_labels
637 self.pao_basis = pao_basis
638 self.basis_sizes = basis_sizes
640 def generate_text(self):
641 yield var('NumberOfSpecies', len(self.species))
642 yield var('NumberOfAtoms', len(self.atoms))
644 yield var('ChemicalSpecieslabel', self.chemical_labels)
645 yield '\n'
646 yield var('PAO.Basis', self.pao_basis)
647 yield var('PAO.BasisSizes', self.basis_sizes)
648 yield '\n'
651@dataclass
652class FileInstruction:
653 src_path: Path
654 targetname: str
656 def copy_to(self, directory):
657 self._link(shutil.copy, directory)
659 def symlink_to(self, directory):
660 self._link(os.symlink, directory)
662 def _link(self, file_operation, directory):
663 dst_path = directory / self.targetname
664 if self.src_path == dst_path:
665 return
667 dst_path.unlink(missing_ok=True)
668 file_operation(self.src_path, dst_path)
671@dataclass
672class FDFWriter:
673 name: str
674 xc: str
675 fdf_user_args: dict
676 more_fdf_args: dict
677 mesh_cutoff: float
678 energy_shift: float
679 spin: str
680 species_numbers: object # ?
681 atomic_coord_format: str
682 kpts: object # ?
683 bandpath: object # ?
684 species_info: object
686 def write(self, fd):
687 for chunk in self.generate_text():
688 fd.write(chunk)
690 def generate_text(self):
691 yield var('SystemName', self.name)
692 yield var('SystemLabel', self.name)
693 yield "\n"
695 # Write explicitly given options first to
696 # allow the user to override anything.
697 fdf_arguments = self.fdf_user_args
698 for key in sorted(fdf_arguments):
699 yield var(key, fdf_arguments[key])
701 # Force siesta to return error on no convergence.
702 # as default consistent with ASE expectations.
703 if 'SCFMustConverge' not in fdf_arguments:
704 yield var('SCFMustConverge', True)
705 yield '\n'
707 yield var('Spin', self.spin)
708 # Spin backwards compatibility.
709 if self.spin == 'collinear':
710 key = 'SpinPolarized'
711 elif self.spin == 'non-collinear':
712 key = 'NonCollinearSpin'
713 else:
714 key = None
716 if key is not None:
717 yield var(key, (True, '# Backwards compatibility.'))
719 # Write functional.
720 functional, authors = self.xc
721 yield var('XC.functional', functional)
722 yield var('XC.authors', authors)
723 yield '\n'
725 # Write mesh cutoff and energy shift.
726 yield var('MeshCutoff', (self.mesh_cutoff, 'eV'))
727 yield var('PAO.EnergyShift', (self.energy_shift, 'eV'))
728 yield '\n'
730 yield from self.species_info.generate_text()
731 yield from self.generate_atoms_text(self.species_info.atoms)
733 for key, value in self.more_fdf_args.items():
734 yield var(key, value)
736 if self.kpts is not None:
737 kpts = np.array(self.kpts)
738 yield from SiestaInput.generate_kpts(kpts)
740 if self.bandpath is not None:
741 lines = bandpath2bandpoints(self.bandpath)
742 assert isinstance(lines, str) # rename this variable?
743 yield lines
744 yield '\n'
746 def generate_atoms_text(self, atoms: Atoms):
747 """Translate the Atoms object to fdf-format."""
749 cell = atoms.cell
750 yield '\n'
752 if cell.rank in [1, 2]:
753 raise ValueError('Expected 3D unit cell or no unit cell. You may '
754 'wish to add vacuum along some directions.')
756 if np.any(cell):
757 yield var('LatticeConstant', '1.0 Ang')
758 yield block('LatticeVectors', cell)
760 yield from generate_atomic_coordinates(
761 atoms, self.species_numbers, self.atomic_coord_format)
763 # Write magnetic moments.
764 magmoms = atoms.get_initial_magnetic_moments()
766 # The DM.InitSpin block must be written to initialize to
767 # no spin. SIESTA default is FM initialization, if the
768 # block is not written, but we must conform to the
769 # atoms object.
770 if len(magmoms) == 0:
771 yield '#Empty block forces ASE initialization.\n'
773 yield '%block DM.InitSpin\n'
774 if len(magmoms) != 0 and isinstance(magmoms[0], np.ndarray):
775 for n, M in enumerate(magmoms):
776 if M[0] != 0:
777 yield (' %d %.14f %.14f %.14f \n'
778 % (n + 1, M[0], M[1], M[2]))
779 elif len(magmoms) != 0 and isinstance(magmoms[0], float):
780 for n, M in enumerate(magmoms):
781 if M != 0:
782 yield ' %d %.14f \n' % (n + 1, M)
783 yield '%endblock DM.InitSpin\n'
784 yield '\n'
786 def link_pseudos_into_directory(self, *, symlink_pseudos=None, directory):
787 if symlink_pseudos is None:
788 symlink_pseudos = os.name != 'nt'
790 for instruction in self.species_info.file_instructions:
791 if symlink_pseudos:
792 instruction.symlink_to(directory)
793 else:
794 instruction.copy_to(directory)
797# Utilities for generating bits of strings.
798#
799# We are re-aliasing format_fdf and format_block in the anticipation
800# that they may change, or we might move this onto a Formatter object
801# which applies consistent spacings etc.
802def var(key, value):
803 return format_fdf(key, value)
806def block(name, data):
807 return format_block(name, data)