scope 0.2.0__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.2.0/PKG-INFO ADDED
@@ -0,0 +1,9 @@
1
+ Metadata-Version: 2.1
2
+ Name: scope
3
+ Version: 0.2.0
4
+ Summary: Metrics logging and analysis.
5
+ Home-page: http://github.com/danijar/scope
6
+ Classifier: Intended Audience :: Science/Research
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Classifier: Programming Language :: Python :: 3
9
+ Description-Content-Type: text/markdown
scope-0.2.0/README.md ADDED
File without changes
@@ -0,0 +1,5 @@
1
+ [tool.pytest.ini_options]
2
+ markers = ['slow']
3
+ addopts = ['--strict-config', '-ra']
4
+ pythonpath = ['.']
5
+ testpaths = ['tests']
@@ -0,0 +1,4 @@
1
+ __version__ = '0.2.0'
2
+
3
+ from .writer import Writer
4
+ from .reader import Reader
@@ -0,0 +1,184 @@
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] == 3), (value.dtype, value.shape)
135
+ return value
136
+
137
+ def encode(self, value):
138
+ fmt = ('jpeg' if self.fmt == 'jpg' else self.fmt).upper()
139
+ fp = io.BytesIO()
140
+ Image.fromarray(value).save(fp, format=fmt, quality=self.quality)
141
+ return fp.getvalue()
142
+
143
+ def decode(self, buffer):
144
+ return np.asarray(Image.open(io.BytesIO(buffer)).convert('RGB'))
145
+
146
+
147
+ class VideoColumn(FileColumn):
148
+
149
+ def __init__(self, logdir, key, fmt='mp4', fps=20, codec='h264'):
150
+ super().__init__(logdir, key, fmt, self.encode, self.decode)
151
+ self.fmt = fmt
152
+ self.fps = fps
153
+ self.codec = codec
154
+
155
+ def validate(self, value):
156
+ assert (
157
+ value.dtype == np.uint8 and value.ndim == 4 and
158
+ value.shape[-1] == 3), (value.dtype, value.shape)
159
+ return value
160
+
161
+ def encode(self, array):
162
+ T, H, W, C = array.shape
163
+ fp = io.BytesIO()
164
+ output = av.open(fp, mode='w', format=self.fmt)
165
+ stream = output.add_stream(self.codec, rate=float(self.fps))
166
+ stream.width = W
167
+ stream.height = H
168
+ stream.pix_fmt = 'yuv420p'
169
+ for t in range(T):
170
+ frame = av.VideoFrame.from_ndarray(array[t], format='rgb24')
171
+ frame.pts = t
172
+ output.mux(stream.encode(frame))
173
+ output.mux(stream.encode(None))
174
+ output.close()
175
+ return fp.getvalue()
176
+
177
+ def decode(self, buffer):
178
+ container = av.open(io.BytesIO(buffer))
179
+ array = []
180
+ for frame in container.decode(video=0):
181
+ array.append(frame.to_ndarray(format='rgb24'))
182
+ array = np.stack(array)
183
+ container.close()
184
+ return array
@@ -0,0 +1,43 @@
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)
@@ -0,0 +1,71 @@
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=32):
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()}
@@ -0,0 +1,9 @@
1
+ Metadata-Version: 2.1
2
+ Name: scope
3
+ Version: 0.2.0
4
+ Summary: Metrics logging and analysis.
5
+ Home-page: http://github.com/danijar/scope
6
+ Classifier: Intended Audience :: Science/Research
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Classifier: Programming Language :: Python :: 3
9
+ Description-Content-Type: text/markdown
@@ -0,0 +1,14 @@
1
+ README.md
2
+ pyproject.toml
3
+ setup.py
4
+ scope/__init__.py
5
+ scope/columns.py
6
+ scope/reader.py
7
+ scope/writer.py
8
+ scope.egg-info/PKG-INFO
9
+ scope.egg-info/SOURCES.txt
10
+ scope.egg-info/dependency_links.txt
11
+ scope.egg-info/top_level.txt
12
+ tests/test_float.py
13
+ tests/test_image.py
14
+ tests/test_video.py
@@ -0,0 +1 @@
1
+ scope
scope-0.2.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
scope-0.2.0/setup.py ADDED
@@ -0,0 +1,34 @@
1
+ import pathlib
2
+ import re
3
+ import setuptools
4
+
5
+
6
+ def parse_requirements(filename):
7
+ requirements = pathlib.Path(filename)
8
+ requirements = requirements.read_text().split('\n')
9
+ requirements = [x for x in requirements if x.strip()]
10
+ return requirements
11
+
12
+
13
+ def parse_version(filename):
14
+ text = (pathlib.Path(__file__).parent / filename).read_text()
15
+ version = re.search(r"__version__ = '(.*)'", text).group(1)
16
+ return version
17
+
18
+
19
+ setuptools.setup(
20
+ name='scope',
21
+ version=parse_version('scope/__init__.py'),
22
+ description='Metrics logging and analysis.',
23
+ url='http://github.com/danijar/scope',
24
+ long_description=pathlib.Path('README.md').read_text(),
25
+ long_description_content_type='text/markdown',
26
+ packages=['scope'],
27
+ include_package_data=True,
28
+ install_requires=parse_requirements('requirements.txt'),
29
+ classifiers=[
30
+ 'Intended Audience :: Science/Research',
31
+ 'License :: OSI Approved :: MIT License',
32
+ 'Programming Language :: Python :: 3',
33
+ ],
34
+ )
@@ -0,0 +1,70 @@
1
+ import pathlib
2
+
3
+ import scope
4
+ import numpy as np
5
+
6
+
7
+ class TestFloat:
8
+
9
+ def test_roundtrip(self, tmpdir):
10
+ logdir = pathlib.Path(tmpdir)
11
+ writer = scope.Writer(logdir, workers=0)
12
+ writer.add(0, {'foo': 12})
13
+ writer.add(5, {'foo': 42, 'bar': np.float64(np.pi)})
14
+ writer.flush()
15
+ assert {x.name for x in logdir.glob('*')} == {'foo.float', 'bar.float'}
16
+ assert (logdir / 'foo.float').stat().st_size == (8 + 8) * 2
17
+ assert (logdir / 'bar.float').stat().st_size == (8 + 8) * 1
18
+ reader = scope.Reader(logdir)
19
+ assert reader.keys() == tuple(sorted(['foo', 'bar']))
20
+ assert reader.length('foo') == 2
21
+ assert reader.length('bar') == 1
22
+ assert equal(reader['foo'], ([0, 5], [12, 42]), (np.int64, np.float64))
23
+ assert equal(reader['bar'], ([5], [np.pi]), (np.int64, np.float64))
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
+ def test_workers(self, tmpdir):
41
+ logdir = pathlib.Path(tmpdir)
42
+ writer = scope.Writer(logdir, workers=8)
43
+ for step in range(10):
44
+ writer.add(step, {'foo': step, 'bar': step})
45
+ writer.flush()
46
+ writer.flush() # Block until previous flush is done.
47
+ assert {x.name for x in logdir.glob('*')} == {'foo.float', 'bar.float'}
48
+ assert (logdir / 'foo.float').stat().st_size == (8 + 8) * 10
49
+ assert (logdir / 'bar.float').stat().st_size == (8 + 8) * 10
50
+ reader = scope.Reader(logdir)
51
+ assert equal(reader['foo'], (np.arange(10), np.arange(10)))
52
+ assert equal(reader['bar'], (np.arange(10), np.arange(10)))
53
+
54
+ def test_namescopes(self, tmpdir):
55
+ logdir = pathlib.Path(tmpdir)
56
+ writer = scope.Writer(logdir, workers=0)
57
+ writer.add(0, {'foo/bar': 12})
58
+ writer.flush()
59
+ assert {x.name for x in logdir.glob('*')} == {'foo-bar.float'}
60
+ reader = scope.Reader(logdir)
61
+ assert reader.keys() == ('foo/bar',)
62
+ assert reader.length('foo/bar') == 1
63
+ assert equal(reader['foo/bar'], ([0], [12]), (np.int64, np.float64))
64
+
65
+
66
+ def equal(actuals, references, dtypes=None):
67
+ dtypes = dtypes or [x.dtype for x in actuals]
68
+ assert len(actuals) == len(references) == len(dtypes)
69
+ references = [np.asarray(x, d) for x, d in zip(actuals, dtypes)]
70
+ return all((x == y).all() for x, y in zip(actuals, references))
@@ -0,0 +1,84 @@
1
+ import pathlib
2
+
3
+ import scope
4
+ import numpy as np
5
+
6
+
7
+ class TestImage:
8
+
9
+ def test_roundtrip(self, tmpdir):
10
+ logdir = pathlib.Path(tmpdir)
11
+ writer = scope.Writer(logdir, workers=0)
12
+ img1 = np.ones((64, 128, 3), np.uint8) + 12
13
+ img2 = np.ones((64, 128, 3), np.uint8) + 255
14
+ writer.add(0, {'foo': img1})
15
+ writer.add(5, {'foo': img2})
16
+ writer.flush()
17
+ assert {x.name for x in logdir.glob('*')} == {'foo.png'}
18
+ assert (logdir / 'foo.png' / 'index').stat().st_size == (8 + 8) * 2
19
+ assert len(list((logdir / 'foo.png').glob('*'))) == 1 + 2
20
+ reader = scope.Reader(logdir)
21
+ assert reader.keys() == ('foo',)
22
+ assert reader.length('foo') == 2
23
+ steps, values = reader['foo']
24
+ 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] == ()
49
+
50
+ def test_workers(self, tmpdir):
51
+ logdir = pathlib.Path(tmpdir)
52
+ writer = scope.Writer(logdir, workers=8)
53
+ for step in range(5):
54
+ for key in ('foo', 'bar', 'baz'):
55
+ writer.add(step, {key: np.full((64, 128, 3), step, np.uint8)})
56
+ writer.flush()
57
+ writer.flush() # Block until previous flush is done.
58
+ assert {x.name for x in logdir.glob('*')} == {
59
+ 'foo.png', 'bar.png', 'baz.png'}
60
+ for key in ('foo', 'bar', 'baz'):
61
+ assert (logdir / f'{key}.png' / 'index').stat().st_size == (8 + 8) * 5
62
+ assert len(list((logdir / f'{key}.png').glob('*'))) == 1 + 5
63
+ reader = scope.Reader(logdir)
64
+ assert reader.keys() == tuple(sorted(['foo', 'bar', 'baz']))
65
+ for key in ('foo', 'bar', 'baz'):
66
+ assert reader.length(key) == 5
67
+ steps, values = reader[key]
68
+ assert (steps == np.arange(5)).all()
69
+ assert all(x.dtype == np.uint8 for x in values)
70
+ reference = np.arange(5, dtype=np.uint8)[:, None, None, None]
71
+ assert (np.array(values) == reference).all()
72
+
73
+ def test_namescopes(self, tmpdir):
74
+ img = np.ones((64, 128, 3), np.uint8) + 12
75
+ logdir = pathlib.Path(tmpdir)
76
+ writer = scope.Writer(logdir, workers=0)
77
+ writer.add(0, {'foo/bar': img})
78
+ writer.flush()
79
+ assert {x.name for x in logdir.glob('*')} == {'foo-bar.png'}
80
+ assert len(list((logdir / 'foo-bar.png').glob('*'))) == 1 + 1
81
+ reader = scope.Reader(logdir)
82
+ assert reader.keys() == ('foo/bar',)
83
+ assert reader.length('foo/bar') == 1
84
+ assert (reader['foo/bar'][1] == img).all()
@@ -0,0 +1,25 @@
1
+ import pathlib
2
+
3
+ import scope
4
+ import numpy as np
5
+
6
+
7
+ class TestVideo:
8
+
9
+ def test_roundtrip(self, tmpdir):
10
+ logdir = pathlib.Path(tmpdir)
11
+ writer = scope.Writer(logdir, workers=0)
12
+ vid1 = np.ones((5, 64, 128, 3), np.uint8) + 12
13
+ vid2 = np.ones((5, 64, 128, 3), np.uint8) + 255
14
+ writer.add(0, {'foo': vid1})
15
+ writer.add(5, {'foo': vid2})
16
+ writer.flush()
17
+ assert {x.name for x in logdir.glob('*')} == {'foo.mp4'}
18
+ assert (logdir / 'foo.mp4' / 'index').stat().st_size == (8 + 8) * 2
19
+ assert len(list((logdir / 'foo.mp4').glob('*'))) == 1 + 2
20
+ reader = scope.Reader(logdir)
21
+ assert reader.keys() == ('foo',)
22
+ assert reader.length('foo') == 2
23
+ steps, values = reader['foo']
24
+ assert (steps == np.array([0, 5])).all()
25
+ assert np.allclose(values, [vid1, vid2], rtol=0.1)