"""Functions for reading tables."""
from __future__ import annotations
import pathlib
from textwrap import dedent
from typing import TYPE_CHECKING
import mammos_entity as me
import mammos_units as u
import matplotlib.pyplot as plt
import numpy as np
import pandas
import pandas as pd
from pydantic import ConfigDict
from pydantic.dataclasses import dataclass
from rich import print
if TYPE_CHECKING:
import matplotlib
DATA_DIR = pathlib.Path(__file__).parent / "data"
def _check_short_label(short_label: str) -> str | int:
"""Check that short label follows the standards and returns material parameters.
Args:
short_label: Short label containing chemical formula and space group
number separated by a hyphen.
Returns:
Chemical formula and space group number.
Raises:
ValueError: Wrong format.
"""
short_label_list = short_label.split("-")
if len(short_label_list) != 2:
raise ValueError(
dedent(
"""
Wrong format for `short_label`.
Please use the format <chemical_formula>-<space_group_number>.
"""
)
)
chemical_formula = short_label_list[0]
space_group_number = int(short_label_list[1])
return chemical_formula, space_group_number
[docs]
def get_spontaneous_magnetization(
chemical_formula: str | None = None,
space_group_name: str | None = None,
space_group_number: int | None = None,
cell_length_a: float | None = None,
cell_length_b: float | None = None,
cell_length_c: float | None = None,
cell_angle_alpha: float | None = None,
cell_angle_beta: float | None = None,
cell_angle_gamma: float | None = None,
cell_volume: float | None = None,
ICSD_label: str | None = None,
OQMD_label: str | None = None,
jfile=None,
momfile=None,
posfile=None,
print_info: bool = False,
) -> MagnetizationData:
"""Get spontaneous magnetization interpolator from a database.
This function retrieves the temperature-dependent spontaneous magnetization
from a local database of spin dynamics calculations.
Data is retrieved by querying material information or UppASD input files.
Args:
chemical_formula: Chemical formula
space_group_name: Space group name
space_group_number: Space group number
cell_length_a: Cell length x
cell_length_b: Cell length y
cell_length_c: Cell length z
cell_angle_alpha: Cell angle alpha
cell_angle_beta: Cell angle beta
cell_angle_gamma: Cell angle gamma
cell_volume: Cell volume
ICSD_label: Label in the NIST Inorganic Crystal Structure Database.
OQMD_label: Label in the the Open Quantum Materials Database.
jfile: TODO
momfile: TODO
posfile: TODO
print_info: Whether to print information about the retrieved material.
Returns:
Interpolator function based on available data.
Examples:
>>> import mammos_spindynamics.db
>>> mammos_spindynamics.db.get_spontaneous_magnetization("Nd2Fe14B")
MagnetizationData(T=..., Ms=...)
"""
if posfile is not None:
table = _load_uppasd_simulation(
jfile=jfile, momfile=momfile, posfile=posfile, print_info=print_info
)
else:
table = _load_ab_initio_data(
print_info=print_info,
chemical_formula=chemical_formula,
space_group_name=space_group_name,
space_group_number=space_group_number,
cell_length_a=cell_length_a,
cell_length_b=cell_length_b,
cell_length_c=cell_length_c,
cell_angle_alpha=cell_angle_alpha,
cell_angle_beta=cell_angle_beta,
cell_angle_gamma=cell_angle_gamma,
cell_volume=cell_volume,
ICSD_label=ICSD_label,
OQMD_label=OQMD_label,
)
return MagnetizationData(
me.Entity("ThermodynamicTemperature", value=table["T[K]"], unit=u.K),
me.Ms(table["M[A/m]"], unit=u.A / u.m),
)
[docs]
@dataclass(config=ConfigDict(arbitrary_types_allowed=True, frozen=True))
class MagnetizationData:
"""Magnetization data.
Contains temperature and spontaneous magnetization data.
"""
T: me.Entity
"""Array of temperatures."""
Ms: me.Entity
"""Array of spontaneous magnetizations for the different temperatures."""
@property
def dataframe(self):
"""Dataframe containing temperature and spontaneous magnetization data."""
return pd.DataFrame(
{
"T": self.T.value,
"Ms": self.Ms.value,
}
)
[docs]
def plot(
self, ax: matplotlib.axes.Axes | None = None, **kwargs
) -> matplotlib.axes.Axes:
"""Plot the spontaneous magnetization data-points."""
if not ax:
_, ax = plt.subplots()
kwargs.setdefault("marker", "x")
ax.plot(self.T.value, self.Ms.value, linestyle="", **kwargs)
ax.set_xlabel(self.T.axis_label)
ax.set_ylabel(self.Ms.axis_label)
if "label" in kwargs:
ax.legend()
return ax
def _load_uppasd_simulation(
jfile: str | pathlib.Path,
momfile: str | pathlib.Path,
posfile: str | pathlib.Path,
print_info: bool = False,
) -> pandas.DataFrame:
"""Find UppASD simulation results with given input files in database.
Args:
jfile: Location of `jfile`
momfile: Location of `momfile`.
posfile: Location of `posfile`.
print_info: Whether to print information about the retrieved material.
Returns:
Table of pre-calculated Temperature-dependent Magnetization values.
Raises:
LookupError: Simulation not found in database.
"""
j = _parse_jfile(jfile)
mom = _parse_momfile(momfile)
pos = _parse_posfile(posfile)
for ii in DATA_DIR.iterdir():
if ii.is_dir() and _check_input_files(ii, j, mom, pos):
table = pd.read_csv(ii / "M.csv", header=[0, 1])
if print_info:
print("Found material in database.")
print(_describe_material(material_label=ii.name))
return table
raise LookupError("Requested simulation not found in database.")
def _parse_jfile(jfile: str | pathlib.Path) -> pandas.DataFrame:
"""Parse jfile, input for UppASD.
See https://uppasd.github.io/UppASD-manual/input/#exchange
for the correct formatting.
Args:
jfile: Location of `jfile`.
Returns:
Dataframe of exchange interactions.
Raises:
SyntaxError: Wrong formatting.
"""
with open(jfile) as ff:
jlines = ff.readlines()
try:
df = pd.DataFrame(
[
[int(x) for x in li[:-1]] + [float(x) for x in li[-1:]]
for li in [line.split() for line in jlines]
],
columns=[
"atom_i",
"atom_j",
"interaction_x",
"interaction_y",
"interaction_z",
"exchange_energy[mRy]",
],
).sort_values(
by=["atom_i", "atom_j", "interaction_x", "interaction_y", "interaction_z"],
)
return df
except ValueError:
raise SyntaxError(
dedent(
"""
Unable to parse jfile.
Please check syntax according to
https://uppasd.github.io/UppASD-manual/input/#exchange
"""
)
) from None
def _parse_momfile(momfile: str | pathlib.Path) -> dict:
"""Parse momfile, input for UppASD.
See https://uppasd.github.io/UppASD-manual/input/#momfile
for the correct formatting.
Args:
momfile: Location of `momfile`.
Returns:
Dictionary of magnetic moment information.
Raises:
SyntaxError: Wrong formatting.
"""
with open(momfile) as ff:
momlines = ff.readlines()
try:
mom = {
int(line[0]): {
"chemical_type": int(line[1]),
"magnetic_moment_magnitude[muB]": float(line[2]),
"magnetic_moment_direction": np.array([float(x) for x in line[3:]]),
}
for line in [ll.split() for ll in momlines]
}
return mom
except ValueError:
raise SyntaxError(
dedent(
"""
Unable to parse momfile.
Please check syntax according to
https://uppasd.github.io/UppASD-manual/input/#momfile
"""
)
) from None
def _parse_posfile(posfile: str | pathlib.Path) -> dict:
"""Parse posfile, input for UppASD.
See https://uppasd.github.io/UppASD-manual/input/#posfile
for the correct formatting.
Args:
posfile: Location of `posfile`.
Returns:
Dictionary of atoms position information.
Raises:
SyntaxError: Wrong formatting.
"""
with open(posfile) as ff:
poslines = ff.readlines()
try:
pos = {
int(line[0]): {
"atom_type": int(line[1]),
"atom_position": np.array([float(x) for x in line[2:]]),
}
for line in [ll.split() for ll in poslines]
}
return pos
except ValueError:
raise SyntaxError(
dedent(
"""
Unable to parse posfile.
Please check syntax according to
https://uppasd.github.io/UppASD-manual/input/#posfile
"""
)
) from None
def _check_input_files(
dir_i: pathlib.Path, j: pandas.DataFrame, mom: dict, pos: dict
) -> bool:
"""Check if UppASD inputs are equivalent to the ones in directory `dir_i`.
The extracted input information `j`, `mom`, and `pos` are compared with the
extracted information from the files in directory `dir_i`.
If the inputs are close enough, this function returns `True`.
Args:
dir_i: Considered directory in the database
j: Dataframe of exchange interactions.
mom: Dictionary of magnetic moment information.
pos: Dictionary of atoms position information.
Returns:
bool: True` if the inputs match almost exactly. `False` otherwise.
"""
if not (
(dir_i / "jfile").is_file()
or (dir_i / "momfile").is_file()
or (dir_i / "posfile").is_file()
):
return False
if (dir_i / "jfile").is_file():
j_i = _parse_jfile(dir_i / "jfile")
if not j_i.drop("exchange_energy[mRy]", axis=1).equals(
j.drop("exchange_energy[mRy]", axis=1)
):
return False
if not np.allclose(
j_i["exchange_energy[mRy]"].to_numpy(),
j["exchange_energy[mRy]"].to_numpy(),
):
return False
if (dir_i / "momfile").is_file():
mom_i = _parse_momfile(dir_i / "momfile")
if len(mom_i) != len(mom):
return False
for index, site in mom_i.items():
if (
site["chemical_type"] != mom[index]["chemical_type"]
or not np.allclose(
site["magnetic_moment_magnitude[muB]"],
mom[index]["magnetic_moment_magnitude[muB]"],
)
or not np.allclose(
site["magnetic_moment_direction"],
mom[index]["magnetic_moment_direction"],
)
):
return False
if (dir_i / "posfile").is_file():
pos_i = _parse_posfile(dir_i / "posfile")
if len(pos_i) != len(pos):
return False
for index, atom in pos_i.items():
if atom["atom_type"] != pos[index]["atom_type"] or not np.allclose(
atom["atom_position"],
pos[index]["atom_position"],
):
return False
return True
def _load_ab_initio_data(print_info: bool = False, **kwargs) -> pandas.DataFrame:
"""Load material with given structure information.
Args:
print_info: Print info
**kwargs: Selection arguments
Returns:
Table of pre-calculated Temperature-dependent Magnetization values.
Raises:
LookupError: Requested material not found in database.
LookupError: Too many results found with this formula.
"""
df = find_materials(**kwargs)
num_results = len(df)
if num_results == 0:
raise LookupError("Requested material not found in database.")
elif num_results > 1: # list all possible choice
error_string = (
"Too many results. Please refine your search.\n"
+ "Avilable materials based on request:\n"
)
for _row, material in df.iterrows():
error_string += _describe_material(material)
raise LookupError(error_string)
material = df.iloc[0]
if print_info:
print("Found material in database.")
print(_describe_material(material))
return pd.read_csv(DATA_DIR / material.label / "M.csv")
[docs]
def find_materials(**kwargs) -> pandas.DataFrame:
"""Find materials in database.
This function retrieves one or known materials from the database
`db.csv` by filtering for any requirements given in `kwargs`.
Args:
kwargs: Selection arguments
Returns:
Dataframe containing materials with requested qualities. Possibly empty.
"""
df = pd.read_csv(
DATA_DIR / "db.csv",
converters={
"chemical_formula": str,
"space_group_name": str,
"space_group_number": int,
"cell_length_a": u.Quantity,
"cell_length_b": u.Quantity,
"cell_length_c": u.Quantity,
"cell_angle_alpha": u.Quantity,
"cell_angle_beta": u.Quantity,
"cell_angle_gamma": u.Quantity,
"cell_volume": u.Quantity,
"ICSD_label": str,
"OQMD_label": str,
"label": str,
},
comment="#",
)
for key, value in kwargs.items():
if value is not None:
if isinstance(value, u.Quantity):
df = df[df[key] == value.to(df[key].unit)]
else:
df = df[df[key] == value]
return df
def _describe_material(
material: pandas.DataFrame | None = None, material_label: str | None = None
) -> str:
"""Describe material in a complete way.
This function returns a string listing the properties of the given material
or the given material label.
Args:
material: Material dataframe containing structure information.
material_label: Label of material in local database.
Returns:
Description of the material.
Raises:
ValueError: If material and material label are both None.
"""
if material is None and material_label is None:
raise ValueError("Material and material label cannot be both empty.")
if material_label is not None:
df = find_materials()
material = df[df["label"] == material_label].iloc[0]
return dedent(
f"""
Chemical Formula: {material.chemical_formula}
Space group name: {material.space_group_name}
Space group number: {material.space_group_number}
Cell length a: {material.cell_length_a}
Cell length b: {material.cell_length_b}
Cell length c: {material.cell_length_c}
Cell angle alpha: {material.cell_angle_alpha}
Cell angle beta: {material.cell_angle_beta}
Cell angle gamma: {material.cell_angle_gamma}
Cell volume: {material.cell_volume}
ICSD_label: {material.ICSD_label}
OQMD_label: {material.OQMD_label}
"""
)