Source code for neighbormodels.interactions

# -*- coding: utf-8 -*-

from typing import Dict, List, Optional, Union

import numpy as np
import pandas as pd
from pandas import DataFrame, Series

from neighbormodels.neighbors import NeighborData

MagneticPatterns = Dict[str, Union[int, float]]


[docs]def build_model( neighbor_data: NeighborData, magnetic_patterns: MagneticPatterns, distance_filter: Optional[Dict[str, List[float]]] = None, ) -> DataFrame: """Builds and returns a data frame describing a pairwise interaction model. The intended use-case for the model is fitting magnetic energies taken from density functional theory calculations and extracting exchange parameters. :param neighbor_data: A named tuple with three field names: ``neighbor_count`` A pandas ``DataFrame`` of neighbor counts aggregated over site-index pairs and separation distances. ``sublattice_pairs`` A pandas ``DataFrame`` of neighbor distances mapped to unique bin intervals. ``structure`` A copy of the ``Structure`` object defining the crystal structure. :param magnetic_patterns: A dictionary of magnetic patterns to be mapped onto the crystal structure and used to compute the interaction coefficients of the model. :param distance_filter: A dictionary that defines pair distances to keep in the model. Any pair not found int he dictionary is filtered out. The dictionary keys define named groups of pair distances to keep, which subsequently are used for naming the interaction parameters. :return: A pandas ``DataFrame`` of the interaction parameter names and coefficients for the pairwise interaction model. """ magnetic_patterns_df: DataFrame = build_magnetic_patterns_data_frame( magnetic_patterns=magnetic_patterns ) return ( magnetic_patterns_df.pipe(compute_interaction_signs) .pipe( compute_model_coefficients, neighbor_data=neighbor_data, distance_filter=distance_filter, ) .pipe(spread_parameter_name_column) )
[docs]def compute_interaction_signs(magnetic_patterns_df: DataFrame) -> DataFrame: """Computes the signs of the pairwise interactions for the magnetic model. :param magnetic_patterns_df: A pandas ``DataFrame`` labeling and specifying magnetic patterns. :return: A pandas ``DataFrame`` of the signs of the pairwise interactions. """ return ( magnetic_patterns_df.merge( right=magnetic_patterns_df, how="inner", on="pattern", suffixes=("_i", "_j") ) .rename(columns={"site_i": "i", "site_j": "j"}) .loc[:, ["pattern", "i", "j", "spin_i", "spin_j"]] .assign(sign=lambda x: x["spin_i"] * x["spin_j"]) )
[docs]def compute_model_coefficients( interaction_signs_df: DataFrame, neighbor_data: NeighborData, distance_filter: Optional[Dict[str, List[float]]], ) -> DataFrame: """Computes the model coefficients by aggregating over the dot product of the neighbor counts and interaction signs. :param interaction_signs_df: A pandas ``DataFrame`` of the signs of the pairwise interactions. :param neighbor_data: A named tuple with three field names: ``neighbor_count`` A pandas ``DataFrame`` of neighbor counts aggregated over site-index pairs and separation distances. ``sublattice_pairs`` A pandas ``DataFrame`` of neighbor distances mapped to unique bin intervals. ``structure`` A copy of the ``Structure`` object defining the crystal structure. :param distance_filter: A dictionary that defines pair distances to keep in the model. Any pair not found int he dictionary is filtered out. The dictionary keys define named groups of pair distances to keep, which subsequently are used for naming the interaction parameters. :return: A pandas ``DataFrame`` of the model coefficients. """ return ( neighbor_data.neighbor_count.pipe( multiply_interaction_signs_and_neighbor_count, interaction_signs_df=interaction_signs_df, ) .pipe( group_subspecie_pairs_and_rank_by_distance, sublattice_pairs=neighbor_data.sublattice_pairs, ) .pipe(apply_distance_filter, distance_filter=distance_filter) .pipe(label_interaction_parameters) .pipe( aggregate_interaction_coefficients, num_sites=neighbor_data.structure.num_sites, ) )
[docs]def multiply_interaction_signs_and_neighbor_count( data_frame: DataFrame, interaction_signs_df: DataFrame ) -> DataFrame: """Adds coefficient column to data frame. Compatible with the pandas ``pipe()`` method. :param data_frame: A pandas ``DataFrame`` of neighbor counts aggregated over site-index pairs and separation distances. :param interaction_signs_df: A pandas ``DataFrame`` of the signs of the pairwise interactions. :return: A copy of input ``data_frame`` with the coefficient column added. """ return ( data_frame.merge(interaction_signs_df, on=["i", "j"]) .sort_values("pattern") .assign(coefficient=lambda x: x["sign"] * x["n"]) )
[docs]def group_subspecie_pairs_and_rank_by_distance( data_frame: DataFrame, sublattice_pairs: DataFrame ) -> DataFrame: """Adds rank column to data frame that sorts subspecie pairs by neighbor distance. Compatible with the pandas ``pipe()`` method. :param data_frame: A pandas ``DataFrame`` of neighbor counts aggregated over site-index pairs and separation distances. :param sublattice_pairs: A pandas ``DataFrame`` of unique sublattice pairs. :return: A copy of input ``data_frame`` with the rank column added. """ return data_frame.merge( sublattice_pairs, on=["subspecies_i", "subspecies_j", "distance_bin"] )
def apply_distance_filter( data_frame: DataFrame, distance_filter: Optional[Dict[str, List[float]]] ) -> DataFrame: """Filters the data frame to only include specific interaction pairs in the model. Compatible with the pandas ``pipe()`` method. :param data_frame: A pandas ``DataFrame`` of neighbor counts aggregated over site-index pairs and separation distances. :param distance_filter: A dictionary that defines pair distances to keep in the model. Any pair not found int he dictionary is filtered out. The dictionary keys define named groups of pair distances to keep, which subsequently are used for naming the interaction parameters. :return: A copy of input ``data_frame`` filtered to only include specific distance pairs and a filter_label column added. """ df: DataFrame = data_frame.copy() if distance_filter: filtered_df_list = [] number_distance_groups = len(distance_filter.keys()) for filter_label, distance_list in distance_filter.items(): df2 = df[ df["distance_bin"].apply( lambda x: any([distance in x for distance in distance_list]) ) ] if number_distance_groups > 1: df2 = df2.assign(filter_label=filter_label) else: df2 = df2.assign(filter_label="") filtered_df_list.append(df2.copy()) df = pd.concat(filtered_df_list).reset_index(drop=True) else: df = df.assign(filter_label="") return df
[docs]def label_interaction_parameters(data_frame: DataFrame) -> DataFrame: """Adds parameter_names column to data frame that labels the unique parameters of the interaction model. Compatible with the pandas ``pipe()`` method. :param data_frame: A pandas ``DataFrame`` of neighbor counts aggregated over site-index pairs and separation distances ranked by subspecies pairs. :return: A copy of input ``data_frame`` with the parameter_name column added. """ df: DataFrame = data_frame.sort_values( ["pattern", "filter_label", "distance_bin"] ).assign( rank=lambda x: x.groupby(["filter_label"]) .apply(lambda y: y[["rank"]].rank(method="dense")) .apply(lambda y: pd.to_numeric(arg=y, downcast="integer")) ).reset_index( drop=True ) parameter_names: Series = "J" + df["filter_label"] + df["rank"].astype(str) single_specie_check: Series = df["subspecies_i"] == df["subspecies_j"] single_specie: bool = single_specie_check.all() if not single_specie: parameter_names += "_" + df["subspecies_i"] + df["subspecies_j"] df["parameter_name"] = parameter_names return df
[docs]def aggregate_interaction_coefficients( data_frame: DataFrame, num_sites: int ) -> DataFrame: """Aggregates interaction coefficients by summing them within groups defined by the magnetic patterns and interaction parameters. Compatible with the pandas ``pipe()`` method. :param data_frame: A pandas ``DataFrame`` of interaction coefficients and parameters grouped by magnetic patterns, subspecies pairs, and neighbor distances. :param num_sites: The total number of magnetic sites in the unit cell. :return: A data frame where the interaction coefficients were summed within groups defined by the magnetic patterns and interaction parameters. """ return ( data_frame.groupby(["pattern", "parameter_name"]) .apply(lambda x: np.sum(x[["coefficient"]]) / num_sites / 2) .reset_index() .sort_values(by=["pattern", "parameter_name"]) .loc[:, ["pattern", "parameter_name", "coefficient"]] )
[docs]def build_magnetic_patterns_data_frame( magnetic_patterns: MagneticPatterns, ) -> DataFrame: """Converts dictionary of magnetic patterns into the pandas ``DataFrame`` format. :param magnetic_patterns: A dictionary of magnetic patterns to be mapped onto the crystal structure and used to compute the interaction coefficients of the model. :return: A pandas ``DataFrame`` labeling and specifying magnetic patterns. """ return ( DataFrame(data=magnetic_patterns) .reset_index(level=0) .rename(columns={"index": "site"}) .melt( id_vars=["site"], value_vars=list(magnetic_patterns.keys()), var_name="pattern", value_name="spin", ) .loc[:, ["pattern", "site", "spin"]] )
[docs]def spread_parameter_name_column(data_frame: DataFrame) -> DataFrame: """Spreads interaction parameter names into their own columns with the interaction coefficient as rows. Compatible with the pandas ``pipe()`` method. :param data_frame: A data frame where the interaction coefficients were summed within groups defined by the magnetic patterns and interaction parameters. :return: A data frame with the parameter names pivoted into their own columns. """ df: DataFrame = data_frame.pivot( index="pattern", columns="parameter_name", values="coefficient" ).reset_index() df.columns.name = "" return df