memmap-replay-buffer 0.2.2__tar.gz → 0.2.4__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.
- {memmap_replay_buffer-0.2.2 → memmap_replay_buffer-0.2.4}/PKG-INFO +46 -5
- {memmap_replay_buffer-0.2.2 → memmap_replay_buffer-0.2.4}/README.md +45 -4
- memmap_replay_buffer-0.2.4/memmap_replay_buffer/frontend/index.html +285 -0
- {memmap_replay_buffer-0.2.2 → memmap_replay_buffer-0.2.4}/memmap_replay_buffer/frontend.py +53 -16
- {memmap_replay_buffer-0.2.2 → memmap_replay_buffer-0.2.4}/memmap_replay_buffer/replay_buffer.py +34 -11
- {memmap_replay_buffer-0.2.2 → memmap_replay_buffer-0.2.4}/memmap_replay_buffer/video.py +74 -23
- {memmap_replay_buffer-0.2.2 → memmap_replay_buffer-0.2.4}/pyproject.toml +1 -1
- memmap_replay_buffer-0.2.4/tests/test_advantage_filtering.py +143 -0
- memmap_replay_buffer-0.2.4/tests/test_episode_collector.py +58 -0
- {memmap_replay_buffer-0.2.2 → memmap_replay_buffer-0.2.4}/tests/test_frontend.py +53 -15
- memmap_replay_buffer-0.2.4/tests/test_get_all_data.py +64 -0
- memmap_replay_buffer-0.2.4/tests/test_n_step.py +205 -0
- memmap_replay_buffer-0.2.4/tests/test_replay_buffer.py +764 -0
- memmap_replay_buffer-0.2.4/tests/test_replay_buffer_h5py.py +158 -0
- memmap_replay_buffer-0.2.4/tests/test_scalar_flexibility.py +45 -0
- memmap_replay_buffer-0.2.4/tests/test_store_episode_parity.py +71 -0
- memmap_replay_buffer-0.2.4/tests/test_time_window.py +51 -0
- memmap_replay_buffer-0.2.4/tests/test_uint8_storage.py +55 -0
- {memmap_replay_buffer-0.2.2 → memmap_replay_buffer-0.2.4}/tests/test_video.py +75 -0
- memmap_replay_buffer-0.2.2/memmap_replay_buffer/frontend/index.html +0 -87
- {memmap_replay_buffer-0.2.2 → memmap_replay_buffer-0.2.4}/.gitignore +0 -0
- {memmap_replay_buffer-0.2.2 → memmap_replay_buffer-0.2.4}/LICENSE +0 -0
- {memmap_replay_buffer-0.2.2 → memmap_replay_buffer-0.2.4}/memmap_replay_buffer/__init__.py +0 -0
- {memmap_replay_buffer-0.2.2 → memmap_replay_buffer-0.2.4}/memmap_replay_buffer/concat_replay_buffer.py +0 -0
- {memmap_replay_buffer-0.2.2 → memmap_replay_buffer-0.2.4}/memmap_replay_buffer/episode_collector.py +0 -0
- {memmap_replay_buffer-0.2.2 → memmap_replay_buffer-0.2.4}/memmap_replay_buffer/replay_buffer_h5py.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: memmap-replay-buffer
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
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
|
|
@@ -205,26 +205,67 @@ buffer.update(episode_ids, returns = returns_for_ids) # specific episodes
|
|
|
205
205
|
buffer.update(0, returns = returns_for_one) # scalar index
|
|
206
206
|
```
|
|
207
207
|
|
|
208
|
+
### Computed fields and selective timestep training
|
|
209
|
+
|
|
210
|
+
Derived fields must be declared in `fields` when the buffer is created, then written after collection. Use `timestep_start` to write contiguous time chunks without loading a whole episode into memory. A common self-play recipe is to compute per-timestep advantages, threshold them into a boolean field, and then let the timestep-level dataset fetch only the selected timesteps.
|
|
211
|
+
|
|
212
|
+
```python
|
|
213
|
+
buffer = ReplayBuffer(
|
|
214
|
+
'./replay_data',
|
|
215
|
+
max_episodes = 1000,
|
|
216
|
+
max_timesteps = 500,
|
|
217
|
+
fields = dict(
|
|
218
|
+
state = ('float', (8,)),
|
|
219
|
+
reward = 'float',
|
|
220
|
+
advantage = 'float',
|
|
221
|
+
high_advantage = ('bool', (), False), # declares the derived mask
|
|
222
|
+
)
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
# compute advantages in episode batches
|
|
226
|
+
buffer.update(episode_ids, advantage = advantages)
|
|
227
|
+
|
|
228
|
+
# or write contiguous time chunks for all episodes
|
|
229
|
+
for start in range(0, max_len, 128):
|
|
230
|
+
buffer.update(
|
|
231
|
+
timestep_start = start,
|
|
232
|
+
advantage = advantage_chunk
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
# set the mask from a comparison of the computed field
|
|
236
|
+
buffer.update(high_advantage = advantage > threshold)
|
|
237
|
+
|
|
238
|
+
# only selected timesteps are returned
|
|
239
|
+
dataloader = buffer.dataloader(
|
|
240
|
+
batch_size = 64,
|
|
241
|
+
timestep_level = True,
|
|
242
|
+
filter_fields = dict(high_advantage = True)
|
|
243
|
+
)
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
`demo_advantage_filtering.py` contains a complete self-play loop that computes advantages in batches, marks extreme positive advantages, and trains only on those timesteps.
|
|
247
|
+
|
|
208
248
|
### Pulling everything at once
|
|
209
249
|
|
|
210
250
|
```python
|
|
211
251
|
all_data = buffer.get_all_data() # dict of tensors, time-padded to the longest episode
|
|
212
252
|
```
|
|
213
253
|
|
|
214
|
-
### Rendering
|
|
254
|
+
### Rendering episodes to video
|
|
215
255
|
|
|
216
|
-
When a field holds one frame per timestep, `save_video` renders an episode to `.mp4` or `.gif` (needs `pip install memmap-replay-buffer[video]`). The image field is auto-detected, and frames may be `uint8` or float in `[0., 1.]`.
|
|
256
|
+
When a field holds one frame per timestep, `save_video` renders an episode to `.mp4` or `.gif` (needs `pip install memmap-replay-buffer[video]`). The image field is auto-detected, and frames may be `uint8` or float in `[0., 1.]`. Pass a list of episodes to concatenate them in time.
|
|
217
257
|
|
|
218
258
|
```python
|
|
219
259
|
buffer.save_video('./episode-0.mp4', episode_index = 0, fps = 30)
|
|
260
|
+
buffer.save_video('./episodes-0-3-5.mp4', episode_index = [0, 3, 5], fps = 30)
|
|
220
261
|
```
|
|
221
262
|
|
|
222
263
|
`trajectory_to_video(frames, path, fps)` is exported for plain `(time, height, width, channels)` arrays.
|
|
223
264
|
|
|
224
|
-
The same is available from the command line, with `--sheet` for a contact sheet and `--serve` for a minimal local frontend (
|
|
265
|
+
The same is available from the command line, with `--sheet` for a contact sheet and `--serve` for a minimal local frontend. `--episode` and `--field` accept comma-delimited lists - with multiple cameras (image-like fields), one video is written per field (`<name>.<field>.<ext>`) and the frontend plays them side by side, synchronized, with a live frame counter to the side. The frontend has frame-level controls: a timeline scrubber, single-frame stepping (buttons, left/right arrows, shift for 10, or mouse wheel over the videos), play/pause, and slowdown playback (down to 0.1x).
|
|
225
266
|
|
|
226
267
|
```bash
|
|
227
|
-
$ inspect-replay-buffer ./image_data --episode 0 --output
|
|
268
|
+
$ inspect-replay-buffer ./image_data --episode 0,3,5 --field front,wrist --output replay.mp4 --serve 8000
|
|
228
269
|
```
|
|
229
270
|
|
|
230
271
|
### Reopening, read-only, and clearing
|
|
@@ -155,26 +155,67 @@ buffer.update(episode_ids, returns = returns_for_ids) # specific episodes
|
|
|
155
155
|
buffer.update(0, returns = returns_for_one) # scalar index
|
|
156
156
|
```
|
|
157
157
|
|
|
158
|
+
### Computed fields and selective timestep training
|
|
159
|
+
|
|
160
|
+
Derived fields must be declared in `fields` when the buffer is created, then written after collection. Use `timestep_start` to write contiguous time chunks without loading a whole episode into memory. A common self-play recipe is to compute per-timestep advantages, threshold them into a boolean field, and then let the timestep-level dataset fetch only the selected timesteps.
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
buffer = ReplayBuffer(
|
|
164
|
+
'./replay_data',
|
|
165
|
+
max_episodes = 1000,
|
|
166
|
+
max_timesteps = 500,
|
|
167
|
+
fields = dict(
|
|
168
|
+
state = ('float', (8,)),
|
|
169
|
+
reward = 'float',
|
|
170
|
+
advantage = 'float',
|
|
171
|
+
high_advantage = ('bool', (), False), # declares the derived mask
|
|
172
|
+
)
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# compute advantages in episode batches
|
|
176
|
+
buffer.update(episode_ids, advantage = advantages)
|
|
177
|
+
|
|
178
|
+
# or write contiguous time chunks for all episodes
|
|
179
|
+
for start in range(0, max_len, 128):
|
|
180
|
+
buffer.update(
|
|
181
|
+
timestep_start = start,
|
|
182
|
+
advantage = advantage_chunk
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# set the mask from a comparison of the computed field
|
|
186
|
+
buffer.update(high_advantage = advantage > threshold)
|
|
187
|
+
|
|
188
|
+
# only selected timesteps are returned
|
|
189
|
+
dataloader = buffer.dataloader(
|
|
190
|
+
batch_size = 64,
|
|
191
|
+
timestep_level = True,
|
|
192
|
+
filter_fields = dict(high_advantage = True)
|
|
193
|
+
)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
`demo_advantage_filtering.py` contains a complete self-play loop that computes advantages in batches, marks extreme positive advantages, and trains only on those timesteps.
|
|
197
|
+
|
|
158
198
|
### Pulling everything at once
|
|
159
199
|
|
|
160
200
|
```python
|
|
161
201
|
all_data = buffer.get_all_data() # dict of tensors, time-padded to the longest episode
|
|
162
202
|
```
|
|
163
203
|
|
|
164
|
-
### Rendering
|
|
204
|
+
### Rendering episodes to video
|
|
165
205
|
|
|
166
|
-
When a field holds one frame per timestep, `save_video` renders an episode to `.mp4` or `.gif` (needs `pip install memmap-replay-buffer[video]`). The image field is auto-detected, and frames may be `uint8` or float in `[0., 1.]`.
|
|
206
|
+
When a field holds one frame per timestep, `save_video` renders an episode to `.mp4` or `.gif` (needs `pip install memmap-replay-buffer[video]`). The image field is auto-detected, and frames may be `uint8` or float in `[0., 1.]`. Pass a list of episodes to concatenate them in time.
|
|
167
207
|
|
|
168
208
|
```python
|
|
169
209
|
buffer.save_video('./episode-0.mp4', episode_index = 0, fps = 30)
|
|
210
|
+
buffer.save_video('./episodes-0-3-5.mp4', episode_index = [0, 3, 5], fps = 30)
|
|
170
211
|
```
|
|
171
212
|
|
|
172
213
|
`trajectory_to_video(frames, path, fps)` is exported for plain `(time, height, width, channels)` arrays.
|
|
173
214
|
|
|
174
|
-
The same is available from the command line, with `--sheet` for a contact sheet and `--serve` for a minimal local frontend (
|
|
215
|
+
The same is available from the command line, with `--sheet` for a contact sheet and `--serve` for a minimal local frontend. `--episode` and `--field` accept comma-delimited lists - with multiple cameras (image-like fields), one video is written per field (`<name>.<field>.<ext>`) and the frontend plays them side by side, synchronized, with a live frame counter to the side. The frontend has frame-level controls: a timeline scrubber, single-frame stepping (buttons, left/right arrows, shift for 10, or mouse wheel over the videos), play/pause, and slowdown playback (down to 0.1x).
|
|
175
216
|
|
|
176
217
|
```bash
|
|
177
|
-
$ inspect-replay-buffer ./image_data --episode 0 --output
|
|
218
|
+
$ inspect-replay-buffer ./image_data --episode 0,3,5 --field front,wrist --output replay.mp4 --serve 8000
|
|
178
219
|
```
|
|
179
220
|
|
|
180
221
|
### Reopening, read-only, and clearing
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>inspect-replay-buffer</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root { color-scheme: dark; }
|
|
9
|
+
* { box-sizing: border-box; }
|
|
10
|
+
body {
|
|
11
|
+
margin: 0;
|
|
12
|
+
min-height: 100vh;
|
|
13
|
+
display: grid;
|
|
14
|
+
place-items: center;
|
|
15
|
+
background: #0f1012;
|
|
16
|
+
color: #e8e8e8;
|
|
17
|
+
font: 13px/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
18
|
+
}
|
|
19
|
+
#stage {
|
|
20
|
+
display: flex;
|
|
21
|
+
flex-direction: column;
|
|
22
|
+
gap: 12px;
|
|
23
|
+
width: min(calc(100vw - 340px), 1200px);
|
|
24
|
+
}
|
|
25
|
+
main {
|
|
26
|
+
display: grid;
|
|
27
|
+
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
|
28
|
+
gap: 12px;
|
|
29
|
+
}
|
|
30
|
+
figure { margin: 0; min-width: 0; }
|
|
31
|
+
figure video { width: 100%; max-height: 74vh; display: block; border-radius: 6px; background: #000; }
|
|
32
|
+
figcaption { margin-top: 6px; color: #8a8f98; font-size: 11px; text-transform: uppercase; letter-spacing: .08em; }
|
|
33
|
+
#controls {
|
|
34
|
+
display: flex;
|
|
35
|
+
align-items: center;
|
|
36
|
+
gap: 10px;
|
|
37
|
+
padding: 8px 12px;
|
|
38
|
+
background: #17181b;
|
|
39
|
+
border: 1px solid #26282c;
|
|
40
|
+
border-radius: 6px;
|
|
41
|
+
}
|
|
42
|
+
#controls button {
|
|
43
|
+
width: 40px;
|
|
44
|
+
padding: 4px 0;
|
|
45
|
+
background: #1f2126;
|
|
46
|
+
color: #e8e8e8;
|
|
47
|
+
border: 1px solid #2e3136;
|
|
48
|
+
border-radius: 4px;
|
|
49
|
+
font: inherit;
|
|
50
|
+
cursor: pointer;
|
|
51
|
+
}
|
|
52
|
+
#controls button:hover { background: #26282c; }
|
|
53
|
+
#timeline { flex: 1; accent-color: #7fb4e6; min-width: 0; }
|
|
54
|
+
#controls .time { color: #8a8f98; white-space: nowrap; }
|
|
55
|
+
#speed {
|
|
56
|
+
background: #1f2126;
|
|
57
|
+
color: #e8e8e8;
|
|
58
|
+
border: 1px solid #2e3136;
|
|
59
|
+
border-radius: 4px;
|
|
60
|
+
padding: 4px;
|
|
61
|
+
font: inherit;
|
|
62
|
+
}
|
|
63
|
+
aside {
|
|
64
|
+
position: fixed;
|
|
65
|
+
top: 50%;
|
|
66
|
+
right: 20px;
|
|
67
|
+
transform: translateY(-50%);
|
|
68
|
+
width: 240px;
|
|
69
|
+
max-height: 88vh;
|
|
70
|
+
overflow: auto;
|
|
71
|
+
padding: 16px;
|
|
72
|
+
background: #17181b;
|
|
73
|
+
border: 1px solid #26282c;
|
|
74
|
+
border-radius: 6px;
|
|
75
|
+
}
|
|
76
|
+
aside h1 { margin: 0 0 12px; font-size: 13px; font-weight: 600; color: #7fb4e6; }
|
|
77
|
+
aside h2 { margin: 16px 0 8px; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .08em; color: #8a8f98; }
|
|
78
|
+
dl { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 4px 12px; margin: 0; }
|
|
79
|
+
dt { color: #8a8f98; }
|
|
80
|
+
dd { margin: 0; overflow-wrap: anywhere; }
|
|
81
|
+
#extras:empty { display: none; }
|
|
82
|
+
@media (max-width: 1000px) {
|
|
83
|
+
body { display: flex; flex-direction: column; align-items: center; gap: 16px; padding: 16px; }
|
|
84
|
+
#stage { width: 100%; max-width: 640px; }
|
|
85
|
+
aside { position: static; transform: none; width: 100%; max-width: 480px; }
|
|
86
|
+
}
|
|
87
|
+
</style>
|
|
88
|
+
</head>
|
|
89
|
+
<body>
|
|
90
|
+
<div id="stage">
|
|
91
|
+
<main id="replays"></main>
|
|
92
|
+
<footer id="controls">
|
|
93
|
+
<button id="step-back" title="previous frame (left arrow)">⟨</button>
|
|
94
|
+
<button id="play-pause" title="play / pause (space)">▶</button>
|
|
95
|
+
<button id="step-forward" title="next frame (right arrow)">⟩</button>
|
|
96
|
+
<input id="timeline" type="range" min="0" value="0" step="1" />
|
|
97
|
+
<span id="time" class="time">0 / 0</span>
|
|
98
|
+
<select id="speed">
|
|
99
|
+
<option value="0.1">0.1x</option>
|
|
100
|
+
<option value="0.25">0.25x</option>
|
|
101
|
+
<option value="0.5">0.5x</option>
|
|
102
|
+
<option value="1" selected>1x</option>
|
|
103
|
+
<option value="2">2x</option>
|
|
104
|
+
<option value="4">4x</option>
|
|
105
|
+
</select>
|
|
106
|
+
</footer>
|
|
107
|
+
</div>
|
|
108
|
+
<aside>
|
|
109
|
+
<h1>replay buffer</h1>
|
|
110
|
+
<dl id="stats"></dl>
|
|
111
|
+
<h2>fields</h2>
|
|
112
|
+
<dl id="field-stats"></dl>
|
|
113
|
+
<h2>time</h2>
|
|
114
|
+
<dl>
|
|
115
|
+
<dt>frame</dt><dd id="frame">0</dd>
|
|
116
|
+
</dl>
|
|
117
|
+
<div id="extras"></div>
|
|
118
|
+
</aside>
|
|
119
|
+
<script>
|
|
120
|
+
const stats = __REPLAY_STATS__;
|
|
121
|
+
const fields = stats.fields ?? [{ name: 'video' }];
|
|
122
|
+
const extras = document.getElementById('extras');
|
|
123
|
+
|
|
124
|
+
const statsList = document.getElementById('stats');
|
|
125
|
+
for (const [key, value] of Object.entries(stats)) {
|
|
126
|
+
if (key === 'fields') continue;
|
|
127
|
+
|
|
128
|
+
const dt = document.createElement('dt');
|
|
129
|
+
dt.textContent = key;
|
|
130
|
+
const dd = document.createElement('dd');
|
|
131
|
+
dd.textContent = Array.isArray(value) ? value.join(', ') : value;
|
|
132
|
+
statsList.append(dt, dd);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const fieldList = document.getElementById('field-stats');
|
|
136
|
+
for (const field of fields) {
|
|
137
|
+
const dt = document.createElement('dt');
|
|
138
|
+
dt.textContent = field.name;
|
|
139
|
+
const dd = document.createElement('dd');
|
|
140
|
+
dd.textContent = [field.shape, field.dtype].filter(Boolean).join(' ') || 'video';
|
|
141
|
+
fieldList.append(dt, dd);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// one video per field, every video follows the first
|
|
145
|
+
|
|
146
|
+
const replays = document.getElementById('replays');
|
|
147
|
+
const single = fields.length === 1;
|
|
148
|
+
|
|
149
|
+
const videos = fields.map((field, index) => {
|
|
150
|
+
const figure = document.createElement('figure');
|
|
151
|
+
const video = document.createElement('video');
|
|
152
|
+
|
|
153
|
+
video.src = single ? '/video' : `/video/${encodeURIComponent(field.name)}`;
|
|
154
|
+
video.controls = false;
|
|
155
|
+
video.autoplay = true;
|
|
156
|
+
video.loop = true;
|
|
157
|
+
video.muted = true;
|
|
158
|
+
video.playsInline = true;
|
|
159
|
+
|
|
160
|
+
const caption = document.createElement('figcaption');
|
|
161
|
+
caption.textContent = field.name;
|
|
162
|
+
|
|
163
|
+
figure.append(video, caption);
|
|
164
|
+
replays.append(figure);
|
|
165
|
+
return video;
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const [master, ...slaves] = videos;
|
|
169
|
+
|
|
170
|
+
const sync = () => {
|
|
171
|
+
for (const slave of slaves) {
|
|
172
|
+
if (Math.abs(slave.currentTime - master.currentTime) > 0.08) {
|
|
173
|
+
slave.currentTime = master.currentTime;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
slaves.forEach(slave => slave.addEventListener('loadedmetadata', sync));
|
|
179
|
+
|
|
180
|
+
master.addEventListener('play', () => {
|
|
181
|
+
playPause.textContent = '⏸';
|
|
182
|
+
slaves.forEach(video => video.play());
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
master.addEventListener('pause', () => {
|
|
186
|
+
playPause.textContent = '▶';
|
|
187
|
+
slaves.forEach(video => video.pause());
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
master.addEventListener('ratechange', () => {
|
|
191
|
+
slaves.forEach(video => video.playbackRate = master.playbackRate);
|
|
192
|
+
speed.value = String(master.playbackRate);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
master.addEventListener('seeking', sync);
|
|
196
|
+
master.addEventListener('seeked', sync);
|
|
197
|
+
master.addEventListener('timeupdate', sync);
|
|
198
|
+
|
|
199
|
+
const tick = () => {
|
|
200
|
+
if (!master.paused) sync();
|
|
201
|
+
requestAnimationFrame(tick);
|
|
202
|
+
};
|
|
203
|
+
requestAnimationFrame(tick);
|
|
204
|
+
|
|
205
|
+
// frame-level controls
|
|
206
|
+
|
|
207
|
+
const playPause = document.getElementById('play-pause');
|
|
208
|
+
const timeline = document.getElementById('timeline');
|
|
209
|
+
const timeLabel = document.getElementById('time');
|
|
210
|
+
const speed = document.getElementById('speed');
|
|
211
|
+
const frameLabel = document.getElementById('frame');
|
|
212
|
+
|
|
213
|
+
const totalFrames = () => stats.frames ?? (Number.isFinite(master.duration) ? Math.round(master.duration * stats.fps) : 0);
|
|
214
|
+
const frameFromTime = (time) => Math.max(0, Math.min(Math.round(time * stats.fps), totalFrames() - 1));
|
|
215
|
+
|
|
216
|
+
const updateTime = () => {
|
|
217
|
+
const frame = frameFromTime(master.currentTime);
|
|
218
|
+
const total = totalFrames();
|
|
219
|
+
|
|
220
|
+
frameLabel.textContent = `${frame} / ${total}`;
|
|
221
|
+
timeLabel.textContent = `${frame} / ${total}`;
|
|
222
|
+
timeline.max = String(Math.max(total - 1, 0));
|
|
223
|
+
|
|
224
|
+
if (!scrubbing) timeline.value = String(frame);
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
let scrubbing = false;
|
|
228
|
+
timeline.addEventListener('pointerdown', () => scrubbing = true);
|
|
229
|
+
window.addEventListener('pointerup', () => { if (scrubbing) { scrubbing = false; updateTime(); } });
|
|
230
|
+
|
|
231
|
+
const seekToFrame = (frame) => {
|
|
232
|
+
master.currentTime = Math.max(0, Math.min(frame, totalFrames() - 1)) / stats.fps;
|
|
233
|
+
sync();
|
|
234
|
+
updateTime();
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
timeline.addEventListener('input', () => {
|
|
238
|
+
master.pause();
|
|
239
|
+
seekToFrame(Number(timeline.value));
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
playPause.addEventListener('click', () => master.paused ? master.play() : master.pause());
|
|
243
|
+
document.getElementById('step-back').addEventListener('click', () => { master.pause(); seekToFrame(frameFromTime(master.currentTime) - 1); });
|
|
244
|
+
document.getElementById('step-forward').addEventListener('click', () => { master.pause(); seekToFrame(frameFromTime(master.currentTime) + 1); });
|
|
245
|
+
|
|
246
|
+
speed.addEventListener('change', () => master.playbackRate = Number(speed.value));
|
|
247
|
+
|
|
248
|
+
// mouse wheel over the videos steps through frames
|
|
249
|
+
|
|
250
|
+
replays.addEventListener('wheel', (event) => {
|
|
251
|
+
event.preventDefault();
|
|
252
|
+
master.pause();
|
|
253
|
+
seekToFrame(frameFromTime(master.currentTime) + Math.sign(event.deltaY));
|
|
254
|
+
}, { passive: false });
|
|
255
|
+
|
|
256
|
+
window.addEventListener('keydown', (event) => {
|
|
257
|
+
if (event.target.matches('input, select, button, textarea')) return;
|
|
258
|
+
|
|
259
|
+
const step = event.shiftKey ? 10 : 1;
|
|
260
|
+
|
|
261
|
+
if (event.key === ' ') {
|
|
262
|
+
event.preventDefault();
|
|
263
|
+
master.paused ? master.play() : master.pause();
|
|
264
|
+
} else if (event.key === 'ArrowLeft') {
|
|
265
|
+
event.preventDefault();
|
|
266
|
+
master.pause();
|
|
267
|
+
seekToFrame(frameFromTime(master.currentTime) - step);
|
|
268
|
+
} else if (event.key === 'ArrowRight') {
|
|
269
|
+
event.preventDefault();
|
|
270
|
+
master.pause();
|
|
271
|
+
seekToFrame(frameFromTime(master.currentTime) + step);
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
master.addEventListener('timeupdate', updateTime);
|
|
276
|
+
master.addEventListener('loadedmetadata', updateTime);
|
|
277
|
+
master.addEventListener('play', updateTime);
|
|
278
|
+
master.addEventListener('pause', updateTime);
|
|
279
|
+
updateTime();
|
|
280
|
+
|
|
281
|
+
// future: sync value predictions / actions / other per-timestep elements to `master.currentTime`
|
|
282
|
+
window.replay = { stats, fields, videos, master, timeline, seekToFrame, extras };
|
|
283
|
+
</script>
|
|
284
|
+
</body>
|
|
285
|
+
</html>
|
|
@@ -1,17 +1,40 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
import mimetypes
|
|
4
5
|
import threading
|
|
5
6
|
import webbrowser
|
|
6
7
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
7
8
|
from pathlib import Path
|
|
8
|
-
from urllib.parse import urlparse
|
|
9
|
+
from urllib.parse import unquote, urlparse
|
|
9
10
|
|
|
10
11
|
# helpers
|
|
11
12
|
|
|
13
|
+
def normalize_stats(stats: dict):
|
|
14
|
+
""" fold the legacy single-field stats keys into a `fields` list """
|
|
15
|
+
stats = dict(stats)
|
|
16
|
+
|
|
17
|
+
if 'fields' not in stats and 'field' in stats:
|
|
18
|
+
stats['fields'] = [dict(
|
|
19
|
+
name = stats.pop('field'),
|
|
20
|
+
shape = stats.pop('shape', ''),
|
|
21
|
+
dtype = stats.pop('dtype', '')
|
|
22
|
+
)]
|
|
23
|
+
|
|
24
|
+
return stats
|
|
25
|
+
|
|
26
|
+
def normalize_videos(videos):
|
|
27
|
+
if isinstance(videos, dict):
|
|
28
|
+
return {str(name): Path(path) for name, path in videos.items()}
|
|
29
|
+
|
|
30
|
+
return dict(video = Path(videos))
|
|
31
|
+
|
|
32
|
+
def content_type(path: Path):
|
|
33
|
+
return mimetypes.guess_type(path.name)[0] or 'video/mp4'
|
|
34
|
+
|
|
12
35
|
def render_page(stats: dict):
|
|
13
36
|
template_path = Path(__file__).parent / 'frontend' / 'index.html'
|
|
14
|
-
return template_path.read_text().replace('__REPLAY_STATS__', json.dumps(stats))
|
|
37
|
+
return template_path.read_text().replace('__REPLAY_STATS__', json.dumps(normalize_stats(stats)))
|
|
15
38
|
|
|
16
39
|
def parse_range(range_header: str | None, size: int):
|
|
17
40
|
if not range_header or not range_header.startswith('bytes='):
|
|
@@ -28,7 +51,7 @@ def parse_range(range_header: str | None, size: int):
|
|
|
28
51
|
# server
|
|
29
52
|
|
|
30
53
|
class ReplayBufferHandler(BaseHTTPRequestHandler):
|
|
31
|
-
|
|
54
|
+
video_paths: dict[str, Path]
|
|
32
55
|
page: str
|
|
33
56
|
|
|
34
57
|
protocol_version = 'HTTP/1.1'
|
|
@@ -40,11 +63,25 @@ class ReplayBufferHandler(BaseHTTPRequestHandler):
|
|
|
40
63
|
path = urlparse(self.path).path
|
|
41
64
|
|
|
42
65
|
if path == '/':
|
|
43
|
-
self.send_page()
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
66
|
+
return self.send_page()
|
|
67
|
+
|
|
68
|
+
video_path = self.video_for_path(path)
|
|
69
|
+
|
|
70
|
+
if video_path is not None:
|
|
71
|
+
return self.send_video(video_path)
|
|
72
|
+
|
|
73
|
+
self.send_error(404)
|
|
74
|
+
|
|
75
|
+
def video_for_path(self, path):
|
|
76
|
+
# `/video` serves the first video for backward compatibility
|
|
77
|
+
|
|
78
|
+
if path == '/video':
|
|
79
|
+
return next(iter(self.video_paths.values()), None)
|
|
80
|
+
|
|
81
|
+
if path.startswith('/video/'):
|
|
82
|
+
return self.video_paths.get(unquote(path[len('/video/'):]))
|
|
83
|
+
|
|
84
|
+
return None
|
|
48
85
|
|
|
49
86
|
def send_page(self):
|
|
50
87
|
body = self.page.encode()
|
|
@@ -55,15 +92,15 @@ class ReplayBufferHandler(BaseHTTPRequestHandler):
|
|
|
55
92
|
self.end_headers()
|
|
56
93
|
self.wfile.write(body)
|
|
57
94
|
|
|
58
|
-
def send_video(self):
|
|
59
|
-
size =
|
|
95
|
+
def send_video(self, video_path):
|
|
96
|
+
size = video_path.stat().st_size
|
|
60
97
|
byte_range = parse_range(self.headers.get('Range'), size)
|
|
61
98
|
|
|
62
99
|
start, end = byte_range if byte_range else (0, size - 1)
|
|
63
100
|
is_partial = byte_range is not None
|
|
64
101
|
|
|
65
102
|
self.send_response(206 if is_partial else 200)
|
|
66
|
-
self.send_header('Content-Type',
|
|
103
|
+
self.send_header('Content-Type', content_type(video_path))
|
|
67
104
|
self.send_header('Content-Length', str(end - start + 1))
|
|
68
105
|
self.send_header('Accept-Ranges', 'bytes')
|
|
69
106
|
|
|
@@ -74,7 +111,7 @@ class ReplayBufferHandler(BaseHTTPRequestHandler):
|
|
|
74
111
|
|
|
75
112
|
# stream the selected slice, seeking works via repeated range requests
|
|
76
113
|
|
|
77
|
-
with open(
|
|
114
|
+
with open(video_path, 'rb') as f:
|
|
78
115
|
f.seek(start)
|
|
79
116
|
remaining = end - start + 1
|
|
80
117
|
|
|
@@ -87,16 +124,16 @@ class ReplayBufferHandler(BaseHTTPRequestHandler):
|
|
|
87
124
|
self.wfile.write(chunk)
|
|
88
125
|
remaining -= len(chunk)
|
|
89
126
|
|
|
90
|
-
def make_server(
|
|
127
|
+
def make_server(videos: str | Path | dict[str, Path], stats: dict, host = '127.0.0.1', port = 8000):
|
|
91
128
|
handler = type('Handler', (ReplayBufferHandler,), dict(
|
|
92
|
-
|
|
129
|
+
video_paths = normalize_videos(videos),
|
|
93
130
|
page = render_page(stats)
|
|
94
131
|
))
|
|
95
132
|
|
|
96
133
|
return ThreadingHTTPServer((host, port), handler)
|
|
97
134
|
|
|
98
|
-
def serve(
|
|
99
|
-
httpd = make_server(
|
|
135
|
+
def serve(videos: str | Path | dict[str, Path], stats: dict, host = '127.0.0.1', port = 8000, open_browser = True, block = True):
|
|
136
|
+
httpd = make_server(videos, stats, host, port)
|
|
100
137
|
url = f'http://{host}:{httpd.server_address[1]}/'
|
|
101
138
|
|
|
102
139
|
print(f'serving replay at {url} (ctrl-c to stop)')
|
{memmap_replay_buffer-0.2.2 → memmap_replay_buffer-0.2.4}/memmap_replay_buffer/replay_buffer.py
RENAMED
|
@@ -11,7 +11,7 @@ import einx
|
|
|
11
11
|
import numpy as np
|
|
12
12
|
import torch
|
|
13
13
|
from beartype import beartype
|
|
14
|
-
from beartype.typing import Any, Literal
|
|
14
|
+
from beartype.typing import Any, Literal, Sequence
|
|
15
15
|
from einops import rearrange
|
|
16
16
|
from numpy import ndarray
|
|
17
17
|
from numpy.lib.format import open_memmap
|
|
@@ -987,10 +987,18 @@ class ReplayBufferBase:
|
|
|
987
987
|
|
|
988
988
|
@can_write
|
|
989
989
|
@beartype
|
|
990
|
-
def update(
|
|
990
|
+
def update(
|
|
991
|
+
self,
|
|
992
|
+
indices: int | list | ndarray | slice | None = None,
|
|
993
|
+
timestep_start: int = 0,
|
|
994
|
+
**data
|
|
995
|
+
):
|
|
991
996
|
if len(data) == 0:
|
|
992
997
|
raise ValueError('No data provided to `update`')
|
|
993
998
|
|
|
999
|
+
if timestep_start < 0:
|
|
1000
|
+
raise ValueError(f'timestep_start must be non-negative, got {timestep_start}')
|
|
1001
|
+
|
|
994
1002
|
indices, scalar_index = expand_indices(indices, self.max_episodes)
|
|
995
1003
|
|
|
996
1004
|
if not exists(indices):
|
|
@@ -1028,11 +1036,11 @@ class ReplayBufferBase:
|
|
|
1028
1036
|
|
|
1029
1037
|
time_dim = value.shape[1]
|
|
1030
1038
|
|
|
1031
|
-
if time_dim > self.max_timesteps:
|
|
1039
|
+
if timestep_start + time_dim > self.max_timesteps:
|
|
1032
1040
|
raise ValueError(f'You exceeded the `max_timesteps` ({self.max_timesteps}) set on the replay buffer. Please increase it on init.')
|
|
1033
1041
|
|
|
1034
1042
|
value = np.broadcast_to(value, (len(indices), time_dim, *self.shapes[name]))
|
|
1035
|
-
self.data[name][indices, :time_dim] = value[order]
|
|
1043
|
+
self.data[name][indices, timestep_start:timestep_start + time_dim] = value[order]
|
|
1036
1044
|
|
|
1037
1045
|
else:
|
|
1038
1046
|
value = cast_to_target_shape(value, self.meta_shapes[name], num_leading_dims = 1)
|
|
@@ -1235,8 +1243,12 @@ class ReplayBufferBase:
|
|
|
1235
1243
|
|
|
1236
1244
|
# video
|
|
1237
1245
|
|
|
1246
|
+
def image_fields(self):
|
|
1247
|
+
""" all fields that look like images, in field declaration order """
|
|
1248
|
+
return [name for name in self.shapes if len(self.shapes[name]) >= 2]
|
|
1249
|
+
|
|
1238
1250
|
def default_image_field(self):
|
|
1239
|
-
candidates =
|
|
1251
|
+
candidates = self.image_fields()
|
|
1240
1252
|
|
|
1241
1253
|
if len(candidates) == 1:
|
|
1242
1254
|
return candidates[0]
|
|
@@ -1246,17 +1258,28 @@ class ReplayBufferBase:
|
|
|
1246
1258
|
|
|
1247
1259
|
raise ValueError(f'multiple image-like fields {candidates} - pass `field` explicitly')
|
|
1248
1260
|
|
|
1249
|
-
def episode_frames(self, episode_index: int = 0, field: str | None = None):
|
|
1250
|
-
|
|
1261
|
+
def episode_frames(self, episode_index: int | Sequence[int] = 0, field: str | None = None):
|
|
1262
|
+
if not exists(field):
|
|
1263
|
+
field = self.default_image_field()
|
|
1251
1264
|
|
|
1252
1265
|
if field not in self.fieldnames:
|
|
1253
1266
|
raise ValueError(f'invalid field name `{field}` - must be one of {self.fieldnames}')
|
|
1254
1267
|
|
|
1255
|
-
|
|
1256
|
-
|
|
1268
|
+
episode_indices = [episode_index] if isinstance(episode_index, (int, np.integer)) else list(episode_index)
|
|
1269
|
+
|
|
1270
|
+
if len(episode_indices) == 0:
|
|
1271
|
+
raise ValueError('at least one episode index is required')
|
|
1272
|
+
|
|
1273
|
+
for index in episode_indices:
|
|
1274
|
+
if not (0 <= index < self.num_episodes):
|
|
1275
|
+
raise ValueError(f'episode_index {index} out of range - buffer has {self.num_episodes} episodes')
|
|
1276
|
+
|
|
1277
|
+
frames = [self.data[field][index, :int(self.episode_lens[index])] for index in episode_indices]
|
|
1278
|
+
|
|
1279
|
+
if len(frames) == 1:
|
|
1280
|
+
return frames[0].copy()
|
|
1257
1281
|
|
|
1258
|
-
|
|
1259
|
-
return self.data[field][episode_index, :episode_len].copy()
|
|
1282
|
+
return np.concatenate(frames, axis = 0)
|
|
1260
1283
|
|
|
1261
1284
|
def save_video(self, path: str | Path, episode_index: int = 0, field: str | None = None, fps: int = 30, **kwargs):
|
|
1262
1285
|
from memmap_replay_buffer.video import trajectory_to_video
|