Coverage for /builds/ase/ase/ase/calculators/siesta/siesta.py: 81.55%
374 statements
« prev ^ index » next coverage.py v7.5.3, created at 2025-08-02 00:12 +0000
« prev ^ index » next coverage.py v7.5.3, created at 2025-08-02 00:12 +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:
9http://www.uam.es/departamentos/ciencias/fismateriac/siesta
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, Dict, List
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 - label : The basename of all files created during
164 calculation.
165 - mesh_cutoff : Energy in eV.
166 The mesh cutoff energy for determining number of
167 grid points in the matrix-element calculation.
168 - energy_shift : Energy in eV
169 The confining energy of the basis set generation.
170 - kpts : Tuple of 3 integers, the k-points in different
171 directions.
172 - xc : The exchange-correlation potential. Can be set to
173 any allowed value for either the Siesta
174 XC.funtional or XC.authors keyword. Default "LDA"
175 - basis_set : "SZ"|"SZP"|"DZ"|"DZP"|"TZP", strings which specify
176 the type of functions basis set.
177 - spin : "non-polarized"|"collinear"|
178 "non-collinear|spin-orbit".
179 The level of spin description to be used.
180 - species : None|list of Species objects. The species objects
181 can be used to to specify the basis set,
182 pseudopotential and whether the species is ghost.
183 The tag on the atoms object and the element is used
184 together to identify the species.
185 - pseudo_path : None|path. This path is where
186 pseudopotentials are taken from.
187 If None is given, then then the path given
188 in $SIESTA_PP_PATH will be used.
189 - pseudo_qualifier: None|string. This string will be added to the
190 pseudopotential path that will be retrieved.
191 For hydrogen with qualifier "abc" the
192 pseudopotential "H.abc.psf" will be retrieved.
193 - symlink_pseudos: None|bool
194 If true, symlink pseudopotentials
195 into the calculation directory, else copy them.
196 Defaults to true on Unix and false on Windows.
197 - atoms : The Atoms object.
198 - restart : str. Prefix for restart file.
199 May contain a directory.
200 Default is None, don't restart.
201 - fdf_arguments: Explicitly given fdf arguments. Dictonary using
202 Siesta keywords as given in the manual. List values
203 are written as fdf blocks with each element on a
204 separate line, while tuples will write each element
205 in a single line. ASE units are assumed in the
206 input.
207 - atomic_coord_format: "xyz"|"zmatrix", strings to switch between
208 the default way of entering the system's geometry
209 (via the block AtomicCoordinatesAndAtomicSpecies)
210 and a recent method via the block Zmatrix. The
211 block Zmatrix allows to specify basic geometry
212 constrains such as realized through the ASE classes
213 FixAtom, FixedLine and FixedPlane.
214 """
216 # Put in the default arguments.
217 parameters = self.default_parameters.__class__(**kwargs)
219 # Call the base class.
220 FileIOCalculator.__init__(
221 self,
222 command=command,
223 profile=profile,
224 directory=directory,
225 **parameters)
227 def __getitem__(self, key):
228 """Convenience method to retrieve a parameter as
229 calculator[key] rather than calculator.parameters[key]
231 Parameters:
232 -key : str, the name of the parameters to get.
233 """
234 return self.parameters[key]
236 def species(self, atoms):
237 """Find all relevant species depending on the atoms object and
238 species input.
240 Parameters :
241 - atoms : An Atoms object.
242 """
243 return SiestaInput.get_species(
244 atoms, list(self['species']), self['basis_set'])
246 @deprecated(
247 "The keyword 'UNPOLARIZED' has been deprecated,"
248 "and replaced by 'non-polarized'",
249 category=FutureWarning,
250 callback=_nonpolarized_alias,
251 )
252 def set(self, **kwargs):
253 """Set all parameters.
255 Parameters:
256 -kwargs : Dictionary containing the keywords defined in
257 SiestaParameters.
259 .. deprecated:: 3.18.2
260 The keyword 'UNPOLARIZED' has been deprecated and replaced by
261 'non-polarized'
262 """
264 # XXX Inserted these next few lines because set() would otherwise
265 # discard all previously set keywords to their defaults! --askhl
266 current = self.parameters.copy()
267 current.update(kwargs)
268 kwargs = current
270 # Find not allowed keys.
271 default_keys = list(self.__class__.default_parameters)
272 offending_keys = set(kwargs) - set(default_keys)
273 if len(offending_keys) > 0:
274 mess = "'set' does not take the keywords: %s "
275 raise ValueError(mess % list(offending_keys))
277 # Use the default parameters.
278 parameters = self.__class__.default_parameters.copy()
279 parameters.update(kwargs)
280 kwargs = parameters
282 # Check energy inputs.
283 for arg in ['mesh_cutoff', 'energy_shift']:
284 value = kwargs.get(arg)
285 if value is None:
286 continue
287 if not (isinstance(value, (float, int)) and value > 0):
288 mess = "'{}' must be a positive number(in eV), \
289 got '{}'".format(arg, value)
290 raise ValueError(mess)
292 # Check the functional input.
293 xc = kwargs.get('xc', 'LDA')
294 if isinstance(xc, (tuple, list)) and len(xc) == 2:
295 functional, authors = xc
296 if functional.lower() not in [k.lower() for k in self.allowed_xc]:
297 mess = f"Unrecognized functional keyword: '{functional}'"
298 raise ValueError(mess)
300 lsauthorslower = [a.lower() for a in self.allowed_xc[functional]]
301 if authors.lower() not in lsauthorslower:
302 mess = "Unrecognized authors keyword for %s: '%s'"
303 raise ValueError(mess % (functional, authors))
305 elif xc in self.allowed_xc:
306 functional = xc
307 authors = self.allowed_xc[xc][0]
308 else:
309 found = False
310 for key, value in self.allowed_xc.items():
311 if xc in value:
312 found = True
313 functional = key
314 authors = xc
315 break
317 if not found:
318 raise ValueError(f"Unrecognized 'xc' keyword: '{xc}'")
319 kwargs['xc'] = (functional, authors)
321 # Check fdf_arguments.
322 if kwargs['fdf_arguments'] is None:
323 kwargs['fdf_arguments'] = {}
325 if not isinstance(kwargs['fdf_arguments'], dict):
326 raise TypeError("fdf_arguments must be a dictionary.")
328 # Call baseclass.
329 FileIOCalculator.set(self, **kwargs)
331 def set_fdf_arguments(self, fdf_arguments):
332 """ Set the fdf_arguments after the initialization of the
333 calculator.
334 """
335 self.validate_fdf_arguments(fdf_arguments)
336 FileIOCalculator.set(self, fdf_arguments=fdf_arguments)
338 def validate_fdf_arguments(self, fdf_arguments):
339 """ Raises error if the fdf_argument input is not a
340 dictionary of allowed keys.
341 """
342 # None is valid
343 if fdf_arguments is None:
344 return
346 # Type checking.
347 if not isinstance(fdf_arguments, dict):
348 raise TypeError("fdf_arguments must be a dictionary.")
350 def write_input(self, atoms, properties=None, system_changes=None):
351 """Write input (fdf)-file.
352 See calculator.py for further details.
354 Parameters:
355 - atoms : The Atoms object to write.
356 - properties : The properties which should be calculated.
357 - system_changes : List of properties changed since last run.
358 """
360 super().write_input(
361 atoms=atoms,
362 properties=properties,
363 system_changes=system_changes)
365 filename = self.getpath(ext='fdf')
367 more_fdf_args = {}
369 # Use the saved density matrix if only 'cell' and 'positions'
370 # have changed.
371 if (system_changes is None or
372 ('numbers' not in system_changes and
373 'initial_magmoms' not in system_changes and
374 'initial_charges' not in system_changes)):
376 more_fdf_args['DM.UseSaveDM'] = True
378 if 'density' in properties:
379 more_fdf_args['SaveRho'] = True
381 species, species_numbers = self.species(atoms)
383 pseudo_path = (self['pseudo_path']
384 or self.profile.configvars.get('pseudo_path')
385 or self.cfg.get('SIESTA_PP_PATH'))
387 if not pseudo_path:
388 raise Exception(
389 'Please configure pseudo_path or SIESTA_PP_PATH envvar')
391 species_info = SpeciesInfo(
392 atoms=atoms,
393 pseudo_path=Path(pseudo_path),
394 pseudo_qualifier=self.pseudo_qualifier(),
395 species=species)
397 writer = FDFWriter(
398 name=self.prefix,
399 xc=self['xc'],
400 spin=self['spin'],
401 mesh_cutoff=self['mesh_cutoff'],
402 energy_shift=self['energy_shift'],
403 fdf_user_args=self['fdf_arguments'],
404 more_fdf_args=more_fdf_args,
405 species_numbers=species_numbers,
406 atomic_coord_format=self['atomic_coord_format'].lower(),
407 kpts=self['kpts'],
408 bandpath=self['bandpath'],
409 species_info=species_info,
410 )
412 with open(filename, 'w') as fd:
413 writer.write(fd)
415 writer.link_pseudos_into_directory(
416 symlink_pseudos=self['symlink_pseudos'],
417 directory=Path(self.directory))
419 def read(self, filename):
420 """Read structural parameters from file .XV file
421 Read other results from other files
422 filename : siesta.XV
423 """
425 fname = self.getpath(filename)
426 if not fname.exists():
427 raise ReadError(f"The restart file '{fname}' does not exist")
428 with fname.open() as fd:
429 self.atoms = read_siesta_xv(fd)
430 self.read_results()
432 def getpath(self, fname=None, ext=None):
433 """ Returns the directory/fname string """
434 if fname is None:
435 fname = self.prefix
436 if ext is not None:
437 fname = f'{fname}.{ext}'
438 return Path(self.directory) / fname
440 def pseudo_qualifier(self):
441 """Get the extra string used in the middle of the pseudopotential.
442 The retrieved pseudopotential for a specific element will be
443 'H.xxx.psf' for the element 'H' with qualifier 'xxx'. If qualifier
444 is set to None then the qualifier is set to functional name.
445 """
446 if self['pseudo_qualifier'] is None:
447 return self['xc'][0].lower()
448 else:
449 return self['pseudo_qualifier']
451 def read_results(self):
452 """Read the results."""
453 from ase.io.siesta_output import OutputReader
454 reader = OutputReader(prefix=self.prefix,
455 directory=Path(self.directory),
456 bandpath=self['bandpath'])
457 results = reader.read_results()
458 self.results.update(results)
460 self.results['ion'] = self.read_ion(self.atoms)
462 def read_ion(self, atoms):
463 """
464 Read the ion.xml file of each specie
465 """
466 species, _species_numbers = self.species(atoms)
468 ion_results = {}
469 for species_number, spec in enumerate(species, start=1):
470 symbol = spec['symbol']
471 atomic_number = atomic_numbers[symbol]
473 if spec['pseudopotential'] is None:
474 if self.pseudo_qualifier() == '':
475 label = symbol
476 else:
477 label = f"{symbol}.{self.pseudo_qualifier()}"
478 pseudopotential = self.getpath(label, 'psf')
479 else:
480 pseudopotential = Path(spec['pseudopotential'])
481 label = pseudopotential.stem
483 name = f"{label}.{species_number}"
484 if spec['ghost']:
485 name = f"{name}.ghost"
486 atomic_number = -atomic_number
488 if name not in ion_results:
489 fname = self.getpath(name, 'ion.xml')
490 if fname.is_file():
491 ion_results[name] = get_ion(str(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'])
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 pseudo_name = ''
629 if src_path.suffix != '.psf':
630 pseudo_name = f'{label}{src_path.suffix}'
631 string = ' %d %d %s %s' % (species_number, atomic_number, label,
632 pseudo_name)
633 chemical_labels.append(string)
634 if isinstance(spec['basis_set'], PAOBasisBlock):
635 pao_basis.append(spec['basis_set'].script(label))
636 else:
637 basis_sizes.append((" " + label, spec['basis_set']))
639 self.file_instructions = file_instructions
640 self.chemical_labels = chemical_labels
641 self.pao_basis = pao_basis
642 self.basis_sizes = basis_sizes
644 def generate_text(self):
645 yield var('NumberOfSpecies', len(self.species))
646 yield var('NumberOfAtoms', len(self.atoms))
648 yield var('ChemicalSpecieslabel', self.chemical_labels)
649 yield '\n'
650 yield var('PAO.Basis', self.pao_basis)
651 yield var('PAO.BasisSizes', self.basis_sizes)
652 yield '\n'
655@dataclass
656class FileInstruction:
657 src_path: Path
658 targetname: str
660 def copy_to(self, directory):
661 self._link(shutil.copy, directory)
663 def symlink_to(self, directory):
664 self._link(os.symlink, directory)
666 def _link(self, file_operation, directory):
667 dst_path = directory / self.targetname
668 if self.src_path == dst_path:
669 return
671 dst_path.unlink(missing_ok=True)
672 file_operation(self.src_path, dst_path)
675@dataclass
676class FDFWriter:
677 name: str
678 xc: str
679 fdf_user_args: dict
680 more_fdf_args: dict
681 mesh_cutoff: float
682 energy_shift: float
683 spin: str
684 species_numbers: object # ?
685 atomic_coord_format: str
686 kpts: object # ?
687 bandpath: object # ?
688 species_info: object
690 def write(self, fd):
691 for chunk in self.generate_text():
692 fd.write(chunk)
694 def generate_text(self):
695 yield var('SystemName', self.name)
696 yield var('SystemLabel', self.name)
697 yield "\n"
699 # Write explicitly given options first to
700 # allow the user to override anything.
701 fdf_arguments = self.fdf_user_args
702 for key in sorted(fdf_arguments):
703 yield var(key, fdf_arguments[key])
705 # Force siesta to return error on no convergence.
706 # as default consistent with ASE expectations.
707 if 'SCFMustConverge' not in fdf_arguments:
708 yield var('SCFMustConverge', True)
709 yield '\n'
711 yield var('Spin', self.spin)
712 # Spin backwards compatibility.
713 if self.spin == 'collinear':
714 key = 'SpinPolarized'
715 elif self.spin == 'non-collinear':
716 key = 'NonCollinearSpin'
717 else:
718 key = None
720 if key is not None:
721 yield var(key, (True, '# Backwards compatibility.'))
723 # Write functional.
724 functional, authors = self.xc
725 yield var('XC.functional', functional)
726 yield var('XC.authors', authors)
727 yield '\n'
729 # Write mesh cutoff and energy shift.
730 yield var('MeshCutoff', (self.mesh_cutoff, 'eV'))
731 yield var('PAO.EnergyShift', (self.energy_shift, 'eV'))
732 yield '\n'
734 yield from self.species_info.generate_text()
735 yield from self.generate_atoms_text(self.species_info.atoms)
737 for key, value in self.more_fdf_args.items():
738 yield var(key, value)
740 if self.kpts is not None:
741 kpts = np.array(self.kpts)
742 yield from SiestaInput.generate_kpts(kpts)
744 if self.bandpath is not None:
745 lines = bandpath2bandpoints(self.bandpath)
746 assert isinstance(lines, str) # rename this variable?
747 yield lines
748 yield '\n'
750 def generate_atoms_text(self, atoms: Atoms):
751 """Translate the Atoms object to fdf-format."""
753 cell = atoms.cell
754 yield '\n'
756 if cell.rank in [1, 2]:
757 raise ValueError('Expected 3D unit cell or no unit cell. You may '
758 'wish to add vacuum along some directions.')
760 if np.any(cell):
761 yield var('LatticeConstant', '1.0 Ang')
762 yield block('LatticeVectors', cell)
764 yield from generate_atomic_coordinates(
765 atoms, self.species_numbers, self.atomic_coord_format)
767 # Write magnetic moments.
768 magmoms = atoms.get_initial_magnetic_moments()
770 # The DM.InitSpin block must be written to initialize to
771 # no spin. SIESTA default is FM initialization, if the
772 # block is not written, but we must conform to the
773 # atoms object.
774 if len(magmoms) == 0:
775 yield '#Empty block forces ASE initialization.\n'
777 yield '%block DM.InitSpin\n'
778 if len(magmoms) != 0 and isinstance(magmoms[0], np.ndarray):
779 for n, M in enumerate(magmoms):
780 if M[0] != 0:
781 yield (' %d %.14f %.14f %.14f \n'
782 % (n + 1, M[0], M[1], M[2]))
783 elif len(magmoms) != 0 and isinstance(magmoms[0], float):
784 for n, M in enumerate(magmoms):
785 if M != 0:
786 yield ' %d %.14f \n' % (n + 1, M)
787 yield '%endblock DM.InitSpin\n'
788 yield '\n'
790 def link_pseudos_into_directory(self, *, symlink_pseudos=None, directory):
791 if symlink_pseudos is None:
792 symlink_pseudos = os.name != 'nt'
794 for instruction in self.species_info.file_instructions:
795 if symlink_pseudos:
796 instruction.symlink_to(directory)
797 else:
798 instruction.copy_to(directory)
801# Utilities for generating bits of strings.
802#
803# We are re-aliasing format_fdf and format_block in the anticipation
804# that they may change, or we might move this onto a Formatter object
805# which applies consistent spacings etc.
806def var(key, value):
807 return format_fdf(key, value)
810def block(name, data):
811 return format_block(name, data)