memmap-replay-buffer 0.2.0__tar.gz → 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. {memmap_replay_buffer-0.2.0 → memmap_replay_buffer-0.2.1}/PKG-INFO +1 -1
  2. {memmap_replay_buffer-0.2.0 → memmap_replay_buffer-0.2.1}/memmap_replay_buffer/__init__.py +2 -0
  3. memmap_replay_buffer-0.2.1/memmap_replay_buffer/episode_collector.py +284 -0
  4. {memmap_replay_buffer-0.2.0 → memmap_replay_buffer-0.2.1}/memmap_replay_buffer/replay_buffer.py +6 -2
  5. {memmap_replay_buffer-0.2.0 → memmap_replay_buffer-0.2.1}/memmap_replay_buffer/replay_buffer_h5py.py +6 -2
  6. {memmap_replay_buffer-0.2.0 → memmap_replay_buffer-0.2.1}/pyproject.toml +1 -1
  7. memmap_replay_buffer-0.2.1/tests/test_rollout_collector.py +187 -0
  8. memmap_replay_buffer-0.2.0/memmap_replay_buffer/episode_collector.py +0 -105
  9. {memmap_replay_buffer-0.2.0 → memmap_replay_buffer-0.2.1}/.gitignore +0 -0
  10. {memmap_replay_buffer-0.2.0 → memmap_replay_buffer-0.2.1}/LICENSE +0 -0
  11. {memmap_replay_buffer-0.2.0 → memmap_replay_buffer-0.2.1}/README.md +0 -0
  12. {memmap_replay_buffer-0.2.0 → memmap_replay_buffer-0.2.1}/memmap_replay_buffer/concat_replay_buffer.py +0 -0
  13. {memmap_replay_buffer-0.2.0 → memmap_replay_buffer-0.2.1}/tests/test_e2e.py +0 -0
  14. {memmap_replay_buffer-0.2.0 → memmap_replay_buffer-0.2.1}/tests/test_edge_cases.py +0 -0
  15. {memmap_replay_buffer-0.2.0 → memmap_replay_buffer-0.2.1}/tests/test_episode_collector.py +0 -0
  16. {memmap_replay_buffer-0.2.0 → memmap_replay_buffer-0.2.1}/tests/test_get_all_data.py +0 -0
  17. {memmap_replay_buffer-0.2.0 → memmap_replay_buffer-0.2.1}/tests/test_lazy_init.py +0 -0
  18. {memmap_replay_buffer-0.2.0 → memmap_replay_buffer-0.2.1}/tests/test_mixed_storage.py +0 -0
  19. {memmap_replay_buffer-0.2.0 → memmap_replay_buffer-0.2.1}/tests/test_n_step.py +0 -0
  20. {memmap_replay_buffer-0.2.0 → memmap_replay_buffer-0.2.1}/tests/test_nstep_timestep_gotcha.py +0 -0
  21. {memmap_replay_buffer-0.2.0 → memmap_replay_buffer-0.2.1}/tests/test_replay_buffer.py +0 -0
  22. {memmap_replay_buffer-0.2.0 → memmap_replay_buffer-0.2.1}/tests/test_replay_buffer_h5py.py +0 -0
  23. {memmap_replay_buffer-0.2.0 → memmap_replay_buffer-0.2.1}/tests/test_scalar_flexibility.py +0 -0
  24. {memmap_replay_buffer-0.2.0 → memmap_replay_buffer-0.2.1}/tests/test_store_episode_parity.py +0 -0
  25. {memmap_replay_buffer-0.2.0 → memmap_replay_buffer-0.2.1}/tests/test_time_window.py +0 -0
  26. {memmap_replay_buffer-0.2.0 → memmap_replay_buffer-0.2.1}/tests/test_uint8_storage.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: memmap-replay-buffer
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Simple Replay Buffer for RL
5
5
  Project-URL: Homepage, https://pypi.org/project/memmap-replay-buffer/
6
6
  Project-URL: Repository, https://codeberg.org/lucidrains/memmap-replay-buffer
@@ -10,3 +10,5 @@ try:
10
10
  from memmap_replay_buffer.replay_buffer_h5py import ReplayBufferH5PY as ReplayBufferH5PY
11
11
  except ImportError:
12
12
  pass
13
+
14
+ from memmap_replay_buffer.episode_collector import EpisodeCollector as EpisodeCollector
@@ -0,0 +1,284 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import defaultdict, namedtuple
4
+ from typing import Any
5
+
6
+ import numpy as np
7
+ from beartype import beartype
8
+ from numpy import ndarray
9
+ import torch
10
+ from torch import is_tensor
11
+ from torch.utils.data import DataLoader, Dataset, default_collate
12
+
13
+ from memmap_replay_buffer.replay_buffer import tree_map_to_device
14
+
15
+ # helpers
16
+
17
+ def exists(v):
18
+ return v is not None
19
+
20
+ def default(v, d):
21
+ return v if exists(v) else d
22
+
23
+ def is_group_batched(value, num_groups):
24
+ if is_tensor(value) or isinstance(value, ndarray):
25
+ return value.ndim > 0 and value.shape[0] == num_groups
26
+
27
+ if isinstance(value, (list, tuple)):
28
+ return len(value) == num_groups
29
+
30
+ return False
31
+
32
+ def stack(values, dim = 0):
33
+ if is_tensor(values[0]):
34
+ return torch.stack(values, dim = dim)
35
+
36
+ return np.stack(values, axis = dim)
37
+
38
+ # dataset
39
+
40
+ class TensorDictDataset(Dataset):
41
+ def __init__(self, data: dict[str, torch.Tensor]):
42
+ self.data = data
43
+ self.num_samples = len(next(iter(data.values())))
44
+
45
+ def __len__(self):
46
+ return self.num_samples
47
+
48
+ def __getitem__(self, index):
49
+ return {name: value[index] for name, value in self.data.items()}
50
+
51
+ # class
52
+
53
+ class EpisodeCollector:
54
+ """ accumulates a group-batched rollout step by step, then either stores it or trains on it in memory
55
+
56
+ layouts are explicit, so the time and group axes can never be silently swapped
57
+
58
+ `append` - one step, group-batched (num_groups, *shape) or (num_groups, 1, *shape)
59
+ `extend` - whole trajectories, time-major (time, num_groups, *shape), e.g. returns / advantages
60
+ `env_major` / `time_major` - read back (num_groups, time, ...) / (time, num_groups, ...)
61
+ `all_data` / `dataloader` - env-major tensors / flattened minibatches, entirely in memory
62
+ `store` - write one episode per group, then `clear`
63
+ `clear` - discard the current rollout
64
+ """
65
+
66
+ @beartype
67
+ def __init__(
68
+ self,
69
+ replay_buffer: Any, # ReplayBuffer | ReplayBufferH5PY, without a hard dependency on h5py
70
+ num_groups: int,
71
+ fieldnames: tuple[str, ...] | None = None,
72
+ meta_fieldnames: tuple[str, ...] | None = None,
73
+ strict: bool = True
74
+ ):
75
+ self.replay_buffer = replay_buffer
76
+ self.num_groups = num_groups
77
+
78
+ self.fieldnames = set(default(fieldnames, tuple(replay_buffer.fieldnames)))
79
+ self.meta_fieldnames = set(default(meta_fieldnames, tuple(replay_buffer.meta_fieldnames)))
80
+
81
+ self.shapes = replay_buffer.shapes
82
+ self.meta_shapes = replay_buffer.meta_shapes
83
+ self.strict = strict
84
+
85
+ self.clear()
86
+
87
+ def __len__(self):
88
+ return self.step_count
89
+
90
+ def clear(self):
91
+ self.episode_data = [defaultdict(list) for _ in range(self.num_groups)]
92
+ self.step_count = 0
93
+
94
+ # fields
95
+
96
+ def field_kind(self, name):
97
+ if name in self.fieldnames:
98
+ return 'data'
99
+
100
+ if name in self.meta_fieldnames:
101
+ return 'meta'
102
+
103
+ if self.strict:
104
+ raise KeyError(f'unknown field {name} - valid fields are {sorted(self.fieldnames | self.meta_fieldnames)}')
105
+
106
+ return None
107
+
108
+ def field_shape(self, name):
109
+ return self.meta_shapes[name] if name in self.meta_fieldnames else self.shapes[name]
110
+
111
+ def per_group(self, name, value):
112
+ # (num_groups, *shape) / (num_groups, 1, *shape) -> num_groups values of shape (*shape)
113
+
114
+ if isinstance(value, (list, tuple)):
115
+ return list(value)
116
+
117
+ shape = tuple(self.field_shape(name))
118
+ rest = tuple(value.shape[1:])
119
+
120
+ assert rest in (shape, (1, *shape)), f'field {name} - per-group shape {rest} should be {shape} or (1, *shape)'
121
+
122
+ if rest != shape:
123
+ value = value[:, 0]
124
+
125
+ return list(value)
126
+
127
+ # adding data
128
+
129
+ @beartype
130
+ def append(self, **data):
131
+ # one step, group-batched values
132
+
133
+ for name, value in data.items():
134
+ kind = self.field_kind(name)
135
+
136
+ if not exists(kind):
137
+ continue
138
+
139
+ if kind == 'meta':
140
+ values = [value] * self.num_groups
141
+ else:
142
+ shape = tuple(self.field_shape(name))
143
+
144
+ assert is_group_batched(value, self.num_groups), (
145
+ f'field {name} - `append` expects per-step group-batched values of shape ({self.num_groups}, *{shape}) '
146
+ f'or ({self.num_groups}, 1, *{shape}) - for whole trajectories in time-major (time, {self.num_groups}, *{shape}), use `extend`'
147
+ )
148
+
149
+ values = self.per_group(name, value)
150
+
151
+ for group_index, group_value in enumerate(values):
152
+ self.episode_data[group_index][name].append(group_value)
153
+
154
+ self.step_count += 1
155
+
156
+ @beartype
157
+ def extend(self, **trajectories):
158
+ # whole trajectories, time-major (time, num_groups, *shape)
159
+
160
+ for name, value in trajectories.items():
161
+ kind = self.field_kind(name)
162
+
163
+ if not exists(kind):
164
+ continue
165
+
166
+ assert is_tensor(value) or isinstance(value, ndarray), f'field {name} - `extend` expects a tensor / ndarray of shape (time, {self.num_groups}, *shape)'
167
+
168
+ time = value.shape[0]
169
+
170
+ assert value.ndim > 1 and value.shape[1] == self.num_groups, (
171
+ f'field {name} - `extend` expects time-major (time, {self.num_groups}, *shape), got {tuple(value.shape)}'
172
+ )
173
+
174
+ if len(self) == 0:
175
+ self.step_count = time
176
+ else:
177
+ assert time == len(self), f'field {name} - time dim {time} does not match the {len(self)} collected steps'
178
+
179
+ shape = tuple(self.field_shape(name))
180
+ assert tuple(value.shape[2:]) == shape, f'field {name} - feature shape {tuple(value.shape[2:])} does not match {shape}'
181
+
182
+ for group_index in range(self.num_groups):
183
+ self.episode_data[group_index][name].extend(list(value[:, group_index]))
184
+
185
+ # reading back
186
+
187
+ def groups(self, name):
188
+ # one value per group, i.e. a list of num_groups tensors / arrays
189
+
190
+ if self.field_kind(name) == 'meta':
191
+ return [data[name][-1] for data in self.episode_data]
192
+
193
+ return [stack(data[name], dim = 0) for data in self.episode_data]
194
+
195
+ def time_major(self, name):
196
+ # (time, num_groups, *shape) for data fields, (num_groups,) for meta fields
197
+
198
+ return stack(self.groups(name), dim = 1 if self.field_kind(name) == 'data' else 0)
199
+
200
+ def env_major(self, name):
201
+ # (num_groups, time, *shape), the layout generalized advantage estimation expects
202
+
203
+ values = self.time_major(name)
204
+
205
+ if self.field_kind(name) == 'meta':
206
+ return values
207
+
208
+ return values.swapaxes(0, 1)
209
+
210
+ # in-memory training - no storing required
211
+
212
+ def all_data(self, fields = None, device: torch.device | str | None = None):
213
+ # dict of env-major tensors, (num_groups, time, *shape), for the collected data fields
214
+
215
+ if len(self) == 0:
216
+ raise ValueError('collector is empty')
217
+
218
+ collected = tuple(name for name in self.episode_data[0] if name in self.fieldnames)
219
+
220
+ data = {name: self.env_major(name) for name in default(fields, collected)}
221
+ return tree_map_to_device(data, device)
222
+
223
+ def dataloader(
224
+ self,
225
+ batch_size,
226
+ fields = None,
227
+ filter_fields = None,
228
+ to_named_tuple = None,
229
+ shuffle = False,
230
+ device: torch.device | str | None = None,
231
+ **kwargs
232
+ ):
233
+ # minibatches flattened over (num_groups, time) for the current rollout, entirely in memory
234
+
235
+ data = {name: torch.as_tensor(value).reshape(-1, *value.shape[2:]) for name, value in self.all_data().items()}
236
+
237
+ if exists(filter_fields) and len(filter_fields) > 0:
238
+ mask = None
239
+ for name, filter_value in filter_fields.items():
240
+ field_mask = data[name] == filter_value
241
+ mask = field_mask if not exists(mask) else (mask & field_mask)
242
+
243
+ data = {name: value[mask] for name, value in data.items()}
244
+
245
+ fields = default(fields, tuple(data.keys()))
246
+ data = {name: data[name] for name in fields}
247
+
248
+ dataset = TensorDictDataset(data)
249
+
250
+ NamedTupleCls = None
251
+ if exists(to_named_tuple):
252
+ sanitized_fields = tuple(f.lstrip('_') if f.startswith('_') else f for f in to_named_tuple)
253
+ NamedTupleCls = namedtuple('Batch', sanitized_fields)
254
+
255
+ def collate_fn(data):
256
+ batch = default_collate(data)
257
+
258
+ if exists(NamedTupleCls):
259
+ for field in to_named_tuple:
260
+ if field not in batch:
261
+ raise ValueError(f'field `{field}` not found in batch. available fields: {list(batch.keys())}')
262
+
263
+ batch = NamedTupleCls(**{san: batch[orig] for orig, san in zip(to_named_tuple, sanitized_fields)})
264
+
265
+ return tree_map_to_device(batch, device)
266
+
267
+ return DataLoader(dataset, batch_size = batch_size, shuffle = shuffle, collate_fn = collate_fn, **kwargs)
268
+
269
+ # storing
270
+
271
+ @beartype
272
+ def store(self, **meta_data):
273
+ for group_index, group_data in enumerate(self.episode_data):
274
+ store_kwargs = {name: stack(values, dim = 0) if name in self.fieldnames else values[-1] for name, values in group_data.items()}
275
+
276
+ for name, value in meta_data.items():
277
+ if name not in self.meta_fieldnames:
278
+ continue
279
+
280
+ store_kwargs[name] = value[group_index] if is_group_batched(value, self.num_groups) else value
281
+
282
+ self.replay_buffer.store_episode(**store_kwargs)
283
+
284
+ self.clear()
@@ -1394,12 +1394,16 @@ class ReplayBuffer:
1394
1394
  self,
1395
1395
  num_groups: int,
1396
1396
  fieldnames: tuple[str, ...] | None = None,
1397
- meta_fieldnames: tuple[str, ...] | None = None
1397
+ meta_fieldnames: tuple[str, ...] | None = None,
1398
+ **collector_kwargs
1398
1399
  ):
1399
1400
  from memmap_replay_buffer.episode_collector import EpisodeCollector
1400
1401
  return EpisodeCollector(
1401
1402
  self,
1402
1403
  num_groups,
1403
1404
  fieldnames = fieldnames,
1404
- meta_fieldnames = meta_fieldnames
1405
+ meta_fieldnames = meta_fieldnames,
1406
+ **collector_kwargs
1405
1407
  )
1408
+
1409
+ create_rollout_collector = create_collector
@@ -917,16 +917,20 @@ class ReplayBufferH5PY:
917
917
  self,
918
918
  num_groups: int,
919
919
  fieldnames: tuple[str, ...] | None = None,
920
- meta_fieldnames: tuple[str, ...] | None = None
920
+ meta_fieldnames: tuple[str, ...] | None = None,
921
+ **collector_kwargs
921
922
  ):
922
923
  from memmap_replay_buffer.episode_collector import EpisodeCollector
923
924
  return EpisodeCollector(
924
925
  self,
925
926
  num_groups,
926
927
  fieldnames = fieldnames,
927
- meta_fieldnames = meta_fieldnames
928
+ meta_fieldnames = meta_fieldnames,
929
+ **collector_kwargs
928
930
  )
929
931
 
932
+ create_rollout_collector = create_collector
933
+
930
934
  @can_write
931
935
  def _store_episodes_batch(self, data: dict[str, np.ndarray]):
932
936
  batch_size = next(iter(data.values())).shape[0]
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "memmap-replay-buffer"
3
- version = "0.2.0"
3
+ version = "0.2.1"
4
4
  description = "Simple Replay Buffer for RL"
5
5
  authors = [
6
6
  { name = "Phil Wang", email = "lucidrains@gmail.com" }
@@ -0,0 +1,187 @@
1
+ import pytest
2
+ import torch
3
+
4
+ from memmap_replay_buffer import ReplayBuffer
5
+
6
+ FIELDS = dict(
7
+ obs = ('float', (3,)),
8
+ action = 'int',
9
+ reward = 'float',
10
+ next_obs = ('float', (3,)),
11
+ value = 'float',
12
+ returns = 'float',
13
+ advantage = 'float',
14
+ )
15
+
16
+ def make_buffer(tmp_path):
17
+ return ReplayBuffer(
18
+ tmp_path / 'buffer',
19
+ max_episodes = 4,
20
+ max_timesteps = 16,
21
+ fields = FIELDS,
22
+ circular = True,
23
+ overwrite = True
24
+ )
25
+
26
+ def test_rollout_collector_alignment_roundtrip(tmp_path):
27
+ T, N = 5, 2
28
+
29
+ buffer = make_buffer(tmp_path)
30
+ collector = buffer.create_collector(num_groups = N)
31
+
32
+ obs_all = torch.randn(T + 1, N, 3)
33
+ obs, next_obs = obs_all[:-1], obs_all[1:]
34
+ action = torch.randint(0, 4, (T, N))
35
+ reward = torch.randn(T, N)
36
+ value = torch.randn(T, N)
37
+
38
+ for t in range(T):
39
+ collector.append(
40
+ obs = obs[t],
41
+ action = action[t],
42
+ reward = reward[t],
43
+ next_obs = next_obs[t],
44
+ value = value[t]
45
+ )
46
+
47
+ # whole-trajectory fields, explicitly time-major
48
+
49
+ returns = torch.randn(T, N)
50
+ advantage = torch.randn(T, N)
51
+ collector.extend(returns = returns, advantage = advantage)
52
+
53
+ assert len(collector) == T
54
+
55
+ # read-back views must agree with the inputs
56
+
57
+ assert torch.equal(collector.time_major('obs'), obs)
58
+ assert torch.equal(collector.env_major('obs'), obs.transpose(0, 1))
59
+ assert torch.equal(collector.time_major('returns'), returns)
60
+ assert torch.equal(collector.env_major('advantage'), advantage.transpose(0, 1))
61
+
62
+ collector.store()
63
+
64
+ # stored rows are per group and time-ordered, never scrambled
65
+
66
+ for i in range(N):
67
+ assert torch.allclose(torch.tensor(buffer.data['obs'][i, :T]), obs[:, i])
68
+ assert torch.allclose(torch.tensor(buffer.data['next_obs'][i, :T]), next_obs[:, i])
69
+ assert torch.allclose(torch.tensor(buffer.data['returns'][i, :T]), returns[:, i])
70
+ assert torch.allclose(torch.tensor(buffer.data['advantage'][i, :T]), advantage[:, i])
71
+ assert torch.allclose(torch.tensor(buffer.data['value'][i, :T]), value[:, i])
72
+
73
+ # the transition continuity invariant that catches any permutation
74
+
75
+ assert torch.allclose(torch.tensor(buffer.data['next_obs'][i, :T - 1]), torch.tensor(buffer.data['obs'][i, 1:T]))
76
+
77
+ def test_collector_in_memory_training(tmp_path):
78
+ T, N = 5, 3
79
+
80
+ buffer = make_buffer(tmp_path)
81
+ collector = buffer.create_collector(num_groups = N)
82
+
83
+ obs = torch.randn(T, N, 3)
84
+ action = torch.randint(0, 4, (T, N))
85
+ value = torch.randn(T, N)
86
+ returns = torch.randn(T, N)
87
+ advantage = torch.randn(T, N)
88
+
89
+ for t in range(T):
90
+ collector.append(obs = obs[t], action = action[t], value = value[t])
91
+
92
+ collector.extend(returns = returns, advantage = advantage)
93
+
94
+ # env-major snapshot, no disk access
95
+
96
+ all_data = collector.all_data()
97
+ assert torch.equal(all_data['obs'], obs.transpose(0, 1))
98
+
99
+ # flattened minibatches, optionally filtered
100
+
101
+ dataloader = collector.dataloader(
102
+ batch_size = 4,
103
+ filter_fields = dict(action = 1),
104
+ to_named_tuple = ('obs', 'action', 'returns', 'advantage')
105
+ )
106
+
107
+ batches = list(dataloader)
108
+ assert sum(len(batch.obs) for batch in batches) == (action == 1).sum().item()
109
+
110
+ expected = obs.transpose(0, 1).reshape(-1, 3)[(action.transpose(0, 1) == 1).reshape(-1)]
111
+ assert torch.allclose(torch.cat([batch.obs for batch in batches]), expected)
112
+
113
+ # nothing was stored
114
+
115
+ assert buffer.num_episodes == 0
116
+
117
+ # clearing resets the rollout
118
+
119
+ collector.clear()
120
+ assert len(collector) == 0
121
+
122
+ with pytest.raises(ValueError, match = 'empty'):
123
+ collector.all_data()
124
+
125
+ def test_rollout_collector_rejects_time_major_append(tmp_path):
126
+ T, N = 5, 2
127
+
128
+ buffer = make_buffer(tmp_path)
129
+ collector = buffer.create_collector(num_groups = N)
130
+
131
+ with pytest.raises(AssertionError, match = 'extend'):
132
+ collector.append(obs = torch.randn(T, N, 3))
133
+
134
+ def test_rollout_collector_rejects_wrong_time_dim_in_extend(tmp_path):
135
+ T, N = 4, 3
136
+
137
+ buffer = make_buffer(tmp_path)
138
+ collector = buffer.create_collector(num_groups = N)
139
+
140
+ for t in range(T):
141
+ collector.append(
142
+ obs = torch.randn(N, 3),
143
+ action = torch.zeros(N, dtype = torch.long),
144
+ reward = torch.zeros(N),
145
+ next_obs = torch.randn(N, 3),
146
+ value = torch.zeros(N)
147
+ )
148
+
149
+ with pytest.raises(AssertionError, match = 'time dim'):
150
+ collector.extend(returns = torch.randn(T + 1, N))
151
+
152
+ def test_rollout_collector_unit_time_dim_and_strict(tmp_path):
153
+ N = 2
154
+
155
+ buffer = make_buffer(tmp_path)
156
+ collector = buffer.create_collector(num_groups = N)
157
+
158
+ # the (N, 1, *shape) form is accepted and stored identically to (N, *shape)
159
+
160
+ for t in range(3):
161
+ collector.append(
162
+ obs = torch.full((N, 1, 3), float(t)),
163
+ action = torch.full((N, 1), t),
164
+ reward = torch.full((N, 1), float(t)),
165
+ next_obs = torch.zeros(N, 1, 3),
166
+ value = torch.zeros(N, 1)
167
+ )
168
+
169
+ collector.store()
170
+
171
+ data = buffer.get_all_data()
172
+ assert data['obs'].shape[:2] == (N, 3)
173
+ assert data['action'][0].tolist() == [0, 1, 2]
174
+ assert data['action'][1].tolist() == [0, 1, 2]
175
+
176
+ with pytest.raises(KeyError, match = 'unknown field'):
177
+ collector.append(typo_field = torch.zeros(N))
178
+
179
+ collector_lax = buffer.create_collector(num_groups = N, strict = False)
180
+ collector_lax.append(
181
+ obs = torch.zeros(N, 3),
182
+ action = torch.zeros(N, dtype = torch.long),
183
+ reward = torch.zeros(N),
184
+ next_obs = torch.zeros(N, 3),
185
+ value = torch.zeros(N),
186
+ typo_field = torch.zeros(N)
187
+ )
@@ -1,105 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from collections import defaultdict
4
-
5
- import numpy as np
6
- from beartype import beartype
7
- from numpy import ndarray
8
- from torch import cat, is_tensor, stack
9
-
10
- from memmap_replay_buffer.replay_buffer import ReplayBuffer
11
- from memmap_replay_buffer.replay_buffer_h5py import ReplayBufferH5PY
12
-
13
- # helpers
14
-
15
- def exists(v):
16
- return v is not None
17
-
18
- def default(v, d):
19
- return v if exists(v) else d
20
-
21
- def is_group_batched(value, num_groups):
22
- if is_tensor(value) or isinstance(value, ndarray):
23
- return value.shape[0] == num_groups
24
-
25
- if isinstance(value, (list, tuple)):
26
- return len(value) == num_groups
27
-
28
- return False
29
-
30
- def combine_trajectory(values):
31
- first = values[0]
32
- if is_tensor(first):
33
- return stack(values) if first.ndim == 0 else cat(values, dim = 0)
34
-
35
- if isinstance(first, ndarray):
36
- return np.stack(values) if first.ndim == 0 else np.concatenate(values, axis = 0)
37
-
38
- return np.array(values)
39
-
40
- class EpisodeCollector:
41
- @beartype
42
- def __init__(
43
- self,
44
- replay_buffer: ReplayBuffer | ReplayBufferH5PY,
45
- num_groups: int,
46
- fieldnames: tuple[str, ...] | None = None,
47
- meta_fieldnames: tuple[str, ...] | None = None
48
- ):
49
- self.replay_buffer = replay_buffer
50
- self.num_groups = num_groups
51
-
52
- self.fieldnames = default(fieldnames, tuple(replay_buffer.fieldnames))
53
- self.meta_fieldnames = default(meta_fieldnames, tuple(replay_buffer.meta_fieldnames))
54
-
55
- self.reset()
56
-
57
- def reset(self):
58
- self.episode_data = [defaultdict(list) for _ in range(self.num_groups)]
59
-
60
- @beartype
61
- def append(
62
- self,
63
- **data
64
- ):
65
- for name, value in data.items():
66
- if name not in self.fieldnames and name not in self.meta_fieldnames:
67
- continue
68
-
69
- if is_group_batched(value, self.num_groups):
70
- for i in range(self.num_groups):
71
- self.episode_data[i][name].append(value[i])
72
- else:
73
- for i in range(self.num_groups):
74
- self.episode_data[i][name].append(value)
75
-
76
- @beartype
77
- def store(self, **meta_data):
78
- for i in range(self.num_groups):
79
- group_data = self.episode_data[i]
80
-
81
- store_kwargs = dict()
82
-
83
- # Handle collected data
84
- for name, values in group_data.items():
85
- if name in self.fieldnames:
86
- combined = combine_trajectory(values)
87
- else:
88
- # Meta fields: take the last appended value
89
- combined = values[-1]
90
-
91
- store_kwargs[name] = combined
92
-
93
- # Merge with extra meta data provided to store()
94
- for name, value in meta_data.items():
95
- if name not in self.meta_fieldnames:
96
- continue
97
-
98
- if is_group_batched(value, self.num_groups):
99
- store_kwargs[name] = value[i]
100
- else:
101
- store_kwargs[name] = value
102
-
103
- self.replay_buffer.store_episode(**store_kwargs)
104
-
105
- self.reset()