Coverage for /builds/ase/ase/ase/calculators/mixing.py: 90.67%
75 statements
« prev ^ index » next coverage.py v7.5.3, created at 2025-08-02 00:12 +0000
« prev ^ index » next coverage.py v7.5.3, created at 2025-08-02 00:12 +0000
1# fmt: off
3from ase.calculators.calculator import (
4 BaseCalculator,
5 CalculatorSetupError,
6 all_changes,
7)
8from ase.stress import full_3x3_to_voigt_6_stress
11class Mixer:
12 def __init__(self, calcs, weights):
13 self.check_input(calcs, weights)
14 common_properties = set.intersection(
15 *(set(calc.implemented_properties) for calc in calcs)
16 )
17 self.implemented_properties = list(common_properties)
18 self.calcs = calcs
19 self.weights = weights
21 @staticmethod
22 def check_input(calcs, weights):
23 if len(calcs) == 0:
24 raise CalculatorSetupError("Please provide a list of Calculators")
25 if len(weights) != len(calcs):
26 raise ValueError(
27 "The length of the weights must be the same as"
28 " the number of Calculators!"
29 )
31 def get_properties(self, properties, atoms):
32 results = {}
34 def get_property(prop):
35 contribs = [calc.get_property(prop, atoms) for calc in self.calcs]
36 # ensure that the contribution shapes are the same for stress prop
37 if prop == "stress":
38 shapes = [contrib.shape for contrib in contribs]
39 if not all(shape == shapes[0] for shape in shapes):
40 if prop == "stress":
41 contribs = self.make_stress_voigt(contribs)
42 else:
43 raise ValueError(
44 f"The shapes of the property {prop}"
45 " are not the same from all"
46 " calculators"
47 )
48 results[f"{prop}_contributions"] = contribs
49 results[prop] = sum(
50 weight * value for weight, value in zip(self.weights, contribs)
51 )
53 for prop in properties: # get requested properties
54 get_property(prop)
55 for prop in self.implemented_properties: # cache all available props
56 if all(prop in calc.results for calc in self.calcs):
57 get_property(prop)
58 return results
60 @staticmethod
61 def make_stress_voigt(stresses):
62 new_contribs = []
63 for contrib in stresses:
64 if contrib.shape == (6,):
65 new_contribs.append(contrib)
66 elif contrib.shape == (3, 3):
67 new_cont = full_3x3_to_voigt_6_stress(contrib)
68 new_contribs.append(new_cont)
69 else:
70 raise ValueError(
71 "The shapes of the stress"
72 " property are not the same"
73 " from all calculators"
74 )
75 return new_contribs
78class LinearCombinationCalculator(BaseCalculator):
79 """Weighted summation of multiple calculators."""
81 def __init__(self, calcs, weights):
82 """Implementation of sum of calculators.
84 calcs: list
85 List of an arbitrary number of :mod:`ase.calculators` objects.
86 weights: list of float
87 Weights for each calculator in the list.
88 """
89 super().__init__()
90 self.mixer = Mixer(calcs, weights)
91 self.implemented_properties = self.mixer.implemented_properties
93 def calculate(self, atoms, properties, system_changes):
94 """Calculates all the specific property for each calculator and
95 returns with the summed value.
97 """
98 self.atoms = atoms.copy() # for caching of results
99 self.results = self.mixer.get_properties(properties, atoms)
101 def __str__(self):
102 calculators = ", ".join(
103 calc.__class__.__name__ for calc in self.mixer.calcs
104 )
105 return f"{self.__class__.__name__}({calculators})"
108class MixedCalculator(LinearCombinationCalculator):
109 """
110 Mixing of two calculators with different weights
112 H = weight1 * H1 + weight2 * H2
114 Has functionality to get the energy contributions from each calculator
116 Parameters
117 ----------
118 calc1 : ASE-calculator
119 calc2 : ASE-calculator
120 weight1 : float
121 weight for calculator 1
122 weight2 : float
123 weight for calculator 2
124 """
126 def __init__(self, calc1, calc2, weight1, weight2):
127 super().__init__([calc1, calc2], [weight1, weight2])
129 def set_weights(self, w1, w2):
130 self.mixer.weights[0] = w1
131 self.mixer.weights[1] = w2
133 def get_energy_contributions(self, atoms=None):
134 """Return the potential energy from calc1 and calc2 respectively"""
135 self.calculate(
136 properties=["energy"],
137 atoms=atoms,
138 system_changes=all_changes
139 )
140 return self.results["energy_contributions"]
143class SumCalculator(LinearCombinationCalculator):
144 """SumCalculator for combining multiple calculators.
146 This calculator can be used when there are different calculators
147 for the different chemical environment or for example during delta
148 leaning. It works with a list of arbitrary calculators and
149 evaluates them in sequence when it is required. The supported
150 properties are the intersection of the implemented properties in
151 each calculator.
153 """
155 def __init__(self, calcs):
156 """Implementation of sum of calculators.
158 calcs: list
159 List of an arbitrary number of :mod:`ase.calculators` objects.
160 """
162 weights = [1.0] * len(calcs)
163 super().__init__(calcs, weights)
166class AverageCalculator(LinearCombinationCalculator):
167 """AverageCalculator for equal summation of multiple calculators (for
168 thermodynamic purposes)."""
170 def __init__(self, calcs):
171 """Implementation of average of calculators.
173 calcs: list
174 List of an arbitrary number of :mod:`ase.calculators` objects.
175 """
176 n = len(calcs)
178 if n == 0:
179 raise CalculatorSetupError(
180 "The value of the calcs must be a list of Calculators"
181 )
183 weights = [1 / n] * n
184 super().__init__(calcs, weights)