Source code for aiida_crystal17.data.symmetry

#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright 2019 Chris Sewell
#
# This file is part of aiida-crystal17.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms and conditions
# of version 3 of the GNU Lesser General Public License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
import copy
import tempfile

# from aiida.common.exceptions import ValidationError
from aiida.common.extendeddicts import AttributeDict
from aiida.common.utils import classproperty
from aiida.orm import Data
from jsonschema import ValidationError as SchemeError
import numpy as np
import spglib

from aiida_crystal17.validation import load_schema, validate_against_schema


[docs]class SymmetryData(Data): """ Stores data regarding the symmetry of a structure - symmetry operations are stored on file (in the style of ArrayData) - the rest of the values (and the number of symmetry operators) are stored as attributes in the database """ _ops_filename = "operations.npy" _data_schema = None @classproperty def data_schema(cls): """return the data schema, which is loaded from file the first time it is called""" if cls._data_schema is None: cls._data_schema = load_schema("symmetry.schema.json") return copy.deepcopy(cls._data_schema) def __init__(self, **kwargs): """Stores the symmetry data for a structure - symmetry operations are stored on file (in the style of ArrayData) - the rest of the values are stored as attributes in the database :param data: the data to set """ data = kwargs.pop("data", None) super(SymmetryData, self).__init__(**kwargs) if data is not None: self.set_data(data)
[docs] def _validate(self): super(SymmetryData, self)._validate() fname = self._ops_filename if fname not in self.list_object_names(): raise SchemeError("operations not set") validate_against_schema(self.get_dict(), self.data_schema)
[docs] def set_data(self, data): """ Replace the current data with another one. :param data: The dictionary to set. """ from aiida.common.exceptions import ModificationNotAllowed # first validate the inputs validate_against_schema(data, self.data_schema) # store all but the symmetry operations as attributes backup_dict = copy.deepcopy(dict(self.attributes)) try: # Clear existing attributes and set the new dictionary self._update_attributes( {k: v for k, v in data.items() if k != "operations"} ) self.set_attribute("num_symops", len(data["operations"])) except ModificationNotAllowed: # pylint: disable=try-except-raise # I re-raise here to avoid to go in the generic 'except' below that # would raise the same exception again raise except Exception: # Try to restore the old data self.clear_attributes() self._update_attributes(backup_dict) raise # store the symmetry operations on file self._set_operations(data["operations"])
[docs] def _update_attributes(self, data): """ Update the current attribute with the keys provided in the dictionary. :param data: a dictionary with the keys to substitute. It works like dict.update(), adding new keys and overwriting existing keys. """ for k, v in data.items(): self.set_attribute(k, v)
[docs] def _set_operations(self, ops): fname = self._ops_filename if fname in self.list_object_names(): self.delete_object(fname) with tempfile.NamedTemporaryFile() as handle: # Store in a temporary file, and then add to the node np.save(handle, ops) # Flush and rewind the handle, otherwise the command to store it in # the repo will write an empty file handle.flush() handle.seek(0) # Write the numpy array to the repository, # keeping the byte representation self.put_object_from_filelike(handle, fname, mode="wb", encoding=None)
[docs] def _get_operations(self): filename = self._ops_filename if filename not in self.list_object_names(): raise KeyError("symmetry operations not set for node pk={}".format(self.pk)) # Open a handle in binary read mode as the arrays are written # as binary files as well with self.open(filename, mode="rb") as handle: array = np.load(handle) return array.tolist()
@property def data(self): """ Return the data as an AttributeDict """ data = dict(self.attributes) if "num_symops" in data: data.pop("num_symops") data["operations"] = self._get_operations() return AttributeDict(data)
[docs] def get_dict(self): """get dictionary of data""" data = dict(self.attributes) if "num_symops" in data: data.pop("num_symops") data["operations"] = self._get_operations() return data
[docs] def get_description(self): """ return a short string description of the data """ desc = [] hall_number = self.get_attribute("hall_number", None) num_symops = self.get_attribute("num_symops", None) if hall_number is not None: desc.append("hall_number: {}".format(hall_number)) if num_symops is not None: desc.append("symmops: {}".format(num_symops)) return "\n".join(desc)
@property def num_symops(self): return self.get_attribute("num_symops", None) @property def hall_number(self): return self.get_attribute("hall_number", None) @property def spacegroup_info(self): """Translate Hall number to space group type information. Returned as an attribute dict """ info = spglib.get_spacegroup_type(self.hall_number) if info is None: raise ValueError("the hall number could not be converted") return AttributeDict(info)
[docs] def add_path(self, src_abs, dst_path): from aiida.common.exceptions import ModificationNotAllowed raise ModificationNotAllowed( "Cannot add files or directories to StructSettingsData object" )
[docs] def compare_operations(self, ops, decimal=5): """compare operations against stored ones :param ops: list of (flattened) symmetry operations :param decimal: number of decimal points to round values to :returns: dict of differences """ ops_orig = self._get_operations() # create a set for each ops_orig = set([tuple([round(i, decimal) for i in op]) for op in ops_orig]) ops_new = set([tuple([round(i, decimal) for i in op]) for op in ops]) differences = {} if ops_orig.difference(ops_new): differences["missing"] = ops_orig.difference(ops_new) if ops_new.difference(ops_orig): differences["additional"] = ops_new.difference(ops_orig) return differences