Coverage for /builds/debichem-team/python-ase/ase/calculators/siesta/siesta.py: 81.55%
374 statements
« prev ^ index » next coverage.py v7.5.3, created at 2025-03-06 04:00 +0000
« prev ^ index » next coverage.py v7.5.3, created at 2025-03-06 04:00 +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'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 if name not in ion_results:
487 fname = self.getpath(name, 'ion.xml')
488 if fname.is_file():
489 ion_results[name] = get_ion(str(fname))
491 return ion_results
493 def band_structure(self):
494 return self.results['bandstructure']
496 def get_fermi_level(self):
497 return self.results['fermi_energy']
499 def get_k_point_weights(self):
500 return self.results['kpoint_weights']
502 def get_ibz_k_points(self):
503 return self.results['kpoints']
505 def get_eigenvalues(self, kpt=0, spin=0):
506 return self.results['eigenvalues'][spin, kpt]
508 def get_number_of_spins(self):
509 return self.results['eigenvalues'].shape[0]
512def generate_atomic_coordinates(atoms: Atoms, species_numbers,
513 atomic_coord_format: str):
514 """Write atomic coordinates.
516 Parameters
517 ----------
518 fd : IO
519 An open file object.
520 atoms : Atoms
521 An atoms object.
522 """
523 if atomic_coord_format == 'xyz':
524 return generate_atomic_coordinates_xyz(atoms, species_numbers)
525 elif atomic_coord_format == 'zmatrix':
526 return generate_atomic_coordinates_zmatrix(atoms, species_numbers)
527 else:
528 raise RuntimeError(
529 f'Unknown atomic_coord_format: {atomic_coord_format}')
532def generate_atomic_coordinates_zmatrix(atoms: Atoms, species_numbers):
533 """Write atomic coordinates in Z-matrix format.
535 Parameters
536 ----------
537 fd : IO
538 An open file object.
539 atoms : Atoms
540 An atoms object.
541 """
542 yield '\n'
543 yield var('ZM.UnitsLength', 'Ang')
544 yield '%block Zmatrix\n'
545 yield ' cartesian\n'
547 fstr = "{:5d}" + "{:20.10f}" * 3 + "{:3d}" * 3 + "{:7d} {:s}\n"
548 a2constr = SiestaInput.make_xyz_constraints(atoms)
549 a2p, a2s = atoms.get_positions(), atoms.symbols
550 for ia, (sp, xyz, ccc, sym) in enumerate(
551 zip(species_numbers, a2p, a2constr, a2s)):
552 yield fstr.format(
553 sp, xyz[0], xyz[1], xyz[2], ccc[0],
554 ccc[1], ccc[2], ia + 1, sym)
555 yield '%endblock Zmatrix\n'
557 # origin = tuple(-atoms.get_celldisp().flatten())
558 # yield block('AtomicCoordinatesOrigin', [origin])
561def generate_atomic_coordinates_xyz(atoms: Atoms, species_numbers):
562 """Write atomic coordinates.
564 Parameters
565 ----------
566 fd : IO
567 An open file object.
568 atoms : Atoms
569 An atoms object.
570 """
571 yield '\n'
572 yield var('AtomicCoordinatesFormat', 'Ang')
573 yield block('AtomicCoordinatesAndAtomicSpecies',
574 [[*atom.position, number]
575 for atom, number in zip(atoms, species_numbers)])
576 yield '\n'
578 # origin = tuple(-atoms.get_celldisp().flatten())
579 # yield block('AtomicCoordinatesOrigin', [origin])
582@dataclass
583class SpeciesInfo:
584 atoms: Atoms
585 pseudo_path: Path
586 pseudo_qualifier: str
587 species: dict # actually a kind of Parameters object, should refactor
589 def __post_init__(self):
590 pao_basis = []
591 chemical_labels = []
592 basis_sizes = []
593 file_instructions = []
595 for species_number, spec in enumerate(self.species, start=1):
596 symbol = spec['symbol']
597 atomic_number = atomic_numbers[symbol]
599 if spec['pseudopotential'] is None:
600 if self.pseudo_qualifier == '':
601 label = symbol
602 else:
603 label = f"{symbol}.{self.pseudo_qualifier}"
604 src_path = self.pseudo_path / f"{label}.psf"
605 else:
606 src_path = Path(spec['pseudopotential'])
608 if not src_path.is_absolute():
609 src_path = self.pseudo_path / src_path
610 if not src_path.exists():
611 src_path = self.pseudo_path / f"{symbol}.psml"
613 name = src_path.name
614 name = name.split('.')
615 name.insert(-1, str(species_number))
616 if spec['ghost']:
617 name.insert(-1, 'ghost')
618 atomic_number = -atomic_number
620 name = '.'.join(name)
622 instr = FileInstruction(src_path, name)
623 file_instructions.append(instr)
625 label = '.'.join(np.array(name.split('.'))[:-1])
626 pseudo_name = ''
627 if src_path.suffix != '.psf':
628 pseudo_name = f'{label}{src_path.suffix}'
629 string = ' %d %d %s %s' % (species_number, atomic_number, label,
630 pseudo_name)
631 chemical_labels.append(string)
632 if isinstance(spec['basis_set'], PAOBasisBlock):
633 pao_basis.append(spec['basis_set'].script(label))
634 else:
635 basis_sizes.append((" " + label, spec['basis_set']))
637 self.file_instructions = file_instructions
638 self.chemical_labels = chemical_labels
639 self.pao_basis = pao_basis
640 self.basis_sizes = basis_sizes
642 def generate_text(self):
643 yield var('NumberOfSpecies', len(self.species))
644 yield var('NumberOfAtoms', len(self.atoms))
646 yield var('ChemicalSpecieslabel', self.chemical_labels)
647 yield '\n'
648 yield var('PAO.Basis', self.pao_basis)
649 yield var('PAO.BasisSizes', self.basis_sizes)
650 yield '\n'
653@dataclass
654class FileInstruction:
655 src_path: Path
656 targetname: str
658 def copy_to(self, directory):
659 self._link(shutil.copy, directory)
661 def symlink_to(self, directory):
662 self._link(os.symlink, directory)
664 def _link(self, file_operation, directory):
665 dst_path = directory / self.targetname
666 if self.src_path == dst_path:
667 return
669 dst_path.unlink(missing_ok=True)
670 file_operation(self.src_path, dst_path)
673@dataclass
674class FDFWriter:
675 name: str
676 xc: str
677 fdf_user_args: dict
678 more_fdf_args: dict
679 mesh_cutoff: float
680 energy_shift: float
681 spin: str
682 species_numbers: object # ?
683 atomic_coord_format: str
684 kpts: object # ?
685 bandpath: object # ?
686 species_info: object
688 def write(self, fd):
689 for chunk in self.generate_text():
690 fd.write(chunk)
692 def generate_text(self):
693 yield var('SystemName', self.name)
694 yield var('SystemLabel', self.name)
695 yield "\n"
697 # Write explicitly given options first to
698 # allow the user to override anything.
699 fdf_arguments = self.fdf_user_args
700 for key in sorted(fdf_arguments):
701 yield var(key, fdf_arguments[key])
703 # Force siesta to return error on no convergence.
704 # as default consistent with ASE expectations.
705 if 'SCFMustConverge' not in fdf_arguments:
706 yield var('SCFMustConverge', True)
707 yield '\n'
709 yield var('Spin', self.spin)
710 # Spin backwards compatibility.
711 if self.spin == 'collinear':
712 key = 'SpinPolarized'
713 elif self.spin == 'non-collinear':
714 key = 'NonCollinearSpin'
715 else:
716 key = None
718 if key is not None:
719 yield var(key, (True, '# Backwards compatibility.'))
721 # Write functional.
722 functional, authors = self.xc
723 yield var('XC.functional', functional)
724 yield var('XC.authors', authors)
725 yield '\n'
727 # Write mesh cutoff and energy shift.
728 yield var('MeshCutoff', (self.mesh_cutoff, 'eV'))
729 yield var('PAO.EnergyShift', (self.energy_shift, 'eV'))
730 yield '\n'
732 yield from self.species_info.generate_text()
733 yield from self.generate_atoms_text(self.species_info.atoms)
735 for key, value in self.more_fdf_args.items():
736 yield var(key, value)
738 if self.kpts is not None:
739 kpts = np.array(self.kpts)
740 yield from SiestaInput.generate_kpts(kpts)
742 if self.bandpath is not None:
743 lines = bandpath2bandpoints(self.bandpath)
744 assert isinstance(lines, str) # rename this variable?
745 yield lines
746 yield '\n'
748 def generate_atoms_text(self, atoms: Atoms):
749 """Translate the Atoms object to fdf-format."""
751 cell = atoms.cell
752 yield '\n'
754 if cell.rank in [1, 2]:
755 raise ValueError('Expected 3D unit cell or no unit cell. You may '
756 'wish to add vacuum along some directions.')
758 if np.any(cell):
759 yield var('LatticeConstant', '1.0 Ang')
760 yield block('LatticeVectors', cell)
762 yield from generate_atomic_coordinates(
763 atoms, self.species_numbers, self.atomic_coord_format)
765 # Write magnetic moments.
766 magmoms = atoms.get_initial_magnetic_moments()
768 # The DM.InitSpin block must be written to initialize to
769 # no spin. SIESTA default is FM initialization, if the
770 # block is not written, but we must conform to the
771 # atoms object.
772 if len(magmoms) == 0:
773 yield '#Empty block forces ASE initialization.\n'
775 yield '%block DM.InitSpin\n'
776 if len(magmoms) != 0 and isinstance(magmoms[0], np.ndarray):
777 for n, M in enumerate(magmoms):
778 if M[0] != 0:
779 yield (' %d %.14f %.14f %.14f \n'
780 % (n + 1, M[0], M[1], M[2]))
781 elif len(magmoms) != 0 and isinstance(magmoms[0], float):
782 for n, M in enumerate(magmoms):
783 if M != 0:
784 yield ' %d %.14f \n' % (n + 1, M)
785 yield '%endblock DM.InitSpin\n'
786 yield '\n'
788 def link_pseudos_into_directory(self, *, symlink_pseudos=None, directory):
789 if symlink_pseudos is None:
790 symlink_pseudos = os.name != 'nt'
792 for instruction in self.species_info.file_instructions:
793 if symlink_pseudos:
794 instruction.symlink_to(directory)
795 else:
796 instruction.copy_to(directory)
799# Utilities for generating bits of strings.
800#
801# We are re-aliasing format_fdf and format_block in the anticipation
802# that they may change, or we might move this onto a Formatter object
803# which applies consistent spacings etc.
804def var(key, value):
805 return format_fdf(key, value)
808def block(name, data):
809 return format_block(name, data)