Source code for SimulationPotential.SimulationPotential

from multiprocessing.connection import Listener
import math
import logging

from ase.calculators.emt import EMT

from SimulationUtilities import Configuration_Processing
from SimulationUtilities.Communication_Codes import comm_code


[docs]class SimulationPotential: """ The purpose of this object is to provide an independent service that computes the forces and potential energy for the system. The object listens for a TCP connection containing a molecular configuration. The forces and potential energy are then sent using TCP via the Listener object provided by the multiprocessing package. If a kill code is received then the server shuts down. Attributes: CONFIGURATION (dict) : A dictionary containing the parsed values from the file in configuration_file. ADDRESS (str, int) : A tuple containing a string representing the hostname/IP and an integer for the service port. AUTHKEY (str) : A string containing the authorisation key for the listener method. """
[docs] def __init__(self, configuration_file, logfile=None, log_level=logging.INFO, hostname='localhost', port=5001, authkey='password'): """The constructor for the SimulationPotential class. Note: This class is intended to be used in conjunction with running SimulationServer and SimulationClient objects. See the run_potential_server method for more details. Args: configuration_file (str) : Directory and filename of the configuration file. logfile (str, optional) : Directory and filename of the log file. Is created if doesn't exist, overwritten if it does. log_level (int, optional) : Specify level of logging required as described in the logging package documentation. hostname (str, optional) : Hostname/IP which the SimulationPotential will run on. Default value is 'localhost' to run simulations locally. port (int, optional) : Port number which the SimulationPotential will run on. Default value is 5000, just because. authkey (str, optional) : Authentication key used to secure process communications. Default to None for local computations to increase speed. """ # Set the SimulationPotential log output to write to logfile at prescribed log level if specified. Otherwise # write to console output. Setting to DEBUG will cause poor performance and should only be used to debug. if logfile is not None: logging.basicConfig(filename=logfile, level=log_level, filemode='w') else: logging.basicConfig(level=logging.INFO) # Read configuration from configuration_file and store in SimulationPotential's CONFIGURATION attribute. self.CONFIGURATION = Configuration_Processing.read_configuration_file(configuration_file) # Set ADDRESS and AUTHKEY attributes for Listener object in the run_simulation method. self.ADDRESS = (hostname, port) self.AUTHKEY = authkey
[docs] def run_potential_server(self, small_number=1e-12): """Start the instance of SimulationPotential ready to receive requests for metric values. Note: This class assumes that after starting there will be at least one running instance of SimulationClient pointing to ADDRESS, otherwise this process will remain indefinitely blocked. Args: small_number (str) : A small number used to represent the zero metric value. This is used instead of zero to prevent divide by zero errors when evaluating the gradient of the metric. """ # Extract the ASE atoms object for molecule and set the calculator to the pure Python EMT implementation molecule = self.CONFIGURATION['molecule'] molecule.set_calculator(EMT()) # Set up the listener for communication at ADDRESS. This doesn't queue connections as it is **assumed** that # each SimulationClient has it's own pool of SimulationPotential servers. logging.info('Starting Potential Server on %s', str(self.ADDRESS)) server = Listener(self.ADDRESS, authkey=self.AUTHKEY) while True: # The Simulation server only receives requests from running instances of SimulationServer objects. The # SimulationServer waits for an incoming connection and is blocked until it receives one. logging.debug('Listening for connection from SimulationClient instance...') client = server.accept() # Receive request from a SimulationClient. Request is in form of dictionary that is stored in # client_response logging.debug('Connection to SimulationClient received from %s.', server.last_accepted) client_response = client.recv() # Prepare/reset list container for server response values = [] # This is the main decision logic for handling a connection from a client server. # If the SimulationClient indicates it is providing points to evaluate the metric on then compute those # values and prepare a response. if client_response['status_code'] == comm_code('CLIENT_PROVIDES_POINT'): logging.debug('Client provides point data to evaluate.') # Close connection before computing metric values. To free up network traffic. client.close() # For each point received in client request, update the positions in our ASE atoms object and then # compute the potential energy and forces. for point in client_response['points']: molecule.set_positions(Configuration_Processing.convert_vector_to_atoms(point[0])) values.append([[math.sqrt(max([self.CONFIGURATION['metric_parameters'][0] - molecule.get_potential_energy(), small_number])), Configuration_Processing.convert_atoms_to_vector(molecule.get_forces())], point[1]]) # Compile response ready for when client asks for it. server_response = {'status_code': comm_code('SERVER_PROVIDES_VALUES'), 'values': values} elif client_response['status_code'] == comm_code('CLIENT_ASKS_FOR_VALUES'): # Send computed potential energies and forces to client. logging.debug('Client requests result from computation.') client.send(server_response) client.close() elif client_response['status_code'] == comm_code('KILL'): # Break main loop of SimulationPotential and subsequently close the server. logging.debug('Shut-down signal received.') break # Close the SimulationPotential logging.info('Shutting down SimulationPotential.') server.close()