Source code for mammos_spindynamics.uppasd._data

"""UppASD Data module.

This module finds, parses and operates on data generated from the UppASD software.

The :py:func:`~mammos_spindynamics.uppasd.read` function can be used to parse any of
three possible directories:

- :py:class:`~mammos_spindynamics.uppasd.MammosUppasdData` is the base directory where
  all sweeps and runs are found.
- :py:class:`~mammos_spindynamics.uppasd.TemperatureSweepData` is the output directory
  of a temperature sweep.
- :py:class:`~mammos_spindynamics.uppasd.RunData` is the output directory of a single
  run.

The :py:func:`~mammos_spindynamics.uppasd.read` function loads data lazily, only parsing
the type of the output directory initially.
"""

from __future__ import annotations

import re
from io import StringIO
from pathlib import Path
from typing import TYPE_CHECKING

import mammos_entity as me
import mammos_units as u
import numpy as np
import pandas as pd
import yaml

if TYPE_CHECKING:
    import pathlib

    import mammos_entity
    import numpy
    import pandas


[docs] def read( out: pathlib.Path | str, ) -> MammosUppasdData | RunData | TemperatureSweepData: """Read UppASD calculations results directory. This function returns different classes based on the content of the `mammos_spindynamics.yaml` generated from the execution of UppASD via the `mammos_spindynamics` Python library. Args: out: Output directory to read. Returns: Relevant Data object based on the content of the given directory. """ out = Path(out) if not out.is_dir(): raise RuntimeError(f"Output directory {out} does not exist.") if (info_yaml := out / "mammos_spindynamics.yaml").is_file(): with open(info_yaml) as f: info = yaml.safe_load(f) if info["metadata"]["mode"] == "mammos_uppasd_data": return MammosUppasdData(out) elif info["metadata"]["mode"] == "run": return RunData(out) elif info["metadata"]["mode"] == "temperature_sweep": return TemperatureSweepData(out) else: mode = info["metadata"]["mode"] raise RuntimeError( f"Unable to understand mode: '{mode}' in mammos_spindynamics.yaml. " ) else: raise RuntimeError(f"mammos_spindynamics.yaml file not found in path: {out}.")
[docs] class MammosUppasdData: """Collection of UppASD Result instances. This class collects all temperature sweeps and runs executed in the output directory specified by the `path` attribute. Attributes: out: Output directory containing temperature sweeps and runs. """
[docs] def __init__(self, out: pathlib.Path | str): """Initialize MammosUppasdData given the directory containing all runs. Args: out: Output directory containing temperature sweeps and runs. Examples: >>> from mammos_spindynamics.uppasd import MammosUppasdData >>> MammosUppasdData("Fe3Y") MammosUppasdData('Fe3Y') """ self.out = Path(out)
def __repr__(self): """Define repr.""" return f"MammosUppasdData('{self.out}')" def __getitem__(self, idx): """Extract i-th run.""" runs = [ run_dir for run_dir in self.out.iterdir() if re.match(r"\d+-(run|temperature_sweep)$", run_dir.name) ] runs.sort(key=lambda x: int(x.name.split("-")[0])) return read(runs[idx])
[docs] def get(self, **kwargs) -> RunData | TemperatureSweepData: """Select run based on different filters specified as keyword arguments. Exceptions are raised if the filters give no match or too many matches. Args: **kwargs: Keys are the names to filter on, values must match exactly. Returns: :py:class:`~mammos_spindynamics.uppasd.RunData` or :py:class:`~mammos_spindynamics.uppasd.TemperatureSweepData` satisfying given filters. Raises: LookupError: No run has been found satisfying all filters. LookupError: Too many results have been found. """ df = self.info() for key, value in kwargs.items(): df = df[df[key] == value] if len(df) == 0: raise LookupError( f"Requested run not found.\nSelection: {kwargs}\n" f"Available data: {self.info()}" ) if len(df) > 1: error_string = ( "Too many results. Please refine your search.\n" + "Avilable materials based on request:\n" ) error_string += str(df) raise LookupError(error_string) return read(self.out / df.iloc[0]["name"])
[docs] def info( self, include_description: bool = True, include_time_elapsed: bool = True, include_parameters: bool = True, ) -> pandas.DataFrame: """Return a Dataframe containing information about all available UppASD runs. Args: include_description: Whether to include the description in the dataframe columns. include_time_elapsed: Whether to include elapsed time in the dataframe columns. include_parameters: Whether to include the run parameters in the dataframe columns. Returns: Dataframe with relevant information. """ metadata_keys = [] if include_description: metadata_keys += ["description"] if include_time_elapsed: metadata_keys += ["time_elapsed"] all_runs = [] index = [] for run in self: if run: info_ = {"name": run.out.name} index.append(int(run.out.name.split("-")[0])) if metadata_keys: metadata_ = {k: run.metadata.get(k) for k in metadata_keys} info_ = {**info_, **metadata_} if include_parameters: info_ = {**info_, **run.parameters} all_runs.append(info_) return pd.DataFrame(all_runs, index=index)
[docs] class RunData: """UppASD Data parser class for a single run. Attributes: out: Output directory containing the run output. metadata: Dictionary of the run metadata. parameters: Dictionary of the run parameters. """
[docs] def __init__(self, out: pathlib.Path): """Initialize RunData given the directory path of a single run. Args: out: Output directory containing the run output. """ self.out = Path(out) with open(self.out / "mammos_spindynamics.yaml") as f: info = yaml.safe_load(f) self._input_dictionary = _parse_inpsd_file(self.inpsd) self.metadata = info["metadata"] self.parameters = info["parameters"] df = pd.read_csv(self.cumulants, sep=r"\s+") self._cumulant_data = df.iloc[-1]
def __repr__(self): """Define repr.""" return f"RunData('{self.out}')"
[docs] def info( self, include_description: bool = True, include_time_elapsed: bool = True, include_parameters: bool = True, ) -> pandas.DataFrame: """Return a Dataframe containing information about the UppASD run. Args: include_description: Whether to include the description in the dataframe columns. include_time_elapsed: Whether to include elapsed time in the dataframe columns. include_parameters: Whether to include the run parameters in the dataframe columns. Returns: Dataframe with relevant information. """ out = {"name": [self.out.name]} if include_description: out["description"] = [self.metadata["description"]] if include_time_elapsed: out["time_elapsed"] = [self.metadata["time_elapsed"]] if include_parameters: parameters_dictionary = {k: [v] for k, v in self.parameters.items()} out = {**out, **parameters_dictionary} return pd.DataFrame(out)
@property def T(self) -> mammos_entity.Entity: """Get temperature of the run. Returns: Entity Thermodynamic Temperature in Kelvin. """ return me.T(self._input_dictionary["temp"]) @property def inpsd(self) -> pathlib.Path: """Get path of ``inpsd.dat`` input file. Returns: Path of input file defining simulation parameters. """ return Path(self.out / "inpsd.dat") @property def exchange(self) -> pathlib.Path: """Get path of file containing exchange interactions. Returns: Path of file defining exchange interactions. """ return self.out / self._input_dictionary["exchange"] @property def momfile(self) -> pathlib.Path: """Get path of file containing magnetic moments. Returns: Path of file defining magnetic moments. """ return self.out / self._input_dictionary["momfile"] @property def posfile(self) -> pathlib.Path: """Get path of file containing atomic positions. Returns: Path of file defining atomic positions. """ return self.out / self._input_dictionary["posfile"] @property def cumulants(self) -> pathlib.Path: """Get ``cumulants.*.out`` file. Returns: Path of file containing magnetic information. Raises: RuntimeError: Could not find the cumulants file. RuntimeError: More than one cumulants file were found. """ cumulants = list(self.out.glob("cumulants.*.out")) if not cumulants: raise RuntimeError(f"Could not find a cumulants.<simid>.out in {self.out}") if len(cumulants) > 1: # we assume that this will never happen raise RuntimeError( f"Found multiple candidates for cumulants.<simid>.out in {self.out}:\n" f"{cumulants}" ) return cumulants[0] @property def restartfile(self) -> pathlib.Path: """Get path of restart file. Returns: Path of restart file for consecutive runs. Raises: RuntimeError: Could not find the restart file. RuntimeError: More than one restart file were found. """ restart_files = list(self.out.glob("restart.*.out")) if not restart_files: raise RuntimeError(f"Could not find restart.<simid>.out in {self.out}") if len(restart_files) > 1: # we assume that this will never happen raise RuntimeError( f"Found multiple candidates for restart.<simid>.out in {self.out}:\n" f"{restart_files}" ) return restart_files[0] @property def n_magnetic_atoms(self) -> int: """Get number of magnetic atoms. Returns: Number of magnetic atoms as defined in the ``momfile``. """ with open(self.momfile, encoding="utf-8") as file: n = len(file.readlines()) return n @property def Ms(self) -> mammos_entity.Entity: """Get spontaneous magnetization of the run. Returns: Entity Spontaneous Magnetization in Ampere per meter. """ cell = self._input_dictionary["cell"] lattice_const = self._input_dictionary["alat"] * u.m cell_volume = np.dot(cell[0], np.cross(cell[1], cell[2])) * lattice_const**3 Ms_mu_B_per_atom = float(self._cumulant_data["<M>"]) * u.mu_B Ms = Ms_mu_B_per_atom * self.n_magnetic_atoms / cell_volume return me.Ms(Ms, unit="kA/m") @property def Cv(self) -> mammos_entity.Entity: """Get specific heat capacity at constant volume. Returns: Entity IsochoricHeatCapacity in Joule per Kelvin. """ k_B = u.constants.k_B Cv = float(self._cumulant_data["C_v(tot)"]) * k_B * self.n_magnetic_atoms return me.Entity("IsochoricHeatCapacity", Cv) @property def E(self) -> mammos_entity.Entity: """Get energy. Returns: Entity Energy in Joule. """ E = float(self._cumulant_data["<E>"]) * u.mRy * self.n_magnetic_atoms return me.Entity("Energy", E, unit="J") @property def U_binder(self) -> float: """Get U_binder coefficient. Returns: Binder coefficient. """ U_b = float(self._cumulant_data["U_{Binder}"]) return U_b
[docs] class TemperatureSweepData: """UppASD Data parser for a temperature sweep. A temperature sweep is a sequence of runs with changing temperature and possibly other simulation parameters. Attributes: out: Output directory containing the sweep output. metadata: Dictionary of the sweep metadata. parameters: Dictionary of the sweep parameters. """
[docs] def __init__(self, out: pathlib.Path | str): """Initialize TemperatureSweepData given the directory path of a sweep run. Args: out: Output directory containing the sweep output. """ self.out = Path(out) with open(self.out / "mammos_spindynamics.yaml") as f: info = yaml.safe_load(f) self.metadata = info["metadata"] self.parameters = info["parameters"]
def __repr__(self): """Define repr.""" return f"TemperatureSweepData('{self.out}')" def __getitem__(self, idx): """Extract i-th run.""" runs = [ run_dir for run_dir in self.out.iterdir() if re.match(r"\d+-run", run_dir.name) ] runs.sort(key=lambda x: int(x.name.split("-")[0])) return read(runs[idx])
[docs] def info( self, include_description: bool = True, include_time_elapsed: bool = True, include_parameters: bool = True, ) -> pandas.DataFrame: """Return a Dataframe containing information about the temperature sweep. Args: include_description: Whether to include the description in the dataframe columns. include_time_elapsed: Whether to include elapsed time in the dataframe columns. include_parameters: Whether to include the run parameters in the dataframe columns. Returns: Dataframe with relevant information. """ metadata_keys = [] if include_description: metadata_keys += ["description"] if include_time_elapsed: metadata_keys += ["time_elapsed"] all_runs = [] index = [] for run in self: if run: info_ = {"name": run.out.name} index.append( int( run.out.name.replace("-run", "").replace( "-temperature_sweep", "" ) ) ) if metadata_keys: metadata_ = {k: run.metadata[k] for k in metadata_keys} info_ = {**info_, **metadata_} if include_parameters: info_ = {**info_, **run.parameters} all_runs.append(info_) return pd.DataFrame(all_runs, index=index)
[docs] def get(self, **kwargs) -> RunData: """Select run based on different filters specified as keyword arguments. Exceptions are raised if the filters give no match or too many matches. Args: **kwargs: Keys are the names to filter on, values must match exactly. Returns: :py:class:`~mammos_spindynamics.uppasd.RunData` satisfying given filters. Raises: LookupError: No run has been found satisfying all filters. LookupError: Too many results have been found. """ df = self.info() for key, val in kwargs.items(): df = df[df[key] == val] if len(df) == 0: raise LookupError( f"Requested run not found.\nSelection: {kwargs}\n" f"Available data: {self.info()}" ) if len(df) > 1: error_string = ( "Too many results. Please refine your search.\n" + "Avilable materials based on request:\n" ) error_string += str(df) raise LookupError(error_string) return read(self.out / df.iloc[0]["name"])
@property def T(self) -> mammos_entity.Entity: """Get array of temperatures of the sweep. Returns: 1D array Entity ThermodynamicTemperature in Kelvin. """ return me.operations.concat_flat(*[run.T for run in self if run]) @property def Ms(self) -> mammos_entity.Entity: """Get array of spontaneous magnetization of the sweep. Returns: 1D array Entity SpontaneousMagnetization in Kelvin. """ return me.operations.concat_flat(*[run.Ms for run in self if run]) @property def Cv(self) -> mammos_entity.Entity: """Get specific heat capacity at constant volume. For a temperature sweep the heat capacity Cv is evaluated as the derivative of the energy as a function of temperature. Returns: 1D array Entity IsochoricHeatCapacity in Joule per Kelvin. """ k_B = u.constants.k_B.value Cv = np.gradient(self.E.value / k_B, self.T.value, axis=0) return me.Entity("IsochoricHeatCapacity", Cv) @property def U_binder(self) -> numpy.ndarray: """Get array of Binder coefficients. Returns: Array of Binder coefficients. """ return np.array([run.U_binder for run in self if run]) @property def E(self) -> mammos_entity.Entity: """Get energy. Returns: 1D array Entity Energy in Joule. """ return me.operations.concat_flat(*[run.E for run in self if run])
[docs] def save_output(self, out: pathlib.Path | str) -> None: """Save output files M(T) and output.csv in directory `out`. `M(T)` contains all information evaluated from the cumulant files. `output.csv` contains entities for information temperature, magnetization, Binder cumulant and heat capacity. Args: out: Directory location where to save output files. """ out = Path(out) out.mkdir(parents=True, exist_ok=True) with open(self[0].cumulants) as f: lines = f.readlines() header = lines[0] with open(out / "M(T)", "w") as f: f.write(f"{'T':>5} {header}") for run in self: if run: with open(run.cumulants) as f_run: lines = f_run.readlines() f.write(f"{run.T.value:>5.0f} {lines[-1]}") me.EntityCollection( description="Magnetization and heat capacity from UppASD", T=self.T, Ms=self.Ms, U_binder=self.U_binder, Cv=self.Cv, E=self.E, ).to_csv(out / "output.csv")
def _parse_inpsd_file(inpsd_file: pathlib.Path | str) -> dict: """Parse inpsd.dat file.""" string_parameters = { "simid", "exchange", "momfile", "posfile", "restartfile", } float_parameters = { "alat", "temp", } with open(inpsd_file, encoding="utf-8") as file: lines = file.readlines() parameters = {} for i, line in enumerate(lines): for par in string_parameters: if re.match(f"{par} .*", line): parameters[par] = line.split()[1] for par in float_parameters: if re.match(f"{par} .*", line): try: parameters[par] = float(line.split()[1]) except ValueError: continue if re.match("(bc|BC) .*", line): parameters["bc"] = line.removeprefix("bc").split()[:3] if re.match("cell .*", line): a = np.genfromtxt(StringIO(line.removeprefix("cell"))) b = np.genfromtxt(StringIO(lines[i + 1])) c = np.genfromtxt(StringIO(lines[i + 2])) parameters["cell"] = np.vstack((a, b, c)) if re.match("ncell .*", line): parameters["ncell"] = np.genfromtxt(StringIO(line.removeprefix("ncell")))[ :3 ] return parameters