Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add modification to aims input to match atomate2 magnetic order script #3878

Merged
merged 41 commits into from
Sep 7, 2024
Merged
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
745f0f5
Add modification to aims input to match atomate2 magnetic order script
tpurcell90 Jun 12, 2024
786ce93
Merge branch 'master' into magnetism_aims
tpurcell90 Jun 12, 2024
7a7b8ca
Merge branch 'master' into magnetism_aims
shyuep Jun 14, 2024
43a1a19
Aims inputs spin corrections
tpurcell90 Jun 19, 2024
ebcedac
Add parsing for mulliken spins and charges
tpurcell90 Jun 19, 2024
cd00cfd
Merge branch 'magnetism_aims' of github.com:tpurcell90/pymatgen into …
tpurcell90 Jun 19, 2024
b268941
Merge branch 'master' into magnetism_aims
tpurcell90 Jun 19, 2024
d225522
Add tests for Mulliken charges/spins parsing
tpurcell90 Jun 19, 2024
e1ca9ce
Add test for spin inputs
tpurcell90 Jun 19, 2024
3851445
Merge branch 'magnetism_aims' of github.com:tpurcell90/pymatgen into …
tpurcell90 Jun 19, 2024
fbbe9a2
Update h2o_ref
tpurcell90 Jun 19, 2024
ae97cb6
Merge branch 'master' of https://github.com/materialsproject/pymatgen…
tpurcell90 Jul 1, 2024
8bcd02c
pre-commit auto-fixes
pre-commit-ci[bot] Jul 1, 2024
634b11c
Change mulliken_Spins/charges to be magmom and charges
tpurcell90 Jul 10, 2024
08fce80
update species defaults for magntetic analysis
tpurcell90 Jul 10, 2024
f750588
Merge branch 'magnetism_aims' of github.com:tpurcell90/pymatgen into …
tpurcell90 Jul 10, 2024
a1dea0a
Merge branch 'master' into magnetism_aims
tpurcell90 Jul 10, 2024
2f3d0ba
pre-commit auto-fixes
pre-commit-ci[bot] Jul 10, 2024
6e8c3c1
Correct velocity error and discussing with @ansobelev the final error
tpurcell90 Jul 10, 2024
89f5f0f
pre-commit auto-fixes
pre-commit-ci[bot] Jul 10, 2024
6760f3b
Fix lint errors
tpurcell90 Jul 10, 2024
81ccfcc
Fix linter errors in parsers
tpurcell90 Jul 10, 2024
d606e38
Correct error for heterogenous species definitions
tpurcell90 Jul 11, 2024
1da1e20
Some more modifications to work with species spin object
tpurcell90 Jul 17, 2024
20360c6
Add Input set generators for magnetic calculations
tpurcell90 Jul 17, 2024
b9d81e2
Merge branch 'master' into magnetism_aims
tpurcell90 Jul 19, 2024
c4b4c53
Merge branch 'master' into magnetism_aims
tpurcell90 Jul 30, 2024
305c08f
Merge branch 'master' into magnetism_aims
tpurcell90 Aug 6, 2024
92ff1ac
pre-commit auto-fixes
pre-commit-ci[bot] Aug 6, 2024
f3b88eb
Merge branch 'master' into magnetism_aims
tpurcell90 Aug 7, 2024
a30d843
Merge branch 'master' into magnetism_aims
tpurcell90 Aug 23, 2024
343782c
Merge branch 'master' of github.com:materialsproject/pymatgen into ma…
tpurcell90 Sep 6, 2024
e9f84a2
Merge branch 'magnetism_aims' of github.com:tpurcell90/pymatgen into …
tpurcell90 Sep 6, 2024
c78753e
Update src/pymatgen/io/aims/inputs.py
tpurcell90 Sep 6, 2024
3afa17b
Update src/pymatgen/io/aims/inputs.py
tpurcell90 Sep 6, 2024
01a66f2
Update src/pymatgen/io/aims/inputs.py
tpurcell90 Sep 6, 2024
2748141
Apply suggestions from code review
tpurcell90 Sep 6, 2024
977e19e
Suggested edits
tpurcell90 Sep 6, 2024
f94b3c5
Fix linter error by adding () around walrus operator
tpurcell90 Sep 6, 2024
e5575db
Fix Walrus operator for py 3.11 tests
tpurcell90 Sep 6, 2024
2bf2330
Merge branch 'master' into magnetism_aims
tpurcell90 Sep 6, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 106 additions & 26 deletions src/pymatgen/io/aims/inputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from dataclasses import dataclass, field
from pathlib import Path
from typing import TYPE_CHECKING
from warnings import warn

import numpy as np
from monty.io import zopen
Expand Down Expand Up @@ -146,19 +147,30 @@ def from_structure(cls, structure: Structure | Molecule) -> Self:
for lv in structure.lattice.matrix:
content_lines.append(f"lattice_vector {lv[0]: .12e} {lv[1]: .12e} {lv[2]: .12e}")

charges = structure.site_properties.get("charge", np.zeros(len(structure.species)))
magmoms = structure.site_properties.get("magmom", np.zeros(len(structure.species)))
charges = structure.site_properties.get("charge", np.zeros(structure.num_sites))
magmoms = structure.site_properties.get("magmom", [None] * structure.num_sites)
velocities = structure.site_properties.get("velocity", [None for _ in structure.species])

for species, coord, charge, magmom, v in zip(
structure.species, structure.cart_coords, charges, magmoms, velocities, strict=True
):
content_lines.append(f"atom {coord[0]: .12e} {coord[1]: .12e} {coord[2]: .12e} {species}")
if isinstance(species, Element):
spin = magmom
element = species
else:
spin = species.spin
element = species.element
if magmom is not None and magmom != spin:
raise ValueError("species.spin and magnetic moments don't agree. Please only define one")

content_lines.append(f"atom {coord[0]: .12e} {coord[1]: .12e} {coord[2]: .12e} {element}")
if charge != 0:
content_lines.append(f" initial_charge {charge:.12e}")
if magmom != 0:
content_lines.append(f" initial_moment {magmom:.12e}")
if (spin is not None) and (spin != 0):
content_lines.append(f" initial_moment {spin:.12e}")
if v is not None and any(v_i != 0.0 for v_i in v):
content_lines.append(f" velocity {' '.join([f'{v_i:.12e}' for v_i in v])}")

return cls(_content="\n".join(content_lines), _structure=structure)

@property
Expand Down Expand Up @@ -441,6 +453,9 @@ def __setitem__(self, key: str, value: Any) -> None:
value (Any): The value for that parameter
"""
if key == "output":
if value in self._parameters[key]:
return

if isinstance(value, str):
value = [value]
self._parameters[key] += value
Expand Down Expand Up @@ -514,6 +529,16 @@ def get_content(
if parameters["xc"] == "LDA":
parameters["xc"] = "pw-lda"

spins = np.array([0.0 if isinstance(sp, Element) else sp.spin for sp in structure.species])
tpurcell90 marked this conversation as resolved.
Show resolved Hide resolved
magmom = structure.site_properties.get("magmom", spins)
if (
parameters.get("spin", "") == "collinear"
and np.all(magmom == 0.0)
and ("default_initial_moment" not in parameters)
):
warn("Removing spin from parameters since no spin information is in the structure", RuntimeWarning)
parameters.pop("spin")

cubes = parameters.pop("cubes", None)

if verbose_header:
Expand Down Expand Up @@ -562,7 +587,18 @@ def get_content(
species_defaults = self._parameters.get("species_dir", "")
if not species_defaults:
raise KeyError("Species' defaults not specified in the parameters")
content += self.get_species_block(structure, species_defaults)

species_dir = None
if isinstance(species_defaults, str):
species_defaults = Path(species_defaults)
if species_defaults.is_absolute():
species_dir = species_defaults.parent
basis_set = species_defaults.name
else:
basis_set = str(species_defaults)
else:
basis_set = species_defaults
content += self.get_species_block(structure, basis_set, species_dir=species_dir)

return content

Expand Down Expand Up @@ -608,23 +644,26 @@ def write_file(

file.write(content)

def get_species_block(self, structure: Structure | Molecule, basis_set: str | dict[str, str]) -> str:
"""Get the basis set information for a structure.
def get_species_block(
self, structure: Structure | Molecule, basis_set: str | dict[str, str], species_dir: str | Path | None = None
) -> str:
"""Get the basis set information for a structure

Args:
structure (Molecule or Structure): The structure to get the basis set information for
basis_set (str | dict[str, str]):
a name of a basis set (`light`, `tight`...) or a mapping from site labels to basis set names.
The name of a basis set can either correspond to the subfolder in `defaults_2020` folder
or be a full path from the `FHI-aims/species_defaults` directory.
or be a full path from the `FHI-aims/species_defaults` directory.\
tpurcell90 marked this conversation as resolved.
Show resolved Hide resolved
species_dir (str | Path | None): The base species directory

Returns:
The block to add to the control.in file for the species

Raises:
ValueError: If a file for the species is not found
"""
species_defaults = SpeciesDefaults.from_structure(structure, basis_set)
species_defaults = SpeciesDefaults.from_structure(structure, basis_set, species_dir)
return str(species_defaults)

def as_dict(self) -> dict[str, Any]:
Expand Down Expand Up @@ -657,7 +696,7 @@ def __init__(self, data: str, label: str | None = None) -> None:
"""
Args:
data (str): A string of the complete species defaults file
label (str): A string representing the name of species.
label (str): A string representing the name of species
"""
self.data = data
self.label = label
Expand All @@ -666,6 +705,36 @@ def __init__(self, data: str, label: str | None = None) -> None:
if "species" in line:
self.label = line.split()[1]

def __eq__(self, sepcies_2: object) -> bool:
"""Returns if two speceies are equal"""
if not isinstance(sepcies_2, AimsSpeciesFile):
return NotImplemented
return self.data == sepcies_2.data

def __lt__(self, sepcies_2: object) -> bool:
"""Returns if two speceies are equal"""
if not isinstance(sepcies_2, AimsSpeciesFile):
return NotImplemented
return self.data < sepcies_2.data

def __le__(self, sepcies_2: object) -> bool:
"""Returns if two speceies are equal"""
if not isinstance(sepcies_2, AimsSpeciesFile):
return NotImplemented
return self.data <= sepcies_2.data

def __gt__(self, sepcies_2: object) -> bool:
"""Returns if two speceies are equal"""
if not isinstance(sepcies_2, AimsSpeciesFile):
return NotImplemented
return self.data > sepcies_2.data

def __ge__(self, sepcies_2: object) -> bool:
"""Returns if two speceies are equal"""
if not isinstance(sepcies_2, AimsSpeciesFile):
return NotImplemented
return self.data >= sepcies_2.data

@classmethod
def from_file(cls, filename: str, label: str | None = None) -> AimsSpeciesFile:
"""Initialize from file.
Expand All @@ -681,7 +750,9 @@ def from_file(cls, filename: str, label: str | None = None) -> AimsSpeciesFile:
return cls(file.read(), label)

@classmethod
def from_element_and_basis_name(cls, element: str, basis: str, *, label: str | None = None) -> AimsSpeciesFile:
def from_element_and_basis_name(
cls, element: str, basis: str, *, species_dir: str | Path | None = None, label: str | None = None
) -> AimsSpeciesFile:
"""Initialize from element and basis names.

Args:
Expand All @@ -703,7 +774,8 @@ def from_element_and_basis_name(cls, element: str, basis: str, *, label: str | N
else:
species_file_name = "00_Emptium_default"

aims_species_dir = SETTINGS.get("AIMS_SPECIES_DIR")
aims_species_dir = SETTINGS.get("AIMS_SPECIES_DIR") if species_dir is None else species_dir
tpurcell90 marked this conversation as resolved.
Show resolved Hide resolved

if aims_species_dir is None:
raise ValueError(
"No AIMS_SPECIES_DIR variable found in the config file. "
Expand All @@ -724,7 +796,7 @@ def from_element_and_basis_name(cls, element: str, basis: str, *, label: str | N
)

def __str__(self):
"""String representation of the species' defaults file."""
"""String representation of the species' defaults file"""
return re.sub(r"^ *species +\w+", f" species {self.label}", self.data, flags=re.MULTILINE)

@property
Expand All @@ -740,20 +812,21 @@ def as_dict(self) -> dict[str, Any]:

@classmethod
def from_dict(cls, dct: dict[str, Any]) -> AimsSpeciesFile:
"""Deserialization of the AimsSpeciesFile object."""
"""Deserialization of the AimsSpeciesFile object"""
return AimsSpeciesFile(data=dct["data"], label=dct["label"])


class SpeciesDefaults(list, MSONable):
"""A list containing a set of species' defaults objects with
methods to read and write them to files.
methods to read and write them to files
"""

def __init__(
self,
labels: Sequence[str],
basis_set: str | dict[str, str],
*,
species_dir: str | Path | None = None,
elements: dict[str, str] | None = None,
) -> None:
"""
Expand All @@ -763,55 +836,62 @@ def __init__(
a name of a basis set (`light`, `tight`...) or a mapping from site labels to basis set names.
The name of a basis set can either correspond to the subfolder in `defaults_2020` folder
or be a full path from the `FHI-aims/species_defaults` directory.
species_dir (str | Path | None): The base species directory
elements (dict[str, str] | None):
a mapping from site labels to elements. If some label is not in this mapping,
it coincides with an element.
"""
super().__init__()
self.labels = labels
self.basis_set = basis_set
self.species_dir = species_dir
if elements is None:
elements = {}
self.elements = {}
for label in self.labels:
if ",spin" in label:
label = label.split(",")[0]
tpurcell90 marked this conversation as resolved.
Show resolved Hide resolved
self.elements[label] = elements.get(label, label)
self._set_species()

def _set_species(self) -> None:
"""Initialize species defaults from the instance data."""
"""Initialize species defaults from the instance data"""
del self[:]

for label in self.labels:
el = self.elements[label]
for label, el in self.elements.items():
if isinstance(self.basis_set, dict):
basis_set = self.basis_set.get(label, None)
if basis_set is None:
raise ValueError(f"Basis set not found for specie {label} (represented by element {el})")
else:
basis_set = self.basis_set
self.append(AimsSpeciesFile.from_element_and_basis_name(el, basis_set, label=label))
self.append(
AimsSpeciesFile.from_element_and_basis_name(el, basis_set, species_dir=self.species_dir, label=label)
)

def __str__(self):
"""String representation of the species' defaults."""
"""String representation of the species' defaults"""
return "".join([str(x) for x in self])

@classmethod
def from_structure(cls, struct: Structure | Molecule, basis_set: str | dict[str, str]):
def from_structure(
cls, struct: Structure | Molecule, basis_set: str | dict[str, str], species_dir: str | Path | None = None
):
"""Initialize species defaults from a structure."""
labels = []
elements = {}
for label, el in sorted(zip(struct.labels, struct.species, strict=True)):
if not isinstance(el, Element):
tpurcell90 marked this conversation as resolved.
Show resolved Hide resolved
raise TypeError("FHI-aims does not support fractional compositions")
el = el.element
if (label is None) or (el is None):
raise ValueError("Something is terribly wrong with the structure")
if label not in labels:
labels.append(label)
elements[label] = el.name
return SpeciesDefaults(labels, basis_set, elements=elements)
return SpeciesDefaults(labels, basis_set, species_dir=species_dir, elements=elements)

def to_dict(self):
"""Dictionary representation of the species' defaults."""
"""Dictionary representation of the species' defaults"""
return {
"labels": self.labels,
"elements": self.elements,
Expand All @@ -822,5 +902,5 @@ def to_dict(self):

@classmethod
def from_dict(cls, dct: dict[str, Any]) -> SpeciesDefaults:
"""Deserialization of the SpeciesDefaults object."""
"""Deserialization of the SpeciesDefaults object"""
return SpeciesDefaults(dct["labels"], dct["basis_set"], elements=dct["elements"])
Loading