# qmpy/analysis/symmetry/spacegroup.py
import os
import six
import fractions as frac
import numpy as np
import logging
from django.db import models
import qmpy.utils as utils
from .routines import *
logger = logging.getLogger(__name__)
class TranslationError(Exception):
pass
class RotationError(Exception):
pass
class OperationError(Exception):
pass
class WyckoffSiteError(Exception):
pass
class SpacegroupError(Exception):
pass
[docs]class Translation(models.Model):
"""
A translation operation.
Relationships:
| :mod:`~qmpy.Spacegroup` via spacegroup
| :mod:`~qmpy.Operation` via operation
Attributes:
| id
| x, y, z: Translation vector. Accessed via `vector`.
Examples::
>>> trans = Translation.get([0, 0, 0.5])
<Translation: 0,0,+1/2>
>>> print(trans)
0,0,+1/2
>>> print(trans.vector)
array([ 0. , 0. , 0.5])
"""
x = models.FloatField()
y = models.FloatField()
z = models.FloatField()
class Meta:
app_label = "qmpy"
db_table = "translations"
@property
def vector(self):
return np.array([self.x, self.y, self.z])
@vector.setter
def vector(self, vector):
self.x, self.y, self.z = vector
@classmethod
def get(cls, vector):
fields = ["x", "y", "z"]
arr_dict = dict(list(zip(fields, vector)))
obj, new = cls.objects.get_or_create(**arr_dict)
if new:
obj.save()
return obj
def __str__(self):
ops = []
for t in self.vector:
if t == 0:
s = "0"
elif t < 0:
s = "{}".format(frac.Fraction(str(t)))
else:
s = "+{}".format(frac.Fraction(str(t)))
ops.append(s)
return ",".join(ops)
[docs]class Rotation(models.Model):
"""
A rotation operation.
Relationships:
| :mod:`~qmpy.Spacegroup` via spacegroup
| :mod:`~qmpy.Operation` via operation
Attributes:
| id
| a11, a12, a13
| a21, a22, a23
| a31, a32, a33: Rotation matrix. Accessed via `matrix`.
Examples::
>>> rot = Rotation.get([[1, 0, 0], [-1, -1, 0], [0, 1, -1]])
<Rotation: +x,-x-y,+y-z>
>>> print(rot)
+x,-y,-z
>>> print rot.matrix
array([[ 1., 0., 0.],
[ -1., 1., 0.],
[0., 1., -1.]])
"""
a11 = models.FloatField()
a12 = models.FloatField()
a13 = models.FloatField()
a21 = models.FloatField()
a22 = models.FloatField()
a23 = models.FloatField()
a31 = models.FloatField()
a32 = models.FloatField()
a33 = models.FloatField()
class Meta:
app_label = "qmpy"
db_table = "rotations"
@property
def matrix(self):
return np.array(
[
[self.a11, self.a12, self.a13],
[self.a21, self.a22, self.a23],
[self.a31, self.a32, self.a33],
]
)
@matrix.setter
def matrix(self, matrix):
self.a11, self.a12, self.a13 = matrix[0]
self.a21, self.a22, self.a23 = matrix[1]
self.a31, self.a32, self.a33 = matrix[2]
@classmethod
def get(cls, matrix):
fields = ["a11", "a12", "a13", "a21", "a22", "a23", "a31", "a32", "a33"]
matrix = np.ravel(matrix)
mat_dict = dict(list(zip(fields, matrix)))
obj, new = cls.objects.get_or_create(**mat_dict)
if new:
obj.save()
return obj
def __str__(self):
ops = []
indict = {0: "x", 1: "y", 2: "z"}
for r in self.matrix:
s = ""
for i, x in enumerate(r):
if x == 0:
continue
elif x == 1:
s += "+{}".format(indict[i])
elif x == -1:
s += "-{}".format(indict[i])
elif x > 0:
s += "+{}{}".format(frac.Fraction(str(x)), indict[i])
else:
s += "{}{}".format(frac.Fraction(str(x)), indict[i])
ops.append(s)
return ",".join(ops)
[docs]class Operation(models.Model):
""" A symmetry operation (rotation + translation).
Relationships:
| :mod:`~qmpy.Spacegroup` via spacegroup
| :mod:`~qmpy.Rotation` via rotation_set
| :mod:`~qmpy.Translation` via translation_set
Attributes:
| id
Examples::
>>> op = Operation.get('x+y-1/2,-z-y+1/2,x-z+1/2')
>>> print op
<Operation: +x+y+1/2,-y-z+1/2,+x-z+1/2>
"""
rotation = models.ForeignKey("Rotation", on_delete=models.CASCADE)
translation = models.ForeignKey("Translation", on_delete=models.CASCADE)
class Meta:
app_label = "qmpy"
db_table = "operations"
[docs] @classmethod
def get(cls, value):
"""
Accepts symmetry operation strings, i.e. "+x, x+1/2, x+y-z" or a tuple
of rotation matrix and translation vector.
Example::
>>> Operation.get("x,y,-y")
>>> Operation.get(( rot, trans ))
"""
if isinstance(value, six.string_types):
rot, trans = utils.parse_sitesym(value)
elif isinstance(value, tuple):
rot, trans = value
else:
err_msg = "Operation needs to be string or" " (rotation, translation) tuple"
raise OperationError(err_msg)
rot = Rotation.get(rot)
trans = Translation.get(trans)
op, new = cls.objects.get_or_create(rotation=rot, translation=trans)
if new:
op.save()
return op
def __str__(self):
ops = []
indict = {0: "x", 1: "y", 2: "z"}
for r, t in zip(self.rotation.matrix, self.translation.vector):
s = ""
for i, x in enumerate(r):
if x == 0:
continue
elif x == 1:
s += "+{}".format(indict[i])
elif x == -1:
s += "-{}".format(indict[i])
elif x > 0:
s += "+{}{}".format(frac.Fraction(str(x)), indict[i])
else:
s += "{}{}".format(frac.Fraction(str(x)), indict[i])
if t == 0:
pass
elif t < 0:
s += "-{}".format(frac.Fraction("%08f" % t))
else:
s += "+{}".format(frac.Fraction("%08f" % t))
ops.append(s)
return ",".join(ops)
[docs]class WyckoffSite(models.Model):
"""
Base class for a Wyckoff site. (e.g. a "b" site).
Relationships:
| :mod:`~qmpy.Spacegroup` via spacegroup
| :mod:`~qmpy.Atom` via atom_set
| :mod:`~qmpy.Site` via site_set
Attributes:
| id
| symbol: Site symbol
| multiplicity: Site multiplicity
| x, y, z: Coordinate symbols.
"""
spacegroup = models.ForeignKey(
"Spacegroup", related_name="site_set", on_delete=models.CASCADE
)
symbol = models.CharField(max_length=1)
multiplicity = models.IntegerField(blank=True, null=True)
x = models.CharField(max_length=8)
y = models.CharField(max_length=8)
z = models.CharField(max_length=8)
class Meta:
app_label = "qmpy"
db_table = "wyckoffsites"
def __str__(self):
return "%s%d" % (self.symbol, self.multiplicity)
@classmethod
def get(cls, symbol, spacegroup):
site, new = cls.objects.get_or_create(spacegroup=spacegroup, symbol=symbol)
if new:
site.save()
return site
[docs]class Spacegroup(models.Model):
"""
Base class for a space group.
Relationships:
| :mod:`~qmpy.Structure` via structure_set
| :mod:`~qmpy.Translation` via centering_vectors
| :mod:`~qmpy.Operation` via operations
| :mod:`~qmpy.WyckoffSite` via site_set
Attributes:
| number: Spacegroup #. (primary key)
| centrosymmetric: (bool) Is the spacegroup centrosymmetric.
| hall: Hall symbol.
| hm: Hermann-Mauguin symobl.
| lattice_system: Cubic, Hexagonal, Tetragonal, Orthorhombic,
| Monoclinic or Triclinic.
| pearson: Pearson symbol
| schoenflies: Schoenflies symbol.
"""
number = models.IntegerField(primary_key=True)
hm = models.CharField(max_length=30, blank=True, null=True)
hall = models.CharField(max_length=30, blank=True, null=True)
pearson = models.CharField(max_length=30)
schoenflies = models.CharField(max_length=30)
operations = models.ManyToManyField(Operation, blank=True)
centering_vectors = models.ManyToManyField(Translation)
lattice_system = models.CharField(max_length=20)
centrosymmetric = models.BooleanField(default=False)
_sym_ops = None
_rots = None
_trans = None
class Meta:
app_label = "qmpy"
db_table = "spacegroups"
[docs] def save(self, *args, **kwargs):
super(Spacegroup, self).save(*args, **kwargs)
for op in self.sym_ops:
op.save()
self.operations.set(self.sym_ops)
@staticmethod
def get(number):
return Spacegroup.objects.get(number=number)
@property
def sym_ops(self):
"""List of (rotation, translation) pairs for the spacegroup"""
if self._sym_ops is None:
self._sym_ops = [op for op in self.operations.all()]
return self._sym_ops
@sym_ops.setter
def sym_ops(self, sym_ops):
self._sym_ops = sym_ops
@property
def rotations(self):
"""List of rotation operations for the spacegroup."""
if self._rots is None:
self._rots = np.array([op.rotation.matrix for op in self.sym_ops])
return self._rots
@property
def translations(self):
"""List of translation operations for the spacegroup."""
if self._trans is None:
self._trans = np.array([op.translation.vector for op in self.sym_ops])
return self._trans
def __str__(self):
return str(self.number)
@property
def wyckoff_sites(self):
"""List of WyckoffSites."""
return self.site_set.all().order_by("symbol")
@property
def symbol(self):
"""Returns the Hermann-Mauguin symbol for the spacegroup"""
return self.hm
def equivalent_sites(self, point, tol=1e-3):
equiv = []
for rot, trans in zip(self.rotations, self.translations):
new = utils.wrap(np.dot(rot, point) + trans)
if not any(
[all([abs(o - n) < tol for o, n in zip(old, new)]) for old in equiv]
):
equiv.append(new)
return equiv
@property
def n_sym_ops(self):
return self.operations.count()
@property
def n_wyckoff_sites(self):
return self.site_set.count()
[docs] def get_site(self, symbol):
"""Gets WyckoffSite by symbol."""
symbol = symbol.strip("0123456789")
return self.site_set.get(symbol__exact=symbol)