1 """ @package forcebalance.evaluator 2 A target which employs the OpenFF Evaluator framework to compute condensed 3 phase physical properties. Currently only force fields in the OpenFF SMIRNOFF 6 author Yudong Qiu, Simon Boothroyd, Lee-Ping Wang 9 from __future__
import division, print_function
18 from forcebalance.output
import getLogger
19 from forcebalance.target
import Target
22 from openff.evaluator
import unit
23 from openff.evaluator.attributes
import UNDEFINED
24 from openff.evaluator.client
import EvaluatorClient, ConnectionOptions, RequestOptions
25 from openff.evaluator.datasets
import PhysicalPropertyDataSet
26 from openff.evaluator.utils.exceptions
import EvaluatorException
27 from openff.evaluator.utils.openmm
import openmm_quantity_to_pint
28 from openff.evaluator.utils.serialization
import TypedJSONDecoder, TypedJSONEncoder
29 from openff.evaluator.forcefield
import ParameterGradientKey
31 warn_once(
"Note: Failed to import the optional openff.evaluator package. ")
34 from openforcefield.typing.engines
import smirnoff
36 warn_once(
"Note: Failed to import the optional openforcefield package. ")
38 logger = getLogger(__name__)
42 """A custom optimisation target which employs the `openff-evaluator` 43 package to rapidly estimate a collection of condensed phase physical 44 properties at each optimisation epoch.""" 47 """Represents the set of options that a `Evaluator_SMIRNOFF` 48 target will be run with. 52 connection_options: openff.evaluator.client.ConnectionOptions 53 The options for how the `evaluator` client should 54 connect to a running server instance. 55 estimation_options: openff.evaluator.client.RequestOptions 56 The options for how properties should be estimated by the 57 `evaluator` (e.g. the uncertainties to which properties 60 The path to a JSON serialized PhysicalPropertyDataSet which 61 contains those physical properties which will be optimised 63 weights: dict of float 64 The weighting of each property which will be optimised against. 65 denominators: dict of str and unit.Quantity 66 The denominators will be used to remove units from the properties 67 and scale their values. 68 polling_interval: float 69 The time interval with which to check whether the evaluator has 70 finished fulfilling the request (in seconds). 85 """Converts this class into a JSON string. 90 The JSON representation of this class. 98 property_name: self.
weights[property_name]
99 for property_name
in self.
weights 112 separators=(
",",
": "),
113 cls=TypedJSONEncoder,
118 """Creates this class from a JSON string. 122 json_source: str or file-like object 123 The JSON representation of this class. 126 if isinstance(json_source, str):
127 with open(json_source,
"r") as file: 128 dictionary = json.load(file, cls=TypedJSONDecoder) 130 dictionary = json.load(json_source, cls=TypedJSONDecoder)
132 if "polling_interval" not in dictionary:
133 dictionary[
"polling_interval"] = 600
136 "connection_options" in dictionary
137 and "estimation_options" in dictionary
138 and "data_set_path" in dictionary
139 and "weights" in dictionary
140 and "denominators" in dictionary
141 and "polling_interval" in dictionary
146 value.connection_options = ConnectionOptions()
147 value.connection_options.__setstate__(dictionary[
"connection_options"])
149 value.estimation_options = RequestOptions()
150 value.estimation_options.__setstate__(dictionary[
"estimation_options"])
152 value.data_set_path = dictionary[
"data_set_path"]
155 property_name: dictionary[
"weights"][property_name]
156 for property_name
in dictionary[
"weights"]
158 value.denominators = {
159 property_name: dictionary[
"denominators"][property_name]
160 for property_name
in dictionary[
"denominators"]
163 value.polling_interval = dictionary[
"polling_interval"]
167 def __init__(self, options, tgt_opts, forcefield):
169 super(Evaluator_SMIRNOFF, self).
__init__(options, tgt_opts, forcefield)
195 self.set_option(tgt_opts,
"evaluator_input", forceprint=
True)
200 def _initialize(self):
201 """Initializes the evaluator target from an input json file. 203 1. Reads the user specified input file. 204 2. Creates a `evaluator` client object. 205 3. Loads in a reference experimental data set. 206 4. Assigns and normalises weights for each property. 210 print(os.path.join(self.tgtdir, self.evaluator_input))
211 options_file_path = os.path.join(self.tgtdir, self.evaluator_input)
214 for property_type, denominator
in self.
_options.denominators.items():
222 data_set_path = os.path.join(self.tgtdir, self.
_options.data_set_path)
228 "The physical property data set to optimise against is empty." 237 number_of_properties = {
239 for x
in property_types
250 property_type = physical_property.__class__.__name__
252 value = physical_property.value.to(self.
_default_units[property_type])
255 if physical_property.uncertainty != UNDEFINED:
257 uncertainty = physical_property.uncertainty.to(
263 physical_property.thermodynamic_state.temperature,
264 physical_property.thermodynamic_state.pressure,
267 dict_for_print[
"%s %s-%s" % tuple_key] =
"%s+/-%s" % (
273 dict_for_print, title=
"Reference %s data" % substance.identifier,
282 self.
_options.weights[property_type]
283 / number_of_properties[property_type]
286 def _parameter_value_from_gradient_key(self, gradient_key):
287 """Extracts the value of the parameter in the current 288 open force field object pointed to by a given 289 `ParameterGradientKey` object. 293 gradient_key: openff.evaluator.forcefield.ParameterGradientKey 294 The gradient key which points to the parameter of interest. 299 The value of the parameter. 301 Returns True if the parameter is a cosmetic one. 304 parameter_handler = self.FF.openff_forcefield.get_parameter_handler(
307 parameter = parameter_handler.parameters[gradient_key.smirks]
309 attribute_split = re.split(
r"(\d+)", gradient_key.attribute)
310 attribute_split = list(filter(
None, attribute_split))
312 parameter_attribute =
None 313 parameter_value =
None 315 if hasattr(parameter, gradient_key.attribute):
317 parameter_attribute = gradient_key.attribute
318 parameter_value = getattr(parameter, parameter_attribute)
320 elif len(attribute_split) == 2:
322 parameter_attribute = attribute_split[0]
324 if hasattr(parameter, parameter_attribute):
325 parameter_index = int(attribute_split[1]) - 1
327 parameter_value_list = getattr(parameter, parameter_attribute)
328 parameter_value = parameter_value_list[parameter_index]
333 parameter_attribute
is None 334 or parameter_attribute
in parameter._cosmetic_attribs
338 return openmm_quantity_to_pint(parameter_value), is_cosmetic
340 def _extract_physical_parameter_values(self):
341 """Extracts an array of the values of the physical parameters 342 (which are not cosmetic) from the current `FF.openff_forcefield` 348 The array of values of shape (len(self._gradient_key_mappings),) 358 parameter_values[parameter_index] = parameter_value.to(
362 return parameter_values
364 def _build_pvals_jacobian(self, mvals, perturbation_amount=1.0e-4):
365 """Build the matrix which maps the gradients of properties with 366 respect to physical parameters to gradients with respect to 367 force balance mathematical parameters. 372 The current force balance mathematical parameters. 373 perturbation_amount: float 374 The amount to perturb the mathematical parameters by 375 when calculating the finite difference gradients. 380 A matrix of d(Physical Parameter)/d(Mathematical Parameter). 385 for index
in range(len(mvals)):
387 reverse_mvals = mvals.copy()
388 reverse_mvals[index] -= perturbation_amount
390 self.FF.make(reverse_mvals)
393 forward_mvals = mvals.copy()
394 forward_mvals[index] += perturbation_amount
396 self.FF.make(forward_mvals)
399 gradients = (forward_physical_values - reverse_physical_values) / (
400 2.0 * perturbation_amount
402 jacobian_list.append(gradients)
407 jacobian = np.array(jacobian_list)
410 def submit_jobs(self, mvals, AGrad=True, AHess=True):
412 Submit jobs for evaluating the objective function 417 mvals array containing the math values of the parameters 419 Flag for computing gradients of not 421 Flag for computing hessian or not 425 1. This function is called before wq_complete() and get(). 426 2. This function should not block. 432 force_field = smirnoff.ForceField(
433 self.FF.offxml, allow_cosmetic_attributes=
True 437 with tempfile.NamedTemporaryFile(mode=
"w", suffix=
".offxml")
as file:
438 force_field.to_file(file.name, discard_cosmetic_attributes=
True)
439 force_field = smirnoff.ForceField(file.name)
442 parameter_gradient_keys = []
451 for field_list
in self.FF.pfields:
453 string_key = field_list[0]
454 key_split = string_key.split(
"/")
456 parameter_tag = key_split[0].strip()
457 parameter_smirks = key_split[3].strip()
458 parameter_attribute = key_split[2].strip()
461 parameter_gradient_key = ParameterGradientKey(
463 smirks=parameter_smirks,
464 attribute=parameter_attribute,
469 parameter_gradient_key
472 if parameter_value
is None or is_cosmetic:
476 parameter_unit = parameter_value.units
477 parameter_gradient_keys.append(parameter_gradient_key)
487 force_field_source=force_field,
488 options=self.
_options.estimation_options,
489 parameter_gradient_keys=parameter_gradient_keys,
493 "Requesting the estimation of {} properties, and their " 494 "gradients with respect to {} parameters.\n".format(
501 True, polling_interval=self.
_options.polling_interval
506 "No `EvaluatorServer` could be found to submit the calculations to. " 507 "Please double check that a server is running, and that the connection " 508 "settings specified in the input script are correct." 512 def _check_estimation_request(estimation_request):
513 """Checks whether an estimation request has finished with any exceptions. 517 estimation_request: openff.evaluator.client.Request 518 The request to check. 520 results, _ = estimation_request.results()
523 raise ValueError(
"Trying to extract the results of an unfinished request.")
527 if isinstance(results, EvaluatorException):
530 "An uncaught exception occured within the evaluator " 531 "framework: %s" % str(results)
534 if len(results.unsuccessful_properties) > 0:
536 exceptions =
"\n".join(str(result)
for result
in results.exceptions)
539 "Some properties could not be estimated:\n\n%s." % exceptions
542 def _extract_property_data(self, estimation_request, mvals, AGrad):
543 """Extract the property estimates #and their gradients# 544 from a relevant evaluator request object. 548 estimation_request: openff.evaluator.client.Request 549 The request to extract the data from. 553 estimated_data: openff.evaluator.datasets.PhysicalPropertyDataSet 554 The data set of estimated properties. 555 estimated_gradients: dict of str and np.array 556 The estimated gradients in a dictionary with keys of the estimated 557 properties unique ids, and values of the properties gradients of shape 561 Evaluator_SMIRNOFF._check_estimation_request(estimation_request)
564 results, _ = estimation_request.results()
567 results_path = os.path.join(self.root, self.rundir,
"results.json")
568 results.json(results_path)
571 calculation_layer_counts = {}
573 for physical_property
in results.estimated_properties:
575 calculation_layer = physical_property.source.fidelity
577 if calculation_layer
not in calculation_layer_counts:
578 calculation_layer_counts[calculation_layer] = 0
580 calculation_layer_counts[calculation_layer] += 1
584 for layer_type
in calculation_layer_counts:
586 count = calculation_layer_counts[layer_type]
589 "{} properties were estimated using the {} layer.\n".format(
596 if len(results.exceptions) > 0:
598 exceptions =
"\n\n".join(str(result)
for result
in results.exceptions)
599 exceptions = exceptions.replace(
"\\n",
"\n")
605 exceptions_path = os.path.join(
606 self.root, self.rundir,
"non_fatal_exceptions.txt" 609 with open(exceptions_path,
"w")
as file:
610 file.write(exceptions)
613 "A number of non-fatal exceptions occurred. These were saved to " 614 "the %s file." % exceptions_path
617 estimated_gradients = {}
620 return results.estimated_properties, estimated_gradients
627 for physical_property
in results.estimated_properties:
629 property_class = physical_property.__class__.__name__
631 estimated_gradients[physical_property.id] = np.zeros(
635 for gradient
in physical_property.gradients:
644 if isinstance(gradient.value, unit.Quantity):
645 gradient_value = gradient.value.to(gradient_unit).magnitude
647 gradient_value = gradient.value
648 assert isinstance(gradient_value, float)
650 estimated_gradients[physical_property.id][
654 for property_id
in estimated_gradients:
656 pval_gradients = estimated_gradients[property_id]
657 mval_gradients = np.matmul(jacobian, pval_gradients)
659 estimated_gradients[property_id] = mval_gradients
661 return results.estimated_properties, estimated_gradients
665 Check if all jobs are finished 666 This function should have a sleep in it if not finished. 671 True if all jobs are finished, False if not 677 isinstance(estimation_results, EvaluatorException)
678 or len(estimation_results.queued_properties) == 0
681 def get(self, mvals, AGrad=True, AHess=True):
683 Get the objective function value, gradient, and hessian 688 mvals array containing the math values of the parameters 690 Flag for computing gradients of not 692 Flag for computing hessian or not 697 Answer = {'X':obj_value, 'G':obj_grad, 'H':obj_hess} 699 obj_grad: np.ndarray of shape (n_param, ) 700 obj_hess: np.ndarray of shape (n_param, n_param) 704 1. obj_grad is all zero when AGrad == False 705 2. obj_hess is all zero when AHess == False or AGrad == False, because the 706 hessian estimate depends on gradients 720 obj_grad = np.zeros(self.FF.np)
721 obj_hess = np.zeros((self.FF.np, self.FF.np))
731 self.
_options.denominators[property_type]
742 reference_value = reference_property.value.to(
746 target_property = next(
747 x
for x
in estimated_data_set
if x.id == reference_property.id
749 target_value = target_property.value.to(
753 target_error = np.nan
755 if target_property.uncertainty != UNDEFINED:
757 target_error = target_property.uncertainty.to(
761 diff = target_value - reference_value
763 obj_contrib = weight * (diff / denominator) ** 2
764 obj_value += obj_contrib
766 temperature = reference_property.thermodynamic_state.temperature
767 pressure = reference_property.thermodynamic_state.pressure
771 temperature.to(unit.kelvin),
772 pressure.to(unit.atmosphere),
773 target_property.substance.identifier,
788 grad_array = estimated_gradients[reference_property.id]
790 obj_grad += 2.0 * weight * diff * grad_array / denominator ** 2
796 * (np.outer(grad_array, grad_array))
800 return {
"X": obj_value,
"G": obj_grad,
"H": obj_hess}
804 print information into the output file about the last objective function evaluated 805 This function should be called after get() 810 % detail[:3]:
"%9.3f %14.3f +- %-7.3f % 7.3f % 9.5f % 9.5f % 9.5f " 812 for detail
in details
815 "%s %s\nTemperature Pressure Substance Reference Calculated +- " 816 "Stdev Delta Weight Denom Term " % (self.name, property_name)
819 dict_for_print, title=title, bold=
True, color=4, keywidth=15
Nifty functions, intended to be imported by any module within ForceBalance.
def _build_pvals_jacobian(self, mvals, perturbation_amount=1.0e-4)
Build the matrix which maps the gradients of properties with respect to physical parameters to gradie...
def wq_complete(self)
Check if all jobs are finished This function should have a sleep in it if not finished.
_pending_estimate_request
def submit_jobs(self, mvals, AGrad=True, AHess=True)
Submit jobs for evaluating the objective function.
def __init__(self, options, tgt_opts, forcefield)
def _initialize(self)
Initializes the evaluator target from an input json file.
Represents the set of options that a Evaluator_SMIRNOFF target will be run with.
def to_json(self)
Converts this class into a JSON string.
def get(self, mvals, AGrad=True, AHess=True)
Get the objective function value, gradient, and hessian.
def printcool_dictionary(Dict, title="Dictionary Keys : Values", bold=False, color=2, keywidth=25, topwidth=50, center=True, leftpad=0)
See documentation for printcool; this is a nice way to print out keys/values in a dictionary...
A custom optimisation target which employs the openff-evaluator package to rapidly estimate a collect...
def printcool(text, sym="#", bold=False, color=2, ansi=None, bottom='-', minwidth=50, center=True, sym2="=")
Cool-looking printout for slick formatting of output.
def _extract_physical_parameter_values(self)
Extracts an array of the values of the physical parameters (which are not cosmetic) from the current ...
def indicate(self)
print information into the output file about the last objective function evaluated This function shou...
def warn_once(warning, warnhash=None)
Prints a warning but will only do so once in a given run.
def _extract_property_data(self, estimation_request, mvals, AGrad)
Extract the property estimates #and their gradients# from a relevant evaluator request object...
def from_json(cls, json_source)
Creates this class from a JSON string.
def _parameter_value_from_gradient_key(self, gradient_key)
Extracts the value of the parameter in the current open force field object pointed to by a given Para...