Commit 67c17a0e authored by Lauri Himanen's avatar Lauri Himanen
Browse files

Merged.

parent f9476740
Subproject commit 022a2af6bad45364dbdfac6b6c913f04186ac7d4
Subproject commit 15d0110cbeda05aaea05e4d30ba3aeb0874dafef
Subproject commit fe15759f080e8176d88af91447949243608b0d7e
Subproject commit b39569c5fa69254c90f91ec430d28a0941efbe95
......@@ -396,9 +396,8 @@ def prototypes_update(ctx, filepath, matches_only):
)
# Try to first see if the space group can be matched with the one in AFLOW
tolerance = config.normalize.symmetry_tolerance
try:
symm = SymmetryAnalyzer(atoms, tolerance)
symm = SymmetryAnalyzer(atoms, config.normalize.prototype_symmetry_tolerance)
spg_number = symm.get_space_group_number()
wyckoff_matid = symm.get_wyckoff_letters_conventional()
norm_system = symm.get_conventional_system()
......@@ -419,7 +418,5 @@ def prototypes_update(ctx, filepath, matches_only):
.format(n_prototypes, n_unmatched, n_failed)
)
aflow_prototypes["matid_symmetry_tolerance"] = tolerance
# Write data file to the specified path
write_prototype_data_file(aflow_prototypes, filepath)
......@@ -71,6 +71,19 @@ class NomadConfig(dict):
CELERY_WORKER_ROUTING = 'worker'
CELERY_QUEUE_ROUTING = 'queue'
version = '0.7.6'
commit = gitinfo.commit
release = 'devel'
domain = 'DFT'
service = 'unknown nomad service'
auxfile_cutoff = 100
parser_matching_size = 9128
console_log_level = logging.WARNING
max_upload_size = 32 * (1024 ** 3)
raw_file_strip_cutoff = 1000
use_empty_parsers = False
springer_db_relative_path = 'normalizing/data/SM_all08.db'
springer_db_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), springer_db_relative_path)
rabbitmq = NomadConfig(
host='localhost',
......@@ -78,11 +91,6 @@ rabbitmq = NomadConfig(
password='rabbitmq'
)
def rabbitmq_url():
return 'pyamqp://%s:%s@%s//' % (rabbitmq.user, rabbitmq.password, rabbitmq.host)
celery = NomadConfig(
max_memory=64e6, # 64 GB
timeout=1800, # 1/2 h
......@@ -155,20 +163,6 @@ tests = NomadConfig(
)
def api_url(ssl: bool = True):
return '%s://%s/%s/api' % (
'https' if services.https and ssl else 'http',
services.api_host.strip('/'),
services.api_base_path.strip('/'))
def gui_url():
base = api_url(True)[:-3]
if base.endswith('/'):
base = base[:-1]
return '%s/gui' % base
mail = NomadConfig(
enabled=False,
with_login=False,
......@@ -187,8 +181,16 @@ normalize = NomadConfig(
# Symmetry tolerance controls the precision used by spglib in order to find
# symmetries. The atoms are allowed to move 1/2*symmetry_tolerance from
# their symmetry positions in order for spglib to still detect symmetries.
# The unit is angstroms.
# The unit is angstroms. The value of 0.1 is used e.g. by Materials Project
# according to
# https://pymatgen.org/pymatgen.symmetry.analyzer.html#pymatgen.symmetry.analyzer.SpacegroupAnalyzer
symmetry_tolerance=0.1,
# The symmetry tolerance used in aflow prototype matching. Should only be
# changed before re-running the prototype detection.
prototype_symmetry_tolerance=0.1,
# Maximum number of atoms in the single cell of a 2D material for it to be
# considered valid.
max_2d_single_cell_size=7,
# The distance tolerance between atoms for grouping them into the same
# cluster. Used in detecting system type.
cluster_threshold=3.1,
......@@ -208,21 +210,43 @@ datacite = NomadConfig(
password='*'
)
version = '0.7.6'
commit = gitinfo.commit
release = 'devel'
domain = 'DFT'
service = 'unknown nomad service'
auxfile_cutoff = 100
parser_matching_size = 9128
console_log_level = logging.WARNING
max_upload_size = 32 * (1024 ** 3)
raw_file_strip_cutoff = 1000
use_empty_parsers = False
def rabbitmq_url():
return 'pyamqp://%s:%s@%s//' % (rabbitmq.user, rabbitmq.password, rabbitmq.host)
springer_db_relative_path = 'normalizing/data/SM_all08.db'
springer_db_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), springer_db_relative_path)
def api_url(ssl: bool = True):
return '%s://%s/%s/api' % (
'https' if services.https and ssl else 'http',
services.api_host.strip('/'),
services.api_base_path.strip('/'))
def gui_url():
base = api_url(True)[:-3]
if base.endswith('/'):
base = base[:-1]
return '%s/gui' % base
def check_config():
"""Used to check that the current configuration is valid. Should only be
called once after the final config is loaded.
Raises:
AssertionError: if there is a contradiction or invalid values in the
config file settings.
"""
# The AFLOW symmetry information is checked once on import
proto_symmetry_tolerance = normalize.prototype_symmetry_tolerance
symmetry_tolerance = normalize.symmetry_tolerance
if proto_symmetry_tolerance != symmetry_tolerance:
raise AssertionError(
"The AFLOW prototype information is outdated due to changed tolerance "
"for symmetry detection. Please update the AFLOW prototype information "
"by running the CLI command 'nomad admin ops prototype-update "
"--matches-only'."
)
def normalize_loglevel(value, default_level=logging.INFO):
......@@ -353,3 +377,4 @@ def load_config(config_file: str = os.environ.get('NOMAD_CONFIG', 'nomad.yaml'))
load_config()
check_config()
# -*- coding: utf-8 -*-
aflow_prototypes = {
"matid_symmetry_tolerance": 0.1,
"prototypes_by_spacegroup": {
1: [
{
......@@ -19,16 +19,237 @@ import numpy as np
from nomad.normalizing.data.aflow_prototypes import aflow_prototypes
from nomad import config
# The AFLOW symmetry information is checked once on import
old_symmetry_tolerance = aflow_prototypes["matid_symmetry_tolerance"]
symmetry_tolerance = config.normalize.symmetry_tolerance
if old_symmetry_tolerance != symmetry_tolerance:
raise AssertionError(
"The AFLOW prototype information is outdated due to changed "
"tolerance for symmetry detection. Please update the AFLOW "
"prototype information by running once the function "
"'update_aflow_prototype_information'."
)
def get_summed_atomic_mass(atomic_numbers: np.ndarray) -> float:
"""Calculates the summed atomic mass for the given atomic numbers.
Args:
atomic_numbers: Array of valid atomic numbers
Returns:
The atomic mass in kilograms.
"""
# It is assumed that the atomic numbers are valid at this point.
mass = np.sum(NUMBER_TO_MASS_MAP_KG[atomic_numbers])
return mass
def get_symmetry_string(space_group: int, wyckoff_sets: Dict) -> str:
"""Used to serialize symmetry information into a string. The Wyckoff
positions are assumed to be normalized and ordered as is the case if using
the matid-library.
Args:
space_group: 3D space group number
wyckoff_sets: Wyckoff sets that map a Wyckoff letter to related
information
Returns:
A string that encodes the symmetry properties of an atomistic
structure.
"""
wyckoff_strings = []
for group in wyckoff_sets:
element = group.element
wyckoff_letter = group.wyckoff_letter
n_atoms = len(group.indices)
i_string = "{} {} {}".format(element, wyckoff_letter, n_atoms)
wyckoff_strings.append(i_string)
wyckoff_string = ", ".join(sorted(wyckoff_strings))
string = "{} {}".format(space_group, wyckoff_string)
return string
def get_lattice_parameters(normalized_cell: np.ndarray) -> np.ndarray:
"""Calculate the lattice parameters for the normalized cell.
Args:
normalized_cell: The normalized cell as a 3x3 array. Each row is a
basis vector.
Returns:
Six parameters a, b, c, alpha, beta, gamma (in this order) as a numpy
array. Here is an explanation of each parameter:
a = length of first basis vector
b = length of second basis vector
c = length of third basis vector
alpha = angle between b and c
beta = angle between a and c
gamma = angle between a and b
"""
if normalized_cell is None:
return None
# Lengths
lengths = np.linalg.norm(normalized_cell, axis=1)
a, b, c = lengths
# Angles
angles = np.zeros(3)
for i in range(3):
j = (i + 1) % 3
k = (i + 2) % 3
angles[i] = np.dot(
normalized_cell[j],
normalized_cell[k]) / (lengths[j] * lengths[k])
angles = np.clip(angles, -1.0, 1.0)
alpha, beta, gamma = np.arccos(angles)
return [a, b, c, alpha, beta, gamma]
def get_hill_decomposition(atom_labels: np.ndarray, reduced: bool = False) -> Tuple[List[str], List[int]]:
"""Given a list of atomic labels, returns the chemical formula using the
Hill system (https://en.wikipedia.org/wiki/Hill_system) with an exception
for binary ionic compounds where the cation is always given first.
Args:
atom_labels: Atom labels.
reduced: Whether to divide the number of atoms by the greatest common
divisor
Returns:
An ordered list of chemical symbols and the corresponding counts.
"""
# Count occurancy of elements
names = []
counts = []
unordered_names, unordered_counts = np.unique(atom_labels, return_counts=True)
element_count_map = dict(zip(unordered_names, unordered_counts))
# Apply basic Hill system:
# 1. Is Carbon part of the system?
if "C" in element_count_map:
names.append("C")
counts.append(element_count_map["C"])
del element_count_map['C']
# 1a. add hydrogren
if "H" in element_count_map:
names.append("H")
counts.append(element_count_map["H"])
del element_count_map["H"]
# 2. all remaining elements in alphabetic order
for element in sorted(element_count_map):
names.append(element)
counts.append(element_count_map[element])
# 3. Binary ionic compounds: cation first, anion second
# If any of the most electronegative elements is first
# by alphabetic order, we move it to second
if len(counts) == 2 and names != ["C", "H"]:
order = {
"F": 1,
"O": 2,
"N": 3,
"Cl": 4,
"Br": 5,
"C": 6,
"Se": 7,
"S": 8,
"I": 9,
"As": 10,
"H": 11,
"P": 12,
"Ge": 13,
"Te": 14,
"B": 15,
"Sb": 16,
"Po": 17,
"Si": 18,
"Bi": 19
}
if (names[0] in order):
if (names[1] in order):
if(order[names[0]] < order[names[1]]):
# For non-metals:
# Swap symbols and counts if first element
# is more electronegative than the second one,
# because the more electronegative element is the anion
names[0], names[1] = names[1], names[0]
counts[0], counts[1] = counts[1], counts[0]
else:
# Swap symbols and counts always if second element
# is any other element,i.e.,
# put non-metal last because it is the anion
names[0], names[1] = names[1], names[0]
counts[0], counts[1] = counts[1], counts[0]
# TODO: implement all further exceptions regarding ordering
# in chemical formulas:
# - ionic compounds (ordering wrt to ionization)
# - oxides, acids, hydroxides...
# Reduce if requested
if reduced:
greatest_common_divisor = reduce(gcd, counts)
counts = np.array(counts) / greatest_common_divisor
return names, counts
def get_formula_string(symbols: List[str], counts: List[int]) -> str:
"""Used to form a single formula string from a list of chemical speices and
their counts.
Args:
symbols: List of chemical species
counts: List of chemical species occurences
Returns:
The formula as a string.
"""
formula = ""
for symbol, count in zip(symbols, counts):
if count > 1:
formula += "%s%d" % (symbol, count)
else:
formula += symbol
return formula
def find_vacuum_directions(system: Atoms, threshold: float) -> np.array:
"""Searches for vacuum gaps that are separating the periodic copies.
Args:
system: The structure to analyze
threshold: Vacuum threshold in angstroms
Returns:
np.ndarray: An array with a boolean for each lattice basis
direction indicating if there is enough vacuum to separate the
copies in that direction.
"""
rel_pos = system.get_scaled_positions()
pbc = system.get_pbc()
# Find the maximum vacuum gap for all basis vectors
gaps = np.empty(3, dtype=bool)
for axis in range(3):
if not pbc[axis]:
gaps[axis] = True
continue
comp = rel_pos[:, axis]
ind = np.sort(comp)
ind_rolled = np.roll(ind, 1, axis=0)
distances = ind - ind_rolled
# The first distance is from first to last, so it needs to be
# wrapped around
distances[0] += 1
# Find maximum gap in cartesian coordinates
max_gap = np.max(distances)
basis = system.get_cell()[axis, :]
max_gap_cartesian = np.linalg.norm(max_gap * basis)
has_vacuum_gap = max_gap_cartesian >= threshold
gaps[axis] = has_vacuum_gap
return gaps
>>>>>>> Stashed changes
def get_normalized_wyckoff(atomic_numbers: np.array, wyckoff_letters: np.array) -> Dict[str, Dict[str, int]]:
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment