File: mixing.py

package info (click to toggle)
python-ase 3.26.0-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 15,484 kB
  • sloc: python: 148,112; xml: 2,728; makefile: 110; javascript: 47
file content (184 lines) | stat: -rw-r--r-- 6,061 bytes parent folder | download
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
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# fmt: off

from ase.calculators.calculator import (
    BaseCalculator,
    CalculatorSetupError,
    all_changes,
)
from ase.stress import full_3x3_to_voigt_6_stress


class Mixer:
    def __init__(self, calcs, weights):
        self.check_input(calcs, weights)
        common_properties = set.intersection(
            *(set(calc.implemented_properties) for calc in calcs)
        )
        self.implemented_properties = list(common_properties)
        self.calcs = calcs
        self.weights = weights

    @staticmethod
    def check_input(calcs, weights):
        if len(calcs) == 0:
            raise CalculatorSetupError("Please provide a list of Calculators")
        if len(weights) != len(calcs):
            raise ValueError(
                "The length of the weights must be the same as"
                " the number of Calculators!"
            )

    def get_properties(self, properties, atoms):
        results = {}

        def get_property(prop):
            contribs = [calc.get_property(prop, atoms) for calc in self.calcs]
            # ensure that the contribution shapes are the same for stress prop
            if prop == "stress":
                shapes = [contrib.shape for contrib in contribs]
                if not all(shape == shapes[0] for shape in shapes):
                    if prop == "stress":
                        contribs = self.make_stress_voigt(contribs)
                    else:
                        raise ValueError(
                            f"The shapes of the property {prop}"
                            " are not the same from all"
                            " calculators"
                        )
            results[f"{prop}_contributions"] = contribs
            results[prop] = sum(
                weight * value for weight, value in zip(self.weights, contribs)
            )

        for prop in properties:  # get requested properties
            get_property(prop)
        for prop in self.implemented_properties:  # cache all available props
            if all(prop in calc.results for calc in self.calcs):
                get_property(prop)
        return results

    @staticmethod
    def make_stress_voigt(stresses):
        new_contribs = []
        for contrib in stresses:
            if contrib.shape == (6,):
                new_contribs.append(contrib)
            elif contrib.shape == (3, 3):
                new_cont = full_3x3_to_voigt_6_stress(contrib)
                new_contribs.append(new_cont)
            else:
                raise ValueError(
                    "The shapes of the stress"
                    " property are not the same"
                    " from all calculators"
                )
        return new_contribs


class LinearCombinationCalculator(BaseCalculator):
    """Weighted summation of multiple calculators."""

    def __init__(self, calcs, weights):
        """Implementation of sum of calculators.

        calcs: list
            List of an arbitrary number of :mod:`ase.calculators` objects.
        weights: list of float
            Weights for each calculator in the list.
        """
        super().__init__()
        self.mixer = Mixer(calcs, weights)
        self.implemented_properties = self.mixer.implemented_properties

    def calculate(self, atoms, properties, system_changes):
        """Calculates all the specific property for each calculator and
        returns with the summed value.

        """
        self.atoms = atoms.copy()  # for caching of results
        self.results = self.mixer.get_properties(properties, atoms)

    def __str__(self):
        calculators = ", ".join(
            calc.__class__.__name__ for calc in self.mixer.calcs
        )
        return f"{self.__class__.__name__}({calculators})"


class MixedCalculator(LinearCombinationCalculator):
    """
    Mixing of two calculators with different weights

    H = weight1 * H1 + weight2 * H2

    Has functionality to get the energy contributions from each calculator

    Parameters
    ----------
    calc1 : ASE-calculator
    calc2 : ASE-calculator
    weight1 : float
        weight for calculator 1
    weight2 : float
        weight for calculator 2
    """

    def __init__(self, calc1, calc2, weight1, weight2):
        super().__init__([calc1, calc2], [weight1, weight2])

    def set_weights(self, w1, w2):
        self.mixer.weights[0] = w1
        self.mixer.weights[1] = w2

    def get_energy_contributions(self, atoms=None):
        """Return the potential energy from calc1 and calc2 respectively"""
        self.calculate(
            properties=["energy"],
            atoms=atoms,
            system_changes=all_changes
        )
        return self.results["energy_contributions"]


class SumCalculator(LinearCombinationCalculator):
    """SumCalculator for combining multiple calculators.

    This calculator can be used when there are different calculators
    for the different chemical environment or for example during delta
    leaning. It works with a list of arbitrary calculators and
    evaluates them in sequence when it is required.  The supported
    properties are the intersection of the implemented properties in
    each calculator.

    """

    def __init__(self, calcs):
        """Implementation of sum of calculators.

        calcs: list
            List of an arbitrary number of :mod:`ase.calculators` objects.
        """

        weights = [1.0] * len(calcs)
        super().__init__(calcs, weights)


class AverageCalculator(LinearCombinationCalculator):
    """AverageCalculator for equal summation of multiple calculators (for
    thermodynamic purposes)."""

    def __init__(self, calcs):
        """Implementation of average of calculators.

        calcs: list
            List of an arbitrary number of :mod:`ase.calculators` objects.
        """
        n = len(calcs)

        if n == 0:
            raise CalculatorSetupError(
                "The value of the calcs must be a list of Calculators"
            )

        weights = [1 / n] * n
        super().__init__(calcs, weights)