# Copyright (c) 2019-2020 The Regents of the University of Michigan
# This file is part of the fedorov project, released under the BSD 3-Clause
# License.
# Maintainer: Pengji Zhou
import copy
import json
import os
import re
import numpy as np
import pandas as pd
from . import data, space_group
_WYCKOFF_FILE = "space_group_{}_Wyckoff_site_data.json"
[docs]class Prototype:
"""Crystal prototype class.
This class uses the minimal necessay information needed to fully define a
crystal structures with space group number, wyckoff postions(in letter name
convention) and free parameters for each relavent wyckoff postions.
:param space_group_number:
space group number between 1 and 230
:type space_group_numbers:
int
:param wyckoff_site:
wyckoff site letters included in the prototype
:type wyckoff_site:
str
:param type_by_site:
type name letter for each site set in wyckoff_sites
:type type_by_site:
str
"""
def __init__(
self,
space_group_number=1,
wyckoff_site="",
type_by_site="",
):
if space_group_number > 230 or space_group_number < 1:
raise ValueError(
"space_group_number must be an integer between 0 and 230, "
"default = 1"
)
if not isinstance(wyckoff_site, str) or not wyckoff_site.isalpha():
raise ValueError(
"wyckoff_postions must be string consists of all the Wyckoff "
"postions letters, e.g. 'abcc' denotes one set of Wyckoff "
"postions for both a and b, and two sets at Wyckoff postion c"
)
if type_by_site == "":
type_by_site = "A" * len(wyckoff_site)
elif (
not isinstance(type_by_site, str)
or len(type_by_site) != len(wyckoff_site)
or not type_by_site.isalpha()
):
raise ValueError(
"type_by_site must be string consists of type name (A/B/C, etc)"
"for each Wyckoff site, default all particles with same type A "
"if not provided"
)
wyckoff_site_list = list(wyckoff_site.lower())
type_by_site = list(type_by_site.upper())
wyckoff_data_dir = os.path.join(
data._DATA_PATH, _WYCKOFF_FILE.format(space_group_number)
)
with open(wyckoff_data_dir, "r") as f:
full_wyckoff_positions = json.load(f)
basis_params_list = []
order = 1
for site in wyckoff_site_list:
pos = copy.deepcopy(full_wyckoff_positions[site])
pos = "".join(pos)
for letter in ("x", "y", "z"):
if letter in pos:
basis_params_list.append(letter + str(order))
order += 1
basis_params_value_list = [None] * len(basis_params_list)
self.space_group_number = space_group_number
self.space_group = space_group.SpaceGroup(space_group_number)
self.wyckoff_site_list = wyckoff_site_list
self.full_wyckoff_positions = full_wyckoff_positions
self.type_by_site = type_by_site
self.lattice_params = self.space_group.lattice.lattice_params
self.basis_params = dict(
zip(basis_params_list, basis_params_value_list)
)
def print_info(self):
print(
f"Wyckoff sites:{self.wyckoff_site_list}\n",
f"Particle type for each Wyckoff sites:{self.type_by_site}\n"
f"lattice parameters list:{list(self.lattice_params)}\n"
f"basis parameters list:{self.basis_params}",
)
def update_basis_params(self, user_basis_params):
params = copy.deepcopy(self.basis_params)
for param, value in user_basis_params.items():
if param in params:
if value is not None:
params[param] = value
else:
print(
"warning: {} is not required and not used to define this "
"structure".format(param)
)
for value in params.values():
if value is None:
raise ValueError(
"not all necessary parameters were provided! Call "
"print_info() to see the full list of necessary parameters"
)
return params
[docs] def get_basis_vectors(self, **user_basis_params):
"""Initialize fractional coordinates of the particles in the unitcell.
:param user_basis_params:
user defined parameters for different Wyckoff site degree of
freedom, when applicable
:type user_basis_params:
float
:return:
basis_vectors
:rtype:
np.ndarray
"""
basis_params = self.update_basis_params(user_basis_params)
base_positions = np.zeros((0, 3))
order = 1
for site in self.wyckoff_site_list:
pos = copy.deepcopy(self.full_wyckoff_positions[site])
for letter in ("x", "y", "z"):
if letter + str(order) in basis_params.keys():
exec(
"{} = {}".format(
letter, basis_params[letter + str(order)]
)
)
for i in range(0, 3):
# add * back for eval
target = re.findall(r"(\d[xyz])", pos[i])
for item in target:
pos[i] = pos[i].replace(
item, "*".join(re.findall(r"(\d)([xyz])", item)[0])
)
pos[i] = eval(pos[i])
base_positions = np.append(
base_positions, np.array(pos).reshape(1, -1), axis=0
)
order += 1
return self.space_group.get_basis_vectors(
data.wrap(base_positions), base_type=self.type_by_site
)
def update_lattice_params(self, user_lattice_params):
params = copy.deepcopy(self.lattice_params)
for param, value in user_lattice_params.items():
if param in params:
if value is not None:
params[param] = value
else:
print(
"warning: {} is not required and not used to define this "
"structure".format(param)
)
return params
[docs] def get_lattice_vectors(self, **user_lattice_params):
"""Initialize the unitcell and return lattice vectors [a1, a2, a3]
:param user_lattice_params:
unit cell parameters, provide a, b, c, alpha, beta, gamma where
applicable
:type user_lattice_params:
float
:return:
lattice_vectors
:rtype:
np.ndarray
"""
lattice_params = self.update_lattice_params(user_lattice_params)
return self.space_group.lattice.get_lattice_vectors(**lattice_params)
[docs]class AflowPrototype(Prototype):
"""Aflow prototype class.
This class uses the crystal prototypes in Aflow database to initialize
crystal structures.
:param prototype_index:
prototype index [0, 589] for all 590 prototypes in AFLOW.
:type prototype_index:
int
:param set_type:
allow setting different type name(in A, B, C order) for different atoms
in AFLOW prototype
:type set_type:
bool
"""
_Aflow_database = pd.read_csv(
os.path.join(data._DATA_PATH, "Aflow_processed_data.csv"), index_col=0
)
_name_regex = re.compile(r"'(.*?)'")
def __init__(self, prototype_index=0, set_type=False):
if prototype_index < 0 or prototype_index >= 590:
raise ValueError(
"prototype_index must be an integer between 0 and 590."
)
entry = self._Aflow_database.iloc[prototype_index]
def get_values(value_str: str):
try:
return [float(i) for i in value_str.strip("[]").split(",")]
except Exception:
return []
lattice_params = self._name_regex.findall(entry["lattice_params"])
lattice_params_value = get_values(entry["lattice_params_value"])
lattice_params = dict(zip(lattice_params, lattice_params_value))
basis_params = self._name_regex.findall(entry["basis_params"])
basis_params_value = get_values(entry["basis_params_value"])
basis_params = dict(zip(basis_params, basis_params_value))
# convert Aflow angle unit from degree to rad
for key in {"alpha", "beta", "gamma"} & lattice_params.keys():
lattice_params[key] = lattice_params[key] / 180 * np.pi
space_group_number = entry["space_group"]
# process proper unitcell params
if space_group_number in {146, 148, 155, 160, 161, 166, 167}:
a = lattice_params.pop("a")
c = lattice_params.pop("c/a") * a
lattice_params["a"] = np.sqrt(a**2 / 3 + c**2 / 9)
lattice_params["alpha"] = np.arccos(
(2 * c**2 - 3 * a**2) / (2 * (c**2 + 3 * a**2))
)
else:
a = lattice_params["a"]
if "b/a" in lattice_params:
lattice_params["b"] = lattice_params.pop("b/a") * a
if "c/a" in lattice_params:
lattice_params["c"] = lattice_params.pop("c/a") * a
wyckoff_sites, wyckoff_positions, types = self._get_wyckoff_sites(
entry, space_group_number, set_type
)
self.id = entry["id"]
self.pearson_symbol = entry["pearson_symbol"]
self.prototype = entry["prototype"]
self.space_group_number = space_group_number
self.space_group = space_group.SpaceGroup(space_group_number)
self.wyckoff_site_list = wyckoff_sites
self.full_wyckoff_positions = wyckoff_positions
self.type_by_site = types
self.lattice_params = lattice_params
self.basis_params = basis_params
def print_info(self):
print(
f"Info for the chosen crystal structure prototype:\n"
f"id: {self.id}, (Pearson-Chemistry-SpaceGroup)\n"
f"Wyckoff sites: {self.wyckoff_site_list}\n"
f"available lattice parameters: {self.lattice_params}\n"
f"available basis parameters: {self.basis_params}"
)
def _get_wyckoff_sites(self, entry, space_group_number, set_type):
wyckoff_sites_by_type = self._name_regex.findall(entry["wyckoff_sites"])
wyckoff_sites = sorted("".join(wyckoff_sites_by_type))
wyckoff_data_path = os.path.join(
data._DATA_PATH, _WYCKOFF_FILE.format(space_group_number)
)
with open(wyckoff_data_path, "r") as f:
wyckoff_positions = json.load(f)
# get type label
if not set_type:
return wyckoff_sites, wyckoff_positions, ["A"] * len(wyckoff_sites)
type_by_site = list("A" * len(wyckoff_sites))
sorted_site_string = "".join(wyckoff_sites)
base = ord("A")
for wyckoffs in wyckoff_sites_by_type:
for site in wyckoffs:
order = sorted_site_string.find(site)
sorted_site_string = sorted_site_string.replace(site, "0", 1)
type_by_site[order] = chr(base)
base += 1
return wyckoff_sites, wyckoff_positions, type_by_site
[docs] @classmethod
def from_query(
cls,
pearson_symbol: "str | None" = None,
space_group: "int | None" = None,
prototype: "str | None" = None,
set_type: bool = False,
):
"""Create all `AflowPrototype` matching the given query.
Args:
pearson_symbol (`str`, optional): The Pearson symbol to search for,
defaults to ``None`` which accepts any Pearson symbol.
space_group (`int`, optional): The space group to search for,
defaults to ``None`` which accepts any space group.
prototype (`str`, optional): The chemical prototype to search for,
defaults to ``None`` which accepts any prototype.
set_type (`bool`, optional): Set different type name (in alphabetic
order starting with "A") for different atoms in AFLOW prototype.
Returns:
lattices (list[`AflowPrototype`]): The list of all
`AflowPrototype`'s with a given Pearson symbol.
"""
def query(row):
return (
(prototype is None or row.prototype == prototype)
and (
pearson_symbol is None
or row.pearson_symbol == pearson_symbol
)
and (space_group is None or row.space_group == space_group)
)
return [
cls(i, set_type)
for i, row in cls._Aflow_database.iterrows()
if query(row)
]