gym_recording/recording.py (100 lines of code) (raw):
import os
import time
import json
import glob
import logging
import numpy as np
import gym
from gym import error
from gym.utils import atomic_write, closer
logger = logging.getLogger(__name__)
class TraceRecording(object):
_id_counter = 0
def __init__(self, directory=None):
"""
Create a TraceRecording, writing into directory
"""
if directory is None:
directory = os.path.join('/tmp', 'openai.gym.{}.{}'.format(time.time(), os.getpid()))
os.mkdir(directory)
self.directory = directory
self.file_prefix = 'openaigym.trace.{}.{}'.format(self._id_counter, os.getpid())
TraceRecording._id_counter += 1
self.closed = False
self.actions = []
self.observations = []
self.rewards = []
self.episode_id = 0
self.buffered_step_count = 0
self.buffer_batch_size = 100
self.episodes_first = 0
self.episodes = []
self.batches = []
def add_reset(self, observation):
assert not self.closed
self.end_episode()
self.observations.append(observation)
def add_step(self, action, observation, reward):
assert not self.closed
self.actions.append(action)
self.observations.append(observation)
self.rewards.append(reward)
self.buffered_step_count += 1
def end_episode(self):
"""
if len(observations) == 0, nothing has happened yet.
If len(observations) == 1, then len(actions) == 0, and we have only called reset and done a null episode.
"""
if len(self.observations) > 0:
if len(self.episodes)==0:
self.episodes_first = self.episode_id
self.episodes.append({
'actions': optimize_list_of_ndarrays(self.actions),
'observations': optimize_list_of_ndarrays(self.observations),
'rewards': optimize_list_of_ndarrays(self.rewards),
})
self.actions = []
self.observations = []
self.rewards = []
self.episode_id += 1
if self.buffered_step_count >= self.buffer_batch_size:
self.save_complete()
def save_complete(self):
"""
Save the latest batch and write a manifest listing all the batches.
We save the arrays as raw binary, in a format compatible with np.load.
We could possibly use numpy's compressed format, but the large observations we care about (VNC screens)
don't compress much, only by 30%, and it's a goal to be able to read the files from C++ or a browser someday.
"""
batch_fn = '{}.ep{:09}.json'.format(self.file_prefix, self.episodes_first)
bin_fn = '{}.ep{:09}.bin'.format(self.file_prefix, self.episodes_first)
with atomic_write.atomic_write(os.path.join(self.directory, batch_fn), False) as batch_f:
with atomic_write.atomic_write(os.path.join(self.directory, bin_fn), True) as bin_f:
def json_encode(obj):
if isinstance(obj, np.ndarray):
offset = bin_f.tell()
while offset%8 != 0:
bin_f.write(b'\x00')
offset += 1
obj.tofile(bin_f)
size = bin_f.tell() - offset
return {'__type': 'ndarray', 'shape': obj.shape, 'order': 'C', 'dtype': str(obj.dtype), 'npyfile': bin_fn, 'npyoff': offset, 'size': size}
return obj
json.dump({'episodes': self.episodes}, batch_f, default=json_encode)
bytes_per_step = float(bin_f.tell() + batch_f.tell()) / float(self.buffered_step_count)
self.batches.append({
'first': self.episodes_first,
'len': len(self.episodes),
'fn': batch_fn})
manifest = {'batches': self.batches}
manifest_fn = os.path.join(self.directory, '{}.manifest.json'.format(self.file_prefix))
with atomic_write.atomic_write(os.path.join(self.directory, manifest_fn), False) as f:
json.dump(manifest, f)
# Adjust batch size, aiming for 5 MB per file.
# This seems like a reasonable tradeoff between:
# writing speed (not too much overhead creating small files)
# local memory usage (buffering an entire batch before writing)
# random read access (loading the whole file isn't too much work when just grabbing one episode)
self.buffer_batch_size = max(1, min(50000, int(5000000 / bytes_per_step + 1)))
self.episodes = []
self.episodes_first = None
self.buffered_step_count = 0
def close(self):
"""
Flush any buffered data to disk and close. It should get called automatically at program exit time, but
you can free up memory by calling it explicitly when you're done
"""
if not self.closed:
self.end_episode()
if len(self.episodes) > 0:
self.save_complete()
self.closed = True
logger.info('Wrote traces to %s', self.directory)
def optimize_list_of_ndarrays(x):
"""
Replace a list of ndarrays with a single ndarray with an extra dimension.
Should return unchanged a list of other kinds of observations or actions, like Discrete or Tuple
"""
if type(x) == np.ndarray:
return x
if len(x) == 0:
return np.array([[]])
if type(x[0]) == float or type(x[0]) == int:
return np.array(x)
if type(x[0]) == np.ndarray and len(x[0].shape) == 1:
return np.array(x)
return x