ezmsg-sigproc 2.5.0__py3-none-any.whl → 2.7.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 (64) hide show
  1. ezmsg/sigproc/__version__.py +2 -2
  2. ezmsg/sigproc/activation.py +5 -11
  3. ezmsg/sigproc/adaptive_lattice_notch.py +11 -30
  4. ezmsg/sigproc/affinetransform.py +16 -42
  5. ezmsg/sigproc/aggregate.py +17 -34
  6. ezmsg/sigproc/bandpower.py +12 -20
  7. ezmsg/sigproc/base.py +141 -1276
  8. ezmsg/sigproc/butterworthfilter.py +8 -16
  9. ezmsg/sigproc/butterworthzerophase.py +7 -16
  10. ezmsg/sigproc/cheby.py +4 -10
  11. ezmsg/sigproc/combfilter.py +5 -8
  12. ezmsg/sigproc/coordinatespaces.py +142 -0
  13. ezmsg/sigproc/decimate.py +3 -7
  14. ezmsg/sigproc/denormalize.py +6 -11
  15. ezmsg/sigproc/detrend.py +3 -4
  16. ezmsg/sigproc/diff.py +8 -17
  17. ezmsg/sigproc/downsample.py +11 -20
  18. ezmsg/sigproc/ewma.py +11 -28
  19. ezmsg/sigproc/ewmfilter.py +1 -1
  20. ezmsg/sigproc/extract_axis.py +3 -4
  21. ezmsg/sigproc/fbcca.py +34 -59
  22. ezmsg/sigproc/filter.py +19 -45
  23. ezmsg/sigproc/filterbank.py +37 -74
  24. ezmsg/sigproc/filterbankdesign.py +7 -14
  25. ezmsg/sigproc/fir_hilbert.py +13 -30
  26. ezmsg/sigproc/fir_pmc.py +5 -10
  27. ezmsg/sigproc/firfilter.py +12 -14
  28. ezmsg/sigproc/gaussiansmoothing.py +5 -9
  29. ezmsg/sigproc/kaiser.py +11 -15
  30. ezmsg/sigproc/math/abs.py +4 -3
  31. ezmsg/sigproc/math/add.py +121 -0
  32. ezmsg/sigproc/math/clip.py +4 -1
  33. ezmsg/sigproc/math/difference.py +100 -36
  34. ezmsg/sigproc/math/invert.py +3 -3
  35. ezmsg/sigproc/math/log.py +5 -6
  36. ezmsg/sigproc/math/scale.py +2 -0
  37. ezmsg/sigproc/messages.py +1 -2
  38. ezmsg/sigproc/quantize.py +3 -6
  39. ezmsg/sigproc/resample.py +17 -38
  40. ezmsg/sigproc/rollingscaler.py +12 -37
  41. ezmsg/sigproc/sampler.py +19 -37
  42. ezmsg/sigproc/scaler.py +11 -22
  43. ezmsg/sigproc/signalinjector.py +7 -18
  44. ezmsg/sigproc/slicer.py +14 -34
  45. ezmsg/sigproc/spectral.py +3 -3
  46. ezmsg/sigproc/spectrogram.py +12 -19
  47. ezmsg/sigproc/spectrum.py +17 -38
  48. ezmsg/sigproc/transpose.py +12 -24
  49. ezmsg/sigproc/util/asio.py +25 -156
  50. ezmsg/sigproc/util/axisarray_buffer.py +12 -26
  51. ezmsg/sigproc/util/buffer.py +22 -43
  52. ezmsg/sigproc/util/message.py +17 -31
  53. ezmsg/sigproc/util/profile.py +23 -174
  54. ezmsg/sigproc/util/sparse.py +7 -15
  55. ezmsg/sigproc/util/typeresolution.py +17 -83
  56. ezmsg/sigproc/wavelets.py +10 -19
  57. ezmsg/sigproc/window.py +29 -83
  58. ezmsg_sigproc-2.7.0.dist-info/METADATA +60 -0
  59. ezmsg_sigproc-2.7.0.dist-info/RECORD +64 -0
  60. ezmsg/sigproc/synth.py +0 -774
  61. ezmsg_sigproc-2.5.0.dist-info/METADATA +0 -72
  62. ezmsg_sigproc-2.5.0.dist-info/RECORD +0 -63
  63. {ezmsg_sigproc-2.5.0.dist-info → ezmsg_sigproc-2.7.0.dist-info}/WHEEL +0 -0
  64. /ezmsg_sigproc-2.5.0.dist-info/licenses/LICENSE.txt → /ezmsg_sigproc-2.7.0.dist-info/licenses/LICENSE +0 -0
@@ -1,20 +1,21 @@
1
1
  from typing import Generator
2
+
2
3
  import ezmsg.core as ez
4
+ from ezmsg.baseproc import (
5
+ BaseStatefulProcessor,
6
+ BaseTransformerUnit,
7
+ CompositeProcessor,
8
+ )
3
9
  from ezmsg.util.messages.axisarray import AxisArray
4
10
  from ezmsg.util.messages.modify import modify_axis
5
11
 
6
- from .window import Anchor, WindowTransformer
7
12
  from .spectrum import (
8
- WindowFunction,
9
- SpectralTransform,
10
13
  SpectralOutput,
14
+ SpectralTransform,
11
15
  SpectrumTransformer,
16
+ WindowFunction,
12
17
  )
13
- from .base import (
14
- CompositeProcessor,
15
- BaseStatefulProcessor,
16
- BaseTransformerUnit,
17
- )
18
+ from .window import Anchor, WindowTransformer
18
19
 
19
20
 
20
21
  class SpectrogramSettings(ez.Settings):
@@ -41,9 +42,7 @@ class SpectrogramSettings(ez.Settings):
41
42
  """The :obj:`SpectralOutput` format."""
42
43
 
43
44
 
44
- class SpectrogramTransformer(
45
- CompositeProcessor[SpectrogramSettings, AxisArray, AxisArray]
46
- ):
45
+ class SpectrogramTransformer(CompositeProcessor[SpectrogramSettings, AxisArray, AxisArray]):
47
46
  @staticmethod
48
47
  def _initialize_processors(
49
48
  settings: SpectrogramSettings,
@@ -54,9 +53,7 @@ class SpectrogramTransformer(
54
53
  newaxis="win",
55
54
  window_dur=settings.window_dur,
56
55
  window_shift=settings.window_shift,
57
- zero_pad_until="shift"
58
- if settings.window_shift is not None
59
- else "input",
56
+ zero_pad_until="shift" if settings.window_shift is not None else "input",
60
57
  anchor=settings.window_anchor,
61
58
  ),
62
59
  "spectrum": SpectrumTransformer(
@@ -69,11 +66,7 @@ class SpectrogramTransformer(
69
66
  }
70
67
 
71
68
 
72
- class Spectrogram(
73
- BaseTransformerUnit[
74
- SpectrogramSettings, AxisArray, AxisArray, SpectrogramTransformer
75
- ]
76
- ):
69
+ class Spectrogram(BaseTransformerUnit[SpectrogramSettings, AxisArray, AxisArray, SpectrogramTransformer]):
77
70
  SETTINGS = SpectrogramSettings
78
71
 
79
72
 
ezmsg/sigproc/spectrum.py CHANGED
@@ -1,21 +1,20 @@
1
1
  import enum
2
- from functools import partial
3
2
  import typing
3
+ from functools import partial
4
4
 
5
+ import ezmsg.core as ez
5
6
  import numpy as np
6
7
  import numpy.typing as npt
7
- import ezmsg.core as ez
8
- from ezmsg.util.messages.axisarray import (
9
- AxisArray,
10
- slice_along_axis,
11
- replace,
12
- )
13
-
14
- from .base import (
8
+ from ezmsg.baseproc import (
15
9
  BaseStatefulTransformer,
16
10
  BaseTransformerUnit,
17
11
  processor_state,
18
12
  )
13
+ from ezmsg.util.messages.axisarray import (
14
+ AxisArray,
15
+ replace,
16
+ slice_along_axis,
17
+ )
19
18
 
20
19
 
21
20
  class OptionsEnum(enum.Enum):
@@ -127,17 +126,13 @@ class SpectrumState:
127
126
  window: npt.NDArray | None = None
128
127
 
129
128
 
130
- class SpectrumTransformer(
131
- BaseStatefulTransformer[SpectrumSettings, AxisArray, AxisArray, SpectrumState]
132
- ):
129
+ class SpectrumTransformer(BaseStatefulTransformer[SpectrumSettings, AxisArray, AxisArray, SpectrumState]):
133
130
  def _hash_message(self, message: AxisArray) -> int:
134
131
  axis = self.settings.axis or message.dims[0]
135
132
  ax_idx = message.get_axis_idx(axis)
136
133
  ax_info = message.axes[axis]
137
134
  targ_len = message.data.shape[ax_idx]
138
- return hash(
139
- (targ_len, message.data.ndim, message.data.dtype.kind, ax_idx, ax_info.gain)
140
- )
135
+ return hash((targ_len, message.data.ndim, message.data.dtype.kind, ax_idx, ax_info.gain))
141
136
 
142
137
  def _reset_state(self, message: AxisArray) -> None:
143
138
  axis = self.settings.axis or message.dims[0]
@@ -156,8 +151,7 @@ class SpectrumTransformer(
156
151
  + [1] * (message.data.ndim - 1 - ax_idx)
157
152
  )
158
153
  if self.settings.transform != SpectralTransform.RAW_COMPLEX and not (
159
- self.settings.transform == SpectralTransform.REAL
160
- or self.settings.transform == SpectralTransform.IMAG
154
+ self.settings.transform == SpectralTransform.REAL or self.settings.transform == SpectralTransform.IMAG
161
155
  ):
162
156
  scale = np.sum(window**2.0) * ax_info.gain
163
157
 
@@ -170,30 +164,21 @@ class SpectrumTransformer(
170
164
  if (not b_complex) and self.settings.output == SpectralOutput.POSITIVE:
171
165
  # If input is not complex and desired output is SpectralOutput.POSITIVE, we can save some computation
172
166
  # by using rfft and rfftfreq.
173
- self.state.fftfun = partial(
174
- np.fft.rfft, n=nfft, axis=ax_idx, norm=self.settings.norm
175
- )
167
+ self.state.fftfun = partial(np.fft.rfft, n=nfft, axis=ax_idx, norm=self.settings.norm)
176
168
  freqs = np.fft.rfftfreq(nfft, d=ax_info.gain * targ_len / nfft)
177
169
  else:
178
- self.state.fftfun = partial(
179
- np.fft.fft, n=nfft, axis=ax_idx, norm=self.settings.norm
180
- )
170
+ self.state.fftfun = partial(np.fft.fft, n=nfft, axis=ax_idx, norm=self.settings.norm)
181
171
  freqs = np.fft.fftfreq(nfft, d=ax_info.gain * targ_len / nfft)
182
172
  if self.settings.output == SpectralOutput.POSITIVE:
183
173
  self.state.f_sl = slice(None, nfft // 2 + 1 - (nfft % 2))
184
174
  elif self.settings.output == SpectralOutput.NEGATIVE:
185
175
  freqs = np.fft.fftshift(freqs, axes=-1)
186
176
  self.state.f_sl = slice(None, nfft // 2 + 1)
187
- elif (
188
- self.settings.do_fftshift
189
- and self.settings.output == SpectralOutput.FULL
190
- ):
177
+ elif self.settings.do_fftshift and self.settings.output == SpectralOutput.FULL:
191
178
  freqs = np.fft.fftshift(freqs, axes=-1)
192
179
  freqs = freqs[self.state.f_sl]
193
180
  freqs = freqs.tolist() # To please type checking
194
- self.state.freq_axis = AxisArray.LinearAxis(
195
- unit="Hz", gain=freqs[1] - freqs[0], offset=freqs[0]
196
- )
181
+ self.state.freq_axis = AxisArray.LinearAxis(unit="Hz", gain=freqs[1] - freqs[0], offset=freqs[0])
197
182
  self.state.new_dims = (
198
183
  message.dims[:ax_idx]
199
184
  + [
@@ -232,11 +217,7 @@ class SpectrumTransformer(
232
217
  ax_idx = message.get_axis_idx(axis)
233
218
  targ_len = message.data.shape[ax_idx]
234
219
 
235
- new_axes = {
236
- k: v
237
- for k, v in message.axes.items()
238
- if k not in [self.settings.out_axis, axis]
239
- }
220
+ new_axes = {k: v for k, v in message.axes.items() if k not in [self.settings.out_axis, axis]}
240
221
  new_axes[self.settings.out_axis or axis] = self.state.freq_axis
241
222
 
242
223
  if self.state.window is not None:
@@ -261,9 +242,7 @@ class SpectrumTransformer(
261
242
  return msg_out
262
243
 
263
244
 
264
- class Spectrum(
265
- BaseTransformerUnit[SpectrumSettings, AxisArray, AxisArray, SpectrumTransformer]
266
- ):
245
+ class Spectrum(BaseTransformerUnit[SpectrumSettings, AxisArray, AxisArray, SpectrumTransformer]):
267
246
  SETTINGS = SpectrumSettings
268
247
 
269
248
 
@@ -1,16 +1,16 @@
1
1
  from types import EllipsisType
2
- import numpy as np
3
- import ezmsg.core as ez
4
- from ezmsg.util.messages.axisarray import (
5
- AxisArray,
6
- replace,
7
- )
8
2
 
9
- from .base import (
3
+ import ezmsg.core as ez
4
+ import numpy as np
5
+ from ezmsg.baseproc import (
10
6
  BaseStatefulTransformer,
11
7
  BaseTransformerUnit,
12
8
  processor_state,
13
9
  )
10
+ from ezmsg.util.messages.axisarray import (
11
+ AxisArray,
12
+ replace,
13
+ )
14
14
 
15
15
 
16
16
  class TransposeSettings(ez.Settings):
@@ -30,9 +30,7 @@ class TransposeState:
30
30
  axes_ints: tuple[int, ...] | None = None
31
31
 
32
32
 
33
- class TransposeTransformer(
34
- BaseStatefulTransformer[TransposeSettings, AxisArray, AxisArray, TransposeState]
35
- ):
33
+ class TransposeTransformer(BaseStatefulTransformer[TransposeSettings, AxisArray, AxisArray, TransposeState]):
36
34
  """
37
35
  Downsampled data simply comprise every `factor`th sample.
38
36
  This should only be used following appropriate lowpass filtering.
@@ -67,11 +65,7 @@ class TransposeTransformer(
67
65
  if ax not in message.dims:
68
66
  raise ValueError(f"Axis {ax} not found in message dims.")
69
67
  suffix.append(message.dims.index(ax))
70
- ells = [
71
- _
72
- for _ in range(message.data.ndim)
73
- if _ not in prefix and _ not in suffix
74
- ]
68
+ ells = [_ for _ in range(message.data.ndim) if _ not in prefix and _ not in suffix]
75
69
  re_ix = tuple(prefix + ells + suffix)
76
70
  if re_ix == tuple(range(message.data.ndim)):
77
71
  self._state.axes_ints = None
@@ -100,17 +94,13 @@ class TransposeTransformer(
100
94
  # If the memory is already contiguous in the correct order, np.require won't do anything.
101
95
  msg_out = replace(
102
96
  message,
103
- data=np.require(
104
- message.data, requirements=self.settings.order.upper()[0]
105
- ),
97
+ data=np.require(message.data, requirements=self.settings.order.upper()[0]),
106
98
  )
107
99
  else:
108
100
  dims_out = [message.dims[ix] for ix in self.state.axes_ints]
109
101
  data_out = np.transpose(message.data, axes=self.state.axes_ints)
110
102
  if self.settings.order is not None:
111
- data_out = np.require(
112
- data_out, requirements=self.settings.order.upper()[0]
113
- )
103
+ data_out = np.require(data_out, requirements=self.settings.order.upper()[0])
114
104
  msg_out = replace(
115
105
  message,
116
106
  data=data_out,
@@ -119,9 +109,7 @@ class TransposeTransformer(
119
109
  return msg_out
120
110
 
121
111
 
122
- class Transpose(
123
- BaseTransformerUnit[TransposeSettings, AxisArray, AxisArray, TransposeTransformer]
124
- ):
112
+ class Transpose(BaseTransformerUnit[TransposeSettings, AxisArray, AxisArray, TransposeTransformer]):
125
113
  SETTINGS = TransposeSettings
126
114
 
127
115
 
@@ -1,156 +1,25 @@
1
- import asyncio
2
- from concurrent.futures import ThreadPoolExecutor
3
- import contextlib
4
- import inspect
5
- import threading
6
- from typing import Any, Coroutine, TypeVar
7
-
8
- T = TypeVar("T")
9
-
10
-
11
- class CoroutineExecutionError(Exception):
12
- """Custom exception for coroutine execution failures"""
13
-
14
- pass
15
-
16
-
17
- def run_coroutine_sync(coroutine: Coroutine[Any, Any, T], timeout: float = 30) -> T:
18
- """
19
- Executes an asyncio coroutine synchronously, with enhanced error handling.
20
-
21
- Args:
22
- coroutine: The asyncio coroutine to execute
23
- timeout: Maximum time in seconds to wait for coroutine completion (default: 30)
24
-
25
- Returns:
26
- The result of the coroutine execution
27
-
28
- Raises:
29
- CoroutineExecutionError: If execution fails due to threading or event loop issues
30
- TimeoutError: If execution exceeds the timeout period
31
- Exception: Any exception raised by the coroutine
32
- """
33
-
34
- def run_in_new_loop() -> T:
35
- """
36
- Creates and runs a new event loop in the current thread.
37
- Ensures proper cleanup of the loop.
38
- """
39
- new_loop = asyncio.new_event_loop()
40
- asyncio.set_event_loop(new_loop)
41
- try:
42
- return new_loop.run_until_complete(
43
- asyncio.wait_for(coroutine, timeout=timeout)
44
- )
45
- finally:
46
- with contextlib.suppress(Exception):
47
- # Clean up any pending tasks
48
- pending = asyncio.all_tasks(new_loop)
49
- for task in pending:
50
- task.cancel()
51
- new_loop.run_until_complete(
52
- asyncio.gather(*pending, return_exceptions=True)
53
- )
54
- new_loop.close()
55
-
56
- try:
57
- loop = asyncio.get_running_loop()
58
- except RuntimeError:
59
- try:
60
- return asyncio.run(asyncio.wait_for(coroutine, timeout=timeout))
61
- except Exception as e:
62
- raise CoroutineExecutionError(
63
- f"Failed to execute coroutine: {str(e)}"
64
- ) from e
65
-
66
- if threading.current_thread() is threading.main_thread():
67
- if not loop.is_running():
68
- try:
69
- return loop.run_until_complete(
70
- asyncio.wait_for(coroutine, timeout=timeout)
71
- )
72
- except Exception as e:
73
- raise CoroutineExecutionError(
74
- f"Failed to execute coroutine in main loop: {str(e)}"
75
- ) from e
76
- else:
77
- with ThreadPoolExecutor() as pool:
78
- try:
79
- future = pool.submit(run_in_new_loop)
80
- return future.result(timeout=timeout)
81
- except Exception as e:
82
- raise CoroutineExecutionError(
83
- f"Failed to execute coroutine in thread: {str(e)}"
84
- ) from e
85
- else:
86
- try:
87
- future = asyncio.run_coroutine_threadsafe(coroutine, loop)
88
- return future.result(timeout=timeout)
89
- except Exception as e:
90
- raise CoroutineExecutionError(
91
- f"Failed to execute coroutine threadsafe: {str(e)}"
92
- ) from e
93
-
94
-
95
- class SyncToAsyncGeneratorWrapper:
96
- """
97
- A wrapper for synchronous generators to be used in an async context.
98
- """
99
-
100
- def __init__(self, gen):
101
- self._gen = gen
102
- self._closed = False
103
- # Prime the generator to ready for first send/next call
104
- try:
105
- is_not_primed = inspect.getgeneratorstate(self._gen) is inspect.GEN_CREATED
106
- except AttributeError as e:
107
- raise TypeError(
108
- "The provided generator is not a valid generator object"
109
- ) from e
110
- if is_not_primed:
111
- try:
112
- next(self._gen)
113
- except StopIteration:
114
- self._closed = True
115
- except Exception as e:
116
- raise RuntimeError(f"Failed to prime generator: {e}") from e
117
-
118
- async def asend(self, value):
119
- if self._closed:
120
- raise StopAsyncIteration("Generator is closed")
121
- try:
122
- return await asyncio.to_thread(self._gen.send, value)
123
- except StopIteration as e:
124
- self._closed = True
125
- raise StopAsyncIteration("Generator is closed") from e
126
- except Exception as e:
127
- raise RuntimeError(f"Error while sending value to generator: {e}") from e
128
-
129
- async def __anext__(self):
130
- if self._closed:
131
- raise StopAsyncIteration("Generator is closed")
132
- try:
133
- return await asyncio.to_thread(self._gen.__next__)
134
- except StopIteration as e:
135
- self._closed = True
136
- raise StopAsyncIteration("Generator is closed") from e
137
- except Exception as e:
138
- raise RuntimeError(
139
- f"Error while getting next value from generator: {e}"
140
- ) from e
141
-
142
- async def aclose(self):
143
- if self._closed:
144
- return
145
- try:
146
- await asyncio.to_thread(self._gen.close)
147
- except Exception as e:
148
- raise RuntimeError(f"Error while closing generator: {e}") from e
149
- finally:
150
- self._closed = True
151
-
152
- def __aiter__(self):
153
- return self
154
-
155
- def __getattr__(self, name):
156
- return getattr(self._gen, name)
1
+ """
2
+ Backwards-compatible re-exports from ezmsg.baseproc.util.asio.
3
+
4
+ New code should import directly from ezmsg.baseproc instead.
5
+ """
6
+
7
+ import warnings
8
+
9
+ warnings.warn(
10
+ "Importing from 'ezmsg.sigproc.util.asio' is deprecated. Please import from 'ezmsg.baseproc.util.asio' instead.",
11
+ DeprecationWarning,
12
+ stacklevel=2,
13
+ )
14
+
15
+ from ezmsg.baseproc.util.asio import ( # noqa: E402
16
+ CoroutineExecutionError,
17
+ SyncToAsyncGeneratorWrapper,
18
+ run_coroutine_sync,
19
+ )
20
+
21
+ __all__ = [
22
+ "CoroutineExecutionError",
23
+ "SyncToAsyncGeneratorWrapper",
24
+ "run_coroutine_sync",
25
+ ]
@@ -1,14 +1,15 @@
1
+ """AxisArray support for .buffer.HybridBuffer."""
2
+
1
3
  import math
2
4
  import typing
3
5
 
4
- from array_api_compat import get_namespace
5
6
  import numpy as np
6
- from ezmsg.util.messages.axisarray import AxisArray, LinearAxis, CoordinateAxis
7
+ from array_api_compat import get_namespace
8
+ from ezmsg.util.messages.axisarray import AxisArray, CoordinateAxis, LinearAxis
7
9
  from ezmsg.util.messages.util import replace
8
10
 
9
11
  from .buffer import HybridBuffer
10
12
 
11
-
12
13
  Array = typing.TypeVar("Array")
13
14
 
14
15
 
@@ -68,9 +69,7 @@ class HybridAxisBuffer:
68
69
  if hasattr(first_axis, "data"):
69
70
  # Initialize a CoordinateAxis buffer
70
71
  if len(first_axis.data) > 1:
71
- _axis_gain = (first_axis.data[-1] - first_axis.data[0]) / (
72
- len(first_axis.data) - 1
73
- )
72
+ _axis_gain = (first_axis.data[-1] - first_axis.data[0]) / (len(first_axis.data) - 1)
74
73
  else:
75
74
  _axis_gain = 1.0
76
75
  self._coords_gain_estimate = _axis_gain
@@ -107,8 +106,7 @@ class HybridAxisBuffer:
107
106
  )
108
107
  if axis.gain != self._linear_axis.gain:
109
108
  raise ValueError(
110
- f"Buffer initialized with gain={self._linear_axis.gain}, "
111
- f"but received gain={axis.gain}."
109
+ f"Buffer initialized with gain={self._linear_axis.gain}, but received gain={axis.gain}."
112
110
  )
113
111
  if self._linear_n_available + n_samples > self.capacity:
114
112
  # Simulate overflow by advancing the offset and decreasing
@@ -117,16 +115,12 @@ class HybridAxisBuffer:
117
115
  self.seek(n_to_discard)
118
116
  # Update the offset corresponding to the oldest sample in the buffer
119
117
  # by anchoring on the new offset and accounting for the samples already available.
120
- self._linear_axis.offset = (
121
- axis.offset - self._linear_n_available * axis.gain
122
- )
118
+ self._linear_axis.offset = axis.offset - self._linear_n_available * axis.gain
123
119
  self._linear_n_available += n_samples
124
120
 
125
121
  def peek(self, n_samples: int | None = None) -> LinearAxis | CoordinateAxis:
126
122
  if self._coords_buffer is not None:
127
- return replace(
128
- self._coords_template, data=self._coords_buffer.peek(n_samples)
129
- )
123
+ return replace(self._coords_template, data=self._coords_buffer.peek(n_samples))
130
124
  else:
131
125
  # Return a shallow copy.
132
126
  return replace(self._linear_axis, offset=self._linear_axis.offset)
@@ -184,13 +178,9 @@ class HybridAxisBuffer:
184
178
  else:
185
179
  return None
186
180
 
187
- def searchsorted(
188
- self, values: typing.Union[float, Array], side: str = "left"
189
- ) -> typing.Union[int, Array]:
181
+ def searchsorted(self, values: typing.Union[float, Array], side: str = "left") -> typing.Union[int, Array]:
190
182
  if self._coords_buffer is not None:
191
- return self._coords_buffer.xp.searchsorted(
192
- self._coords_buffer.peek(self.available()), values, side=side
193
- )
183
+ return self._coords_buffer.xp.searchsorted(self._coords_buffer.peek(self.available()), values, side=side)
194
184
  else:
195
185
  if self.available() == 0:
196
186
  if isinstance(values, float):
@@ -312,9 +302,7 @@ class HybridAxisArrayBuffer:
312
302
  axes={**self._template_msg.axes, self._axis: out_axis},
313
303
  )
314
304
 
315
- def peek_axis(
316
- self, n_samples: int | None = None
317
- ) -> LinearAxis | CoordinateAxis | None:
305
+ def peek_axis(self, n_samples: int | None = None) -> LinearAxis | CoordinateAxis | None:
318
306
  """Retrieves the axis data without advancing the read head."""
319
307
  if self._data_buffer is None:
320
308
  return None
@@ -369,9 +357,7 @@ class HybridAxisArrayBuffer:
369
357
  """
370
358
  return self._axis_buffer.gain
371
359
 
372
- def axis_searchsorted(
373
- self, values: typing.Union[float, Array], side: str = "left"
374
- ) -> typing.Union[int, Array]:
360
+ def axis_searchsorted(self, values: typing.Union[float, Array], side: str = "left") -> typing.Union[int, Array]:
375
361
  """
376
362
  Find the indices into which the given values would be inserted
377
363
  into the target axis data to maintain order.