<commit_before># 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

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)
    meta_data = models.ManyToManyField('MetaData')

    element_set = models.ManyToManyField('Element', null=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,
            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

    @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, basestring):
            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.save()
            comp.element_set = comp.comp.keys()
            return comp

    @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, basestring):
            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 = elements
        self._elements = None

    @property
    def total_energy(self):
        calcs = self.calculation_set.filter(converged=True, 
                            label__in=['standard', 'static'])
        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 )<commit_msg>Use total_energy = min(static) (if present), not min(static, standard)<commit_after># 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

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)
    meta_data = models.ManyToManyField('MetaData')

    element_set = models.ManyToManyField('Element', null=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,
            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

    @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, basestring):
            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.save()
            comp.element_set = comp.comp.keys()
            return comp

    @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, basestring):
            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 = 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 )
{'fraction_non_alphanumeric': 0.1036735386442691, 'fraction_numerical': 0.00542971069822686, 'mean_word_length': 2.179929862422444, 'pattern_counts': {'":': 0, '<': 19, '<?xml version=': 0, '>': 45, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7633489', 'n_tokens_mistral': 3511, 'n_tokens_neox': 3386, 'n_words': 820}