Source code for openff.bespokefit.schema.fitting

import abc
from typing import Dict, List, Optional

from openff.fragmenter.fragment import WBOFragmenter
from openff.toolkit.topology import Molecule
from openff.toolkit.typing.engines.smirnoff import ForceField
from openff.units import unit
from typing_extensions import Literal

from openff.bespokefit._pydantic import Field, SchemaBase, conlist
from openff.bespokefit.fragmentation import FragmentationEngine
from openff.bespokefit.schema.optimizers import OptimizerSchema
from openff.bespokefit.schema.smirnoff import (
    BaseSMIRKSParameter,
    SMIRNOFFHyperparameters,
    SMIRNOFFParameter,
)
from openff.bespokefit.schema.targets import TargetSchema
from openff.bespokefit.utilities.smirks import SMIRKSettings, SMIRKSType


[docs]class OptimizationStageSchema(SchemaBase, abc.ABC): """A schema that encodes a single stage in a multi-stage optimization. A common example may be one in which in the first stage a charge model is trained, followed by the valence parameters being trained, and finally the torsion parameters are trained. """ optimizer: OptimizerSchema = Field( ..., description="The optimizer to use and its associated settings.", ) # TODO: Add a validator to make sure that for each type of parameter in # ``parameters`` there is a corresponding setting in # ``parameter_hyperparameters``. parameters: List[SMIRNOFFParameter] = Field( ..., description="A list of the specific force field parameters that should be " "optimized.", ) parameter_hyperparameters: List[SMIRNOFFHyperparameters] = Field( ..., description="The hyperparameters that describe how classes of parameters, e.g. " "the force constant and length of a bond parameter, should be restrained during " "the optimisation such as through the inclusion of harmonic priors.", ) targets: List[TargetSchema] = Field( [], description="The fittings targets to simultaneously optimize against.", ) @property def n_targets(self) -> int: """Returns the number of targets to be fit.""" return len(self.targets)
[docs]class BaseOptimizationSchema(SchemaBase, abc.ABC): """A schema which encodes how a particular force field should be optimized against a set of fitting targets simultaneously. """ type: Literal["base"] = "base" id: Optional[str] = Field( None, description="The unique id given to this optimization." ) initial_force_field: str = Field( ..., description="The path to the force field to optimize OR an XML serialized " "SMIRNOFF force field.", ) stages: conlist(OptimizationStageSchema, min_items=1) = Field( ..., description="The fitting stages that should be performed sequentially. The " "force field produced by one stage will be used as input to the subsequent " "stage.", ) @property def initial_parameter_values( self, ) -> Dict[BaseSMIRKSParameter, Dict[str, unit.Quantity]]: """A list of the initial force field parameters that will be optimized.""" initial_force_field = ForceField(self.initial_force_field) return { parameter: { attribute: getattr( initial_force_field[parameter.type].parameters[parameter.smirks], attribute, ) for attribute in parameter.attributes } for stage in self.stages for parameter in stage.parameters }
[docs]class OptimizationSchema(BaseOptimizationSchema): """The schema for a general optimization that does not require bespoke stages such as fragmentation of bespoke QC calculations. """ type: Literal["general"] = "general"
[docs]class BespokeOptimizationSchema(BaseOptimizationSchema): """A schema which encodes how a bespoke force field should be created for a specific molecule.""" type: Literal["bespoke"] = "bespoke" smiles: str = Field( ..., description="The SMILES representation of the molecule to generate bespoke " "parameters for.", ) initial_force_field_hash: str = Field( ..., description="The hash values of the initial input force field with " "no bespokefit modifications. Used for internal hashing", ) fragmentation_engine: Optional[FragmentationEngine] = Field( WBOFragmenter(), description="The fragmentation engine that should be used to fragment the " "molecule. If no engine is provided the molecules will not be fragmented.", ) target_torsion_smirks: Optional[List[str]] = Field( ..., description="A list of SMARTS patterns that should be used to identify the " "**bonds** within the target molecule to generate bespoke torsions around. Each " "SMARTS pattern should include **two** indexed atoms that correspond to the " "two atoms involved in the central bond." "\n" "By default bespoke torsion parameters (if requested) will be constructed for " "all non-terminal 'rotatable bonds'", ) smirk_settings: SMIRKSettings = Field( SMIRKSettings(), description="The settings that should be used when generating SMIRKS patterns for this optimization stage.", ) @property def molecule(self) -> Molecule: """Return the openff molecule of the mapped smiles.""" return Molecule.from_mapped_smiles(self.smiles) @property def target_smirks(self) -> List[SMIRKSType]: """Returns a list of the target smirks types based on the selected hyper parameters. Used to determine which parameters should be fit. """ return list( { SMIRKSType(parameter.type) for stage in self.stages for parameter in stage.parameter_hyperparameters } )