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

\[O = w \cdot \left| E_\text{pred}(\{r\}_\text{ref}) - E_\text{ref} \right|^2,\]

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.

  • A CalculatorFactory

  • A ParameterApplier

  • 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

\[O = \sum_i w^i \cdot \left| E^i_\text{pred}(\{r^i\}_\text{ref}) - E^i_\text{ref} \right|^2,\]

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