support/gym_remote/bridge.py (295 lines of code) (raw):
import gym
import gym.spaces
import json
import numpy as np
import os
import socket
gym_version = tuple(int(x) for x in gym.__version__.split('.'))
class Channel:
def __init__(self):
self.sock = None
self.dirty = False
self._value = None
self.annotations = {}
def set_socket(self, sock):
self.sock = sock
def set_base(self, base):
pass
def parse(self, value):
return value
def unparse(self, value):
return value
@property
def value(self):
return self.unparse(self._value)
@value.setter
def value(self, value):
self._value = self.parse(value)
self.dirty = True
def serialize(self):
return self._value
def deserialize(self, value):
self._value = self.parse(value)
self.dirty = False
@staticmethod
def make(type, shape, annotations):
types = {
'int': IntChannel,
'float': FloatChannel,
'bool': BoolChannel,
'int_fold': IntFoldChannel,
'np': NpChannel,
}
cls = types[type]
if shape:
ob = cls(*eval(shape, {}, {'dtype': np.dtype}))
else:
ob = cls()
if annotations:
for key, value in annotations.items():
ob.annotate(key, value)
return ob
def annotate(self, name, value):
self.annotations[name] = str(value)
class IntChannel(Channel):
TYPE = 'int'
SHAPE = None
def parse(self, value):
return int(value)
class FloatChannel(Channel):
TYPE = 'float'
SHAPE = None
def parse(self, value):
return float(value)
class BoolChannel(Channel):
TYPE = 'bool'
SHAPE = None
def parse(self, value):
return bool(value)
class IntFoldChannel(Channel):
TYPE = 'int_fold'
def __init__(self, folds, dtype=np.int8):
super(IntFoldChannel, self).__init__()
self.folds = np.multiply.accumulate([1] + list(folds)[:-1], dtype=int)
self.ranges = np.array(folds, dtype=int)
self.dtype = dtype
self.SHAPE = str(folds) + ','
def parse(self, value):
folded = np.dot(self.folds, value % self.ranges)
return int(folded)
def unparse(self, value):
if value is None:
return None
unfolded = np.full(self.ranges.shape, value) // self.folds % self.ranges
return unfolded.astype(self.dtype)
def deserialize(self, value):
self._value = int(value)
self.dirty = False
class NpChannel(Channel):
TYPE = 'np'
def __init__(self, shape, dtype):
super(NpChannel, self).__init__()
self.SHAPE = '%s, %s' % (shape, 'dtype("%s")' % np.dtype(dtype).str)
self.shape = shape
self.dtype = dtype
def set_base(self, base):
self._value = np.memmap(base, mode='w+', dtype=self.dtype, shape=self.shape)
@property
def value(self):
return self._value
@value.setter
def value(self, value):
np.copyto(self._value, value)
self.dirty = True
def serialize(self):
return True
def deserialize(self, value):
self.dirty = False
class Bridge:
Timeout = socket.timeout
Closed = BrokenPipeError
def __init__(self, base):
self.base = base
self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
def close(message):
self.close()
if 'exception' in message:
import gym_remote.exceptions as gre
exception = gre.make(message['exception'], message['reason'])
else:
exception = self.Closed(message['reason'])
raise exception
def exception(message):
import gym_remote.exceptions as gre
raise gre.make(message['exception'], message['reason'])
self._channels = {}
self.connection = None
self._buffer = []
self._message_handlers = {
'update': self.update_vars,
'close': close,
'exception': exception
}
def __del__(self):
self.close()
def add_channel(self, name, channel):
if name in self._channels:
raise KeyError(name)
self._channels[name] = channel
channel.set_base(os.path.join(self.base, name))
return channel
def wrap(self, name, space):
channel = None
if isinstance(space, gym.spaces.MultiBinary):
if space.n < 64:
channel = IntFoldChannel([2] * space.n, np.uint8)
else:
channel = NpChannel((space.n,), np.uint8)
channel.annotate('n', space.n)
channel.annotate('type', 'MultiBinary')
elif isinstance(space, gym.spaces.Discrete):
channel = IntChannel()
channel.annotate('n', space.n)
channel.annotate('type', 'Discrete')
elif isinstance(space, gym.spaces.MultiDiscrete):
if gym_version >= (0, 9, 6):
channel = NpChannel(space.shape, np.int64)
channel.annotate('shape', space.shape[0])
else:
channel = NpChannel((space.shape,), np.int64)
channel.annotate('shape', space.shape)
channel.annotate('type', 'MultiDiscrete')
elif isinstance(space, gym.spaces.Box):
channel = NpChannel(space.shape, space.high.dtype)
channel.annotate('type', 'Box')
channel.annotate('shape', space.shape)
if not channel:
raise NotImplementedError('Unsupported space')
return self.add_channel(name, channel)
@staticmethod
def unwrap(space):
if space.annotations['type'] == 'MultiBinary':
return gym.spaces.MultiBinary(int(space.annotations['n']))
if space.annotations['type'] == 'Discrete':
return gym.spaces.Discrete(int(space.annotations['n']))
if space.annotations['type'] == 'MultiDiscrete':
if gym_version >= (0, 9, 6):
return gym.spaces.MultiDiscrete(space.shape[0])
else:
return gym.spaces.MultiDiscrete(space.shape)
if space.annotations['type'] == 'Box':
kwargs = {}
if gym_version >= (0, 9, 6):
kwargs['dtype'] = space.dtype
return gym.spaces.Box(low=0, high=255, shape=space.shape, **kwargs)
def configure_channels(self, channel_info):
for name, info in channel_info.items():
self._channels[name] = Channel.make(*info)
def describe_channels(self):
description = {}
for name, channel in self._channels.items():
description[name] = (channel.TYPE, channel.SHAPE, channel.annotations)
return description
def listen(self):
sock_path = os.path.join(self.base, 'sock')
self.sock.bind(sock_path)
self.sock.listen(1)
def connect(self):
sock_path = os.path.join(self.base, 'sock')
self.sock.connect(sock_path)
self.connection = self.sock
def server_accept(self):
self.connection, _ = self.sock.accept()
for name, channel in self._channels.items():
channel.set_socket(self.connection)
description = self.describe_channels()
self._send_message('description', description)
def configure_client(self):
description = self._recv_message()
assert description['type'] == 'description'
self.configure_channels(description['content'])
for name, channel in self._channels.items():
channel.set_socket(self.connection)
channel.set_base(os.path.join(self.base, name))
return dict(self._channels)
def _try_send(self, type, content):
try:
self._send_message(type, content)
except self.Closed as e:
try:
while True:
self.recv()
except self.Closed as f:
e = f
self.close()
raise e
def _send_message(self, type, content):
if not self.connection:
raise self.Closed
message = {
'type': type,
'content': content
}
# All messages end in a form feed
message = json.dumps(message) + '\f'
self.connection.sendall(message.encode('utf8'))
def _recv_message(self):
if not self.connection:
raise self.Closed
while len(self._buffer) < 2:
# There are no fully buffered messages
message = self.connection.recv(4096)
if not message:
raise self.Closed
message = message.split(b'\f')
if self._buffer:
self._buffer[-1] += message.pop(0)
self._buffer.extend(message)
message = self._buffer.pop(0)
return json.loads(message.decode('utf8'))
def update_vars(self, vars):
for name, value in vars.items():
self._channels[name].deserialize(value)
def send(self):
content = {}
for name, channel in self._channels.items():
if channel.dirty:
content[name] = channel.serialize()
self._try_send('update', content)
def recv(self):
message = self._recv_message()
if not message:
raise self.Closed
self._message_handlers[message['type']](message['content'])
return True
def close(self, reason=None, exception=None):
if self.sock:
try:
kwargs = {'reason': reason}
if exception:
kwargs['exception'] = exception.ID
self._send_message('close', kwargs)
except self.Closed:
pass
self.sock.close()
if self.sock and self.connection != self.sock:
if self.connection:
self.connection.close()
try:
os.unlink(os.path.join(self.base, 'sock'))
except OSError:
pass
for name, channel in self._channels.items():
try:
os.unlink(os.path.join(self.base, name))
except OSError:
pass
self.connection = None
self.sock = None
def exception(self, exception, reason=None):
content = {'reason': reason, 'exception': exception.ID}
self._try_send('exception', content)
def settimeout(self, timeout):
self.sock.settimeout(timeout)
if self.connection:
self.connection.settimeout(timeout)
def __del__(self):
self.close()