# qmpy/materials/composition.py
from django.db import models
from django.db.models import F
from qmpy.materials.element import Element
from qmpy.data import elements
from qmpy.utils import *
import qmpy.analysis.thermodynamics as thermo
[docs]class Composition(models.Model):
"""
Base class for a composition.
Relationships:
| :mod:`~qmpy.Calculation` via calculation_set
| :mod:`~qmpy.Element` via element_set
| :mod:`~qmpy.Entry` via entry_set
| :mod:`~qmpy.ExptFormationEnergy` via exptformationenergy_set
| :mod:`~qmpy.FormationEnergy` via formationenergy_set
| :mod:`~qmpy.MetaData` via meta_data
| :mod:`~qmpy.Structure` via structure_set
| :mod:`~qmpy.Prototype` via prototype_set
Attributes:
| formula: Electronegativity sorted and normalized composition string.
| e.g. Fe2O3, LiFeO2
| generic: Genericized composition string. e.g. A2B3, ABC2.
| mass: Mass per atom in AMUs
| meidema: Meidema model energy for the composition
| ntypes: Number of elements.
"""
formula = models.CharField(primary_key=True, max_length=255)
generic = models.CharField(max_length=255, blank=True, null=True)
element_list = models.CharField(max_length=255, blank=True, null=True)
meta_data = models.ManyToManyField("MetaData")
element_set = models.ManyToManyField("Element", blank=True)
ntypes = models.IntegerField(null=True)
### other stuff
mass = models.FloatField(blank=True, null=True)
### thermodyanamic stuff
meidema = models.FloatField(blank=True, null=True)
structure = models.ForeignKey(
"Structure", blank=True, on_delete=models.SET_NULL, null=True, related_name="+"
)
_unique = None
_duplicates = None
class Meta:
app_label = "qmpy"
db_table = "compositions"
def __str__(self):
return self.name
def __eq__(self, other):
return self.compare(other)
def compare(self, other, tol=1e-3):
if self.space != other.space:
return False
for k in self.space:
if abs(self.unit_comp[k] - other.unit_comp[k]) > tol:
return False
return True
[docs] @classmethod
def get(cls, composition):
"""
Classmethod for getting Composition objects - if the Composition
existsin the database, it is returned. If not, a new Composition is
created.
Examples::
>>> Composition.get('Fe2O3')
<Composition: Fe2O3>
"""
if isinstance(composition, str):
composition = parse_comp(composition)
comp = reduce_comp(composition)
f = " ".join(["%s%g" % (k, comp[k]) for k in sorted(comp.keys())])
comps = cls.objects.filter(formula=f)
if comps.exists():
return comps[0]
else:
comp = Composition(formula=f)
comp.ntypes = len(comp.comp)
comp.generic = format_generic_comp(comp.comp)
comp.element_list = "_".join(list(comp.comp.keys())) + "_"
comp.save()
comp.element_set.set(list(comp.comp.keys()))
return comp
[docs] @classmethod
def get_list(cls, bounds, calculated=False, uncalculated=False):
"""
Classmethod for finding all compositions within the space bounded by a
sequence of compositions.
Examples::
>>> from pprint import pprint
>>> comps = Composition.get_list(['Fe','O'], calculated=True)
>>> pprint(list(comps))
[<Composition: Fe>,
<Composition: FeO>,
<Composition: FeO3>,
<Composition: Fe2O3>,
<Composition: Fe3O4>,
<Composition: Fe4O5>,
<Composition: O>]
"""
space = set()
if isinstance(bounds, str):
bounds = bounds.split("-")
if len(bounds) == 1:
return [Composition.get(bounds[0])]
for b in bounds:
bound = parse_comp(b)
space |= set(bound.keys())
in_elts = Element.objects.filter(symbol__in=space)
out_elts = Element.objects.exclude(symbol__in=space)
comps = Composition.objects.filter(
element_set__in=in_elts, ntypes__lte=len(space)
)
comps = Composition.objects.exclude(element_set__in=out_elts)
comps = comps.exclude(entry=None)
if calculated:
comps = comps.exclude(formationenergy=None)
if uncalculated:
comps = comps.filter(formationenergy=None)
return comps.distinct()
@property
def entries(self):
entries = self.entry_set.filter(id=F("duplicate_of__id"))
if not entries.exists():
return []
return sorted(entries, key=lambda x: 100 if x.energy is None else x.energy)
@property
def ground_state(self):
"""Return the most stable entry at the composition."""
e = self.entries
if not e:
return None
return self.entries[0]
_elements = None
@property
def elements(self):
if self._elements is None:
self._elements = list(self.element_set.all())
return self._elements
@elements.setter
def elements(self, elements):
self.element_set.set(elements)
self._elements = None
@property
def total_energy(self):
calcs = self.calculation_set.filter(converged=True, label="static")
if not calcs.exists():
calcs = self.calculation_set.filter(converged=True, label="standard")
if not calcs.exists():
return
return min(c.energy_pa for c in calcs)
@property
def energy(self):
calcs = self.calculation_set.filter(
converged=True, label__in=["standard", "static"]
)
if not calcs.exists():
return
return min(c.formation_energy() for c in calcs)
@property
def delta_e(self):
"""Return the lowest formation energy."""
formations = self.formationenergy_set.exclude(delta_e=None)
formations = formations.filter(fit="standard")
if not formations.exists():
return
return min(formations.values_list("delta_e", flat=True))
@property
def icsd_delta_e(self):
"""
Return the lowest formation energy calculated from experimentally
measured structures - i.e. excluding prototypes.
"""
calcs = self.calculation_set.exclude(delta_e=None)
calcs = calcs.filter(path__contains="icsd")
if not calcs.exists():
return
return min(calcs.values_list("delta_e", flat=True))
@property
def ndistinct(self):
"""Return the number of distinct entries."""
return len(self.entries)
@property
def comp(self):
"""Return an element:amount composition dictionary."""
return parse_comp(self.formula)
@property
def unit_comp(self):
"""
Return an element:amoutn composition dictionary normalized to a unit
composition.
"""
return unit_comp(self.comp)
@property
def name(self):
return format_comp(reduce_comp(self.comp))
@property
def name_unreduced(self):
return format_comp(self.comp)
@property
def latex(self):
return format_latex(reduce_comp(self.comp))
@property
def html(self):
return format_html(reduce_comp(self.comp))
@property
def space(self):
"""Return the set of element symbols"""
return set(self.comp.keys())
@property
def experiment(self):
"""Return the lowest experimantally measured formation energy at the
compositoin.
"""
expts = self.exptformationenergy_set.filter(dft=False)
if not expts.exists():
return
return min(expts.values_list("delta_e", flat=True))
def relative_stability_plot(self, data=data):
if not self.energy:
return Renderer()
if data:
ps = thermo.PhaseSpace(self.name, data=data)
else:
ps = thermo.PhaseSpace(self.name)
return ps.phase_diagram
def get_mass(self):
return sum([elements[k]["mass"] * v for k, v in list(self.unit_comp.items())])
def get_similar(self):
return Composition.objects.filter(generic=self.generic)
def find_unique(self):
unique = []
# vih
for e1 in self.entry_set.all():
for i, e2 in enumerate(unique):
if e1.structure == e2.structure:
ind = i
else:
unique.append(e1)
continue
exist = unique[ind]
if e1.energy is None and e2.energy is None:
e1.duplicate_of = exist.entry
elif e2 is None:
s1.entry.duplicate_of = exist.entry
elif e1 is None:
exist.entry.duplicate_of = s1.entry
s1.entry.duplicate_of = None
unique[ind] = s1
elif e1 <= e2:
s1.entry.duplicate_of = exist.entry
else:
exist.entry.duplicate_of = s1.entry
s1.entry.duplicate_of = None
unique[ind] = s1
self.unique = dict([(s.entry, []) for s in unique])
for s in inputs:
if not s.entry.duplicate_of is None:
self.unique[s.entry.duplicate_of].append(s.entry)