ezmsg-sigproc 1.7.0__py3-none-any.whl → 2.10.0__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 (66) hide show
  1. ezmsg/sigproc/__version__.py +22 -4
  2. ezmsg/sigproc/activation.py +31 -40
  3. ezmsg/sigproc/adaptive_lattice_notch.py +212 -0
  4. ezmsg/sigproc/affinetransform.py +171 -169
  5. ezmsg/sigproc/aggregate.py +190 -97
  6. ezmsg/sigproc/bandpower.py +60 -55
  7. ezmsg/sigproc/base.py +143 -33
  8. ezmsg/sigproc/butterworthfilter.py +34 -38
  9. ezmsg/sigproc/butterworthzerophase.py +305 -0
  10. ezmsg/sigproc/cheby.py +23 -17
  11. ezmsg/sigproc/combfilter.py +160 -0
  12. ezmsg/sigproc/coordinatespaces.py +159 -0
  13. ezmsg/sigproc/decimate.py +15 -10
  14. ezmsg/sigproc/denormalize.py +78 -0
  15. ezmsg/sigproc/detrend.py +28 -0
  16. ezmsg/sigproc/diff.py +82 -0
  17. ezmsg/sigproc/downsample.py +72 -81
  18. ezmsg/sigproc/ewma.py +217 -0
  19. ezmsg/sigproc/ewmfilter.py +1 -1
  20. ezmsg/sigproc/extract_axis.py +39 -0
  21. ezmsg/sigproc/fbcca.py +307 -0
  22. ezmsg/sigproc/filter.py +254 -148
  23. ezmsg/sigproc/filterbank.py +226 -214
  24. ezmsg/sigproc/filterbankdesign.py +129 -0
  25. ezmsg/sigproc/fir_hilbert.py +336 -0
  26. ezmsg/sigproc/fir_pmc.py +209 -0
  27. ezmsg/sigproc/firfilter.py +117 -0
  28. ezmsg/sigproc/gaussiansmoothing.py +89 -0
  29. ezmsg/sigproc/kaiser.py +106 -0
  30. ezmsg/sigproc/linear.py +120 -0
  31. ezmsg/sigproc/math/abs.py +23 -22
  32. ezmsg/sigproc/math/add.py +120 -0
  33. ezmsg/sigproc/math/clip.py +33 -25
  34. ezmsg/sigproc/math/difference.py +117 -43
  35. ezmsg/sigproc/math/invert.py +18 -25
  36. ezmsg/sigproc/math/log.py +38 -33
  37. ezmsg/sigproc/math/scale.py +24 -25
  38. ezmsg/sigproc/messages.py +1 -2
  39. ezmsg/sigproc/quantize.py +68 -0
  40. ezmsg/sigproc/resample.py +278 -0
  41. ezmsg/sigproc/rollingscaler.py +232 -0
  42. ezmsg/sigproc/sampler.py +209 -254
  43. ezmsg/sigproc/scaler.py +93 -218
  44. ezmsg/sigproc/signalinjector.py +44 -43
  45. ezmsg/sigproc/slicer.py +74 -102
  46. ezmsg/sigproc/spectral.py +3 -3
  47. ezmsg/sigproc/spectrogram.py +70 -70
  48. ezmsg/sigproc/spectrum.py +187 -173
  49. ezmsg/sigproc/transpose.py +134 -0
  50. ezmsg/sigproc/util/__init__.py +0 -0
  51. ezmsg/sigproc/util/asio.py +25 -0
  52. ezmsg/sigproc/util/axisarray_buffer.py +365 -0
  53. ezmsg/sigproc/util/buffer.py +449 -0
  54. ezmsg/sigproc/util/message.py +17 -0
  55. ezmsg/sigproc/util/profile.py +23 -0
  56. ezmsg/sigproc/util/sparse.py +115 -0
  57. ezmsg/sigproc/util/typeresolution.py +17 -0
  58. ezmsg/sigproc/wavelets.py +147 -154
  59. ezmsg/sigproc/window.py +248 -210
  60. ezmsg_sigproc-2.10.0.dist-info/METADATA +60 -0
  61. ezmsg_sigproc-2.10.0.dist-info/RECORD +65 -0
  62. {ezmsg_sigproc-1.7.0.dist-info → ezmsg_sigproc-2.10.0.dist-info}/WHEEL +1 -1
  63. ezmsg/sigproc/synth.py +0 -621
  64. ezmsg_sigproc-1.7.0.dist-info/METADATA +0 -58
  65. ezmsg_sigproc-1.7.0.dist-info/RECORD +0 -36
  66. /ezmsg_sigproc-1.7.0.dist-info/licenses/LICENSE.txt → /ezmsg_sigproc-2.10.0.dist-info/licenses/LICENSE +0 -0
@@ -0,0 +1,65 @@
1
+ ezmsg/sigproc/__init__.py,sha256=8K4IcOA3-pfzadoM6s2Sfg5460KlJUocGgyTJTJl96U,52
2
+ ezmsg/sigproc/__version__.py,sha256=2cFLkHY3I9n23ToIWm38PohvvIEYjdsebREPVvOKAiU,706
3
+ ezmsg/sigproc/activation.py,sha256=83vnTa3ZcC4Q3VSWcGfaqhCEqYRNySUOyVpMHZXfz-c,2755
4
+ ezmsg/sigproc/adaptive_lattice_notch.py,sha256=ThUR48mbSHuThkimtD0j4IXNMrOVcpZgGhE7PCYfXhU,8818
5
+ ezmsg/sigproc/affinetransform.py,sha256=jl7DiSa5Yb0qsmFJbfSiSeGmvK1SGoBgycFC5JU5DVY,9434
6
+ ezmsg/sigproc/aggregate.py,sha256=7Hdz1m-S6Cl9h0oRQHeS_UTGBemhOB4XdFyX6cGcdHo,9362
7
+ ezmsg/sigproc/bandpower.py,sha256=dAhH56sUrXNhcRFymTTwjdM_KcU5OxFzrR_sxIPAxyw,2264
8
+ ezmsg/sigproc/base.py,sha256=SJvKEb8gw6mUMwlV5sH0iPG0bXrgS8tvkPwhI-j89MQ,3672
9
+ ezmsg/sigproc/butterworthfilter.py,sha256=NKTGkgjvlmC1Dc9gD2Z6UBzUq12KicfnczrzM5ZTosk,5255
10
+ ezmsg/sigproc/butterworthzerophase.py,sha256=CU6cXkI6j1LQCEz0sr2IthAPCq_TEtbvSb7h2Nw1w74,11820
11
+ ezmsg/sigproc/cheby.py,sha256=B8pGt5_pOBpNZCmaibNl_NKkyuasd8ZEJXeTDCTaino,3711
12
+ ezmsg/sigproc/combfilter.py,sha256=MSxr1I-jBePW_9AuCiv3RQ1HUNxIsNhLk0q1Iu8ikAw,4766
13
+ ezmsg/sigproc/coordinatespaces.py,sha256=bp_0fTS9b27OQqLoFzgE3f9rb287P8y0S1dWWGrS08o,5298
14
+ ezmsg/sigproc/decimate.py,sha256=DCX9p4ZrcGoQ9di-jmPKqiKERTkvTAtSqLg8chQLp84,2336
15
+ ezmsg/sigproc/denormalize.py,sha256=OACzoINCISNUmZcR2pqWfxi3p8uuBLCz1PMw4XNwOCs,3035
16
+ ezmsg/sigproc/detrend.py,sha256=2C-dhVy-ty4b22aE07kIF3VjqCoFitoZqyrX6hAjXYQ,894
17
+ ezmsg/sigproc/diff.py,sha256=nDf9-XywZKlfeainIeubl_NccmkuAdLjYMcWSPMquss,3179
18
+ ezmsg/sigproc/downsample.py,sha256=Jqxt1Va1FrQLH1wUQpP0U79iQARTTHEKklKHy7yGL2o,3679
19
+ ezmsg/sigproc/ewma.py,sha256=ecLq1ZlON85NWdW_5cz9chWirxjf4y9zCPMOt3IqQuk,7723
20
+ ezmsg/sigproc/ewmfilter.py,sha256=9AuvVn3TDdf5R4bVolYNM46AtDVHFJa8MLGltY6mYPE,4795
21
+ ezmsg/sigproc/extract_axis.py,sha256=T2e9zW8N0bwOj4_zlB3I0oT0iwaBijpzF6wrFsCfD24,1593
22
+ ezmsg/sigproc/fbcca.py,sha256=JYFWsMDRJEWwUNujr4EsFL5t1ux-cnBGamNVrCRO_RA,12043
23
+ ezmsg/sigproc/filter.py,sha256=IWg3J5IGVdv7kfp4zaKRzfjEuRa5xNzcq6fHtq4tPCM,11604
24
+ ezmsg/sigproc/filterbank.py,sha256=tD7fn4dZzEvsmp_sSn16VVJ4WcJ5sN5lsSuNTLDCQZ8,13056
25
+ ezmsg/sigproc/filterbankdesign.py,sha256=vLXQVJwoFEK4V6umqzcr1PJKcwv6pGO29klSWQXk7y0,4712
26
+ ezmsg/sigproc/fir_hilbert.py,sha256=ByZDsDFjbJx-EgLZF85vZCPQbprQaiMEuG2dyvTPiDY,10855
27
+ ezmsg/sigproc/fir_pmc.py,sha256=Ze2pd9K8XrdDhgJZG2E7x-5C2hfd6kIns4bYjTJsBvc,6997
28
+ ezmsg/sigproc/firfilter.py,sha256=7r_I476nYuixJsuwc_hQ0Fbq8WB0gnYBZUKs3zultOQ,3790
29
+ ezmsg/sigproc/gaussiansmoothing.py,sha256=BfXm9YQoOtieM4ABK2KRgxeQz055rd7mqtTVqmjT3Rk,2672
30
+ ezmsg/sigproc/kaiser.py,sha256=dIwgHbLXUHPtdotsGNLE9VG_clhcMgvVnSoFkMVgF9M,3483
31
+ ezmsg/sigproc/linear.py,sha256=b3NRzQNBvdU2jqenZT9XXFHax9Mavbj2xFiVxOwl1Ms,4662
32
+ ezmsg/sigproc/messages.py,sha256=KQczHTeifn4BZycChN8ZcpfZoQW3lC_xuFmN72QT97s,925
33
+ ezmsg/sigproc/quantize.py,sha256=uSM2z2xXwL0dgSltyzLEmlKjaJZ2meA3PDWX8_bM0Hs,2195
34
+ ezmsg/sigproc/resample.py,sha256=3mm9pvxryNVhQuTCIMW3ToUkUfbVOCsIgvXUiurit1Y,11389
35
+ ezmsg/sigproc/rollingscaler.py,sha256=e-smSKDhmDD2nWIf6I77CtRxQp_7sHS268SGPi7aXp8,8499
36
+ ezmsg/sigproc/sampler.py,sha256=iOk2YoUX22u9iTjFKimzP5V074RDBVcmswgfyxvZRZo,10761
37
+ ezmsg/sigproc/scaler.py,sha256=oBZa6uzyftChvk6aqBD5clil6pedx3IF-dptrb74EA0,5888
38
+ ezmsg/sigproc/signalinjector.py,sha256=mB62H2b-ScgPtH1jajEpxgDHqdb-RKekQfgyNncsE8Y,2874
39
+ ezmsg/sigproc/slicer.py,sha256=xLXxWf722V08ytVwvPimYjDKKj0pkC2HjdgCVaoaOvs,5195
40
+ ezmsg/sigproc/spectral.py,sha256=wFzuihS7qJZMQcp0ds_qCG-zCbvh5DyhFRjn2wA9TWQ,322
41
+ ezmsg/sigproc/spectrogram.py,sha256=g8xYWENzle6O5uEF-vfjsF5gOSDnJTwiu3ZudicO470,2893
42
+ ezmsg/sigproc/spectrum.py,sha256=AAxrywIYpAPENRWvaaM2VjcKEaqMt6ra1E3Ao26jdZs,9523
43
+ ezmsg/sigproc/transpose.py,sha256=uRGieeYRFTO9gAhWzkq49f-Qo49rpDQhn9r3nLuwx4I,4983
44
+ ezmsg/sigproc/wavelets.py,sha256=mN9TrZencyvKBfnuMiGZ_lzrE1O7DhVo05EYgCjXncg,7428
45
+ ezmsg/sigproc/window.py,sha256=ZlawY4TPbLc46qIgcKhP4X7dpMDo4zNlnfzgV1eFaGU,15335
46
+ ezmsg/sigproc/math/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
47
+ ezmsg/sigproc/math/abs.py,sha256=Zrt0xhGnhf90Vk00S1PYkbeCOQshqkvh1HLj26N4AL0,979
48
+ ezmsg/sigproc/math/add.py,sha256=UikpS_vhaY58ssf46C1r4_tnRUFMl86xCmd5Y8GFZ1Q,3666
49
+ ezmsg/sigproc/math/clip.py,sha256=1D6mUlOzBB7L35G_KKYZmfg7nYlbuDdITV4EH0R-yUo,1529
50
+ ezmsg/sigproc/math/difference.py,sha256=uUYZgbLe-GrFSN6EOFjs9fQZllp827IluxL6m8TJuH8,4791
51
+ ezmsg/sigproc/math/invert.py,sha256=nz8jbfvDoez6s9NmAprBtTAI5oSDj0wNUPk8j13XiVk,855
52
+ ezmsg/sigproc/math/log.py,sha256=JhjSqLnQnvx_3F4txRYHuUPSJ12Yj2HvRTsCMNvlxpo,2022
53
+ ezmsg/sigproc/math/scale.py,sha256=4_xHcHNuf13E1fxIF5vbkPfkN4En6zkfPIKID7lCERk,1133
54
+ ezmsg/sigproc/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
55
+ ezmsg/sigproc/util/asio.py,sha256=aAj0e7OoBvkRy28k05HL2s9YPCTxOddc05xMN-qd4lQ,577
56
+ ezmsg/sigproc/util/axisarray_buffer.py,sha256=TGDeC6CXmmp7OUuiGd6xYQijRGYDE4QGdWxjK5Vs3nE,14057
57
+ ezmsg/sigproc/util/buffer.py,sha256=83Gm0IuowmcMlXgLFB_rz8_ZPhkwG4DNNejyWJDKJl8,19658
58
+ ezmsg/sigproc/util/message.py,sha256=ppN3IYtIAwrxWG9JOvgWFn1wDdIumkEzYFfqpH9VQkY,338
59
+ ezmsg/sigproc/util/profile.py,sha256=eVOo9pXgusrnH1yfRdd2RsM7Dbe2UpyC0LJ9MfGpB08,416
60
+ ezmsg/sigproc/util/sparse.py,sha256=NjbJitCtO0B6CENTlyd9c-lHEJwoCan-T3DIgPyeShw,4834
61
+ ezmsg/sigproc/util/typeresolution.py,sha256=fMFzLi63dqCIclGFLcMdM870OYxJnkeWw6aWKNMk718,362
62
+ ezmsg_sigproc-2.10.0.dist-info/METADATA,sha256=lsHkW6Abh6GK3bOOl5MHtXthhwOQWuVUrByPz2AYOfk,1909
63
+ ezmsg_sigproc-2.10.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
64
+ ezmsg_sigproc-2.10.0.dist-info/licenses/LICENSE,sha256=seu0tKhhAMPCUgc1XpXGGaCxY1YaYvFJwqFuQZAl2go,1100
65
+ ezmsg_sigproc-2.10.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.26.3
2
+ Generator: hatchling 1.28.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
ezmsg/sigproc/synth.py DELETED
@@ -1,621 +0,0 @@
1
- import asyncio
2
- from dataclasses import field
3
- import time
4
- import typing
5
-
6
- import numpy as np
7
- import ezmsg.core as ez
8
- from ezmsg.util.generator import consumer
9
- from ezmsg.util.messages.axisarray import AxisArray
10
- from ezmsg.util.messages.util import replace
11
-
12
- from .butterworthfilter import ButterworthFilter, ButterworthFilterSettings
13
- from .base import GenAxisArray
14
-
15
-
16
- def clock(dispatch_rate: float | None) -> typing.Generator[ez.Flag, None, None]:
17
- """
18
- Construct a generator that yields events at a specified rate.
19
-
20
- Args:
21
- dispatch_rate: event rate in seconds.
22
-
23
- Returns:
24
- A generator object that yields :obj:`ez.Flag` events at a specified rate.
25
- """
26
- n_dispatch = -1
27
- t_0 = time.time()
28
- while True:
29
- if dispatch_rate is not None:
30
- n_dispatch += 1
31
- t_next = t_0 + n_dispatch / dispatch_rate
32
- time.sleep(max(0, t_next - time.time()))
33
- yield ez.Flag()
34
-
35
-
36
- async def aclock(dispatch_rate: float | None) -> typing.AsyncGenerator[ez.Flag, None]:
37
- """
38
- ``asyncio`` version of :obj:`clock`.
39
-
40
- Returns:
41
- asynchronous generator object. Must use `anext` or `async for`.
42
- """
43
- t_0 = time.time()
44
- n_dispatch = -1
45
- while True:
46
- if dispatch_rate is not None:
47
- n_dispatch += 1
48
- t_next = t_0 + n_dispatch / dispatch_rate
49
- await asyncio.sleep(t_next - time.time())
50
- yield ez.Flag()
51
-
52
-
53
- class ClockSettings(ez.Settings):
54
- """Settings for :obj:`Clock`. See :obj:`clock` for parameter description."""
55
-
56
- # Message dispatch rate (Hz), or None (fast as possible)
57
- dispatch_rate: float | None
58
-
59
-
60
- class ClockState(ez.State):
61
- cur_settings: ClockSettings
62
- gen: typing.AsyncGenerator
63
-
64
-
65
- class Clock(ez.Unit):
66
- """Unit for :obj:`clock`."""
67
-
68
- SETTINGS = ClockSettings
69
- STATE = ClockState
70
-
71
- INPUT_SETTINGS = ez.InputStream(ClockSettings)
72
- OUTPUT_CLOCK = ez.OutputStream(ez.Flag)
73
-
74
- async def initialize(self) -> None:
75
- self.STATE.cur_settings = self.SETTINGS
76
- self.construct_generator()
77
-
78
- def construct_generator(self):
79
- self.STATE.gen = aclock(self.STATE.cur_settings.dispatch_rate)
80
-
81
- @ez.subscriber(INPUT_SETTINGS)
82
- async def on_settings(self, msg: ClockSettings) -> None:
83
- self.STATE.cur_settings = msg
84
- self.construct_generator()
85
-
86
- @ez.publisher(OUTPUT_CLOCK)
87
- async def generate(self) -> typing.AsyncGenerator:
88
- while True:
89
- out = await self.STATE.gen.__anext__()
90
- if out:
91
- yield self.OUTPUT_CLOCK, out
92
-
93
-
94
- # COUNTER - Generate incrementing integer. fs and dispatch_rate parameters combine to give many options. #
95
- async def acounter(
96
- n_time: int,
97
- fs: float | None,
98
- n_ch: int = 1,
99
- dispatch_rate: float | str | None = None,
100
- mod: int | None = None,
101
- ) -> typing.AsyncGenerator[AxisArray, None]:
102
- """
103
- Construct an asynchronous generator to generate AxisArray objects at a specified rate
104
- and with the specified sampling rate.
105
-
106
- NOTE: This module uses asyncio.sleep to delay appropriately in realtime mode.
107
- This method of sleeping/yielding execution priority has quirky behavior with
108
- sub-millisecond sleep periods which may result in unexpected behavior (e.g.
109
- fs = 2000, n_time = 1, realtime = True -- may result in ~1400 msgs/sec)
110
-
111
- Args:
112
- n_time: Number of samples to output per block.
113
- fs: Sampling rate of signal output in Hz.
114
- n_ch: Number of channels to synthesize
115
- dispatch_rate: Message dispatch rate (Hz), 'realtime' or None (fast as possible)
116
- Note: if dispatch_rate is a float then time offsets will be synthetic and the
117
- system will run faster or slower than wall clock time.
118
- mod: If set to an integer, counter will rollover at this number.
119
-
120
- Returns:
121
- An asynchronous generator.
122
- """
123
-
124
- # TODO: Adapt this to use ezmsg.util.rate?
125
-
126
- counter_start: int = 0 # next sample's first value
127
-
128
- b_realtime = False
129
- b_manual_dispatch = False
130
- b_ext_clock = False
131
- if dispatch_rate is not None:
132
- if isinstance(dispatch_rate, str):
133
- if dispatch_rate.lower() == "realtime":
134
- b_realtime = True
135
- elif dispatch_rate.lower() == "ext_clock":
136
- b_ext_clock = True
137
- else:
138
- b_manual_dispatch = True
139
-
140
- n_sent: int = 0 # It is convenient to know how many samples we have sent.
141
- clock_zero: float = time.time() # time associated with first sample
142
- template = AxisArray(
143
- data=np.array([[]]),
144
- dims=["time", "ch"],
145
- axes={
146
- "time": AxisArray.TimeAxis(fs=fs),
147
- "ch": AxisArray.CoordinateAxis(
148
- data=np.array([f"Ch{_}" for _ in range(n_ch)]), dims=["ch"]
149
- ),
150
- },
151
- key="acounter",
152
- )
153
-
154
- while True:
155
- # 1. Sleep, if necessary, until we are at the end of the current block
156
- if b_realtime:
157
- n_next = n_sent + n_time
158
- t_next = clock_zero + n_next / fs
159
- await asyncio.sleep(t_next - time.time())
160
- elif b_manual_dispatch:
161
- n_disp_next = 1 + n_sent / n_time
162
- t_disp_next = clock_zero + n_disp_next / dispatch_rate
163
- await asyncio.sleep(t_disp_next - time.time())
164
-
165
- # 2. Prepare counter data.
166
- block_samp = np.arange(counter_start, counter_start + n_time)[:, np.newaxis]
167
- if mod is not None:
168
- block_samp %= mod
169
- block_samp = np.tile(block_samp, (1, n_ch))
170
-
171
- # 3. Prepare offset - the time associated with block_samp[0]
172
- if b_realtime:
173
- offset = t_next - n_time / fs
174
- elif b_ext_clock:
175
- offset = time.time()
176
- else:
177
- # Purely synthetic.
178
- offset = n_sent / fs
179
- # offset += clock_zero # ??
180
-
181
- # 4. yield output
182
- yield replace(
183
- template,
184
- data=block_samp,
185
- axes={
186
- "time": replace(template.axes["time"], offset=offset),
187
- "ch": template.axes["ch"],
188
- },
189
- )
190
-
191
- # 5. Update state for next iteration (after next yield)
192
- counter_start = block_samp[-1, 0] + 1 # do not % mod
193
- n_sent += n_time
194
-
195
-
196
- class CounterSettings(ez.Settings):
197
- # TODO: Adapt this to use ezmsg.util.rate?
198
- """
199
- Settings for :obj:`Counter`.
200
- See :obj:`acounter` for a description of the parameters.
201
- """
202
-
203
- n_time: int # Number of samples to output per block
204
- fs: float # Sampling rate of signal output in Hz
205
- n_ch: int = 1 # Number of channels to synthesize
206
-
207
- # Message dispatch rate (Hz), 'realtime', 'ext_clock', or None (fast as possible)
208
- # Note: if dispatch_rate is a float then time offsets will be synthetic and the
209
- # system will run faster or slower than wall clock time.
210
- dispatch_rate: float | str | None = None
211
-
212
- # If set to an integer, counter will rollover
213
- mod: int | None = None
214
-
215
-
216
- class CounterState(ez.State):
217
- gen: typing.AsyncGenerator[AxisArray, ez.Flag | None]
218
- cur_settings: CounterSettings
219
- new_generator: asyncio.Event
220
-
221
-
222
- class Counter(ez.Unit):
223
- """Generates monotonically increasing counter. Unit for :obj:`acounter`."""
224
-
225
- SETTINGS = CounterSettings
226
- STATE = CounterState
227
-
228
- INPUT_CLOCK = ez.InputStream(ez.Flag)
229
- INPUT_SETTINGS = ez.InputStream(CounterSettings)
230
- OUTPUT_SIGNAL = ez.OutputStream(AxisArray)
231
-
232
- async def initialize(self) -> None:
233
- self.STATE.new_generator = asyncio.Event()
234
- self.validate_settings(self.SETTINGS)
235
-
236
- @ez.subscriber(INPUT_SETTINGS)
237
- async def on_settings(self, msg: CounterSettings) -> None:
238
- self.validate_settings(msg)
239
-
240
- def validate_settings(self, settings: CounterSettings) -> None:
241
- if isinstance(
242
- settings.dispatch_rate, str
243
- ) and self.SETTINGS.dispatch_rate not in ["realtime", "ext_clock"]:
244
- raise ValueError(f"Unknown dispatch_rate: {self.SETTINGS.dispatch_rate}")
245
- self.STATE.cur_settings = settings
246
- self.construct_generator()
247
-
248
- def construct_generator(self):
249
- self.STATE.gen = acounter(
250
- self.STATE.cur_settings.n_time,
251
- self.STATE.cur_settings.fs,
252
- n_ch=self.STATE.cur_settings.n_ch,
253
- dispatch_rate=self.STATE.cur_settings.dispatch_rate,
254
- mod=self.STATE.cur_settings.mod,
255
- )
256
- self.STATE.new_generator.set()
257
-
258
- @ez.subscriber(INPUT_CLOCK)
259
- @ez.publisher(OUTPUT_SIGNAL)
260
- async def on_clock(self, clock: ez.Flag):
261
- if self.STATE.cur_settings.dispatch_rate == "ext_clock":
262
- out = await self.STATE.gen.__anext__()
263
- yield self.OUTPUT_SIGNAL, out
264
-
265
- @ez.publisher(OUTPUT_SIGNAL)
266
- async def run_generator(self) -> typing.AsyncGenerator:
267
- while True:
268
- await self.STATE.new_generator.wait()
269
- self.STATE.new_generator.clear()
270
-
271
- if self.STATE.cur_settings.dispatch_rate == "ext_clock":
272
- continue
273
-
274
- while not self.STATE.new_generator.is_set():
275
- out = await self.STATE.gen.__anext__()
276
- yield self.OUTPUT_SIGNAL, out
277
-
278
-
279
- @consumer
280
- def sin(
281
- axis: str | None = "time",
282
- freq: float = 1.0,
283
- amp: float = 1.0,
284
- phase: float = 0.0,
285
- ) -> typing.Generator[AxisArray, AxisArray, None]:
286
- """
287
- Construct a generator of sinusoidal waveforms in AxisArray objects.
288
-
289
- Args:
290
- axis: The name of the axis over which the sinusoid passes.
291
- Note: The axis must exist in the msg.axes and be of type AxisArray.LinearAxis.
292
- freq: The frequency of the sinusoid, in Hz.
293
- amp: The amplitude of the sinusoid.
294
- phase: The initial phase of the sinusoid, in radians.
295
-
296
- Returns:
297
- A primed generator that expects .send(axis_array) of sample counts
298
- and yields an AxisArray of sinusoids.
299
- """
300
- msg_out = AxisArray(np.array([]), dims=[""])
301
-
302
- ang_freq = 2.0 * np.pi * freq
303
-
304
- while True:
305
- msg_in: AxisArray = yield msg_out
306
- # msg_in is expected to be sample counts
307
-
308
- axis_name = axis
309
- if axis_name is None:
310
- axis_name = msg_in.dims[0]
311
-
312
- w = (ang_freq * msg_in.get_axis(axis_name).gain) * msg_in.data
313
- out_data = amp * np.sin(w + phase)
314
- msg_out = replace(msg_in, data=out_data)
315
-
316
-
317
- class SinGeneratorSettings(ez.Settings):
318
- """
319
- Settings for :obj:`SinGenerator`.
320
- See :obj:`sin` for parameter descriptions.
321
- """
322
-
323
- time_axis: str | None = "time"
324
- freq: float = 1.0 # Oscillation frequency in Hz
325
- amp: float = 1.0 # Amplitude
326
- phase: float = 0.0 # Phase offset (in radians)
327
-
328
-
329
- class SinGenerator(GenAxisArray):
330
- """
331
- Unit for :obj:`sin`.
332
- """
333
-
334
- SETTINGS = SinGeneratorSettings
335
-
336
- def construct_generator(self):
337
- self.STATE.gen = sin(
338
- axis=self.SETTINGS.time_axis,
339
- freq=self.SETTINGS.freq,
340
- amp=self.SETTINGS.amp,
341
- phase=self.SETTINGS.phase,
342
- )
343
-
344
-
345
- class OscillatorSettings(ez.Settings):
346
- """Settings for :obj:`Oscillator`"""
347
-
348
- n_time: int
349
- """Number of samples to output per block."""
350
-
351
- fs: float
352
- """Sampling rate of signal output in Hz"""
353
-
354
- n_ch: int = 1
355
- """Number of channels to output per block"""
356
-
357
- dispatch_rate: float | str | None = None
358
- """(Hz) | 'realtime' | 'ext_clock'"""
359
-
360
- freq: float = 1.0
361
- """Oscillation frequency in Hz"""
362
-
363
- amp: float = 1.0
364
- """Amplitude"""
365
-
366
- phase: float = 0.0
367
- """Phase offset (in radians)"""
368
-
369
- sync: bool = False
370
- """Adjust `freq` to sync with sampling rate"""
371
-
372
-
373
- class Oscillator(ez.Collection):
374
- """
375
- :obj:`Collection that chains :obj:`Counter` and :obj:`SinGenerator`.
376
- """
377
-
378
- SETTINGS = OscillatorSettings
379
-
380
- INPUT_CLOCK = ez.InputStream(ez.Flag)
381
- OUTPUT_SIGNAL = ez.OutputStream(AxisArray)
382
-
383
- COUNTER = Counter()
384
- SIN = SinGenerator()
385
-
386
- def configure(self) -> None:
387
- # Calculate synchronous settings if necessary
388
- freq = self.SETTINGS.freq
389
- mod = None
390
- if self.SETTINGS.sync:
391
- period = 1.0 / self.SETTINGS.freq
392
- mod = round(period * self.SETTINGS.fs)
393
- freq = 1.0 / (mod / self.SETTINGS.fs)
394
-
395
- self.COUNTER.apply_settings(
396
- CounterSettings(
397
- n_time=self.SETTINGS.n_time,
398
- fs=self.SETTINGS.fs,
399
- n_ch=self.SETTINGS.n_ch,
400
- dispatch_rate=self.SETTINGS.dispatch_rate,
401
- mod=mod,
402
- )
403
- )
404
-
405
- self.SIN.apply_settings(
406
- SinGeneratorSettings(
407
- freq=freq, amp=self.SETTINGS.amp, phase=self.SETTINGS.phase
408
- )
409
- )
410
-
411
- def network(self) -> ez.NetworkDefinition:
412
- return (
413
- (self.INPUT_CLOCK, self.COUNTER.INPUT_CLOCK),
414
- (self.COUNTER.OUTPUT_SIGNAL, self.SIN.INPUT_SIGNAL),
415
- (self.SIN.OUTPUT_SIGNAL, self.OUTPUT_SIGNAL),
416
- )
417
-
418
-
419
- class RandomGeneratorSettings(ez.Settings):
420
- loc: float = 0.0
421
- """loc argument for :obj:`numpy.random.normal`"""
422
-
423
- scale: float = 1.0
424
- """scale argument for :obj:`numpy.random.normal`"""
425
-
426
-
427
- class RandomGenerator(ez.Unit):
428
- """
429
- Replaces input data with random data and yields the result.
430
- """
431
-
432
- SETTINGS = RandomGeneratorSettings
433
-
434
- INPUT_SIGNAL = ez.InputStream(AxisArray)
435
- OUTPUT_SIGNAL = ez.OutputStream(AxisArray)
436
-
437
- @ez.subscriber(INPUT_SIGNAL)
438
- @ez.publisher(OUTPUT_SIGNAL)
439
- async def generate(self, msg: AxisArray) -> typing.AsyncGenerator:
440
- random_data = np.random.normal(
441
- size=msg.shape, loc=self.SETTINGS.loc, scale=self.SETTINGS.scale
442
- )
443
-
444
- yield self.OUTPUT_SIGNAL, replace(msg, data=random_data)
445
-
446
-
447
- class NoiseSettings(ez.Settings):
448
- """
449
- See :obj:`CounterSettings` and :obj:`RandomGeneratorSettings`.
450
- """
451
-
452
- n_time: int # Number of samples to output per block
453
- fs: float # Sampling rate of signal output in Hz
454
- n_ch: int = 1 # Number of channels to output
455
- dispatch_rate: float | str | None = None
456
- """(Hz), 'realtime', or 'ext_clock'"""
457
- loc: float = 0.0 # DC offset
458
- scale: float = 1.0 # Scale (in standard deviations)
459
-
460
-
461
- WhiteNoiseSettings = NoiseSettings
462
-
463
-
464
- class WhiteNoise(ez.Collection):
465
- """
466
- A :obj:`Collection` that chains a :obj:`Counter` and :obj:`RandomGenerator`.
467
- """
468
-
469
- SETTINGS = NoiseSettings
470
-
471
- INPUT_CLOCK = ez.InputStream(ez.Flag)
472
- OUTPUT_SIGNAL = ez.OutputStream(AxisArray)
473
-
474
- COUNTER = Counter()
475
- RANDOM = RandomGenerator()
476
-
477
- def configure(self) -> None:
478
- self.RANDOM.apply_settings(
479
- RandomGeneratorSettings(loc=self.SETTINGS.loc, scale=self.SETTINGS.scale)
480
- )
481
-
482
- self.COUNTER.apply_settings(
483
- CounterSettings(
484
- n_time=self.SETTINGS.n_time,
485
- fs=self.SETTINGS.fs,
486
- n_ch=self.SETTINGS.n_ch,
487
- dispatch_rate=self.SETTINGS.dispatch_rate,
488
- mod=None,
489
- )
490
- )
491
-
492
- def network(self) -> ez.NetworkDefinition:
493
- return (
494
- (self.INPUT_CLOCK, self.COUNTER.INPUT_CLOCK),
495
- (self.COUNTER.OUTPUT_SIGNAL, self.RANDOM.INPUT_SIGNAL),
496
- (self.RANDOM.OUTPUT_SIGNAL, self.OUTPUT_SIGNAL),
497
- )
498
-
499
-
500
- PinkNoiseSettings = NoiseSettings
501
-
502
-
503
- class PinkNoise(ez.Collection):
504
- """
505
- A :obj:`Collection` that chains :obj:`WhiteNoise` and :obj:`ButterworthFilter`.
506
- """
507
-
508
- SETTINGS = PinkNoiseSettings
509
-
510
- INPUT_CLOCK = ez.InputStream(ez.Flag)
511
- OUTPUT_SIGNAL = ez.OutputStream(AxisArray)
512
-
513
- WHITE_NOISE = WhiteNoise()
514
- FILTER = ButterworthFilter()
515
-
516
- def configure(self) -> None:
517
- self.WHITE_NOISE.apply_settings(self.SETTINGS)
518
- self.FILTER.apply_settings(
519
- ButterworthFilterSettings(
520
- axis="time",
521
- order=1,
522
- cutoff=self.SETTINGS.fs * 0.01, # Hz
523
- )
524
- )
525
-
526
- def network(self) -> ez.NetworkDefinition:
527
- return (
528
- (self.INPUT_CLOCK, self.WHITE_NOISE.INPUT_CLOCK),
529
- (self.WHITE_NOISE.OUTPUT_SIGNAL, self.FILTER.INPUT_SIGNAL),
530
- (self.FILTER.OUTPUT_SIGNAL, self.OUTPUT_SIGNAL),
531
- )
532
-
533
-
534
- class AddState(ez.State):
535
- queue_a: "asyncio.Queue[AxisArray]" = field(default_factory=asyncio.Queue)
536
- queue_b: "asyncio.Queue[AxisArray]" = field(default_factory=asyncio.Queue)
537
-
538
-
539
- class Add(ez.Unit):
540
- """Add two signals together. Assumes compatible/similar axes/dimensions."""
541
-
542
- STATE = AddState
543
-
544
- INPUT_SIGNAL_A = ez.InputStream(AxisArray)
545
- INPUT_SIGNAL_B = ez.InputStream(AxisArray)
546
- OUTPUT_SIGNAL = ez.OutputStream(AxisArray)
547
-
548
- @ez.subscriber(INPUT_SIGNAL_A)
549
- async def on_a(self, msg: AxisArray) -> None:
550
- self.STATE.queue_a.put_nowait(msg)
551
-
552
- @ez.subscriber(INPUT_SIGNAL_B)
553
- async def on_b(self, msg: AxisArray) -> None:
554
- self.STATE.queue_b.put_nowait(msg)
555
-
556
- @ez.publisher(OUTPUT_SIGNAL)
557
- async def output(self) -> typing.AsyncGenerator:
558
- while True:
559
- a = await self.STATE.queue_a.get()
560
- b = await self.STATE.queue_b.get()
561
-
562
- yield self.OUTPUT_SIGNAL, replace(a, data=a.data + b.data)
563
-
564
-
565
- class EEGSynthSettings(ez.Settings):
566
- """See :obj:`OscillatorSettings`."""
567
-
568
- fs: float = 500.0 # Hz
569
- n_time: int = 100
570
- alpha_freq: float = 10.5 # Hz
571
- n_ch: int = 8
572
-
573
-
574
- class EEGSynth(ez.Collection):
575
- """
576
- A :obj:`Collection` that chains a :obj:`Clock` to both :obj:`PinkNoise`
577
- and :obj:`Oscillator`, then :obj:`Add` s the result.
578
- """
579
-
580
- SETTINGS = EEGSynthSettings
581
-
582
- OUTPUT_SIGNAL = ez.OutputStream(AxisArray)
583
-
584
- CLOCK = Clock()
585
- NOISE = PinkNoise()
586
- OSC = Oscillator()
587
- ADD = Add()
588
-
589
- def configure(self) -> None:
590
- self.CLOCK.apply_settings(
591
- ClockSettings(dispatch_rate=self.SETTINGS.fs / self.SETTINGS.n_time)
592
- )
593
-
594
- self.OSC.apply_settings(
595
- OscillatorSettings(
596
- n_time=self.SETTINGS.n_time,
597
- fs=self.SETTINGS.fs,
598
- n_ch=self.SETTINGS.n_ch,
599
- dispatch_rate="ext_clock",
600
- freq=self.SETTINGS.alpha_freq,
601
- )
602
- )
603
-
604
- self.NOISE.apply_settings(
605
- PinkNoiseSettings(
606
- n_time=self.SETTINGS.n_time,
607
- fs=self.SETTINGS.fs,
608
- n_ch=self.SETTINGS.n_ch,
609
- dispatch_rate="ext_clock",
610
- scale=5.0,
611
- )
612
- )
613
-
614
- def network(self) -> ez.NetworkDefinition:
615
- return (
616
- (self.CLOCK.OUTPUT_CLOCK, self.OSC.INPUT_CLOCK),
617
- (self.CLOCK.OUTPUT_CLOCK, self.NOISE.INPUT_CLOCK),
618
- (self.OSC.OUTPUT_SIGNAL, self.ADD.INPUT_SIGNAL_A),
619
- (self.NOISE.OUTPUT_SIGNAL, self.ADD.INPUT_SIGNAL_B),
620
- (self.ADD.OUTPUT_SIGNAL, self.OUTPUT_SIGNAL),
621
- )