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.
Files changed (38) hide show
  1. ezmsg/sigproc/__init__.py +1 -1
  2. ezmsg/sigproc/__version__.py +16 -1
  3. ezmsg/sigproc/activation.py +75 -0
  4. ezmsg/sigproc/affinetransform.py +234 -0
  5. ezmsg/sigproc/aggregate.py +158 -0
  6. ezmsg/sigproc/bandpower.py +74 -0
  7. ezmsg/sigproc/base.py +38 -0
  8. ezmsg/sigproc/butterworthfilter.py +102 -11
  9. ezmsg/sigproc/decimate.py +7 -4
  10. ezmsg/sigproc/downsample.py +95 -51
  11. ezmsg/sigproc/ewmfilter.py +38 -16
  12. ezmsg/sigproc/filter.py +108 -20
  13. ezmsg/sigproc/filterbank.py +278 -0
  14. ezmsg/sigproc/math/__init__.py +0 -0
  15. ezmsg/sigproc/math/abs.py +28 -0
  16. ezmsg/sigproc/math/clip.py +30 -0
  17. ezmsg/sigproc/math/difference.py +60 -0
  18. ezmsg/sigproc/math/invert.py +29 -0
  19. ezmsg/sigproc/math/log.py +32 -0
  20. ezmsg/sigproc/math/scale.py +31 -0
  21. ezmsg/sigproc/messages.py +2 -3
  22. ezmsg/sigproc/sampler.py +259 -224
  23. ezmsg/sigproc/scaler.py +173 -0
  24. ezmsg/sigproc/signalinjector.py +64 -0
  25. ezmsg/sigproc/slicer.py +133 -0
  26. ezmsg/sigproc/spectral.py +6 -132
  27. ezmsg/sigproc/spectrogram.py +86 -0
  28. ezmsg/sigproc/spectrum.py +259 -0
  29. ezmsg/sigproc/synth.py +299 -105
  30. ezmsg/sigproc/wavelets.py +167 -0
  31. ezmsg/sigproc/window.py +254 -116
  32. ezmsg_sigproc-1.3.1.dist-info/METADATA +59 -0
  33. ezmsg_sigproc-1.3.1.dist-info/RECORD +35 -0
  34. {ezmsg_sigproc-1.2.2.dist-info → ezmsg_sigproc-1.3.1.dist-info}/WHEEL +1 -2
  35. ezmsg_sigproc-1.2.2.dist-info/METADATA +0 -36
  36. ezmsg_sigproc-1.2.2.dist-info/RECORD +0 -17
  37. ezmsg_sigproc-1.2.2.dist-info/top_level.txt +0 -1
  38. {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 dataclasses import dataclass, replace, field
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
- from typing import Optional, AsyncGenerator, Union
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
- SETTINGS: ClockSettings
26
- STATE: ClockState
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
- if self.STATE.cur_settings.dispatch_rate is not None:
42
- await asyncio.sleep(1.0 / self.STATE.cur_settings.dispatch_rate)
43
- yield self.OUTPUT_CLOCK, ez.Flag
44
-
45
-
46
- class CounterSettings(ez.Settings):
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
- TODO: Adapt this to use ezmsg.util.rate?
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
- samp: int = 0 # current sample counter
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: CounterSettings
76
- STATE: CounterState
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.clock_event = asyncio.Event()
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
- async def on_clock(self, _: ez.Flag):
101
- self.STATE.clock_event.set()
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 publish(self) -> AsyncGenerator:
251
+ async def run_generator(self) -> AsyncGenerator:
105
252
  while True:
106
- block_dur = self.STATE.cur_settings.n_time / self.STATE.cur_settings.fs
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
- yield self.OUTPUT_SIGNAL, out
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 SinGeneratorState(ez.State):
153
- ang_freq: float # pre-calculated angular frequency in radians
313
+ class SinGenerator(GenAxisArray):
314
+ """
315
+ Unit for :obj:`sin`.
316
+ """
154
317
 
318
+ SETTINGS = SinGeneratorSettings
155
319
 
156
- class SinGenerator(ez.Unit):
157
- SETTINGS: SinGeneratorSettings
158
- STATE: SinGeneratorState
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
- def initialize(self) -> None:
164
- self.STATE.ang_freq = 2.0 * np.pi * self.SETTINGS.freq
329
+ class OscillatorSettings(ez.Settings):
330
+ """Settings for :obj:`Oscillator`"""
165
331
 
166
- @ez.subscriber(INPUT_SIGNAL)
167
- @ez.publisher(OUTPUT_SIGNAL)
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
- class OscillatorSettings(ez.Settings):
184
- n_time: int # Number of samples to output per block
185
- fs: float # Sampling rate of signal output in Hz
186
- n_ch: int = 1 # Number of channels to output per block
187
- dispatch_rate: Optional[Union[float, str]] = None # (Hz) | 'realtime' | 'ext_clock'
188
- freq: float = 1.0 # Oscillation frequency in Hz
189
- amp: float = 1.0 # Amplitude
190
- phase: float = 0.0 # Phase offset (in radians)
191
- sync: bool = False # Adjust `freq` to sync with sampling rate
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
- SETTINGS: OscillatorSettings
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
- SETTINGS: RandomGeneratorSettings
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
- Union[float, str]
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
- SETTINGS: NoiseSettings
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
- SETTINGS: PinkNoiseSettings
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", order=1, cutoff=self.SETTINGS.fs * 0.01 # Hz
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: AddState
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
- SETTINGS: EEGSynthSettings
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