Source code for optbayesexpt.obe_server
from optbayesexpt.obe_socket import Socket
import numpy as np
[docs]
class OBE_Server(Socket):
"""A TCP socket interface for OptBayesExpt using JSON strings
This class provides communication between an OptBayesExpt object and
user software running in a separate process. Instrumentation software is
not always written in python; the idea of this class is to allow
experiments to use OptBayesExpt from their native languages by issuing
command messages in JSON :obj:`object` format through TCP sockets. The
available commands are documented below in the ``run()`` method.
Args:
initial_args (:obj:`tuple`) Information needed for making OBE objects.
Requires a tuple with the following structure:
0. Model_function (python function)
1. Settings (tuple of settings arrays)
2. Parameter samples (tuple of parameter sample arrays)
3. Constants (tuple of constants)
ip_address(:obj:`str`): an IP address for TCP communications.
Default '127.0.0.1'.
port(:obj:`int`): a TCP port number to use for communications.
Default 61981.
Keyword Args:
**kwargs are passed to the specified OBE_class's `` __init__`` function.
Attributes:
obe_engine (:obj:`OptBayesExpt`): The disembodied brains of the
experimental design scheme. ``OBE_server`` provides TCP
communication between ``obe_engine`` and a client program.
initial_args (:obj:`tuple`): Stored samples from an
initial parameter distribution. Initialized by ``self.__init__()``
and ``self.make_obe()``.
initial_kwargs (:obj:`dict`): keyword arguments stored for future
OBE_class intantiation.
Notes:
1. The messages sent between the client and this server are expected
to be JSON strings, each prependend by the string length formatted
as a 10-digit number. See the obe_socket module docs for
details. Unless otherwise stated the server will reply with
:code:`'0000000004"OK"'` upon successful completion of the command.
2. In pre-v1.0.0 version ``OBE_Server`` was a child class of
``OptBayesExpt``. In versions 1.0.0 and later,
the ``OBE_class`` is an
attribute of ``OBE_Server``. The advantage of the new
arrangement is that OBE_Server can manage a sequence of
experimental runs with differently configured
``OptBayesExpt`` objects.
"""
def __init__(self, initial_args=(),
ip_address='127.0.0.1', port=61981, **kwargs):
Socket.__init__(self, 'server', ip_address=ip_address, port=port)
if initial_args:
self.initial_args = initial_args
if kwargs:
self.initial_kwargs = kwargs
else:
self.initial_kwargs = None
self.obe_engine = None
[docs]
def make_obe(self, obe_class, class_args, **kwargs):
"""Creates and attaches a new OptBayesExpt-like object
A server may need to handle several runs. This function allows
OBE_Server to instantiate new OptBayesExpt objects from scratch.
Enables a server to start a new experimental run with modified
starting conditions.
Args:
obe_class (:obj:`OptBayesExpt`-like) A class reference,
e.g. ``optbayesexpt.OptBayesExpt`` without parentheses
class_args (:obj:`tuple`): the arguments to the obe_class. For
example, ``(model_function, settings, parameters, cons)``.
"""
# save the arguments for possible reuse
# these are owned by the OBE_Server instance as a "birth record"
if class_args:
self.initial_args = class_args
if kwargs:
self.initial_kwargs = kwargs
# create a new OptBayesExpt and attach it as the obe_engine attribute.
self.obe_engine = obe_class(*self.initial_args, **kwargs)
[docs]
def newrun(self, message):
"""A stub to allow customized TCP commands
Invoked by the ``run`` method when a message with a ``'newrun'``
command string is received. The idea is to provide flexible control
from the experiment program. The original intent was to provide a
way for the user program to start a fresh measurement, perhaps with
different setting ranges or a different prior. However, with access
to the OBE_Server and its OptBayesExpt through the ``self``
argument, and with any information the user chooses to send from the
client program, there are many more possibilities.
Args:
self (:obj:`optbayesexpt.OBE_Server`): provides access to the
attributes of ``self`` and also its OptBayesExpt attribute.
message (:obj:`tuple`): User defined information passed from the
client program.
Returns: User defined.
"""
pass
[docs]
def run(self):
"""Listens and responds to TCP messages
Enters a continuous loop, interpreting incoming messages as python
:obj:`dict` objects and responding to command strings found in
``message[ "command"]``. Only one command string is allowed in each
message. Valid command strings are listed below.
**'done'**
Stops the :code:`OBE_server` and allows the server script to
complete.
:code:`{"command": "done"}`
Warning:
It may be important for the client to issue the :code:`done`
command. If the server is allowed to run, it will continue to
use the TCP socket it was assigned during initialization. Later
instances of :code:`OBE_Server` may conflict.
**'getcon'**
Reports the :code:`OptBayesExpt.cons` attribute. See also
getset and getpar
:code:`{"command": "getcon"}`
Reply: a list of model constants.
**'getcov'**
Reports the covariance matrix of the parameter distribution as a
JSON array of arrays.
:code:`{"command": "getcov"}`
Reply: JSON formatted array of arrays
**'getmean'**
Reports the mean value of the parameter distribution as a JSON
array. See also getstd and getcov
:code:`{"command": "getmean"}`
Reply: JSON formatted array.
**'getpar'**
Reports the :code:`OptBayesExpt.parameters` arrays representing
samples from the probability distribution.
:code:`{"command": "getset"}`
Reply: a list of parameter value lists.
**'getset'**
Reports the :code:`OptBayesExpt.sets` attribute
:code:`{"command": "getset"}`
Reply: a list of setting value lists.
**'getstd'**
Reports the standard deviation of the parameter distribution as
a JSON array.
:code:`{"command": "getstd"}`
Reply: JSON formatted array
**'getwgt'**
Reports the particle weights of the probability distribution as
a JSON array.
:code:`{"command": "getwgt"}`
Reply: particle weights as a JSON formatted array
**'goodset'**
Generated measurement settings. Calculates the utility of
possible settings and reports a random selection that is
weighted by the utility. Invokes
:code:`OptBayesExpt.good_setting()`. See also optset.
:code:`{"command": "goodset"[, "pickiness", <integer>]}`
Reply: a list of setting values.
**'newdat'**
Processes measurement results. Refines the parameter probability
distribution based on the *likelihood* of measurement results
reported in the command. Invokes :code:`OptBayesExpt.pdf_update()`.
:code:`{"command": "newdat", "x": <settings tuple>,
"y": <measured values tuple>, "s": <uncertainty tuple>}`
Required components include:
* "x": a tuple containing the measurement settings.
* "y": a tuple of measurement mean values.
* "s": uncertainty of the measurement as a standard deviation.
**'newrun'**
A command string to run the user-defined
:code:`OBE_Server.newrun()` method. The message string must
conform to the 10 digits + JSON object format, and it must avoid
using other defined command strings. Except for those
restrictions, any ``<keyword>: <string>`` pairs are allowed.
:code:`{"command": "newrun"[, ...]}`
See the ``newrun()`` method documentation, above.
**'optset'**
Requests optimal measurement settings. Calculates the utility
of possible settings and reports the settings having the maximum
utility. Invokes :code:`OptBayesExpt.opt_setting()`. See also
goodset.
:code:`{"command": "optset"}`
Reply: a list of setting values.
**'ready'**
Returns 'OK'. Useful for checking communication.
"""
print()
print('SERVER READY')
while True:
# use the Socket.receive() method to get the incoming message
# from client
message = self.receive()
# the messages sent by the client are encoded as json objects
# Decoded, by the ``receive``() method, they are python dicts.
# These get* commands request arrays. Numpy arrays must be
# converted to lists
if 'getset' in message['command']:
self.send(np.array(self.obe_engine.allsettings).tolist())
elif 'getpar' in message['command']:
self.send(self.obe_engine.parameters.tolist())
elif 'getcon' in message['command']:
self.send(self.obe_engine.cons)
elif 'getwgt' in message['command']:
self.send(self.obe_engine.particle_weights.tolist())
# run-time commands
elif 'newrun' in message['command']:
self.newrun(message)
self.send('OK')
elif 'optset' in message['command']:
self.send(self.obe_engine.opt_setting())
elif 'goodset' in message['command']:
if 'pickiness' in list(message):
self.send(self.obe_engine.good_setting(
pickiness=message['pickiness']))
else:
self.send(self.obe_engine.good_setting())
elif 'newdat' in message['command']:
self.obe_engine.pdf_update(
(message['x'], message['y'], message['s']))
self.send('OK')
# report statistics
# Note: json.dumps() is not able to format numpy arrays. To
# send numpy arrays, the :code:`numpy.tolist()` is used to
# list-ify a numpy arrays.
elif 'getpdf' in message['command']:
self.send(self.obe_engine.parameters.tolist())
elif 'getwgt' in message['command']:
self.send(self.obe_engine.particle_weights.tolist())
elif 'getmean' in message['command']:
mean = self.obe_engine.mean()
self.send(mean.tolist())
elif 'getstd' in message['command']:
std = self.obe_engine.std()
self.send(std.tolist())
elif 'getcov' in message['command']:
cov = self.obe_engine.covariance()
self.send(cov.tolist())
elif 'ready' in message['command']:
self.send('OK')
elif 'done' in message['command']:
self.send('OK')
break
else:
# the incoming message wasn't interpreted
pass
# end of run() method.
# end of OBE_Server definition