ezmsg-sigproc 1.2.2__py3-none-any.whl → 1.3.1__py3-none-any.whl
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.
- ezmsg/sigproc/__init__.py +1 -1
- ezmsg/sigproc/__version__.py +16 -1
- ezmsg/sigproc/activation.py +75 -0
- ezmsg/sigproc/affinetransform.py +234 -0
- ezmsg/sigproc/aggregate.py +158 -0
- ezmsg/sigproc/bandpower.py +74 -0
- ezmsg/sigproc/base.py +38 -0
- ezmsg/sigproc/butterworthfilter.py +102 -11
- ezmsg/sigproc/decimate.py +7 -4
- ezmsg/sigproc/downsample.py +95 -51
- ezmsg/sigproc/ewmfilter.py +38 -16
- ezmsg/sigproc/filter.py +108 -20
- ezmsg/sigproc/filterbank.py +278 -0
- ezmsg/sigproc/math/__init__.py +0 -0
- ezmsg/sigproc/math/abs.py +28 -0
- ezmsg/sigproc/math/clip.py +30 -0
- ezmsg/sigproc/math/difference.py +60 -0
- ezmsg/sigproc/math/invert.py +29 -0
- ezmsg/sigproc/math/log.py +32 -0
- ezmsg/sigproc/math/scale.py +31 -0
- ezmsg/sigproc/messages.py +2 -3
- ezmsg/sigproc/sampler.py +259 -224
- ezmsg/sigproc/scaler.py +173 -0
- ezmsg/sigproc/signalinjector.py +64 -0
- ezmsg/sigproc/slicer.py +133 -0
- ezmsg/sigproc/spectral.py +6 -132
- ezmsg/sigproc/spectrogram.py +86 -0
- ezmsg/sigproc/spectrum.py +259 -0
- ezmsg/sigproc/synth.py +299 -105
- ezmsg/sigproc/wavelets.py +167 -0
- ezmsg/sigproc/window.py +254 -116
- ezmsg_sigproc-1.3.1.dist-info/METADATA +59 -0
- ezmsg_sigproc-1.3.1.dist-info/RECORD +35 -0
- {ezmsg_sigproc-1.2.2.dist-info → ezmsg_sigproc-1.3.1.dist-info}/WHEEL +1 -2
- ezmsg_sigproc-1.2.2.dist-info/METADATA +0 -36
- ezmsg_sigproc-1.2.2.dist-info/RECORD +0 -17
- ezmsg_sigproc-1.2.2.dist-info/top_level.txt +0 -1
- {ezmsg_sigproc-1.2.2.dist-info → ezmsg_sigproc-1.3.1.dist-info/licenses}/LICENSE.txt +0 -0
ezmsg/sigproc/synth.py
CHANGED
|
@@ -1,55 +1,188 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
from dataclasses import replace, field
|
|
2
3
|
import time
|
|
3
|
-
from
|
|
4
|
+
from typing import Optional, Generator, AsyncGenerator, Union
|
|
4
5
|
|
|
5
|
-
import ezmsg.core as ez
|
|
6
6
|
import numpy as np
|
|
7
|
-
|
|
7
|
+
import ezmsg.core as ez
|
|
8
|
+
from ezmsg.util.generator import consumer
|
|
8
9
|
from ezmsg.util.messages.axisarray import AxisArray
|
|
9
10
|
|
|
10
11
|
from .butterworthfilter import ButterworthFilter, ButterworthFilterSettings
|
|
12
|
+
from .base import GenAxisArray
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def clock(dispatch_rate: Optional[float]) -> Generator[ez.Flag, None, None]:
|
|
16
|
+
"""
|
|
17
|
+
Construct a generator that yields events at a specified rate.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
dispatch_rate: event rate in seconds.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
A generator object that yields :obj:`ez.Flag` events at a specified rate.
|
|
24
|
+
"""
|
|
25
|
+
n_dispatch = -1
|
|
26
|
+
t_0 = time.time()
|
|
27
|
+
while True:
|
|
28
|
+
if dispatch_rate is not None:
|
|
29
|
+
n_dispatch += 1
|
|
30
|
+
t_next = t_0 + n_dispatch / dispatch_rate
|
|
31
|
+
time.sleep(max(0, t_next - time.time()))
|
|
32
|
+
yield ez.Flag()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
async def aclock(dispatch_rate: Optional[float]) -> AsyncGenerator[ez.Flag, None]:
|
|
36
|
+
"""
|
|
37
|
+
``asyncio`` version of :obj:`clock`.
|
|
11
38
|
|
|
12
|
-
|
|
39
|
+
Returns:
|
|
40
|
+
asynchronous generator object. Must use `anext` or `async for`.
|
|
41
|
+
"""
|
|
42
|
+
t_0 = time.time()
|
|
43
|
+
n_dispatch = -1
|
|
44
|
+
while True:
|
|
45
|
+
if dispatch_rate is not None:
|
|
46
|
+
n_dispatch += 1
|
|
47
|
+
t_next = t_0 + n_dispatch / dispatch_rate
|
|
48
|
+
await asyncio.sleep(t_next - time.time())
|
|
49
|
+
yield ez.Flag()
|
|
13
50
|
|
|
14
51
|
|
|
15
52
|
class ClockSettings(ez.Settings):
|
|
53
|
+
"""Settings for :obj:`Clock`. See :obj:`clock` for parameter description."""
|
|
54
|
+
|
|
16
55
|
# Message dispatch rate (Hz), or None (fast as possible)
|
|
17
56
|
dispatch_rate: Optional[float]
|
|
18
57
|
|
|
19
58
|
|
|
20
59
|
class ClockState(ez.State):
|
|
21
60
|
cur_settings: ClockSettings
|
|
61
|
+
gen: AsyncGenerator
|
|
22
62
|
|
|
23
63
|
|
|
24
64
|
class Clock(ez.Unit):
|
|
25
|
-
|
|
26
|
-
|
|
65
|
+
"""Unit for :obj:`clock`."""
|
|
66
|
+
|
|
67
|
+
SETTINGS = ClockSettings
|
|
68
|
+
STATE = ClockState
|
|
27
69
|
|
|
28
70
|
INPUT_SETTINGS = ez.InputStream(ClockSettings)
|
|
29
71
|
OUTPUT_CLOCK = ez.OutputStream(ez.Flag)
|
|
30
72
|
|
|
31
|
-
def initialize(self) -> None:
|
|
73
|
+
async def initialize(self) -> None:
|
|
32
74
|
self.STATE.cur_settings = self.SETTINGS
|
|
75
|
+
self.construct_generator()
|
|
76
|
+
|
|
77
|
+
def construct_generator(self):
|
|
78
|
+
self.STATE.gen = aclock(self.STATE.cur_settings.dispatch_rate)
|
|
33
79
|
|
|
34
80
|
@ez.subscriber(INPUT_SETTINGS)
|
|
35
81
|
async def on_settings(self, msg: ClockSettings) -> None:
|
|
36
82
|
self.STATE.cur_settings = msg
|
|
83
|
+
self.construct_generator()
|
|
37
84
|
|
|
38
85
|
@ez.publisher(OUTPUT_CLOCK)
|
|
39
86
|
async def generate(self) -> AsyncGenerator:
|
|
40
87
|
while True:
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
88
|
+
out = await self.STATE.gen.__anext__()
|
|
89
|
+
if out:
|
|
90
|
+
yield self.OUTPUT_CLOCK, out
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# COUNTER - Generate incrementing integer. fs and dispatch_rate parameters combine to give many options. #
|
|
94
|
+
async def acounter(
|
|
95
|
+
n_time: int,
|
|
96
|
+
fs: Optional[float],
|
|
97
|
+
n_ch: int = 1,
|
|
98
|
+
dispatch_rate: Optional[Union[float, str]] = None,
|
|
99
|
+
mod: Optional[int] = None,
|
|
100
|
+
) -> AsyncGenerator[AxisArray, None]:
|
|
47
101
|
"""
|
|
48
|
-
|
|
102
|
+
Construct an asynchronous generator to generate AxisArray objects at a specified rate
|
|
103
|
+
and with the specified sampling rate.
|
|
104
|
+
|
|
49
105
|
NOTE: This module uses asyncio.sleep to delay appropriately in realtime mode.
|
|
50
106
|
This method of sleeping/yielding execution priority has quirky behavior with
|
|
51
107
|
sub-millisecond sleep periods which may result in unexpected behavior (e.g.
|
|
52
108
|
fs = 2000, n_time = 1, realtime = True -- may result in ~1400 msgs/sec)
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
n_time: Number of samples to output per block.
|
|
112
|
+
fs: Sampling rate of signal output in Hz.
|
|
113
|
+
n_ch: Number of channels to synthesize
|
|
114
|
+
dispatch_rate: Message dispatch rate (Hz), 'realtime' or None (fast as possible)
|
|
115
|
+
Note: if dispatch_rate is a float then time offsets will be synthetic and the
|
|
116
|
+
system will run faster or slower than wall clock time.
|
|
117
|
+
mod: If set to an integer, counter will rollover at this number.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
An asynchronous generator.
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
# TODO: Adapt this to use ezmsg.util.rate?
|
|
124
|
+
|
|
125
|
+
counter_start: int = 0 # next sample's first value
|
|
126
|
+
|
|
127
|
+
b_realtime = False
|
|
128
|
+
b_manual_dispatch = False
|
|
129
|
+
b_ext_clock = False
|
|
130
|
+
if dispatch_rate is not None:
|
|
131
|
+
if isinstance(dispatch_rate, str):
|
|
132
|
+
if dispatch_rate.lower() == "realtime":
|
|
133
|
+
b_realtime = True
|
|
134
|
+
elif dispatch_rate.lower() == "ext_clock":
|
|
135
|
+
b_ext_clock = True
|
|
136
|
+
else:
|
|
137
|
+
b_manual_dispatch = True
|
|
138
|
+
|
|
139
|
+
n_sent: int = 0 # It is convenient to know how many samples we have sent.
|
|
140
|
+
clock_zero: float = time.time() # time associated with first sample
|
|
141
|
+
|
|
142
|
+
while True:
|
|
143
|
+
# 1. Sleep, if necessary, until we are at the end of the current block
|
|
144
|
+
if b_realtime:
|
|
145
|
+
n_next = n_sent + n_time
|
|
146
|
+
t_next = clock_zero + n_next / fs
|
|
147
|
+
await asyncio.sleep(t_next - time.time())
|
|
148
|
+
elif b_manual_dispatch:
|
|
149
|
+
n_disp_next = 1 + n_sent / n_time
|
|
150
|
+
t_disp_next = clock_zero + n_disp_next / dispatch_rate
|
|
151
|
+
await asyncio.sleep(t_disp_next - time.time())
|
|
152
|
+
|
|
153
|
+
# 2. Prepare counter data.
|
|
154
|
+
block_samp = np.arange(counter_start, counter_start + n_time)[:, np.newaxis]
|
|
155
|
+
if mod is not None:
|
|
156
|
+
block_samp %= mod
|
|
157
|
+
block_samp = np.tile(block_samp, (1, n_ch))
|
|
158
|
+
|
|
159
|
+
# 3. Prepare offset - the time associated with block_samp[0]
|
|
160
|
+
if b_realtime:
|
|
161
|
+
offset = t_next - n_time / fs
|
|
162
|
+
elif b_ext_clock:
|
|
163
|
+
offset = time.time()
|
|
164
|
+
else:
|
|
165
|
+
# Purely synthetic.
|
|
166
|
+
offset = n_sent / fs
|
|
167
|
+
# offset += clock_zero # ??
|
|
168
|
+
|
|
169
|
+
# 4. yield output
|
|
170
|
+
yield AxisArray(
|
|
171
|
+
block_samp,
|
|
172
|
+
dims=["time", "ch"],
|
|
173
|
+
axes={"time": AxisArray.Axis.TimeAxis(fs=fs, offset=offset)},
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# 5. Update state for next iteration (after next yield)
|
|
177
|
+
counter_start = block_samp[-1, 0] + 1 # do not % mod
|
|
178
|
+
n_sent += n_time
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class CounterSettings(ez.Settings):
|
|
182
|
+
# TODO: Adapt this to use ezmsg.util.rate?
|
|
183
|
+
"""
|
|
184
|
+
Settings for :obj:`Counter`.
|
|
185
|
+
See :obj:`acounter` for a description of the parameters.
|
|
53
186
|
"""
|
|
54
187
|
|
|
55
188
|
n_time: int # Number of samples to output per block
|
|
@@ -57,6 +190,8 @@ class CounterSettings(ez.Settings):
|
|
|
57
190
|
n_ch: int = 1 # Number of channels to synthesize
|
|
58
191
|
|
|
59
192
|
# Message dispatch rate (Hz), 'realtime', 'ext_clock', or None (fast as possible)
|
|
193
|
+
# Note: if dispatch_rate is a float then time offsets will be synthetic and the
|
|
194
|
+
# system will run faster or slower than wall clock time.
|
|
60
195
|
dispatch_rate: Optional[Union[float, str]] = None
|
|
61
196
|
|
|
62
197
|
# If set to an integer, counter will rollover
|
|
@@ -64,24 +199,23 @@ class CounterSettings(ez.Settings):
|
|
|
64
199
|
|
|
65
200
|
|
|
66
201
|
class CounterState(ez.State):
|
|
202
|
+
gen: AsyncGenerator[AxisArray, Optional[ez.Flag]]
|
|
67
203
|
cur_settings: CounterSettings
|
|
68
|
-
|
|
69
|
-
clock_event: asyncio.Event
|
|
204
|
+
new_generator: asyncio.Event
|
|
70
205
|
|
|
71
206
|
|
|
72
207
|
class Counter(ez.Unit):
|
|
73
|
-
"""Generates monotonically increasing counter"""
|
|
208
|
+
"""Generates monotonically increasing counter. Unit for :obj:`acounter`."""
|
|
74
209
|
|
|
75
|
-
SETTINGS
|
|
76
|
-
STATE
|
|
210
|
+
SETTINGS = CounterSettings
|
|
211
|
+
STATE = CounterState
|
|
77
212
|
|
|
78
213
|
INPUT_CLOCK = ez.InputStream(ez.Flag)
|
|
79
214
|
INPUT_SETTINGS = ez.InputStream(CounterSettings)
|
|
80
215
|
OUTPUT_SIGNAL = ez.OutputStream(AxisArray)
|
|
81
216
|
|
|
82
|
-
def initialize(self) -> None:
|
|
83
|
-
self.STATE.
|
|
84
|
-
self.STATE.clock_event.clear()
|
|
217
|
+
async def initialize(self) -> None:
|
|
218
|
+
self.STATE.new_generator = asyncio.Event()
|
|
85
219
|
self.validate_settings(self.SETTINGS)
|
|
86
220
|
|
|
87
221
|
@ez.subscriber(INPUT_SETTINGS)
|
|
@@ -93,106 +227,139 @@ class Counter(ez.Unit):
|
|
|
93
227
|
settings.dispatch_rate, str
|
|
94
228
|
) and self.SETTINGS.dispatch_rate not in ["realtime", "ext_clock"]:
|
|
95
229
|
raise ValueError(f"Unknown dispatch_rate: {self.SETTINGS.dispatch_rate}")
|
|
96
|
-
|
|
97
230
|
self.STATE.cur_settings = settings
|
|
231
|
+
self.construct_generator()
|
|
232
|
+
|
|
233
|
+
def construct_generator(self):
|
|
234
|
+
self.STATE.gen = acounter(
|
|
235
|
+
self.STATE.cur_settings.n_time,
|
|
236
|
+
self.STATE.cur_settings.fs,
|
|
237
|
+
n_ch=self.STATE.cur_settings.n_ch,
|
|
238
|
+
dispatch_rate=self.STATE.cur_settings.dispatch_rate,
|
|
239
|
+
mod=self.STATE.cur_settings.mod,
|
|
240
|
+
)
|
|
241
|
+
self.STATE.new_generator.set()
|
|
98
242
|
|
|
99
243
|
@ez.subscriber(INPUT_CLOCK)
|
|
100
|
-
|
|
101
|
-
|
|
244
|
+
@ez.publisher(OUTPUT_SIGNAL)
|
|
245
|
+
async def on_clock(self, clock: ez.Flag):
|
|
246
|
+
if self.STATE.cur_settings.dispatch_rate == "ext_clock":
|
|
247
|
+
out = await self.STATE.gen.__anext__()
|
|
248
|
+
yield self.OUTPUT_SIGNAL, out
|
|
102
249
|
|
|
103
250
|
@ez.publisher(OUTPUT_SIGNAL)
|
|
104
|
-
async def
|
|
251
|
+
async def run_generator(self) -> AsyncGenerator:
|
|
105
252
|
while True:
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
dispatch_rate = self.STATE.cur_settings.dispatch_rate
|
|
109
|
-
if dispatch_rate is not None:
|
|
110
|
-
if isinstance(dispatch_rate, str):
|
|
111
|
-
if dispatch_rate == "realtime":
|
|
112
|
-
await asyncio.sleep(block_dur)
|
|
113
|
-
elif dispatch_rate == "ext_clock":
|
|
114
|
-
await self.STATE.clock_event.wait()
|
|
115
|
-
self.STATE.clock_event.clear()
|
|
116
|
-
else:
|
|
117
|
-
await asyncio.sleep(1.0 / dispatch_rate)
|
|
118
|
-
|
|
119
|
-
block_samp = np.arange(self.STATE.cur_settings.n_time)[:, np.newaxis]
|
|
120
|
-
|
|
121
|
-
t_samp = block_samp + self.STATE.samp
|
|
122
|
-
self.STATE.samp = t_samp[-1] + 1
|
|
123
|
-
|
|
124
|
-
if self.STATE.cur_settings.mod is not None:
|
|
125
|
-
t_samp %= self.STATE.cur_settings.mod
|
|
126
|
-
self.STATE.samp %= self.STATE.cur_settings.mod
|
|
127
|
-
|
|
128
|
-
t_samp = np.tile(t_samp, (1, self.STATE.cur_settings.n_ch))
|
|
129
|
-
|
|
130
|
-
offset_adj = self.STATE.cur_settings.n_time / self.STATE.cur_settings.fs
|
|
131
|
-
|
|
132
|
-
out = AxisArray(
|
|
133
|
-
t_samp,
|
|
134
|
-
dims=["time", "ch"],
|
|
135
|
-
axes=dict(
|
|
136
|
-
time=AxisArray.Axis.TimeAxis(
|
|
137
|
-
fs=self.STATE.cur_settings.fs, offset=time.time() - offset_adj
|
|
138
|
-
)
|
|
139
|
-
),
|
|
140
|
-
)
|
|
253
|
+
await self.STATE.new_generator.wait()
|
|
254
|
+
self.STATE.new_generator.clear()
|
|
141
255
|
|
|
142
|
-
|
|
256
|
+
if self.STATE.cur_settings.dispatch_rate == "ext_clock":
|
|
257
|
+
continue
|
|
258
|
+
|
|
259
|
+
while not self.STATE.new_generator.is_set():
|
|
260
|
+
out = await self.STATE.gen.__anext__()
|
|
261
|
+
yield self.OUTPUT_SIGNAL, out
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
@consumer
|
|
265
|
+
def sin(
|
|
266
|
+
axis: Optional[str] = "time",
|
|
267
|
+
freq: float = 1.0,
|
|
268
|
+
amp: float = 1.0,
|
|
269
|
+
phase: float = 0.0,
|
|
270
|
+
) -> Generator[AxisArray, AxisArray, None]:
|
|
271
|
+
"""
|
|
272
|
+
Construct a generator of sinusoidal waveforms in AxisArray objects.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
axis: The name of the axis over which the sinusoid passes.
|
|
276
|
+
freq: The frequency of the sinusoid, in Hz.
|
|
277
|
+
amp: The amplitude of the sinusoid.
|
|
278
|
+
phase: The initial phase of the sinusoid, in radians.
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
A primed generator that expects .send(axis_array) of sample counts
|
|
282
|
+
and yields an AxisArray of sinusoids.
|
|
283
|
+
"""
|
|
284
|
+
msg_out = AxisArray(np.array([]), dims=[""])
|
|
285
|
+
|
|
286
|
+
ang_freq = 2.0 * np.pi * freq
|
|
287
|
+
|
|
288
|
+
while True:
|
|
289
|
+
msg_in: AxisArray = yield msg_out
|
|
290
|
+
# msg_in is expected to be sample counts
|
|
291
|
+
|
|
292
|
+
axis_name = axis
|
|
293
|
+
if axis_name is None:
|
|
294
|
+
axis_name = msg_in.dims[0]
|
|
295
|
+
|
|
296
|
+
w = (ang_freq * msg_in.get_axis(axis_name).gain) * msg_in.data
|
|
297
|
+
out_data = amp * np.sin(w + phase)
|
|
298
|
+
msg_out = replace(msg_in, data=out_data)
|
|
143
299
|
|
|
144
300
|
|
|
145
301
|
class SinGeneratorSettings(ez.Settings):
|
|
302
|
+
"""
|
|
303
|
+
Settings for :obj:`SinGenerator`.
|
|
304
|
+
See :obj:`sin` for parameter descriptions.
|
|
305
|
+
"""
|
|
306
|
+
|
|
146
307
|
time_axis: Optional[str] = "time"
|
|
147
308
|
freq: float = 1.0 # Oscillation frequency in Hz
|
|
148
309
|
amp: float = 1.0 # Amplitude
|
|
149
310
|
phase: float = 0.0 # Phase offset (in radians)
|
|
150
311
|
|
|
151
312
|
|
|
152
|
-
class
|
|
153
|
-
|
|
313
|
+
class SinGenerator(GenAxisArray):
|
|
314
|
+
"""
|
|
315
|
+
Unit for :obj:`sin`.
|
|
316
|
+
"""
|
|
154
317
|
|
|
318
|
+
SETTINGS = SinGeneratorSettings
|
|
155
319
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
320
|
+
def construct_generator(self):
|
|
321
|
+
self.STATE.gen = sin(
|
|
322
|
+
axis=self.SETTINGS.time_axis,
|
|
323
|
+
freq=self.SETTINGS.freq,
|
|
324
|
+
amp=self.SETTINGS.amp,
|
|
325
|
+
phase=self.SETTINGS.phase,
|
|
326
|
+
)
|
|
159
327
|
|
|
160
|
-
INPUT_SIGNAL = ez.InputStream(AxisArray)
|
|
161
|
-
OUTPUT_SIGNAL = ez.OutputStream(AxisArray)
|
|
162
328
|
|
|
163
|
-
|
|
164
|
-
|
|
329
|
+
class OscillatorSettings(ez.Settings):
|
|
330
|
+
"""Settings for :obj:`Oscillator`"""
|
|
165
331
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
async def generate(self, msg: AxisArray) -> AsyncGenerator:
|
|
169
|
-
"""
|
|
170
|
-
msg is assumed to be a monotonically increasing counter ..
|
|
171
|
-
.. or at least a counter with an intelligently chosen modulus
|
|
172
|
-
"""
|
|
173
|
-
axis_name = self.SETTINGS.time_axis
|
|
174
|
-
if axis_name is None:
|
|
175
|
-
axis_name = msg.dims[0]
|
|
176
|
-
fs = 1.0 / msg.get_axis(axis_name).gain
|
|
177
|
-
t_sec = msg.data / fs
|
|
178
|
-
w = self.STATE.ang_freq * t_sec
|
|
179
|
-
out_data = self.SETTINGS.amp * np.sin(w + self.SETTINGS.phase)
|
|
180
|
-
yield (self.OUTPUT_SIGNAL, replace(msg, data=out_data))
|
|
332
|
+
n_time: int
|
|
333
|
+
"""Number of samples to output per block."""
|
|
181
334
|
|
|
335
|
+
fs: float
|
|
336
|
+
"""Sampling rate of signal output in Hz"""
|
|
182
337
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
338
|
+
n_ch: int = 1
|
|
339
|
+
"""Number of channels to output per block"""
|
|
340
|
+
|
|
341
|
+
dispatch_rate: Optional[Union[float, str]] = None
|
|
342
|
+
"""(Hz) | 'realtime' | 'ext_clock'"""
|
|
343
|
+
|
|
344
|
+
freq: float = 1.0
|
|
345
|
+
"""Oscillation frequency in Hz"""
|
|
346
|
+
|
|
347
|
+
amp: float = 1.0
|
|
348
|
+
"""Amplitude"""
|
|
349
|
+
|
|
350
|
+
phase: float = 0.0
|
|
351
|
+
"""Phase offset (in radians)"""
|
|
352
|
+
|
|
353
|
+
sync: bool = False
|
|
354
|
+
"""Adjust `freq` to sync with sampling rate"""
|
|
192
355
|
|
|
193
356
|
|
|
194
357
|
class Oscillator(ez.Collection):
|
|
195
|
-
|
|
358
|
+
"""
|
|
359
|
+
:obj:`Collection that chains :obj:`Counter` and :obj:`SinGenerator`.
|
|
360
|
+
"""
|
|
361
|
+
|
|
362
|
+
SETTINGS = OscillatorSettings
|
|
196
363
|
|
|
197
364
|
INPUT_CLOCK = ez.InputStream(ez.Flag)
|
|
198
365
|
OUTPUT_SIGNAL = ez.OutputStream(AxisArray)
|
|
@@ -235,11 +402,18 @@ class Oscillator(ez.Collection):
|
|
|
235
402
|
|
|
236
403
|
class RandomGeneratorSettings(ez.Settings):
|
|
237
404
|
loc: float = 0.0
|
|
405
|
+
"""loc argument for :obj:`numpy.random.normal`"""
|
|
406
|
+
|
|
238
407
|
scale: float = 1.0
|
|
408
|
+
"""scale argument for :obj:`numpy.random.normal`"""
|
|
239
409
|
|
|
240
410
|
|
|
241
411
|
class RandomGenerator(ez.Unit):
|
|
242
|
-
|
|
412
|
+
"""
|
|
413
|
+
Replaces input data with random data and yields the result.
|
|
414
|
+
"""
|
|
415
|
+
|
|
416
|
+
SETTINGS = RandomGeneratorSettings
|
|
243
417
|
|
|
244
418
|
INPUT_SIGNAL = ez.InputStream(AxisArray)
|
|
245
419
|
OUTPUT_SIGNAL = ez.OutputStream(AxisArray)
|
|
@@ -255,12 +429,15 @@ class RandomGenerator(ez.Unit):
|
|
|
255
429
|
|
|
256
430
|
|
|
257
431
|
class NoiseSettings(ez.Settings):
|
|
432
|
+
"""
|
|
433
|
+
See :obj:`CounterSettings` and :obj:`RandomGeneratorSettings`.
|
|
434
|
+
"""
|
|
435
|
+
|
|
258
436
|
n_time: int # Number of samples to output per block
|
|
259
437
|
fs: float # Sampling rate of signal output in Hz
|
|
260
438
|
n_ch: int = 1 # Number of channels to output
|
|
261
|
-
dispatch_rate: Optional[
|
|
262
|
-
|
|
263
|
-
] = None # (Hz), 'realtime', or 'ext_clock'
|
|
439
|
+
dispatch_rate: Optional[Union[float, str]] = None
|
|
440
|
+
"""(Hz), 'realtime', or 'ext_clock'"""
|
|
264
441
|
loc: float = 0.0 # DC offset
|
|
265
442
|
scale: float = 1.0 # Scale (in standard deviations)
|
|
266
443
|
|
|
@@ -269,7 +446,11 @@ WhiteNoiseSettings = NoiseSettings
|
|
|
269
446
|
|
|
270
447
|
|
|
271
448
|
class WhiteNoise(ez.Collection):
|
|
272
|
-
|
|
449
|
+
"""
|
|
450
|
+
A :obj:`Collection` that chains a :obj:`Counter` and :obj:`RandomGenerator`.
|
|
451
|
+
"""
|
|
452
|
+
|
|
453
|
+
SETTINGS = NoiseSettings
|
|
273
454
|
|
|
274
455
|
INPUT_CLOCK = ez.InputStream(ez.Flag)
|
|
275
456
|
OUTPUT_SIGNAL = ez.OutputStream(AxisArray)
|
|
@@ -304,7 +485,11 @@ PinkNoiseSettings = NoiseSettings
|
|
|
304
485
|
|
|
305
486
|
|
|
306
487
|
class PinkNoise(ez.Collection):
|
|
307
|
-
|
|
488
|
+
"""
|
|
489
|
+
A :obj:`Collection` that chains :obj:`WhiteNoise` and :obj:`ButterworthFilter`.
|
|
490
|
+
"""
|
|
491
|
+
|
|
492
|
+
SETTINGS = PinkNoiseSettings
|
|
308
493
|
|
|
309
494
|
INPUT_CLOCK = ez.InputStream(ez.Flag)
|
|
310
495
|
OUTPUT_SIGNAL = ez.OutputStream(AxisArray)
|
|
@@ -316,7 +501,9 @@ class PinkNoise(ez.Collection):
|
|
|
316
501
|
self.WHITE_NOISE.apply_settings(self.SETTINGS)
|
|
317
502
|
self.FILTER.apply_settings(
|
|
318
503
|
ButterworthFilterSettings(
|
|
319
|
-
axis="time",
|
|
504
|
+
axis="time",
|
|
505
|
+
order=1,
|
|
506
|
+
cutoff=self.SETTINGS.fs * 0.01, # Hz
|
|
320
507
|
)
|
|
321
508
|
)
|
|
322
509
|
|
|
@@ -336,7 +523,7 @@ class AddState(ez.State):
|
|
|
336
523
|
class Add(ez.Unit):
|
|
337
524
|
"""Add two signals together. Assumes compatible/similar axes/dimensions."""
|
|
338
525
|
|
|
339
|
-
STATE
|
|
526
|
+
STATE = AddState
|
|
340
527
|
|
|
341
528
|
INPUT_SIGNAL_A = ez.InputStream(AxisArray)
|
|
342
529
|
INPUT_SIGNAL_B = ez.InputStream(AxisArray)
|
|
@@ -360,6 +547,8 @@ class Add(ez.Unit):
|
|
|
360
547
|
|
|
361
548
|
|
|
362
549
|
class EEGSynthSettings(ez.Settings):
|
|
550
|
+
"""See :obj:`OscillatorSettings`."""
|
|
551
|
+
|
|
363
552
|
fs: float = 500.0 # Hz
|
|
364
553
|
n_time: int = 100
|
|
365
554
|
alpha_freq: float = 10.5 # Hz
|
|
@@ -367,7 +556,12 @@ class EEGSynthSettings(ez.Settings):
|
|
|
367
556
|
|
|
368
557
|
|
|
369
558
|
class EEGSynth(ez.Collection):
|
|
370
|
-
|
|
559
|
+
"""
|
|
560
|
+
A :obj:`Collection` that chains a :obj:`Clock` to both :obj:`PinkNoise`
|
|
561
|
+
and :obj:`Oscillator`, then :obj:`Add` s the result.
|
|
562
|
+
"""
|
|
563
|
+
|
|
564
|
+
SETTINGS = EEGSynthSettings
|
|
371
565
|
|
|
372
566
|
OUTPUT_SIGNAL = ez.OutputStream(AxisArray)
|
|
373
567
|
|