• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

materialsproject / pymatgen / 4075885785

pending completion
4075885785

push

github

Shyue Ping Ong
Merge branch 'master' of github.com:materialsproject/pymatgen

96 of 96 new or added lines in 27 files covered. (100.0%)

81013 of 102710 relevant lines covered (78.88%)

0.79 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.12
/pymatgen/analysis/structure_prediction/substitutor.py
1
# Copyright (c) Pymatgen Development Team.
2
# Distributed under the terms of the MIT License.
3

4
"""
1✔
5
This module provides classes for predicting new structures from existing ones.
6
"""
7

8
from __future__ import annotations
1✔
9

10
import functools
1✔
11
import itertools
1✔
12
import logging
1✔
13
from operator import mul
1✔
14

15
from monty.json import MSONable
1✔
16

17
from pymatgen.alchemy.filters import RemoveDuplicatesFilter, RemoveExistingFilter
1✔
18
from pymatgen.alchemy.materials import TransformedStructure
1✔
19
from pymatgen.alchemy.transmuters import StandardTransmuter
1✔
20
from pymatgen.analysis.structure_prediction.substitution_probability import (
1✔
21
    SubstitutionProbability,
22
)
23
from pymatgen.core.periodic_table import get_el_sp
1✔
24
from pymatgen.transformations.standard_transformations import SubstitutionTransformation
1✔
25

26
__author__ = "Will Richards, Geoffroy Hautier"
1✔
27
__copyright__ = "Copyright 2012, The Materials Project"
1✔
28
__version__ = "1.2"
1✔
29
__maintainer__ = "Will Richards"
1✔
30
__email__ = "wrichard@mit.edu"
1✔
31
__date__ = "Aug 31, 2012"
1✔
32

33

34
class Substitutor(MSONable):
1✔
35
    """
36
    This object uses a data mined ionic substitution approach to propose
37
    compounds likely to be stable. It relies on an algorithm presented in
38
    Hautier, G., Fischer, C., Ehrlacher, V., Jain, A., and Ceder, G. (2011).
39
    Data Mined Ionic Substitutions for the Discovery of New Compounds.
40
    Inorganic Chemistry, 50(2), 656-663. doi:10.1021/ic102031h
41
    """
42

43
    def __init__(self, threshold=1e-3, symprec: float = 0.1, **kwargs):
1✔
44
        """
45
        This substitutor uses the substitution probability class to
46
        find good substitutions for a given chemistry or structure.
47

48
        Args:
49
            threshold:
50
                probability threshold for predictions
51
            symprec:
52
                symmetry precision to determine if two structures
53
                are duplicates
54
            kwargs:
55
                kwargs for the SubstitutionProbability object
56
                lambda_table, alpha
57
        """
58
        self._kwargs = kwargs
1✔
59
        self._sp = SubstitutionProbability(**kwargs)
1✔
60
        self._threshold = threshold
1✔
61
        self._symprec = symprec
1✔
62

63
    def get_allowed_species(self):
1✔
64
        """
65
        Returns the species in the domain of the probability function
66
        any other specie will not work
67
        """
68
        return self._sp.species
1✔
69

70
    def pred_from_structures(
1✔
71
        self,
72
        target_species,
73
        structures_list,
74
        remove_duplicates=True,
75
        remove_existing=False,
76
    ):
77
        """
78
        Performs a structure prediction targeting compounds containing all of
79
        the target_species, based on a list of structure (those structures
80
        can for instance come from a database like the ICSD). It will return
81
        all the structures formed by ionic substitutions with a probability
82
        higher than the threshold
83

84
        Notes:
85
        If the default probability model is used, input structures must
86
        be oxidation state decorated. See AutoOxiStateDecorationTransformation
87

88
        This method does not change the number of species in a structure. i.e
89
        if the number of target species is 3, only input structures containing
90
        3 species will be considered.
91

92
        Args:
93
            target_species:
94
                a list of species with oxidation states
95
                e.g., [Species('Li',1),Species('Ni',2), Species('O',-2)]
96

97
            structures_list:
98
                a list of dictionary of the form {'structure':Structure object
99
                ,'id':some id where it comes from}
100
                the id can for instance refer to an ICSD id.
101

102
            remove_duplicates:
103
                if True, the duplicates in the predicted structures will
104
                be removed
105

106
            remove_existing:
107
                if True, the predicted structures that already exist in the
108
                structures_list will be removed
109

110
        Returns:
111
            a list of TransformedStructure objects.
112
        """
113
        target_species = [get_el_sp(sp) for sp in target_species]
1✔
114
        result = []
1✔
115
        transmuter = StandardTransmuter([])
1✔
116
        if len(list(set(target_species) & set(self.get_allowed_species()))) != len(target_species):
1✔
117
            raise ValueError("the species in target_species are not allowed for the probability model you are using")
×
118

119
        for permut in itertools.permutations(target_species):
1✔
120
            for s in structures_list:
1✔
121
                # check if: species are in the domain,
122
                # and the probability of subst. is above the threshold
123
                els = s["structure"].composition.elements
1✔
124
                if (
1✔
125
                    len(els) == len(permut)
126
                    and len(list(set(els) & set(self.get_allowed_species()))) == len(els)
127
                    and self._sp.cond_prob_list(permut, els) > self._threshold
128
                ):
129
                    clean_subst = {els[i]: permut[i] for i in range(0, len(els)) if els[i] != permut[i]}
1✔
130

131
                    if len(clean_subst) == 0:
1✔
132
                        continue
×
133

134
                    transf = SubstitutionTransformation(clean_subst)
1✔
135

136
                    if Substitutor._is_charge_balanced(transf.apply_transformation(s["structure"])):
1✔
137
                        ts = TransformedStructure(
1✔
138
                            s["structure"],
139
                            [transf],
140
                            history=[{"source": s["id"]}],
141
                            other_parameters={
142
                                "type": "structure_prediction",
143
                                "proba": self._sp.cond_prob_list(permut, els),
144
                            },
145
                        )
146
                        result.append(ts)
1✔
147
                        transmuter.append_transformed_structures([ts])
1✔
148

149
        if remove_duplicates:
1✔
150
            transmuter.apply_filter(RemoveDuplicatesFilter(symprec=self._symprec))
1✔
151
        if remove_existing:
1✔
152
            # Make the list of structures from structures_list that corresponds to the
153
            # target species
154
            chemsys = {sp.symbol for sp in target_species}
×
155
            structures_list_target = [
×
156
                st["structure"]
157
                for st in structures_list
158
                if Substitutor._is_from_chemical_system(chemsys, st["structure"])
159
            ]
160
            transmuter.apply_filter(RemoveExistingFilter(structures_list_target, symprec=self._symprec))
×
161
        return transmuter.transformed_structures
1✔
162

163
    @staticmethod
1✔
164
    def _is_charge_balanced(struct):
1✔
165
        """
166
        Checks if the structure object is charge balanced
167
        """
168
        return sum(s.specie.oxi_state for s in struct.sites) == 0.0
1✔
169

170
    @staticmethod
1✔
171
    def _is_from_chemical_system(chemical_system, struct):
1✔
172
        """
173
        Checks if the structure object is from the given chemical system
174
        """
175
        return {sp.symbol for sp in struct.composition} == set(chemical_system)
×
176

177
    def pred_from_list(self, species_list):
1✔
178
        """
179
        There are an exceptionally large number of substitutions to
180
        look at (260^n), where n is the number of species in the
181
        list. We need a more efficient than brute force way of going
182
        through these possibilities. The brute force method would be::
183

184
            output = []
185
            for p in itertools.product(self._sp.species_list
186
                                       , repeat = len(species_list)):
187
                if self._sp.conditional_probability_list(p, species_list)
188
                                       > self._threshold:
189
                    output.append(dict(zip(species_list,p)))
190
            return output
191

192
        Instead of that we do a branch and bound.
193

194
        Args:
195
            species_list:
196
                list of species in the starting structure
197

198
        Returns:
199
            list of dictionaries, each including a substitutions
200
            dictionary, and a probability value
201
        """
202
        species_list = [get_el_sp(sp) for sp in species_list]
1✔
203
        # calculate the highest probabilities to help us stop the recursion
204
        max_probabilities = []
1✔
205
        for s2 in species_list:
1✔
206
            max_p = 0
1✔
207
            for s1 in self._sp.species:
1✔
208
                max_p = max([self._sp.cond_prob(s1, s2), max_p])
1✔
209
            max_probabilities.append(max_p)
1✔
210
        output = []
1✔
211

212
        def _recurse(output_prob, output_species):
1✔
213
            best_case_prob = list(max_probabilities)
1✔
214
            best_case_prob[: len(output_prob)] = output_prob
1✔
215
            if functools.reduce(mul, best_case_prob) > self._threshold:
1✔
216
                if len(output_species) == len(species_list):
1✔
217
                    odict = {
1✔
218
                        "substitutions": dict(zip(species_list, output_species)),
219
                        "probability": functools.reduce(mul, best_case_prob),
220
                    }
221
                    output.append(odict)
1✔
222
                    return
1✔
223
                for sp in self._sp.species:
1✔
224
                    i = len(output_prob)
1✔
225
                    prob = self._sp.cond_prob(sp, species_list[i])
1✔
226
                    _recurse(output_prob + [prob], output_species + [sp])
1✔
227

228
        _recurse([], [])
1✔
229
        logging.info(f"{len(output)} substitutions found")
1✔
230
        return output
1✔
231

232
    def pred_from_comp(self, composition):
1✔
233
        """
234
        Similar to pred_from_list except this method returns a list after
235
        checking that compositions are charge balanced.
236
        """
237
        output = []
1✔
238
        predictions = self.pred_from_list(composition.elements)
1✔
239
        for p in predictions:
1✔
240
            subs = p["substitutions"]
1✔
241
            charge = 0
1✔
242
            for i_el in composition.elements:
1✔
243
                f_el = subs[i_el]
1✔
244
                charge += f_el.oxi_state * composition[i_el]
1✔
245
            if charge == 0:
1✔
246
                output.append(p)
1✔
247
        logging.info(f"{len(output)} charge balanced compositions found")
1✔
248
        return output
1✔
249

250
    def as_dict(self):
1✔
251
        """
252
        Returns: MSONable dict
253
        """
254
        return {
1✔
255
            "name": type(self).__name__,
256
            "version": __version__,
257
            "kwargs": self._kwargs,
258
            "threshold": self._threshold,
259
            "@module": type(self).__module__,
260
            "@class": type(self).__name__,
261
        }
262

263
    @classmethod
1✔
264
    def from_dict(cls, d):
1✔
265
        """
266
        Args:
267
            d (dict): Dict representation
268

269
        Returns:
270
            Class
271
        """
272
        t = d["threshold"]
1✔
273
        kwargs = d["kwargs"]
1✔
274
        return cls(threshold=t, **kwargs)
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc