-
Notifications
You must be signed in to change notification settings - Fork 874
/
__init__.py
155 lines (127 loc) · 5.13 KB
/
__init__.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
"""
Entries are containers for calculated information, which is used in
many analyses. This module contains entry related tools and implements
the base Entry class, which is the basic entity that can be used to
store calculated information. Other Entry classes such as ComputedEntry
and PDEntry inherit from this class.
"""
from __future__ import annotations
from abc import ABCMeta, abstractmethod
from typing import Literal
import numpy as np
from monty.json import MSONable
from pymatgen.core.composition import Composition
__author__ = "Shyue Ping Ong, Anubhav Jain, Ayush Gupta"
__copyright__ = "Copyright 2020, The Materials Project"
__version__ = "1.1"
__maintainer__ = "Shyue Ping Ong"
__email__ = "[email protected]"
__status__ = "Production"
__date__ = "Mar 03, 2020"
class Entry(MSONable, metaclass=ABCMeta):
"""
A lightweight object containing the energy associated with
a specific chemical composition. This base class is not
intended to be instantiated directly. Note that classes
which inherit from Entry must define a .energy property.
"""
def __init__(
self,
composition: Composition | str | dict[str, float],
energy: float,
) -> None:
"""
Initializes an Entry.
Args:
composition (Composition): Composition of the entry. For
flexibility, this can take the form of all the typical input
taken by a Composition, including a {symbol: amt} dict,
a string formula, and others.
energy (float): Energy of the entry.
"""
self._composition = Composition(composition)
self._energy = energy
@property
def is_element(self) -> bool:
"""
:return: Whether composition of entry is an element.
"""
# NOTE _composition rather than composition as GrandPDEntry
# edge case exists if we have a compound where chempots are
# given for all bar one element type
return self._composition.is_element
@property
def composition(self) -> Composition:
"""
:return: the composition of the entry.
"""
return self._composition
@property
@abstractmethod
def energy(self) -> float:
"""
:return: the energy of the entry.
"""
raise NotImplementedError
@property
def energy_per_atom(self) -> float:
"""
:return: the energy per atom of the entry.
"""
return self.energy / self.composition.num_atoms
def __repr__(self):
return f"{type(self).__name__} : {self.composition} with energy = {self.energy:.4f}"
def __str__(self):
return self.__repr__()
def normalize(self, mode: Literal["formula_unit", "atom"] = "formula_unit") -> Entry:
"""
Normalize the entry's composition and energy.
Args:
mode ("formula_unit" | "atom"): "formula_unit" (the default) normalizes to composition.reduced_formula.
"atom" normalizes such that the composition amounts sum to 1.
"""
factor = self._normalization_factor(mode)
new_composition = self._composition / factor
new_energy = self._energy / factor
new_entry_dict = self.as_dict()
new_entry_dict["composition"] = new_composition.as_dict()
new_entry_dict["energy"] = new_energy
return self.from_dict(new_entry_dict)
def _normalization_factor(self, mode: Literal["formula_unit", "atom"] = "formula_unit") -> float:
# NOTE here we use composition rather than _composition in order to ensure
# that we have the expected behavior downstream in cases where composition
# is overwritten (GrandPotPDEntry, TransformedPDEntry)
if mode == "atom":
factor = self.composition.num_atoms
elif mode == "formula_unit":
factor = self.composition.get_reduced_composition_and_factor()[1]
else:
raise ValueError(f"{mode} is not an allowed option for normalization")
return factor
def as_dict(self) -> dict:
"""
:return: MSONable dict.
"""
return {
"@module": type(self).__module__,
"@class": type(self).__name__,
"energy": self._energy,
"composition": self._composition.as_dict(),
}
def __eq__(self, other: object) -> bool:
if not isinstance(other, type(self)):
return NotImplemented
# NOTE: Scaled duplicates i.e. physically equivalent materials
# are not equal unless normalized separately.
if self is other:
return True
# Equality is defined based on composition and energy
# If structures are involved, it is assumed that a {composition, energy} is
# vanishingly unlikely to be the same if the structures are different
if not np.allclose(self.energy, other.energy):
return False
return self.composition == other.composition
def __hash__(self):
# NOTE truncate _energy to 8 dp to ensure same robustness
# as np.allclose
return hash(f"{type(self).__name__}{self._composition.formula}{self._energy:.8f}")