Source code for openff.bespokefit.optimizers.forcebalance.forcebalance
import importlib
import logging
import os
import subprocess
from typing import Any, Dict
from openff.toolkit.typing.engines.smirnoff import ForceField
from openff.utilities.provenance import get_ambertools_version
from openff.bespokefit.optimizers.forcebalance import ForceBalanceInputFactory
from openff.bespokefit.optimizers.model import BaseOptimizer
from openff.bespokefit.schema import Error
from openff.bespokefit.schema.fitting import OptimizationStageSchema
from openff.bespokefit.schema.optimizers import ForceBalanceSchema
from openff.bespokefit.schema.results import OptimizationStageResults
from openff.bespokefit.schema.targets import (
AbInitioTargetSchema,
OptGeoTargetSchema,
TorsionProfileTargetSchema,
VibrationTargetSchema,
)
from openff.bespokefit.utilities.smirnoff import ForceFieldEditor
_logger = logging.getLogger(__name__)
[docs]class ForceBalanceOptimizer(BaseOptimizer):
"""
An optimizer class which controls the interface with ForceBalance.
"""
[docs] @classmethod
def name(cls) -> str:
return "ForceBalance"
[docs] @classmethod
def description(cls) -> str:
return (
"A systematic force field optimization tool: "
"https://github.com/leeping/forcebalance"
)
[docs] @classmethod
def provenance(cls) -> Dict:
"""
Collect the provenance information for forcebalance.
"""
import forcebalance
import openff.toolkit
versions = {
"forcebalance": forcebalance.__version__,
"openff.toolkit": openff.toolkit.__version__,
}
try:
import openeye
versions["openeye"] = openeye.__version__
except ImportError:
pass
ambertools_version = get_ambertools_version()
if ambertools_version is not None:
versions["ambertools"] = ambertools_version
return versions
[docs] @classmethod
def is_available(cls) -> bool:
try:
importlib.import_module("forcebalance")
return True
except ImportError:
return False
@classmethod
def _schema_class(cls):
return ForceBalanceSchema
@classmethod
def _prepare(
cls,
schema: OptimizationStageSchema,
initial_force_field: ForceField,
root_directory: str,
):
"""The internal implementation of the main ``prepare`` method. The input
``schema`` is assumed to have been validated before being passed to this
method.
"""
_logger.info(f"making new fb folders in {root_directory}")
ForceBalanceInputFactory.generate(root_directory, schema, initial_force_field)
@classmethod
def _optimize(
cls, schema: OptimizationStageSchema, initial_force_field: ForceField
) -> OptimizationStageResults:
with open("log.txt", "w") as log:
_logger.debug("Launching Forcebalance")
subprocess.run(
"ForceBalance optimize.in",
shell=True,
stdout=log,
stderr=log,
)
results = cls._collect_results("")
_logger.debug("OPT finished in folder", os.getcwd())
return results
@classmethod
def _collect_results(cls, root_directory: str) -> OptimizationStageResults:
"""Collect the results of a ForceBalance optimization.
Check the exit state of the optimization before attempting to update the final
smirks parameters.
Parameters
----------
root_directory
The path to the root directory of the ForceBalance optimization.
Returns
-------
The results of the optimization.
"""
results_dictionary = cls._read_output(root_directory)
force_field_editor = ForceFieldEditor(results_dictionary["forcefield"])
results = OptimizationStageResults(
provenance=cls.provenance(),
status=results_dictionary["status"],
error=results_dictionary["error"],
refit_force_field=None
if results_dictionary["error"] is not None
else force_field_editor.force_field.to_string(
discard_cosmetic_attributes=True
),
)
return results
@classmethod
def _read_output(cls, root_directory) -> Dict[str, Any]:
"""Read the output file of the ForceBalance job to determine the exit state of
the fitting and the name of the optimized force field.
Parameters
----------
root_directory
The path to the root directory of the ForceBalance optimization.
Returns
-------
Dict[str, str]
A dictionary containing the exit status of the optimization and the file
path to the optimized forcefield.
"""
result = {"error": None}
try:
with open(os.path.join(root_directory, "optimize.err")) as err:
errlog = err.read()
if "Traceback" in errlog:
raise ValueError(f"ForceBalance job failed: {errlog}")
except IOError:
pass
with open(os.path.join(root_directory, "optimize.out")) as log:
for line in log.readlines():
if "optimization converged" in line.lower():
result["status"] = "success"
break
elif "convergence failure" in line.lower():
result["status"] = "errored"
result["error"] = Error(
type="ConvergenceFailure",
message="The optimization failed to converge.",
)
break
else:
result["status"] = "running"
force_field_dir = os.path.join(root_directory, "result", "optimize")
result["forcefield"] = os.path.join(force_field_dir, "force-field.offxml")
return result
# register all of the available targets.
ForceBalanceOptimizer.register_target(AbInitioTargetSchema)
ForceBalanceOptimizer.register_target(TorsionProfileTargetSchema)
ForceBalanceOptimizer.register_target(OptGeoTargetSchema)
ForceBalanceOptimizer.register_target(VibrationTargetSchema)