Skip to content

Commit 206bef8

Browse files
committedOct 29, 2018
change API for drawing stuff
ability to provide kwargs to matplotlib update zproc add examples
1 parent 663faa8 commit 206bef8

10 files changed

+317
-248
lines changed
 

‎Pipfile

+3-1
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@ name = "pypi"
66
[packages]
77
matplotlib = "*"
88
zproc = "*"
9-
"pyqt5" = "*"
109

1110
[dev-packages]
1211
twine = "*"
1312

1413
[requires]
1514
python_version = "3.6"
15+
16+
[pipenv]
17+
allow_prereleases = true

‎Pipfile.lock

+128-130
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎README.md

+11-11
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ osc = Osc()
1616

1717

1818
@osc.signal
19-
def simple_random_signal(update):
19+
def simple_random_signal(state):
2020
while True:
21-
update(random.random())
21+
state.draw(random.random())
2222
sleep(0.1)
2323

2424

@@ -44,16 +44,16 @@ osc = Osc(nrows=2, ncols=3)
4444

4545

4646
@osc.signal
47-
def signal1(update):
47+
def signal1(state):
4848
while True:
49-
update(random.random())
49+
state.draw((random.random())
5050
sleep(0.1)
5151

5252

5353
@osc.signal
54-
def signal2(update):
54+
def signal2(state):
5555
while True:
56-
update(random.random(), row=1, col=2)
56+
state.draw(random.random(), row=1, col=2)
5757
sleep(0.1)
5858

5959

@@ -63,7 +63,7 @@ osc.start()
6363

6464
<img src="https://i.imgur.com/PPC7z4m.png" height="300" />
6565

66-
P.S. Don't worry about race conditions, `update()` is atomic. (See [zproc](https://github.com/pycampers/zproc))
66+
P.S. Don't worry about race conditions, `state.draw()` is atomic. (See [zproc](https://github.com/pycampers/zproc))
6767

6868
### Dynamic axis scale
6969

@@ -82,11 +82,11 @@ osc = Osc(window_sec=10, intensity=1)
8282

8383

8484
@osc.signal
85-
def increasing_signal(update):
85+
def increasing_signal(state):
8686
delta = 1
8787

8888
while True:
89-
update(random.randint(-delta, delta))
89+
state.draw(random.randint(-delta, delta))
9090
delta += 5
9191
sleep(0.01)
9292

@@ -112,11 +112,11 @@ osc = Osc(normalize=True)
112112

113113

114114
@osc.signal
115-
def increasing_signal(update):
115+
def increasing_signal(state):
116116
delta = 1
117117

118118
while True:
119-
update(random.randint(-delta, delta))
119+
state.draw(random.randint(-delta, delta))
120120
delta += 5
121121
sleep(0.01)
122122

‎examples/increasing.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@
1111

1212

1313
@osc.signal
14-
def increasing_signal(update):
14+
def increasing_signal(state):
1515
delta = 1
1616

1717
while True:
18-
update(random.randint(-delta, delta))
18+
state.draw(random.randint(-delta, delta))
1919
delta += 5
2020
sleep(0.01)
2121

‎examples/normalized.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@
1111

1212

1313
@osc.signal
14-
def increasing_signal(update):
14+
def increasing_signal(state):
1515
delta = 1
1616

1717
while True:
18-
update(random.randint(-delta, delta))
18+
state.draw(random.randint(-delta, delta))
1919
delta += 5
2020
sleep(0.01)
2121

‎examples/parallel_signals.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,16 @@
1010

1111

1212
@osc.signal
13-
def signal1(update):
13+
def signal1(state):
1414
while True:
15-
update(random.random())
15+
state.draw(random.random())
1616
sleep(0.1)
1717

1818

1919
@osc.signal
20-
def signal2(update):
20+
def signal2(state):
2121
while True:
22-
update(random.random(), row=1, col=2)
22+
state.draw(random.random(), row=1, col=2)
2323
sleep(0.1)
2424

2525

‎examples/simple_signal.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99

1010

1111
@osc.signal
12-
def simple_random_signal(update):
12+
def simple_random_signal(state):
1313
while True:
14-
update(random.random())
14+
state.draw(random.random())
1515
sleep(0.1)
1616

1717

‎examples/sys_monitor.py

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import time
2+
3+
import psutil
4+
5+
from oscilloscope import Osc
6+
7+
osc = Osc(nrows=2, ncols=2)
8+
9+
10+
@osc.signal
11+
def cpu_usage(state):
12+
# This will adjust graph to the max value, 100 and set the labels.
13+
state.draw(100, col=0, ylabel="core0")
14+
state.draw(100, col=1, ylabel="core1")
15+
16+
while True:
17+
time.sleep(0.25)
18+
for i, cpu in enumerate(psutil.cpu_percent(percpu=True)):
19+
state.draw(cpu, col=i)
20+
21+
22+
@osc.signal
23+
def ram_usage(state):
24+
# This will adjust graph to the max value, 100 and set the labels.
25+
state.draw(100, row=1, ylabel="ram")
26+
27+
while True:
28+
time.sleep(0.25)
29+
meminfo = psutil.virtual_memory()
30+
state.draw(meminfo.available / meminfo.total * 100, row=1)
31+
32+
33+
@osc.signal
34+
def combined_cpu(state):
35+
# This will adjust graph to the max value, 100 and set the labels.
36+
state.draw(100, row=1, col=1, ylabel="total cpu")
37+
38+
while True:
39+
time.sleep(0.25)
40+
state.draw(psutil.cpu_percent(), row=1, col=1)
41+
42+
43+
osc.start()

‎oscilloscope.py

+120-94
Original file line numberDiff line numberDiff line change
@@ -1,131 +1,178 @@
1-
import matplotlib
2-
3-
matplotlib.use("Qt5Agg")
4-
51
from functools import wraps
6-
from typing import Union
2+
from typing import Union, Callable
73

84
import matplotlib.animation as animation
95
import matplotlib.pyplot as plt
106
import numpy as np
117
import zproc
128
from matplotlib.lines import Line2D
139

14-
ctx = zproc.Context()
10+
zproc_ctx = zproc.Context()
11+
ZPROC_INTERNAL_NAMESPACE = "oscilloscope"
1512

1613

1714
class Normalizer:
18-
def __init__(self):
19-
self.bounds = [0, 0]
20-
self.norm_factor = 0
15+
def __init__(self, output_range: tuple = (0, 100)):
16+
self._input_min = 0
17+
self._input_max = 0
18+
19+
self._output_min, self._output_max = output_range
20+
self._output_diff = self._output_max - self._output_min
2121

22-
def refresh_norm_factor(self):
23-
self.norm_factor = 1 / (self.bounds[1] - self.bounds[0]) * 100
22+
self._norm_factor = 0
2423

25-
def adjust_norm_factor(self, val):
26-
if val < self.bounds[0]:
27-
self.bounds[0] = val
28-
self.refresh_norm_factor()
29-
elif val > self.bounds[1]:
30-
self.bounds[1] = val
31-
self.refresh_norm_factor()
24+
def _refresh_norm_factor(self):
25+
self._norm_factor = 1 / (self._input_max - self._input_min) * self._output_diff
3226

33-
def normalize(self, val):
34-
self.adjust_norm_factor(val)
27+
def _refresh_bounds(self, input_value):
28+
if input_value < self._input_min:
29+
self._input_min = input_value
30+
self._refresh_norm_factor()
31+
elif input_value > self._input_max:
32+
self._input_max = input_value
33+
self._refresh_norm_factor()
3534

36-
return (val - self.bounds[0]) * self.norm_factor
35+
def normalize(self, input_value):
36+
self._refresh_bounds(input_value)
37+
return (input_value - self._input_min) * self._norm_factor + self._output_min
38+
39+
40+
def shift(ax, x):
41+
return np.delete(np.append(ax, x), 0)
3742

3843

3944
class AnimationScope:
4045
def __init__(
4146
self,
42-
ax,
47+
ax: plt.Axes,
4348
window_sec,
4449
frame_interval_sec,
45-
xlabel,
46-
ylabel,
4750
row_index,
4851
col_index,
4952
intensity,
53+
padding_percent,
5054
):
5155
self.row_index = row_index
5256
self.col_index = col_index
5357
self.ax = ax
58+
self.padding_percent = padding_percent
5459

55-
self.bounds = [0, 0]
60+
self.frame_interval_sec = frame_interval_sec
61+
self.num_frames = int(window_sec / self.frame_interval_sec)
5662

57-
num_frames = int(window_sec / frame_interval_sec)
58-
self.time_axis = np.linspace(-window_sec, 0, num_frames)
59-
self.amplitude_axis = np.zeros([1, num_frames])
63+
self.y_values = np.zeros([1, self.num_frames])
64+
self.x_values = np.linspace(-window_sec, 0, self.num_frames)
6065

61-
self.line = Line2D(self.time_axis, self.amplitude_axis, linewidth=intensity)
66+
self.line = Line2D(self.x_values, self.y_values, linewidth=intensity)
6267
self.ax.add_line(self.line)
63-
self.ax.set(xlim=(-window_sec, 0), xlabel=xlabel, ylabel=ylabel)
68+
self.ax.set_xlim(-window_sec, 0)
69+
70+
self.y_limits = np.array([0, np.finfo(np.float).eps])
71+
self.ax.set_ylim(self.y_limits[0], self.y_limits[1])
72+
73+
self._internal_state = zproc.State(
74+
zproc_ctx.server_address, namespace=ZPROC_INTERNAL_NAMESPACE
75+
)
76+
77+
def _adjust_ylim(self):
78+
padding = self.padding_percent * (self.y_limits[1] - self.y_limits[0]) / 100
79+
self.ax.set_ylim(self.y_limits[0] - padding, self.y_limits[1] + padding)
80+
81+
def _adjust_ylim_if_req(self, amplitude):
82+
if amplitude < self.y_limits[0]:
83+
self.y_limits[0] = amplitude
84+
self._adjust_ylim()
85+
elif amplitude > self.y_limits[1]:
86+
self.y_limits[1] = amplitude
87+
self._adjust_ylim()
88+
89+
def draw(self, _):
90+
try:
91+
amplitude, kwargs = self._internal_state[(self.row_index, self.col_index)]
92+
except KeyError:
93+
pass
94+
else:
95+
# set the labels
96+
self.ax.set(**kwargs)
97+
98+
try:
99+
size = np.ceil(self.num_frames / len(amplitude))
100+
self.y_values = np.resize(
101+
np.repeat(np.array([amplitude]), size, axis=1), [1, self.num_frames]
102+
)
64103

65-
def refresh_ylim(self):
66-
self.ax.set_ylim(self.bounds)
104+
self._adjust_ylim_if_req(np.min(self.y_values))
105+
self._adjust_ylim_if_req(np.max(self.y_values))
106+
except TypeError:
107+
self.y_values = shift(self.y_values, amplitude)
108+
self._adjust_ylim_if_req(amplitude)
67109

68-
def adjust_ylim(self, amplitude):
69-
if amplitude < self.bounds[0]:
70-
self.bounds[0] = amplitude
71-
self.refresh_ylim()
72-
elif amplitude > self.bounds[1]:
73-
self.bounds[1] = amplitude
74-
self.refresh_ylim()
110+
# update line
111+
self.line.set_data(self.x_values, self.y_values)
112+
return [self.line]
75113

76-
def draw(self, n):
77-
amplitude = ctx.state.get((self.row_index, self.col_index), 0)
78114

79-
# make adjustments to the ylim if required
80-
self.adjust_ylim(amplitude)
115+
def _signal_process(state: zproc.State, fn: Callable, normalize: bool, *args, **kwargs):
116+
if normalize:
117+
normalizer = Normalizer()
81118

82-
# Add new amplitude to end
83-
self.amplitude_axis = np.append(self.amplitude_axis, amplitude)
119+
def _normalize(val):
120+
return normalizer.normalize(val)
84121

85-
# remove old amplitude from start
86-
self.amplitude_axis = np.delete(self.amplitude_axis, 0)
122+
else:
87123

88-
# update line
89-
self.line.set_data(self.time_axis, self.amplitude_axis)
124+
def _normalize(val):
125+
return val
90126

91-
return (self.line,)
127+
_internal_state = zproc.State(
128+
state.server_address, namespace=ZPROC_INTERNAL_NAMESPACE
129+
)
130+
131+
def draw(amplitude, *, row=0, col=0, **kwargs):
132+
amplitude = _normalize(amplitude)
133+
_internal_state[(row, col)] = amplitude, kwargs
134+
135+
state.draw = draw
136+
fn(state, *args, **kwargs)
92137

93138

94139
class Osc:
95140
def __init__(
96141
self,
97142
*,
98-
fps: Union[float, int] = 60,
143+
fps: Union[float, int] = 24,
99144
window_sec: Union[float, int] = 5,
100145
intensity: Union[float, int] = 2.5,
101146
normalize: bool = False,
102147
xlabel: str = "Time (sec)",
103148
ylabel: str = "Amplitude",
104149
nrows: int = 1,
105150
ncols: int = 1,
151+
padding_percent: Union[float, int] = 0,
106152
):
107153
frame_interval_sec = 1 / fps
108154

109155
self.nrows = nrows
110156
self.ncols = ncols
111157
self.normalize = normalize
158+
self.xlabel = xlabel
159+
self.ylabel = ylabel
112160

113-
self.scopes = []
161+
self.anim_scopes = {}
114162
self.gc_protect = []
115163

116164
fig, axes = plt.subplots(self.nrows, self.ncols, squeeze=False)
117165

118166
for row_index, row_axes in enumerate(axes):
119167
for col_index, ax in enumerate(row_axes):
120168
scope = AnimationScope(
121-
ax,
122-
window_sec,
123-
frame_interval_sec,
124-
xlabel,
125-
ylabel,
126-
row_index,
127-
col_index,
128-
intensity,
169+
ax=ax,
170+
window_sec=window_sec,
171+
frame_interval_sec=frame_interval_sec,
172+
row_index=row_index,
173+
col_index=col_index,
174+
intensity=intensity,
175+
padding_percent=padding_percent,
129176
)
130177

131178
self.gc_protect.append(
@@ -134,48 +181,27 @@ def __init__(
134181
)
135182
)
136183

137-
self.scopes.append(scope)
138-
139-
def signal(self, fn):
140-
@wraps(fn)
141-
def _singal(state, nrows, ncols, normalize):
142-
if normalize:
143-
normalizer = Normalizer()
144-
145-
def update_fn(amplitude, row=0, col=0):
146-
if not 0 <= row < nrows:
147-
raise ValueError(
148-
f'"row" must be one of {list(range(0, nrows))}'
149-
)
150-
if not 0 <= col < ncols:
151-
raise ValueError(
152-
f'"col" must be one of {list(range(0, ncols))}'
153-
)
154-
155-
state[(row, col)] = normalizer.normalize(amplitude)
184+
self.anim_scopes[(row_index, col_index)] = scope
156185

157-
else:
186+
def signal(self, fn=None, **process_kwargs):
187+
if fn is None:
158188

159-
def update_fn(amplitude, row=0, col=0):
160-
if not 0 <= row < nrows:
161-
raise ValueError(
162-
f'"row" must be one of {list(range(0, nrows))}'
163-
)
164-
if not 0 <= col < ncols:
165-
raise ValueError(
166-
f'"col" must be one of {list(range(0, ncols))}'
167-
)
189+
@wraps(fn)
190+
def wrapper(fn):
191+
return self.signal(fn, **process_kwargs)
168192

169-
state[(row, col)] = amplitude
193+
return wrapper
170194

171-
fn(update_fn)
195+
process_kwargs["start"] = False
196+
process_kwargs["args"] = (fn, self.normalize, *process_kwargs.get("args", ()))
172197

173-
return ctx.process(_singal, args=(self.nrows, self.ncols, self.normalize))
198+
return zproc_ctx.process(_signal_process, **process_kwargs)
174199

175200
def start(self):
201+
zproc_ctx.start_all()
176202
plt.show()
203+
zproc_ctx.wait_all()
177204

178205
def stop(self):
179-
ctx.stop_all()
206+
zproc_ctx.stop_all()
180207
plt.close()
181-
print(ctx.process_list)

‎setup.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@
1818
EMAIL = "devxpy@gmail.com"
1919
AUTHOR = "Dev Aggarwal"
2020
REQUIRES_PYTHON = ">=3.6.0"
21-
VERSION = "0.0.2"
21+
VERSION = "0.0.3"
2222

2323
# What packages are required for this module to be executed?
24-
REQUIRED = ["matplotlib", "zproc", "pyqt5"]
24+
REQUIRED = ["matplotlib", "zproc"]
2525

2626
# What packages are optional?
2727
EXTRAS = {}

0 commit comments

Comments
 (0)
Please sign in to comment.