Predefined objective functions¶
The abstract ObjectiveFunctor¶
The ObjectiveFunctor class is an abstract base class for functor based objective functions in ChemFit.
Besides the obvious __call__() method, which computes the value of the objective function for a given parameter set, there is only the get_meta_data() method to be implemented.
This method is supposed to return a dictionary of meta data.
Note
ChemFit also works with objective functions which do not implement the get_meta_data() method (such as regular functions), but some functionality may be lost.
The combined objective function¶
The CombinedObjectiveFunction class is used to turn a list of individual functions into a combined objective function, formed by the (weighted) sum of the individual terms.
Its use is demonstrated in the following
from chemfit.combined_objective_function import CombinedObjectiveFunction
def a(p):
return 1.0 * p["x"]**2
def b(p):
return 1.0 * p["y"]**2
objective_function = CombinedObjectiveFunction([a,b], [1.0, 2.0]) # is equivalent to x**2 + 2*y**2
# Evaluate the objective function
val = objective_function( {"x" : 1.0, "y" : 1.0} )
You can also add terms by using add(), like so
def c(p):
return 2
# now is equivalent to a+b+c
objective_function.add(c)
Lastly, if you have one or more CombinedObjectiveFunction you can add them in a flat hierarchy with add_flat():
# All of these append their terms/weights to the terms of the calling CombinedObjectiveFunction
objective_function.add_flat(other_cob)
objective_function.add_flat([other_cob, other_cob2])
objective_function.add_flat([other_cob, other_cob2], [1.0, 2.0])
Why do we care?¶
Obviously this example makes it seem like the CombinedObjectiveFunction is not useful. After all, we could have just defined it ourselves like so
def objective_function(p):
return a(b) + b(p)
There are two reasons, to use the CombinedObjectiveFunction.
Reason 1: MPI parallelization¶
Technically, MPIWrapperCOB is also an objective function, since it implements the ObjectiveFunctor interface.
It can be used to make a CombinedObjectiveFunction “MPI aware”. For more details see Running with MPI.
Reason 2: Gathering meta data¶
If the individual terms of the CombinedObjectiveFunction, implement the get_meta_data method, we can easily collect the meta data in a list.
from chemfit.abstract_objective_function import ObjectiveFunctor
from chemfit.combined_objective_function import CombinedObjectiveFunction
class MyFunctor(ObjectiveFunctor):
def __init__(self, f: float):
self.f = f
self.meta_data = {}
def get_meta_data(self):
return self.meta_data
def __call__(self, params: dict) -> float:
val = self.f * params["x"] ** 2
self.meta_data["last_value"] = val
return val
def a(p):
return p["y"] ** 2
objective_function = CombinedObjectiveFunction(
[a, MyFunctor(1), MyFunctor(2)]
) # is equivalent to y**2 + x**2 + 2.0*x**2
# Evaluate the objective function
val = objective_function({"x": 1.0, "y": 2.0})
meta_data_list = objective_function.gather_meta_data()
print(meta_data_list) # [None, {'last_value': 1.0}, {'last_value': 2.0}]
Note
As you can see in the example above, None is returned if the get_meta_data method is not implemented.
Note
The main use of get_meta_data() is to gather information about individual terms in a CombinedObjectiveFunction (possibly collected from different MPI ranks).
Tip
For objective functions with many terms, you can use pandas and the DataFrame.from_records method to turn a list of meta data dictionaries into a DataFrame and from there into e.g a CSV or any columnar format.
import pandas as pd
df = pd.DataFrame.from_records(meta_data_list)
df.to_csv("meta_data.csv")
ASE based objective functions¶
The ASE based objective functions derive from chemfit.ase_objective_function.ASEObjectiveFunction.
They are meant for use with the “atomic simulation environment” (ASE).
All of these functions are designed for flexibility (See ASE-Based Quantity Computers) and can accommodate any ase calculator.
ChemFit provides a few pre-defined objective functions of that type, which are explained in the following.
Custom ase-based objective functions can be implemented by deriving from chemfit.ase_objective_function.ASEObjectiveFunction and implementing the __call__(params : dict) -> float operator.
The energy based objective function for a single configuration¶
The EnergyObjectiveFunction represents a single reference configuration and energy pair.
Its main use is to serve as a building block for more complex objective functions.
This objective function has the form
where \(w\) is a weight factor, \(E_\text{pred}(\{r\}_\text{ref})\) is the potential energy of the reference configuration predicted by the calculator and \(E_\text{ref}\) is the reference energy.
- If we want to use this objective function in isolation, we need at least
A filepath to a reference configuration of atom positions
A target energy associated to this reference configuration. This energy might for example have been computed from an ab-initio code.
Optionally (but recommended) a
tag, which is a string identifier for book keeping purposes
Note
The reference atom positions should be saved in a format, which is parseable by ASE’s io.read function (https://wiki.fysik.dtu.dk/ase/ase/io/io.html#ase.io.read) function.
Important: If the file contains multiple “images” of atoms, the first image will be selected as the reference configuration.
From these pieces of information we can construct the objective function:
from chemfit.ase_objective_function import EnergyObjectiveFunction
from my_calculator import MyCalculator
class MyCalculatorFactory:
def __init__(self, some_parameter):
self.some_parameter = some_parameter
def __call__(self, atoms):
atoms.calc = MyCalculator(self.some_parameter)
class MyCalculatorParameterApplier:
def __call__(self, atoms, params):
atoms.calc.my_params.x = params["x"]
atoms.calc.my_params.y = params["y"]
# assume we have the atom positions saved as `atoms.xyz` and we know the reference energy is 1.0 eV
objective_function = EnergyObjectiveFunction(
calc_factory= MyCalculatorFactory(some_parameter=2),
param_applier = MyCalculatorParameterApplier(),
path_to_reference_configuration = "atoms.xyz",
tag = "my_tag",
reference_energy = 1.0
)
# Evaluate the objective function at x=2.0 and y=1.0
val = objective_function( {"x" : 2.0, "y": 1.0} )
The MultiEnergyObjectiveFunction¶
Technically, there is no separate MultiEnergyObjectiveFunction class (there used to be one).
The function construct_multi_energy_objective_function() provides a convenient tool to construct a CombinedObjectiveFunction consisting only of EnergyObjectiveFunction.
The objective function value is computed as
where each index \(i\) refers to a separate configuration/energy pair.
Consequently instead of a single path_to_reference_configuration argument the initializer takes a whole list. Fittingly (wink wink), called path_to_reference_configuration_list.
Two other initializer arguments enjoy a similar promotion, namely: reference_energy_list and tag_list.
Crucially, the objective function takes only a single parameter_applier and calculator_factory.
from chemfit.multi_energy_objective_function import construct_multi_energy_objective_function
# ... assume the same definitions for `MyCalculatorFactory` and `MyCalculatorParameterApplier` from above
objective_function = construct_multi_energy_objective_function(
calc_factory = MyCalculatorFactory(some_parameter=2),
param_applier = MyCalculatorParameterApplier(),
path_to_reference_configuration_list = ["atoms_1.xyz", "atoms_2.xyz"],
tag_list = ["my_tag_1", "my_tag_2"],
reference_energy_list = [1.0, 2.0]
)
# Evaluate the objective function at x=2.0 and y=1.0
val = objective_function( {"x" : 2.0, "y": 1.0} )