# coding: utf-8
""" This file implements all notions of fitness. """
import numpy as np
from matador.utils.cursor_utils import get_array_from_cursor
from matador.utils.chem_utils import get_concentration, get_formation_energy
[docs]class FitnessCalculator:
""" This class calculates the fitnesses of generations,
by some global definition of generation-agnostic fitness.
Parameters:
fitness_metric (str): either 'dummy', 'hull' or 'hull_test'.
fitness_function (callable): function to operate on numpy array of raw fitness values,
hull (QueryConvexHull): matador hull from which to calculate metastability,
sandbagging (bool): whether or not to "sandbag" particular compositions, i.e. lower
a structure's fitness based on the number of nearby phases
"""
def __init__(
self,
fitness_metric="dummy",
fitness_function=None,
hull=None,
sandbagging=False,
debug=False,
):
""" Initialise fitness calculator, if from hull then
extract chemical potentials.
"""
self.testing = False
self.debug = debug
self.fitness_metric = fitness_metric
if self.fitness_metric == "hull":
self._get_raw = self._get_hull_distance
if hull is None:
raise RuntimeError("Cannot calculate hull distance without a hull!")
self.hull = hull
self.chempots = hull.chempot_cursor
elif self.fitness_metric == "dummy":
self._get_raw = self._get_dummy_fitness
elif self.fitness_metric == "hull_test":
self.testing = True
self._get_raw = self._get_hull_distance
self.hull = hull
self.chempots = hull.chempot_cursor
else:
raise RuntimeError("No recognised fitness metric given.")
self.sandbagging = False
if sandbagging:
self.sandbagging = True
self.sandbag_multipliers = dict()
if fitness_function is None:
self.fitness_function = default_fitness_function
else:
self.fitness_function = fitness_function
[docs] def evaluate(self, generation):
""" Assign normalised fitnesses to an entire generation.
Normalisation uses the logistic function such that
`fitness = 1 - tanh(2*distance_from_hull)`,
Parameters:
generation (Generation/list): list/iterator over optimised structures,
"""
raw = self._get_raw(generation)
fitnesses = self.fitness_function(raw)
for ind, _ in enumerate(generation):
generation[ind]["raw_fitness"] = raw[ind]
generation[ind]["fitness"] = fitnesses[ind]
if self.sandbagging:
self.update_sandbag_multipliers(generation)
self.apply_sandbag_multipliers(generation)
[docs] def update_sandbag_multipliers(self, generation, modifier=0.95):
""" Assign composition penalty based on number of nearby structures.
Updates fitness.sandbag_multipliers to a dictionary with chemical concentration as keys
and values of fitness penalty.
Parameters:
generation (Generation): list of optimised structures.
"""
for structure in generation:
if tuple(structure["concentration"]) in self.sandbag_multipliers:
self.sandbag_multipliers[tuple(structure["concentration"])] *= modifier
else:
self.sandbag_multipliers[tuple(structure["concentration"])] = modifier
[docs] def apply_sandbag_multipliers(self, generation, locality=0.05):
""" Scale the generation's fitness by the sandbag modifier. This
updates the 'fitness' key and the 'modifier' key (total scaling) of each document
in the generation.
Parameters:
generation (Generation): list of optimised structures.
Keyword Arguments:
locality (float): tolerance by which two structures are "nearby"
"""
for ind, structure in enumerate(generation):
generation[ind]["modifier"] = 1
for concentration in self.sandbag_multipliers:
if (
np.sqrt(
np.sum(
np.abs(
np.asarray(structure["concentration"])
- np.asarray(list(concentration))
)
** 2
)
)
<= locality
):
generation[ind]["modifier"] *= self.sandbag_multipliers[
concentration
]
generation[ind]["fitness"] *= generation[ind]["modifier"]
def _get_hull_distance(self, generation):
""" Assign distance from the hull from hull for generation,
assigning it.
Parameters:
generation (Generation): list of optimised structures.
Returns:
hull_dist (list(float)): list of distances to the hull.
"""
for ind, populum in enumerate(generation):
generation[ind]["concentration"] = get_concentration(
populum, self.hull.elements
)
generation[ind]["formation_enthalpy_per_atom"] = get_formation_energy(
self.chempots, populum
)
if self.debug:
print(
generation[ind]["concentration"],
generation[ind]["formation_enthalpy_per_atom"],
)
if self.testing:
for ind, populum in enumerate(generation):
generation[ind]["formation_enthalpy_per_atom"] = np.random.rand() - 0.5
structures = np.hstack(
(
get_array_from_cursor(generation, "concentration"),
get_array_from_cursor(
generation, "formation_enthalpy_per_atom"
).reshape(len(generation), 1),
)
)
if self.debug:
print(structures)
hull_dist = self.hull.get_hull_distances(structures, precompute=False)
for ind, populum in enumerate(generation):
generation[ind]["hull_distance"] = hull_dist[ind]
return hull_dist
def _get_dummy_fitness(self, generation):
""" Generate dummy hull distances from -0.01 to 0.05.
Parameters:
generation (Generation): list of optimised structures.
Returns:
list(float): dummy list of hull distances.
"""
return (0.05 * np.random.rand(len(generation)) - 0.01).tolist()
[docs]def default_fitness_function(raw, c=50, offset=0.075):
""" Default fitness function: logistic function.
Parameters:
raw (ndarray): 1D array of raw fitness values.
Returns:
ndarray: 1D array of rescaled fitnesses.
"""
fitnesses = 1 / (1 + np.exp(c * (raw - offset)))
if isinstance(fitnesses, np.float64):
fitnesses = min(1, fitnesses)
else:
fitnesses[fitnesses > 1.0] = 1.0
return fitnesses