Fitting workflows
The settings and steps by which a new bespoke force field is generated is referred to as a fitting workflow within
bespoke fit (represented by the BespokeOptimizationSchema
object), and are usually created by feeding an OpenFF
Molecule
object into a BespokeWorkflowFactory
.
The default workflow
The default workflow is suitable for augmenting a general SMIRNOFF force field (currently the OpenFF 2.0.0 force field) with a new bespoke torsion term for each non-terminal rotatable bond in the input molecule trained to reproduce a bespoke 1D QC torsion scan performed around that bond.
In this section, we’ll build this workflow factory up from nothing. This is just for a demonstration; you can
always build this factory by simply instantiating the BespokeWorkflowFactory
class without arguments.
The default workflow has three steps:
Fragmentation: Break the molecule into parts large enough to have accurate chemistry, but small enough for efficient quantum chemical calculations.
QC Generation: Perform quantum chemical calculations on the fragments to generate reference data for the bespoke parameters to reproduce.
Optimization: Optimize the force field parameters into bespoke parameters that reproduce the quantum chemical data.
Fragmentation
BespokeFit uses OpenFF Fragmenter to fragment molecules. The fragmenter can be configured by subclasses of
Fragmenter’s Fragmenter
class, which are accepted directly by BespokeFit. By default, a fragmentation scheme that
aims to ensure the Wiberg bond order of the rotatable bond that the 1D torsion scan is being performed around is the
same in the parent and the fragment is used:
from openff.fragmenter.fragment import WBOFragmenter
fragmenter = WBOFragmenter()
QC Generation
BespokeFit uses target schemas to define the types of reference data to train the bespoke parameters to. Each target type has a corresponding QC data type that must be generated as part of the bespoke fitting process to use as a reference. A target that measures the deviation between a QC and MM torsion scan, for example, will requre a 1D QC torsion scan to be performed.
The target schema also describes how strongly deviations from the reference data contributes to the overall loss
function to be minimzed during the optimization stage. Target schema classes are subclasses of BaseTargetSchema
,
and schemas for torsion drives, vibration fitting, and several other targets are available in the
openff.bespokefit.schema.targets
module.
from openff.bespokefit.schema.targets import TorsionProfileTargetSchema
target = TorsionProfileTargetSchema()
We can also specify at how we want to generate any reference data, including the program used, method, basis set, and
level of theory. This is specified with instances of the QCSpec
class from QCSubmit. If multiple specifications
are provided, the factory will try them in order until it finds one that is both available on the executing machine and
that supports the target molecule. Note that this may lead to BespokeFit silently behaving differently on machines with
different software installed.
from openff.qcsubmit.common_structures import QCSpec
qc_spec = QCSpec()
Optimization
BespokeFit optimizers are configured by subclasses of BaseOptimizerSchema
. The default workflow uses ForceBalance
to optimize torsion parameters, so we’ll use ForceBalanceSchema
to configure it. The default settings are designed
for optimizing parameters of an OpenFF force field:
from openff.bespokefit.schema.optimizers import ForceBalanceSchema
optimizer = ForceBalanceSchema()
The optimizer also needs an initial force field to use as a starting point. This should be the filename of a force field in offxml format:
initial_ff = "openff-2.0.0.offxml"
Finally, we need to configure hyperparameters that describe the parameter’s
priors and how they can be fitted to the reference data. Hyperparameter classes
inherit from BaseSMIRKSHyperparameters
; specific classes for bonds,
angles, proper and improper torsions, and van der Waals forces are
available. Since we’re only fitting proper torsions, only those hyperparameters
are needed:
from openff.bespokefit.schema.smirnoff import ProperTorsionHyperparameters
hyperparams = [ProperTorsionHyperparameters()]
Putting the factory together
With all the components configured, we can construct the workflow factory with its constructor:
from openff.bespokefit.workflows import BespokeWorkflowFactory
factory = BespokeWorkflowFactory(
fragmentation_engine = fragmenter,
target_templates = [target],
default_qc_specs = [qc_spec],
optimizer = optimizer,
initial_force_field = initial_ff,
parameter_hyperparameters = hyperparams,
)
Note that the workflow factory has a few more fields so that even more behavior can be customized; see its API documentation for details.