1
- import matplotlib
2
-
3
- matplotlib .use ("Qt5Agg" )
4
-
5
1
from functools import wraps
6
- from typing import Union
2
+ from typing import Union , Callable
7
3
8
4
import matplotlib .animation as animation
9
5
import matplotlib .pyplot as plt
10
6
import numpy as np
11
7
import zproc
12
8
from matplotlib .lines import Line2D
13
9
14
- ctx = zproc .Context ()
10
+ zproc_ctx = zproc .Context ()
11
+ ZPROC_INTERNAL_NAMESPACE = "oscilloscope"
15
12
16
13
17
14
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
21
21
22
- def refresh_norm_factor (self ):
23
- self .norm_factor = 1 / (self .bounds [1 ] - self .bounds [0 ]) * 100
22
+ self ._norm_factor = 0
24
23
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
32
26
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 ()
35
34
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 )
37
42
38
43
39
44
class AnimationScope :
40
45
def __init__ (
41
46
self ,
42
- ax ,
47
+ ax : plt . Axes ,
43
48
window_sec ,
44
49
frame_interval_sec ,
45
- xlabel ,
46
- ylabel ,
47
50
row_index ,
48
51
col_index ,
49
52
intensity ,
53
+ padding_percent ,
50
54
):
51
55
self .row_index = row_index
52
56
self .col_index = col_index
53
57
self .ax = ax
58
+ self .padding_percent = padding_percent
54
59
55
- self .bounds = [0 , 0 ]
60
+ self .frame_interval_sec = frame_interval_sec
61
+ self .num_frames = int (window_sec / self .frame_interval_sec )
56
62
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 )
60
65
61
- self .line = Line2D (self .time_axis , self .amplitude_axis , linewidth = intensity )
66
+ self .line = Line2D (self .x_values , self .y_values , linewidth = intensity )
62
67
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
+ )
64
103
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 )
67
109
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 ]
75
113
76
- def draw (self , n ):
77
- amplitude = ctx .state .get ((self .row_index , self .col_index ), 0 )
78
114
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 ()
81
118
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 )
84
121
85
- # remove old amplitude from start
86
- self .amplitude_axis = np .delete (self .amplitude_axis , 0 )
122
+ else :
87
123
88
- # update line
89
- self . line . set_data ( self . time_axis , self . amplitude_axis )
124
+ def _normalize ( val ):
125
+ return val
90
126
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 )
92
137
93
138
94
139
class Osc :
95
140
def __init__ (
96
141
self ,
97
142
* ,
98
- fps : Union [float , int ] = 60 ,
143
+ fps : Union [float , int ] = 24 ,
99
144
window_sec : Union [float , int ] = 5 ,
100
145
intensity : Union [float , int ] = 2.5 ,
101
146
normalize : bool = False ,
102
147
xlabel : str = "Time (sec)" ,
103
148
ylabel : str = "Amplitude" ,
104
149
nrows : int = 1 ,
105
150
ncols : int = 1 ,
151
+ padding_percent : Union [float , int ] = 0 ,
106
152
):
107
153
frame_interval_sec = 1 / fps
108
154
109
155
self .nrows = nrows
110
156
self .ncols = ncols
111
157
self .normalize = normalize
158
+ self .xlabel = xlabel
159
+ self .ylabel = ylabel
112
160
113
- self .scopes = []
161
+ self .anim_scopes = {}
114
162
self .gc_protect = []
115
163
116
164
fig , axes = plt .subplots (self .nrows , self .ncols , squeeze = False )
117
165
118
166
for row_index , row_axes in enumerate (axes ):
119
167
for col_index , ax in enumerate (row_axes ):
120
168
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 ,
129
176
)
130
177
131
178
self .gc_protect .append (
@@ -134,48 +181,27 @@ def __init__(
134
181
)
135
182
)
136
183
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
156
185
157
- else :
186
+ def signal (self , fn = None , ** process_kwargs ):
187
+ if fn is None :
158
188
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 )
168
192
169
- state [( row , col )] = amplitude
193
+ return wrapper
170
194
171
- fn (update_fn )
195
+ process_kwargs ["start" ] = False
196
+ process_kwargs ["args" ] = (fn , self .normalize , * process_kwargs .get ("args" , ()))
172
197
173
- return ctx .process (_singal , args = ( self . nrows , self . ncols , self . normalize ) )
198
+ return zproc_ctx .process (_signal_process , ** process_kwargs )
174
199
175
200
def start (self ):
201
+ zproc_ctx .start_all ()
176
202
plt .show ()
203
+ zproc_ctx .wait_all ()
177
204
178
205
def stop (self ):
179
- ctx .stop_all ()
206
+ zproc_ctx .stop_all ()
180
207
plt .close ()
181
- print (ctx .process_list )
0 commit comments