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)
|