scope 0.2.3__tar.gz → 0.3.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.
- scope-0.3.1/LICENSE +19 -0
- {scope-0.2.3/scope.egg-info → scope-0.3.1}/PKG-INFO +2 -1
- {scope-0.2.3 → scope-0.3.1}/requirements.txt +1 -0
- scope-0.3.1/scope/__init__.py +9 -0
- scope-0.3.1/scope/formats.py +211 -0
- scope-0.3.1/scope/reader.py +45 -0
- scope-0.3.1/scope/writer.py +97 -0
- {scope-0.2.3 → scope-0.3.1/scope.egg-info}/PKG-INFO +2 -1
- {scope-0.2.3 → scope-0.3.1}/scope.egg-info/SOURCES.txt +2 -1
- {scope-0.2.3 → scope-0.3.1}/scope.egg-info/requires.txt +1 -0
- {scope-0.2.3 → scope-0.3.1}/tests/test_float.py +15 -15
- {scope-0.2.3 → scope-0.3.1}/tests/test_image.py +31 -27
- {scope-0.2.3 → scope-0.3.1}/tests/test_video.py +10 -5
- scope-0.2.3/scope/__init__.py +0 -4
- scope-0.2.3/scope/columns.py +0 -188
- scope-0.2.3/scope/reader.py +0 -43
- scope-0.2.3/scope/writer.py +0 -71
- {scope-0.2.3 → scope-0.3.1}/MANIFEST.in +0 -0
- {scope-0.2.3 → scope-0.3.1}/README.md +0 -0
- {scope-0.2.3 → scope-0.3.1}/pyproject.toml +0 -0
- {scope-0.2.3 → scope-0.3.1}/scope.egg-info/dependency_links.txt +0 -0
- {scope-0.2.3 → scope-0.3.1}/scope.egg-info/top_level.txt +0 -0
- {scope-0.2.3 → scope-0.3.1}/setup.cfg +0 -0
- {scope-0.2.3 → scope-0.3.1}/setup.py +0 -0
scope-0.3.1/LICENSE
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (c) 2024 Danijar Hafner
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
SOFTWARE.
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: scope
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Metrics logging and analysis
|
|
5
5
|
Home-page: http://github.com/danijar/scope
|
|
6
6
|
Classifier: Intended Audience :: Science/Research
|
|
7
7
|
Classifier: License :: OSI Approved :: MIT License
|
|
8
8
|
Classifier: Programming Language :: Python :: 3
|
|
9
9
|
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import struct
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import PIL.Image
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Float:
|
|
10
|
+
|
|
11
|
+
@property
|
|
12
|
+
def extension(self):
|
|
13
|
+
return 'float'
|
|
14
|
+
|
|
15
|
+
def valid(self, x):
|
|
16
|
+
return x.ndim == 0 and np.isreal(x)
|
|
17
|
+
|
|
18
|
+
def create(self, path):
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
def write(self, path, steps, values):
|
|
22
|
+
table_append(path, '>qd', steps, values)
|
|
23
|
+
|
|
24
|
+
def read(self, path):
|
|
25
|
+
steps, values = table_read(path, '>qd')
|
|
26
|
+
steps = np.int64(steps)
|
|
27
|
+
values = np.float64(values)
|
|
28
|
+
return steps, values
|
|
29
|
+
|
|
30
|
+
def length(self, path):
|
|
31
|
+
return table_length(path, '>qd')
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Text:
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def extension(self):
|
|
38
|
+
return 'txt'
|
|
39
|
+
|
|
40
|
+
def valid(self, x):
|
|
41
|
+
return isinstance(x, str)
|
|
42
|
+
|
|
43
|
+
def create(self, path):
|
|
44
|
+
path.mkdir(exist_ok=True)
|
|
45
|
+
|
|
46
|
+
def write(self, path, steps, values):
|
|
47
|
+
files_write(path, steps, values, self.encode)
|
|
48
|
+
|
|
49
|
+
def read(self, path):
|
|
50
|
+
return files_read(path)
|
|
51
|
+
|
|
52
|
+
def encode(self, value):
|
|
53
|
+
return value.encode('utf-8')
|
|
54
|
+
|
|
55
|
+
def decode(self, buffer):
|
|
56
|
+
return buffer.deceode('utf-8')
|
|
57
|
+
|
|
58
|
+
def length(self, path):
|
|
59
|
+
return files_length(path)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class Image:
|
|
63
|
+
|
|
64
|
+
def __init__(self, ext='png', quality=90):
|
|
65
|
+
self.ext = ext
|
|
66
|
+
self.quality = quality
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def extension(self):
|
|
70
|
+
return self.ext
|
|
71
|
+
|
|
72
|
+
def valid(self, x):
|
|
73
|
+
return (
|
|
74
|
+
x.dtype == np.uint8 and
|
|
75
|
+
x.ndim == 3 and
|
|
76
|
+
x.shape[-1] in (1, 3))
|
|
77
|
+
|
|
78
|
+
def create(self, path):
|
|
79
|
+
path.mkdir(exist_ok=True)
|
|
80
|
+
|
|
81
|
+
def write(self, path, steps, values):
|
|
82
|
+
files_write(path, steps, values, self.encode)
|
|
83
|
+
|
|
84
|
+
def read(self, path):
|
|
85
|
+
return files_read(path)
|
|
86
|
+
|
|
87
|
+
def encode(self, value):
|
|
88
|
+
if value.shape[-1] == 1:
|
|
89
|
+
value = value.repeat(3, -1)
|
|
90
|
+
fmt = ('jpeg' if self.ext == 'jpg' else self.ext).upper()
|
|
91
|
+
fp = io.BytesIO()
|
|
92
|
+
PIL.Image.fromarray(value).save(fp, fmt, quality=self.quality)
|
|
93
|
+
return fp.getvalue()
|
|
94
|
+
|
|
95
|
+
def decode(self, buffer):
|
|
96
|
+
return np.asarray(PIL.Image.open(io.BytesIO(buffer)).convert('RGB'))
|
|
97
|
+
|
|
98
|
+
def length(self, path):
|
|
99
|
+
return files_length(path)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class Video:
|
|
103
|
+
|
|
104
|
+
def __init__(self, ext='mp4', codec='h264', fps=10):
|
|
105
|
+
self.ext = ext
|
|
106
|
+
self.codec = codec
|
|
107
|
+
self.fps = fps
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def extension(self):
|
|
111
|
+
return self.ext
|
|
112
|
+
|
|
113
|
+
def valid(self, x):
|
|
114
|
+
return (
|
|
115
|
+
x.dtype == np.uint8 and
|
|
116
|
+
x.ndim == 4 and
|
|
117
|
+
x.shape[-1] in (1, 3))
|
|
118
|
+
|
|
119
|
+
def create(self, path):
|
|
120
|
+
path.mkdir(exist_ok=True)
|
|
121
|
+
|
|
122
|
+
def write(self, path, steps, values):
|
|
123
|
+
files_write(path, steps, values, self.encode)
|
|
124
|
+
|
|
125
|
+
def read(self, path):
|
|
126
|
+
return files_read(path)
|
|
127
|
+
|
|
128
|
+
def encode(self, value):
|
|
129
|
+
import av
|
|
130
|
+
if value.shape[-1] == 1:
|
|
131
|
+
value = value.repeat(3, -1)
|
|
132
|
+
T, H, W, _ = value.shape
|
|
133
|
+
fp = io.BytesIO()
|
|
134
|
+
output = av.open(fp, mode='w', format=self.ext)
|
|
135
|
+
stream = output.add_stream(self.codec, rate=float(self.fps))
|
|
136
|
+
stream.width = W
|
|
137
|
+
stream.height = H
|
|
138
|
+
stream.pix_fmt = 'yuv420p'
|
|
139
|
+
for t in range(T):
|
|
140
|
+
frame = av.VideoFrame.from_ndarray(value[t], format='rgb24')
|
|
141
|
+
frame.pts = t
|
|
142
|
+
output.mux(stream.encode(frame))
|
|
143
|
+
output.mux(stream.encode(None))
|
|
144
|
+
output.close()
|
|
145
|
+
return fp.getvalue()
|
|
146
|
+
|
|
147
|
+
def decode(self, buffer):
|
|
148
|
+
import av
|
|
149
|
+
container = av.open(io.BytesIO(buffer))
|
|
150
|
+
value = []
|
|
151
|
+
for frame in container.decode(video=0):
|
|
152
|
+
value.append(frame.to_ndarray(format='rgb24'))
|
|
153
|
+
value = np.stack(value)
|
|
154
|
+
container.close()
|
|
155
|
+
return value
|
|
156
|
+
|
|
157
|
+
def length(self, path):
|
|
158
|
+
return files_length(path)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def table_append(filename, fmt, *cols):
|
|
162
|
+
rows = tuple(zip(*cols))
|
|
163
|
+
size = struct.calcsize(fmt)
|
|
164
|
+
buffer = bytearray(len(rows) * size)
|
|
165
|
+
for index, row in enumerate(rows):
|
|
166
|
+
struct.pack_into(fmt, buffer, index * size, *row)
|
|
167
|
+
with filename.open('ab') as f:
|
|
168
|
+
f.write(buffer)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def table_read(filename, fmt, start=0, stop=None):
|
|
172
|
+
assert stop is None or start < stop, (start, stop)
|
|
173
|
+
if start == 0 and stop is None:
|
|
174
|
+
buffer = filename.read_bytes()
|
|
175
|
+
else:
|
|
176
|
+
size = struct.calcsize(fmt)
|
|
177
|
+
with filename.open('rb') as f:
|
|
178
|
+
start and f.seek(start * size)
|
|
179
|
+
buffer = f.read((stop - start) * size if stop else None)
|
|
180
|
+
rows = struct.iter_unpack(fmt, buffer)
|
|
181
|
+
cols = tuple(zip(*rows))
|
|
182
|
+
return cols
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def table_length(filename, fmt):
|
|
186
|
+
return filename.stat().st_size // struct.calcsize(fmt)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def files_write(path, steps, values, encode):
|
|
190
|
+
rng = np.random.default_rng(seed=None)
|
|
191
|
+
prefix = int(time.time()).to_bytes(4, 'big')
|
|
192
|
+
idents = [prefix + rng.bytes(4) for _ in range(len(steps))]
|
|
193
|
+
for ident, step, value in zip(idents, steps, values):
|
|
194
|
+
filename = f'{step:020}-{ident.hex()}{path.suffix}'
|
|
195
|
+
buffer = encode(value)
|
|
196
|
+
with (path / filename).open('wb') as f:
|
|
197
|
+
f.write(buffer)
|
|
198
|
+
table_append(path / 'index', 'q8s', steps, idents)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def files_read(path):
|
|
202
|
+
steps, idents = table_read(path / 'index', 'q8s')
|
|
203
|
+
filenames = [
|
|
204
|
+
path / f'{step:020}-{ident.hex()}{path.suffix}'
|
|
205
|
+
for step, ident in zip(steps, idents)]
|
|
206
|
+
steps = np.int64(steps)
|
|
207
|
+
return steps, filenames
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def files_length(path):
|
|
211
|
+
return table_length(path / 'index', 'q8s')
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import pathlib
|
|
3
|
+
|
|
4
|
+
from . import formats
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
FORMATS = [
|
|
8
|
+
formats.Text(),
|
|
9
|
+
formats.Float(),
|
|
10
|
+
formats.Image(),
|
|
11
|
+
formats.Video(),
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Reader:
|
|
16
|
+
|
|
17
|
+
def __init__(self, logdir, formats=None):
|
|
18
|
+
formats = formats or FORMATS
|
|
19
|
+
if isinstance(logdir, str):
|
|
20
|
+
logdir = pathlib.Path(logdir)
|
|
21
|
+
self.logdir = logdir
|
|
22
|
+
self.fmts = {x.extension: x for x in formats}
|
|
23
|
+
self.cols = {}
|
|
24
|
+
for child in sorted(logdir.glob('*')):
|
|
25
|
+
basename, ext = child.name.rsplit('.', 1)
|
|
26
|
+
key = basename.replace('-', '/')
|
|
27
|
+
assert re.match(r'[a-z0-9_]+(/[a-z0-9_]+)?', key), key
|
|
28
|
+
self.cols[key] = (child.name, self.fmts[ext])
|
|
29
|
+
|
|
30
|
+
def keys(self):
|
|
31
|
+
return tuple(self.cols.keys())
|
|
32
|
+
|
|
33
|
+
def __getitem__(self, key):
|
|
34
|
+
name, fmt = self.cols[key]
|
|
35
|
+
return fmt.read(self.logdir / name)
|
|
36
|
+
|
|
37
|
+
def length(self, key):
|
|
38
|
+
name, fmt = self.cols[key]
|
|
39
|
+
return fmt.length(self.logdir / name)
|
|
40
|
+
|
|
41
|
+
def load(self, key, filename):
|
|
42
|
+
_, fmt = self.cols[key]
|
|
43
|
+
buffer = (self.logdir / filename).read_bytes()
|
|
44
|
+
value = fmt.decode(buffer)
|
|
45
|
+
return value
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import concurrent.futures
|
|
2
|
+
import dataclasses
|
|
3
|
+
import pathlib
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from . import formats
|
|
9
|
+
|
|
10
|
+
FPS = 10
|
|
11
|
+
|
|
12
|
+
FORMATS = [
|
|
13
|
+
formats.Text(),
|
|
14
|
+
formats.Float(),
|
|
15
|
+
formats.Image(),
|
|
16
|
+
formats.Video(fps=FPS),
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclasses.dataclass
|
|
21
|
+
class Column:
|
|
22
|
+
|
|
23
|
+
fmt: str
|
|
24
|
+
name: str
|
|
25
|
+
created: bool
|
|
26
|
+
steps: list
|
|
27
|
+
values: list
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Writer:
|
|
31
|
+
|
|
32
|
+
def __init__(self, logdir, workers=8, formats=None):
|
|
33
|
+
formats = formats or FORMATS
|
|
34
|
+
if isinstance(logdir, str):
|
|
35
|
+
logdir = pathlib.Path(logdir)
|
|
36
|
+
self.logdir = logdir
|
|
37
|
+
self.logdir.mkdir(parents=True, exist_ok=True)
|
|
38
|
+
self.workers = workers
|
|
39
|
+
self.rng = np.random.default_rng(seed=None)
|
|
40
|
+
self.fmts = FORMATS
|
|
41
|
+
self.cols = {}
|
|
42
|
+
if workers:
|
|
43
|
+
self.pool = concurrent.futures.ThreadPoolExecutor(workers, 'scope')
|
|
44
|
+
self.futures = []
|
|
45
|
+
|
|
46
|
+
def add(self, step, *args, **kwargs):
|
|
47
|
+
assert isinstance(step, (int, np.integer)), type(step)
|
|
48
|
+
step = int(step)
|
|
49
|
+
mapping = dict(*args, **kwargs)
|
|
50
|
+
for key, value in mapping.items():
|
|
51
|
+
if not isinstance(value, str):
|
|
52
|
+
value = np.asarray(value)
|
|
53
|
+
if key not in self.cols:
|
|
54
|
+
assert re.match(r'[a-z0-9_]+(/[a-z0-9_]+)?', key), key
|
|
55
|
+
for fmt in self.fmts:
|
|
56
|
+
if fmt.valid(value):
|
|
57
|
+
break
|
|
58
|
+
else:
|
|
59
|
+
raise NotImplementedError(
|
|
60
|
+
f"No format supports key '{key}' with {self._info(value)}")
|
|
61
|
+
name = key.replace('/', '-') + '.' + fmt.extension
|
|
62
|
+
self.cols[key] = Column(fmt, name, False, [], [])
|
|
63
|
+
col = self.cols[key]
|
|
64
|
+
if not col.fmt.valid(value):
|
|
65
|
+
raise ValueError(
|
|
66
|
+
f"Key '{key}' contains invalid value {self._info(value)}")
|
|
67
|
+
col.steps.append(step)
|
|
68
|
+
col.values.append(value)
|
|
69
|
+
|
|
70
|
+
def flush(self):
|
|
71
|
+
if self.workers:
|
|
72
|
+
list(self.futures)
|
|
73
|
+
jobs = [(c, c.steps, c.values) for c in self.cols.values() if c.steps]
|
|
74
|
+
self.futures = self.pool.map(self._write, *zip(*jobs))
|
|
75
|
+
else:
|
|
76
|
+
for col in self.cols.values():
|
|
77
|
+
if col.steps:
|
|
78
|
+
self._write(col, col.steps, col.values)
|
|
79
|
+
for col in self.cols.values():
|
|
80
|
+
col.steps = []
|
|
81
|
+
col.values = []
|
|
82
|
+
|
|
83
|
+
def _write(self, col, steps, values):
|
|
84
|
+
try:
|
|
85
|
+
path = self.logdir / col.name
|
|
86
|
+
if not col.created:
|
|
87
|
+
col.fmt.create(path)
|
|
88
|
+
col.created = True
|
|
89
|
+
col.fmt.write(path, steps, values)
|
|
90
|
+
except Exception:
|
|
91
|
+
print(f"Exception writing '{col.name}' column")
|
|
92
|
+
raise
|
|
93
|
+
|
|
94
|
+
def _info(self, value):
|
|
95
|
+
if hasattr(value, 'dtype') and hasattr(value, 'shape'):
|
|
96
|
+
return f"dtype '{value.dtype}' and shape '{value.shape}'"
|
|
97
|
+
return f"type '{type(value)}'"
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: scope
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Metrics logging and analysis
|
|
5
5
|
Home-page: http://github.com/danijar/scope
|
|
6
6
|
Classifier: Intended Audience :: Science/Research
|
|
7
7
|
Classifier: License :: OSI Approved :: MIT License
|
|
8
8
|
Classifier: Programming Language :: Python :: 3
|
|
9
9
|
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
@@ -22,21 +22,6 @@ class TestFloat:
|
|
|
22
22
|
assert equal(reader['foo'], ([0, 5], [12, 42]), (np.int64, np.float64))
|
|
23
23
|
assert equal(reader['bar'], ([5], [np.pi]), (np.int64, np.float64))
|
|
24
24
|
|
|
25
|
-
def test_slicing(self, tmpdir):
|
|
26
|
-
logdir = pathlib.Path(tmpdir)
|
|
27
|
-
writer = scope.Writer(logdir, workers=0)
|
|
28
|
-
writer.add(0, {'foo': 12})
|
|
29
|
-
writer.add(5, {'foo': 42})
|
|
30
|
-
writer.flush()
|
|
31
|
-
reader = scope.Reader(logdir)
|
|
32
|
-
assert equal(reader['foo', 0], ([0], [12]))
|
|
33
|
-
assert equal(reader['foo', :2], ([0], [12]))
|
|
34
|
-
assert equal(reader['foo', :5], ([0], [12]))
|
|
35
|
-
assert equal(reader['foo', :6], ([0, 5], [12, 42]))
|
|
36
|
-
assert equal(reader['foo', 1:6], ([5], [42]))
|
|
37
|
-
assert equal(reader['foo', :-1], ([], []))
|
|
38
|
-
assert equal(reader['foo', 7:], ([], []))
|
|
39
|
-
|
|
40
25
|
def test_workers(self, tmpdir):
|
|
41
26
|
logdir = pathlib.Path(tmpdir)
|
|
42
27
|
writer = scope.Writer(logdir, workers=8)
|
|
@@ -62,6 +47,21 @@ class TestFloat:
|
|
|
62
47
|
assert reader.length('foo/bar') == 1
|
|
63
48
|
assert equal(reader['foo/bar'], ([0], [12]), (np.int64, np.float64))
|
|
64
49
|
|
|
50
|
+
# def test_slicing(self, tmpdir):
|
|
51
|
+
# logdir = pathlib.Path(tmpdir)
|
|
52
|
+
# writer = scope.Writer(logdir, workers=0)
|
|
53
|
+
# writer.add(0, {'foo': 12})
|
|
54
|
+
# writer.add(5, {'foo': 42})
|
|
55
|
+
# writer.flush()
|
|
56
|
+
# reader = scope.Reader(logdir)
|
|
57
|
+
# assert equal(reader['foo', 0], ([0], [12]))
|
|
58
|
+
# assert equal(reader['foo', :2], ([0], [12]))
|
|
59
|
+
# assert equal(reader['foo', :5], ([0], [12]))
|
|
60
|
+
# assert equal(reader['foo', :6], ([0, 5], [12, 42]))
|
|
61
|
+
# assert equal(reader['foo', 1:6], ([5], [42]))
|
|
62
|
+
# assert equal(reader['foo', :-1], ([], []))
|
|
63
|
+
# assert equal(reader['foo', 7:], ([], []))
|
|
64
|
+
|
|
65
65
|
|
|
66
66
|
def equal(actuals, references, dtypes=None):
|
|
67
67
|
dtypes = dtypes or [x.dtype for x in actuals]
|
|
@@ -20,32 +20,10 @@ class TestImage:
|
|
|
20
20
|
reader = scope.Reader(logdir)
|
|
21
21
|
assert reader.keys() == ('foo',)
|
|
22
22
|
assert reader.length('foo') == 2
|
|
23
|
-
steps,
|
|
23
|
+
steps, filenames = reader['foo']
|
|
24
|
+
values = [reader.load('foo', x) for x in filenames]
|
|
24
25
|
assert (steps == np.array([0, 5])).all()
|
|
25
|
-
assert (values == np.array([img1, img2])).all()
|
|
26
|
-
|
|
27
|
-
def test_slicing(self, tmpdir):
|
|
28
|
-
logdir = pathlib.Path(tmpdir)
|
|
29
|
-
writer = scope.Writer(logdir, workers=0)
|
|
30
|
-
img1 = np.ones((64, 128, 3), np.uint8) + 12
|
|
31
|
-
img2 = np.ones((64, 128, 3), np.uint8) + 255
|
|
32
|
-
writer.add(0, {'foo': img1})
|
|
33
|
-
writer.add(5, {'foo': img2})
|
|
34
|
-
writer.flush()
|
|
35
|
-
assert {x.name for x in logdir.glob('*')} == {'foo.png'}
|
|
36
|
-
assert (logdir / 'foo.png' / 'index').stat().st_size == (8 + 8) * 2
|
|
37
|
-
reader = scope.Reader(logdir)
|
|
38
|
-
assert reader.keys() == ('foo',)
|
|
39
|
-
assert reader.length('foo') == 2
|
|
40
|
-
steps, values = reader['foo']
|
|
41
|
-
assert (steps == np.array([0, 5])).all()
|
|
42
|
-
assert (values == np.array([img1, img2])).all()
|
|
43
|
-
assert (reader['foo', 0][1] == img1[None]).all()
|
|
44
|
-
assert (reader['foo', :5][1] == img1[None]).all()
|
|
45
|
-
assert (reader['foo', :6][1] == np.array([img1, img2])).all()
|
|
46
|
-
assert (reader['foo', 1:6][1] == img2[None]).all()
|
|
47
|
-
assert reader['foo', :-1][1] == ()
|
|
48
|
-
assert reader['foo', 6:][1] == ()
|
|
26
|
+
assert (np.array(values) == np.array([img1, img2])).all()
|
|
49
27
|
|
|
50
28
|
def test_workers(self, tmpdir):
|
|
51
29
|
logdir = pathlib.Path(tmpdir)
|
|
@@ -64,7 +42,8 @@ class TestImage:
|
|
|
64
42
|
assert reader.keys() == tuple(sorted(['foo', 'bar', 'baz']))
|
|
65
43
|
for key in ('foo', 'bar', 'baz'):
|
|
66
44
|
assert reader.length(key) == 5
|
|
67
|
-
steps,
|
|
45
|
+
steps, filenames = reader[key]
|
|
46
|
+
values = [reader.load(key, x) for x in filenames]
|
|
68
47
|
assert (steps == np.arange(5)).all()
|
|
69
48
|
assert all(x.dtype == np.uint8 for x in values)
|
|
70
49
|
reference = np.arange(5, dtype=np.uint8)[:, None, None, None]
|
|
@@ -81,4 +60,29 @@ class TestImage:
|
|
|
81
60
|
reader = scope.Reader(logdir)
|
|
82
61
|
assert reader.keys() == ('foo/bar',)
|
|
83
62
|
assert reader.length('foo/bar') == 1
|
|
84
|
-
|
|
63
|
+
_, filenames = reader['foo/bar']
|
|
64
|
+
assert len(filenames) == 1
|
|
65
|
+
assert (reader.load('foo/bar', filenames[0]) == img).all()
|
|
66
|
+
|
|
67
|
+
# def test_slicing(self, tmpdir):
|
|
68
|
+
# logdir = pathlib.Path(tmpdir)
|
|
69
|
+
# writer = scope.Writer(logdir, workers=0)
|
|
70
|
+
# img1 = np.ones((64, 128, 3), np.uint8) + 12
|
|
71
|
+
# img2 = np.ones((64, 128, 3), np.uint8) + 255
|
|
72
|
+
# writer.add(0, {'foo': img1})
|
|
73
|
+
# writer.add(5, {'foo': img2})
|
|
74
|
+
# writer.flush()
|
|
75
|
+
# assert {x.name for x in logdir.glob('*')} == {'foo.png'}
|
|
76
|
+
# assert (logdir / 'foo.png' / 'index').stat().st_size == (8 + 8) * 2
|
|
77
|
+
# reader = scope.Reader(logdir)
|
|
78
|
+
# assert reader.keys() == ('foo',)
|
|
79
|
+
# assert reader.length('foo') == 2
|
|
80
|
+
# steps, values = reader['foo']
|
|
81
|
+
# assert (steps == np.array([0, 5])).all()
|
|
82
|
+
# assert (values == np.array([img1, img2])).all()
|
|
83
|
+
# assert (reader['foo', 0][1] == img1[None]).all()
|
|
84
|
+
# assert (reader['foo', :5][1] == img1[None]).all()
|
|
85
|
+
# assert (reader['foo', :6][1] == np.array([img1, img2])).all()
|
|
86
|
+
# assert (reader['foo', 1:6][1] == img2[None]).all()
|
|
87
|
+
# assert reader['foo', :-1][1] == ()
|
|
88
|
+
# assert reader['foo', 6:][1] == ()
|
|
@@ -14,12 +14,17 @@ class TestVideo:
|
|
|
14
14
|
writer.add(0, {'foo': vid1})
|
|
15
15
|
writer.add(5, {'foo': vid2})
|
|
16
16
|
writer.flush()
|
|
17
|
-
|
|
18
|
-
assert (
|
|
19
|
-
|
|
17
|
+
names = {x.name for x in logdir.glob('*')}
|
|
18
|
+
assert len(names) == 1
|
|
19
|
+
name = list(names)[0]
|
|
20
|
+
assert name in ('foo.mp4', 'foo.webm')
|
|
21
|
+
assert (logdir / name / 'index').stat().st_size == (8 + 8) * 2
|
|
22
|
+
assert len(list((logdir / name).glob('*'))) == 1 + 2
|
|
20
23
|
reader = scope.Reader(logdir)
|
|
21
24
|
assert reader.keys() == ('foo',)
|
|
22
25
|
assert reader.length('foo') == 2
|
|
23
|
-
steps,
|
|
26
|
+
steps, filenames = reader['foo']
|
|
24
27
|
assert (steps == np.array([0, 5])).all()
|
|
25
|
-
|
|
28
|
+
values = [reader.load('foo', x) for x in filenames]
|
|
29
|
+
assert all(x.dtype == np.uint8 for x in values)
|
|
30
|
+
assert np.allclose(values, [vid1, vid2], rtol=0.1, atol=3)
|
scope-0.2.3/scope/__init__.py
DELETED
scope-0.2.3/scope/columns.py
DELETED
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
import io
|
|
2
|
-
import struct
|
|
3
|
-
import time
|
|
4
|
-
|
|
5
|
-
import av
|
|
6
|
-
import numpy as np
|
|
7
|
-
from PIL import Image
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def table_length(filename, fmt):
|
|
11
|
-
return filename.stat().st_size // struct.calcsize(fmt)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def table_write(filename, fmt, *cols):
|
|
15
|
-
rows = tuple(zip(*cols))
|
|
16
|
-
size = struct.calcsize(fmt)
|
|
17
|
-
buffer = bytearray(len(rows) * size)
|
|
18
|
-
for index, row in enumerate(rows):
|
|
19
|
-
struct.pack_into(fmt, buffer, index * size, *row)
|
|
20
|
-
with filename.open('ab') as f:
|
|
21
|
-
f.write(buffer)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def table_read(filename, fmt, start=0, stop=None):
|
|
25
|
-
assert stop is None or start < stop, (start, stop)
|
|
26
|
-
size = struct.calcsize(fmt)
|
|
27
|
-
with filename.open('rb') as f:
|
|
28
|
-
start and f.seek(start * size)
|
|
29
|
-
buffer = f.read((stop - start) * size if stop else None)
|
|
30
|
-
rows = struct.iter_unpack(fmt, buffer)
|
|
31
|
-
cols = tuple(zip(*rows))
|
|
32
|
-
return cols
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
class FloatColumn:
|
|
36
|
-
|
|
37
|
-
def __init__(self, logdir, key):
|
|
38
|
-
name = key.replace('/', '-') + '.float'
|
|
39
|
-
self.filename = logdir / name
|
|
40
|
-
|
|
41
|
-
def validate(self, value):
|
|
42
|
-
assert value.dtype in (float, int) and value.ndim == 0, (
|
|
43
|
-
value.dtype, value.shape)
|
|
44
|
-
return value
|
|
45
|
-
|
|
46
|
-
def write(self, values):
|
|
47
|
-
steps, values = zip(*values)
|
|
48
|
-
table_write(self.filename, '>qd', steps, values)
|
|
49
|
-
|
|
50
|
-
def length(self):
|
|
51
|
-
return table_length(self.filename, '>qd')
|
|
52
|
-
|
|
53
|
-
def read(self, start, stop):
|
|
54
|
-
steps, values = table_read(self.filename, '>qd')
|
|
55
|
-
filtered = [(s, v) for s, v in zip(steps, values) if start <= s < stop]
|
|
56
|
-
steps, values = zip(*filtered) if filtered else ([], [])
|
|
57
|
-
steps = np.array(steps, np.int64)
|
|
58
|
-
values = np.array(values, np.float64)
|
|
59
|
-
return steps, values
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
class FileColumn:
|
|
63
|
-
|
|
64
|
-
def __init__(self, logdir, key, ext, encfn, decfn):
|
|
65
|
-
name = key.replace('/', '-') + '.' + ext
|
|
66
|
-
self.folder = logdir / name
|
|
67
|
-
self.folder.mkdir(exist_ok=True)
|
|
68
|
-
self.index = self.folder / 'index'
|
|
69
|
-
self.rng = np.random.default_rng(seed=None)
|
|
70
|
-
self.ext = ext
|
|
71
|
-
self.encfn = encfn
|
|
72
|
-
self.decfn = decfn
|
|
73
|
-
|
|
74
|
-
def validate(self, value):
|
|
75
|
-
raise NotImplementedError
|
|
76
|
-
|
|
77
|
-
def write(self, values):
|
|
78
|
-
prefix = int(time.time()).to_bytes(4, 'big')
|
|
79
|
-
steps, values = zip(*values)
|
|
80
|
-
idents = [prefix + self.rng.bytes(4) for _ in range(len(steps))]
|
|
81
|
-
for ident, step, value in zip(idents, steps, values):
|
|
82
|
-
buffer = self.encfn(value)
|
|
83
|
-
with self._filename(step, ident).open('wb') as f:
|
|
84
|
-
f.write(buffer)
|
|
85
|
-
table_write(self.index, 'q8s', steps, idents)
|
|
86
|
-
|
|
87
|
-
def length(self):
|
|
88
|
-
return table_length(self.index, 'q8s')
|
|
89
|
-
|
|
90
|
-
def read(self, start, stop):
|
|
91
|
-
steps, idents = table_read(self.index, 'q8s')
|
|
92
|
-
filtered = [(s, v) for s, v in zip(steps, idents) if start <= s < stop]
|
|
93
|
-
steps, idents = zip(*filtered) if filtered else ([], [])
|
|
94
|
-
values = []
|
|
95
|
-
for step, ident in zip(steps, idents):
|
|
96
|
-
with self._filename(step, ident).open('rb') as f:
|
|
97
|
-
buffer = f.read()
|
|
98
|
-
values.append(self.decfn(buffer))
|
|
99
|
-
steps = np.array(steps, np.int64)
|
|
100
|
-
values = tuple(values)
|
|
101
|
-
return steps, values
|
|
102
|
-
|
|
103
|
-
def _filename(self, step, ident):
|
|
104
|
-
return self.folder / f'{step:020}-{ident.hex()}.{self.ext}'
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
class TextColumn(FileColumn):
|
|
108
|
-
|
|
109
|
-
def __init__(self, logdir, key, fmt='txt'):
|
|
110
|
-
super().__init__(logdir, key, fmt, self.encode, self.decode)
|
|
111
|
-
self.fmt = fmt
|
|
112
|
-
|
|
113
|
-
def validate(self, value):
|
|
114
|
-
assert isinstance(value, str), type(value)
|
|
115
|
-
return value
|
|
116
|
-
|
|
117
|
-
def encode(self, value):
|
|
118
|
-
return value.encode('utf-8')
|
|
119
|
-
|
|
120
|
-
def decode(self, buffer):
|
|
121
|
-
return buffer.decode('utf-8')
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
class ImageColumn(FileColumn):
|
|
125
|
-
|
|
126
|
-
def __init__(self, logdir, key, fmt='png', quality=None):
|
|
127
|
-
super().__init__(logdir, key, fmt, self.encode, self.decode)
|
|
128
|
-
self.fmt = fmt
|
|
129
|
-
self.quality = quality
|
|
130
|
-
|
|
131
|
-
def validate(self, value):
|
|
132
|
-
assert (
|
|
133
|
-
value.dtype == np.uint8 and value.ndim == 3 and
|
|
134
|
-
value.shape[-1] in (1, 3)), (value.dtype, value.shape)
|
|
135
|
-
return value
|
|
136
|
-
|
|
137
|
-
def encode(self, value):
|
|
138
|
-
if value.shape[-1] == 1:
|
|
139
|
-
value = value.repeat(3, -1)
|
|
140
|
-
fmt = ('jpeg' if self.fmt == 'jpg' else self.fmt).upper()
|
|
141
|
-
fp = io.BytesIO()
|
|
142
|
-
Image.fromarray(value).save(fp, format=fmt, quality=self.quality)
|
|
143
|
-
return fp.getvalue()
|
|
144
|
-
|
|
145
|
-
def decode(self, buffer):
|
|
146
|
-
return np.asarray(Image.open(io.BytesIO(buffer)).convert('RGB'))
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
class VideoColumn(FileColumn):
|
|
150
|
-
|
|
151
|
-
def __init__(self, logdir, key, fmt='mp4', fps=20, codec='h264'):
|
|
152
|
-
super().__init__(logdir, key, fmt, self.encode, self.decode)
|
|
153
|
-
self.fmt = fmt
|
|
154
|
-
self.fps = fps
|
|
155
|
-
self.codec = codec
|
|
156
|
-
|
|
157
|
-
def validate(self, value):
|
|
158
|
-
assert (
|
|
159
|
-
value.dtype == np.uint8 and value.ndim == 4 and
|
|
160
|
-
value.shape[-1] in (1, 3)), (value.dtype, value.shape)
|
|
161
|
-
return value
|
|
162
|
-
|
|
163
|
-
def encode(self, value):
|
|
164
|
-
if value.shape[-1] == 1:
|
|
165
|
-
value = value.repeat(3, -1)
|
|
166
|
-
T, H, W, C = value.shape
|
|
167
|
-
fp = io.BytesIO()
|
|
168
|
-
output = av.open(fp, mode='w', format=self.fmt)
|
|
169
|
-
stream = output.add_stream(self.codec, rate=float(self.fps))
|
|
170
|
-
stream.width = W
|
|
171
|
-
stream.height = H
|
|
172
|
-
stream.pix_fmt = 'yuv420p'
|
|
173
|
-
for t in range(T):
|
|
174
|
-
frame = av.VideoFrame.from_ndarray(value[t], format='rgb24')
|
|
175
|
-
frame.pts = t
|
|
176
|
-
output.mux(stream.encode(frame))
|
|
177
|
-
output.mux(stream.encode(None))
|
|
178
|
-
output.close()
|
|
179
|
-
return fp.getvalue()
|
|
180
|
-
|
|
181
|
-
def decode(self, buffer):
|
|
182
|
-
container = av.open(io.BytesIO(buffer))
|
|
183
|
-
value = []
|
|
184
|
-
for frame in container.decode(video=0):
|
|
185
|
-
value.append(frame.to_ndarray(format='rgb24'))
|
|
186
|
-
value = np.stack(value)
|
|
187
|
-
container.close()
|
|
188
|
-
return value
|
scope-0.2.3/scope/reader.py
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import re
|
|
2
|
-
import pathlib
|
|
3
|
-
|
|
4
|
-
import numpy as np
|
|
5
|
-
|
|
6
|
-
from . import columns
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class Reader:
|
|
10
|
-
|
|
11
|
-
def __init__(self, logdir):
|
|
12
|
-
if isinstance(logdir, str):
|
|
13
|
-
logdir = pathlib.Path(logdir)
|
|
14
|
-
self.coltypes = {
|
|
15
|
-
'float': columns.FloatColumn,
|
|
16
|
-
'png': columns.ImageColumn,
|
|
17
|
-
'mp4': columns.VideoColumn,
|
|
18
|
-
}
|
|
19
|
-
self.columns = {}
|
|
20
|
-
for child in sorted(logdir.glob('*')):
|
|
21
|
-
name, ext = child.name.rsplit('.', 1)
|
|
22
|
-
key = name.replace('-', '/')
|
|
23
|
-
assert re.match(r'[a-z0-9_]+(/[a-z0-9_]+)?', key), key
|
|
24
|
-
self.columns[key] = self.coltypes[ext](logdir, key)
|
|
25
|
-
|
|
26
|
-
def keys(self):
|
|
27
|
-
return tuple(self.columns.keys())
|
|
28
|
-
|
|
29
|
-
def length(self, key):
|
|
30
|
-
return self.columns[key].length()
|
|
31
|
-
|
|
32
|
-
def __getitem__(self, index):
|
|
33
|
-
if isinstance(index, str):
|
|
34
|
-
key, start, stop = index, -np.inf, +np.inf
|
|
35
|
-
else:
|
|
36
|
-
key, pos = index
|
|
37
|
-
if isinstance(pos, int):
|
|
38
|
-
start, stop = pos, pos + 1
|
|
39
|
-
else:
|
|
40
|
-
assert pos.step is None
|
|
41
|
-
start = -np.inf if pos.start is None else pos.start
|
|
42
|
-
stop = +np.inf if pos.stop is None else pos.stop
|
|
43
|
-
return self.columns[key].read(start, stop)
|
scope-0.2.3/scope/writer.py
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import concurrent.futures
|
|
2
|
-
import pathlib
|
|
3
|
-
import re
|
|
4
|
-
from functools import partial as bind
|
|
5
|
-
|
|
6
|
-
import numpy as np
|
|
7
|
-
|
|
8
|
-
from . import columns
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class Writer:
|
|
12
|
-
|
|
13
|
-
def __init__(self, logdir, fps=20, workers=8):
|
|
14
|
-
if isinstance(logdir, str):
|
|
15
|
-
logdir = pathlib.Path(logdir)
|
|
16
|
-
self.logdir = logdir
|
|
17
|
-
self.logdir.mkdir(parents=True, exist_ok=True)
|
|
18
|
-
self.fps = fps
|
|
19
|
-
self.workers = workers
|
|
20
|
-
self.coltypes = [
|
|
21
|
-
(lambda x: isinstance(x, str), columns.TextColumn),
|
|
22
|
-
(lambda x: x.ndim == 0, columns.FloatColumn),
|
|
23
|
-
(lambda x: x.ndim == 3, bind(columns.ImageColumn, fmt='png')),
|
|
24
|
-
(lambda x: x.ndim == 4, bind(columns.VideoColumn, fmt='mp4', fps=fps)),
|
|
25
|
-
]
|
|
26
|
-
self.columns = {}
|
|
27
|
-
self.values = {}
|
|
28
|
-
if workers:
|
|
29
|
-
self.pool = concurrent.futures.ThreadPoolExecutor(workers, 'writer')
|
|
30
|
-
self.futures = []
|
|
31
|
-
|
|
32
|
-
def add(self, step, *args, **kwargs):
|
|
33
|
-
step = int(step)
|
|
34
|
-
mapping = dict(*args, **kwargs)
|
|
35
|
-
for key, value in mapping.items():
|
|
36
|
-
value = value if isinstance(value, str) else np.asarray(value)
|
|
37
|
-
if key not in self.columns:
|
|
38
|
-
assert re.match(r'[a-z0-9_]+(/[a-z0-9_]+)?', key), key
|
|
39
|
-
for applies, coltype in self.coltypes:
|
|
40
|
-
if applies(value):
|
|
41
|
-
break
|
|
42
|
-
else:
|
|
43
|
-
raise NotImplementedError((
|
|
44
|
-
key, value,
|
|
45
|
-
getattr(value, 'shape', None),
|
|
46
|
-
getattr(value, 'dtype', None)))
|
|
47
|
-
self.columns[key] = coltype(self.logdir, key)
|
|
48
|
-
self.values[key] = []
|
|
49
|
-
column = self.columns[key]
|
|
50
|
-
try:
|
|
51
|
-
value = column.validate(value)
|
|
52
|
-
except Exception:
|
|
53
|
-
print(f"Error validating key '{key}' with value '{value}'.")
|
|
54
|
-
raise
|
|
55
|
-
self.values[key].append((step, value))
|
|
56
|
-
|
|
57
|
-
def flush(self):
|
|
58
|
-
keys = [key for key, values in self.values.items() if values]
|
|
59
|
-
if self.workers:
|
|
60
|
-
list(self.futures)
|
|
61
|
-
columns = [self.columns[x] for x in keys]
|
|
62
|
-
values = [self.values[x] for x in keys]
|
|
63
|
-
self.futures = self.pool.map(lambda x, y: x.write(y), columns, values)
|
|
64
|
-
else:
|
|
65
|
-
for key in keys:
|
|
66
|
-
try:
|
|
67
|
-
self.columns[key].write(self.values[key])
|
|
68
|
-
except Exception:
|
|
69
|
-
print(f"Exception writing '{key}' column.")
|
|
70
|
-
raise
|
|
71
|
-
self.values = {key: [] for key in self.values.keys()}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|