ezmsg-sigproc 1.7.0__tar.gz → 1.7.1__tar.gz

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 (67) hide show
  1. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/PKG-INFO +5 -3
  2. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/pyproject.toml +1 -0
  3. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/src/ezmsg/sigproc/__version__.py +9 -4
  4. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/src/ezmsg/sigproc/bandpower.py +1 -0
  5. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/src/ezmsg/sigproc/base.py +3 -0
  6. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/src/ezmsg/sigproc/filterbank.py +2 -1
  7. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/src/ezmsg/sigproc/sampler.py +3 -0
  8. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/src/ezmsg/sigproc/signalinjector.py +3 -0
  9. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/src/ezmsg/sigproc/spectrogram.py +6 -1
  10. ezmsg_sigproc-1.7.1/src/ezmsg/sigproc/util/profile.py +131 -0
  11. ezmsg_sigproc-1.7.1/src/ezmsg/sigproc/util/sparse.py +29 -0
  12. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/src/ezmsg/sigproc/wavelets.py +4 -3
  13. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/src/ezmsg/sigproc/window.py +57 -25
  14. ezmsg_sigproc-1.7.1/tests/helpers/__init__.py +0 -0
  15. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/tests/test_window.py +69 -0
  16. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/uv.lock +67 -1
  17. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/.github/workflows/python-publish-ezmsg-sigproc.yml +0 -0
  18. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/.github/workflows/python-tests.yml +0 -0
  19. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/.gitignore +0 -0
  20. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/.pre-commit-config.yaml +0 -0
  21. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/LICENSE.txt +0 -0
  22. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/README.md +0 -0
  23. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/src/ezmsg/sigproc/__init__.py +0 -0
  24. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/src/ezmsg/sigproc/activation.py +0 -0
  25. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/src/ezmsg/sigproc/affinetransform.py +0 -0
  26. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/src/ezmsg/sigproc/aggregate.py +0 -0
  27. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/src/ezmsg/sigproc/butterworthfilter.py +0 -0
  28. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/src/ezmsg/sigproc/cheby.py +0 -0
  29. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/src/ezmsg/sigproc/decimate.py +0 -0
  30. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/src/ezmsg/sigproc/downsample.py +0 -0
  31. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/src/ezmsg/sigproc/ewmfilter.py +0 -0
  32. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/src/ezmsg/sigproc/filter.py +0 -0
  33. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/src/ezmsg/sigproc/math/__init__.py +0 -0
  34. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/src/ezmsg/sigproc/math/abs.py +0 -0
  35. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/src/ezmsg/sigproc/math/clip.py +0 -0
  36. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/src/ezmsg/sigproc/math/difference.py +0 -0
  37. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/src/ezmsg/sigproc/math/invert.py +0 -0
  38. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/src/ezmsg/sigproc/math/log.py +0 -0
  39. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/src/ezmsg/sigproc/math/scale.py +0 -0
  40. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/src/ezmsg/sigproc/messages.py +0 -0
  41. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/src/ezmsg/sigproc/scaler.py +0 -0
  42. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/src/ezmsg/sigproc/slicer.py +0 -0
  43. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/src/ezmsg/sigproc/spectral.py +0 -0
  44. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/src/ezmsg/sigproc/spectrum.py +0 -0
  45. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/src/ezmsg/sigproc/synth.py +0 -0
  46. {ezmsg_sigproc-1.7.0/tests/helpers → ezmsg_sigproc-1.7.1/src/ezmsg/sigproc/util}/__init__.py +0 -0
  47. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/tests/conftest.py +0 -0
  48. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/tests/helpers/util.py +0 -0
  49. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/tests/resources/xform.csv +0 -0
  50. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/tests/test_activation.py +0 -0
  51. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/tests/test_affine_transform.py +0 -0
  52. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/tests/test_aggregate.py +0 -0
  53. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/tests/test_bandpower.py +0 -0
  54. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/tests/test_butter.py +0 -0
  55. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/tests/test_butterworth.py +0 -0
  56. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/tests/test_decimate.py +0 -0
  57. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/tests/test_downsample.py +0 -0
  58. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/tests/test_filter_system.py +0 -0
  59. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/tests/test_filterbank.py +0 -0
  60. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/tests/test_math.py +0 -0
  61. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/tests/test_sampler.py +0 -0
  62. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/tests/test_scaler.py +0 -0
  63. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/tests/test_slicer.py +0 -0
  64. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/tests/test_spectrogram.py +0 -0
  65. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/tests/test_spectrum.py +0 -0
  66. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/tests/test_synth.py +0 -0
  67. {ezmsg_sigproc-1.7.0 → ezmsg_sigproc-1.7.1}/tests/test_wavelets.py +0 -0
@@ -1,14 +1,16 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: ezmsg-sigproc
3
- Version: 1.7.0
3
+ Version: 1.7.1
4
4
  Summary: Timeseries signal processing implementations in ezmsg
5
5
  Author-email: Griffin Milsap <griffin.milsap@gmail.com>, Preston Peranich <pperanich@gmail.com>, Chadwick Boulay <chadwick.boulay@gmail.com>
6
- License: MIT
6
+ License-Expression: MIT
7
+ License-File: LICENSE.txt
7
8
  Requires-Python: >=3.10.15
8
9
  Requires-Dist: ezmsg>=3.6.0
9
10
  Requires-Dist: numpy>=1.26.0
10
11
  Requires-Dist: pywavelets>=1.6.0
11
12
  Requires-Dist: scipy>=1.13.1
13
+ Requires-Dist: sparse>=0.15.4
12
14
  Provides-Extra: test
13
15
  Requires-Dist: flake8>=7.1.1; extra == 'test'
14
16
  Requires-Dist: frozendict>=2.4.4; extra == 'test'
@@ -15,6 +15,7 @@ dependencies = [
15
15
  "numpy>=1.26.0",
16
16
  "pywavelets>=1.6.0",
17
17
  "scipy>=1.13.1",
18
+ "sparse>=0.15.4",
18
19
  ]
19
20
 
20
21
  [project.optional-dependencies]
@@ -1,8 +1,13 @@
1
- # file generated by setuptools_scm
1
+ # file generated by setuptools-scm
2
2
  # don't change, don't track in version control
3
+
4
+ __all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
5
+
3
6
  TYPE_CHECKING = False
4
7
  if TYPE_CHECKING:
5
- from typing import Tuple, Union
8
+ from typing import Tuple
9
+ from typing import Union
10
+
6
11
  VERSION_TUPLE = Tuple[Union[int, str], ...]
7
12
  else:
8
13
  VERSION_TUPLE = object
@@ -12,5 +17,5 @@ __version__: str
12
17
  __version_tuple__: VERSION_TUPLE
13
18
  version_tuple: VERSION_TUPLE
14
19
 
15
- __version__ = version = '1.7.0'
16
- __version_tuple__ = version_tuple = (1, 7, 0)
20
+ __version__ = version = '1.7.1'
21
+ __version_tuple__ = version_tuple = (1, 7, 1)
@@ -35,6 +35,7 @@ def bandpower(
35
35
  f_spec = spectrogram(
36
36
  window_dur=spectrogram_settings.window_dur,
37
37
  window_shift=spectrogram_settings.window_shift,
38
+ window_anchor=spectrogram_settings.window_anchor,
38
39
  window=spectrogram_settings.window,
39
40
  transform=spectrogram_settings.transform,
40
41
  output=spectrogram_settings.output,
@@ -6,6 +6,8 @@ import ezmsg.core as ez
6
6
  from ezmsg.util.messages.axisarray import AxisArray
7
7
  from ezmsg.util.generator import GenState
8
8
 
9
+ from .util.profile import profile_subpub
10
+
9
11
 
10
12
  class GenAxisArray(ez.Unit):
11
13
  STATE = GenState
@@ -28,6 +30,7 @@ class GenAxisArray(ez.Unit):
28
30
 
29
31
  @ez.subscriber(INPUT_SIGNAL, zero_copy=True)
30
32
  @ez.publisher(OUTPUT_SIGNAL)
33
+ @profile_subpub(trace_oldest=False)
31
34
  async def on_signal(self, message: AxisArray) -> typing.AsyncGenerator:
32
35
  try:
33
36
  ret = self.STATE.gen.send(message)
@@ -214,7 +214,8 @@ def filterbank(
214
214
  pad = np.zeros(dest_arr.shape[:-1] + (n_dest - dest_arr.shape[-1],))
215
215
  dest_arr = np.concatenate(dest_arr, pad, axis=-1)
216
216
  dest_arr.fill(0)
217
- # TODO: Parallelize this loop.
217
+ # Note: I tried several alternatives to this loop; all were slower than this.
218
+ # numba.jit; stride_tricks + np.einsum; threading. Latter might be better with Python 3.13.
218
219
  for k_ix, k in enumerate(kernels):
219
220
  n_out = in_dat.shape[-1] + k.shape[-1] - 1
220
221
  dest_arr[..., k_ix, :n_out] = np.apply_along_axis(
@@ -14,6 +14,8 @@ from ezmsg.util.messages.axisarray import (
14
14
  from ezmsg.util.messages.util import replace
15
15
  from ezmsg.util.generator import consumer
16
16
 
17
+ from .util.profile import profile_subpub
18
+
17
19
 
18
20
  @dataclass(unsafe_hash=True)
19
21
  class SampleTriggerMessage:
@@ -284,6 +286,7 @@ class Sampler(ez.Unit):
284
286
 
285
287
  @ez.subscriber(INPUT_SIGNAL, zero_copy=True)
286
288
  @ez.publisher(OUTPUT_SAMPLE)
289
+ @profile_subpub(trace_oldest=False)
287
290
  async def on_signal(self, msg: AxisArray) -> typing.AsyncGenerator:
288
291
  pub_samples = self.STATE.gen.send(msg)
289
292
  for sample in pub_samples:
@@ -6,6 +6,8 @@ from ezmsg.util.messages.util import replace
6
6
  import numpy as np
7
7
  import numpy.typing as npt
8
8
 
9
+ from .util.profile import profile_subpub
10
+
9
11
 
10
12
  class SignalInjectorSettings(ez.Settings):
11
13
  time_dim: str = "time" # Input signal needs a time dimension with units in sec.
@@ -50,6 +52,7 @@ class SignalInjector(ez.Unit):
50
52
 
51
53
  @ez.subscriber(INPUT_SIGNAL)
52
54
  @ez.publisher(OUTPUT_SIGNAL)
55
+ @profile_subpub(trace_oldest=False)
53
56
  async def inject(self, msg: AxisArray) -> typing.AsyncGenerator:
54
57
  if self.STATE.cur_shape != msg.shape:
55
58
  self.STATE.cur_shape = msg.shape
@@ -5,7 +5,7 @@ from ezmsg.util.messages.axisarray import AxisArray
5
5
  from ezmsg.util.generator import consumer, compose
6
6
  from ezmsg.util.messages.modify import modify_axis
7
7
 
8
- from .window import windowing
8
+ from .window import windowing, Anchor
9
9
  from .spectrum import spectrum, WindowFunction, SpectralTransform, SpectralOutput
10
10
  from .base import GenAxisArray
11
11
 
@@ -14,6 +14,7 @@ from .base import GenAxisArray
14
14
  def spectrogram(
15
15
  window_dur: float | None = None,
16
16
  window_shift: float | None = None,
17
+ window_anchor: str | Anchor = Anchor.BEGINNING,
17
18
  window: WindowFunction = WindowFunction.HANNING,
18
19
  transform: SpectralTransform = SpectralTransform.REL_DB,
19
20
  output: SpectralOutput = SpectralOutput.POSITIVE,
@@ -28,6 +29,7 @@ def spectrogram(
28
29
  Args:
29
30
  window_dur: See :obj:`ezmsg.sigproc.window.windowing`
30
31
  window_shift: See :obj:`ezmsg.sigproc.window.windowing`
32
+ window_anchor: See :obj:`ezmsg.sigproc.window.windowing`
31
33
  window: See :obj:`ezmsg.sigproc.spectrum.spectrum`
32
34
  transform: See :obj:`ezmsg.sigproc.spectrum.spectrum`
33
35
  output: See :obj:`ezmsg.sigproc.spectrum.spectrum`
@@ -44,6 +46,7 @@ def spectrogram(
44
46
  window_dur=window_dur,
45
47
  window_shift=window_shift,
46
48
  zero_pad_until="shift" if window_shift is not None else "input",
49
+ anchor=window_anchor,
47
50
  ),
48
51
  spectrum(axis="time", window=window, transform=transform, output=output),
49
52
  modify_axis(name_map={"win": "time"}),
@@ -66,6 +69,7 @@ class SpectrogramSettings(ez.Settings):
66
69
  window_dur: float | None = None # window duration in seconds
67
70
  window_shift: float | None = None
68
71
  """"window step in seconds. If None, window_shift == window_dur"""
72
+ window_anchor: str | Anchor = Anchor.BEGINNING
69
73
 
70
74
  # See SpectrumSettings for details of following settings:
71
75
  window: WindowFunction = WindowFunction.HAMMING
@@ -84,6 +88,7 @@ class Spectrogram(GenAxisArray):
84
88
  self.STATE.gen = spectrogram(
85
89
  window_dur=self.SETTINGS.window_dur,
86
90
  window_shift=self.SETTINGS.window_shift,
91
+ window_anchor=self.SETTINGS.window_anchor,
87
92
  window=self.SETTINGS.window,
88
93
  transform=self.SETTINGS.transform,
89
94
  output=self.SETTINGS.output,
@@ -0,0 +1,131 @@
1
+ import functools
2
+ import logging
3
+ import os
4
+ from pathlib import Path
5
+ import time
6
+ import typing
7
+
8
+ import ezmsg.core as ez
9
+
10
+
11
+ def get_logger_path() -> Path:
12
+ # Retrieve the logfile name from the environment variable
13
+ logfile = os.environ.get("EZMSG_PROFILE", None)
14
+
15
+ # Determine the log file path, defaulting to "ezprofiler.log" if not set
16
+ logpath = Path(logfile or "ezprofiler.log")
17
+
18
+ # If the log path is not absolute, prepend it with the user's home directory and ".ezmsg/profile"
19
+ if not logpath.is_absolute():
20
+ logpath = Path.home() / ".ezmsg" / "profile" / logpath
21
+
22
+ return logpath
23
+
24
+
25
+ def _setup_logger(append: bool = False) -> logging.Logger:
26
+ logpath = get_logger_path()
27
+ logpath.parent.mkdir(parents=True, exist_ok=True)
28
+
29
+ if not append:
30
+ # Remove the file if it exists
31
+ logpath.unlink(missing_ok=True)
32
+
33
+ # Create a logger with the name "ezprofile"
34
+ _logger = logging.getLogger("ezprofile")
35
+
36
+ # Set the logger's level to EZMSG_LOGLEVEL env var value if it exists, otherwise INFO
37
+ _logger.setLevel(os.environ.get("EZMSG_LOGLEVEL", "INFO").upper())
38
+
39
+ # Create a file handler to write log messages to the log file
40
+ fh = logging.FileHandler(logpath)
41
+ fh.setLevel(logging.DEBUG) # Set the file handler log level to DEBUG
42
+
43
+ # Add the file handler to the logger
44
+ _logger.addHandler(fh)
45
+
46
+ # Add the first row without formatting.
47
+ _logger.debug(",".join(["Time", "Source", "Topic", "SampleTime", "PerfCounter", "Elapsed"]))
48
+
49
+ # Set the log message format
50
+ formatter = logging.Formatter(
51
+ "%(asctime)s,%(message)s",
52
+ datefmt="%Y-%m-%dT%H:%M:%S%z"
53
+ )
54
+ fh.setFormatter(formatter)
55
+
56
+ return _logger
57
+
58
+
59
+ logger = _setup_logger(append=True)
60
+
61
+
62
+ def _process_obj(obj, trace_oldest: bool = True):
63
+ samp_time = None
64
+ if hasattr(obj, "axes") and ("time" in obj.axes or "win" in obj.axes):
65
+ axis = "win" if "win" in obj.axes else "time"
66
+ ax = obj.get_axis(axis)
67
+ len = obj.data.shape[obj.get_axis_idx(axis)]
68
+ if len > 0:
69
+ idx = 0 if trace_oldest else (len - 1)
70
+ if hasattr(ax, "data"):
71
+ samp_time = ax.data[idx]
72
+ else:
73
+ samp_time = ax.value(idx)
74
+ if ax == "win" and "time" in obj.axes:
75
+ if hasattr(obj.axes["time"], "data"):
76
+ samp_time += obj.axes["time"].data[idx]
77
+ else:
78
+ samp_time += obj.axes["time"].value(idx)
79
+ return samp_time
80
+
81
+
82
+ def profile_method(trace_oldest: bool = True):
83
+ """
84
+ Decorator to profile a method by logging its execution time and other details.
85
+
86
+ Args:
87
+ trace_oldest (bool): If True, trace the oldest sample time; otherwise, trace the newest.
88
+
89
+ Returns:
90
+ Callable: The decorated function with profiling.
91
+ """
92
+ def profiling_decorator(func: typing.Callable):
93
+ @functools.wraps(func)
94
+ def wrapped_func(caller, *args, **kwargs):
95
+ start = time.perf_counter()
96
+ res = func(caller, *args, **kwargs)
97
+ stop = time.perf_counter()
98
+ source = '.'.join((caller.__class__.__module__, caller.__class__.__name__))
99
+ topic = f"{caller.address}"
100
+ samp_time = _process_obj(res, trace_oldest=trace_oldest)
101
+ logger.debug(",".join([source, topic, f"{samp_time}", f"{stop}", f"{(stop - start) * 1e3:0.4f}"]))
102
+ return res
103
+ return wrapped_func if logger.level == logging.DEBUG else func
104
+ return profiling_decorator
105
+
106
+
107
+ def profile_subpub(trace_oldest: bool = True):
108
+ """
109
+ Decorator to profile a subscriber-publisher method in an ezmsg Unit
110
+ by logging its execution time and other details.
111
+
112
+ Args:
113
+ trace_oldest (bool): If True, trace the oldest sample time; otherwise, trace the newest.
114
+
115
+ Returns:
116
+ Callable: The decorated async task with profiling.
117
+ """
118
+ def profiling_decorator(func: typing.Callable):
119
+ @functools.wraps(func)
120
+ async def wrapped_task(unit: ez.Unit, msg: typing.Any = None) -> None:
121
+ source = '.'.join((unit.__class__.__module__, unit.__class__.__name__))
122
+ topic = f"{unit.address}"
123
+ start = time.perf_counter()
124
+ async for stream, obj in func(unit, msg):
125
+ stop = time.perf_counter()
126
+ samp_time = _process_obj(obj, trace_oldest=trace_oldest)
127
+ logger.debug(",".join([source, topic, f"{samp_time}", f"{stop}", f"{(stop - start) * 1e3:0.4f}"]))
128
+ start = stop
129
+ yield stream, obj
130
+ return wrapped_task if logger.level == logging.DEBUG else func
131
+ return profiling_decorator
@@ -0,0 +1,29 @@
1
+ import sparse
2
+
3
+
4
+ def sliding_win_oneaxis(
5
+ s: sparse.SparseArray, nwin: int, axis: int, step: int = 1
6
+ ) -> sparse.SparseArray:
7
+ """
8
+ Like `ezmsg.util.messages.axisarray.sliding_win_oneaxis` but for sparse arrays.
9
+
10
+ Args:
11
+ s: The input sparse array.
12
+ nwin: The size of the sliding window.
13
+ axis: The axis along which the sliding window will be applied.
14
+ step: The size of the step between windows. If > 1, the strided window will be sliced with `slice_along_axis`.
15
+
16
+ Returns:
17
+
18
+ """
19
+ if -s.ndim <= axis < 0:
20
+ axis = s.ndim + axis
21
+ targ_slices = [slice(_, _ + nwin) for _ in range(0, s.shape[axis] - nwin + 1, step)]
22
+ s = s.reshape(s.shape[:axis] + (1,) + s.shape[axis:])
23
+ full_slices = (slice(None),) * s.ndim
24
+ full_slices = [
25
+ full_slices[: axis + 1] + (sl,) + full_slices[axis + 2 :] for sl in targ_slices
26
+ ]
27
+ result = sparse.concatenate([s[_] for _ in full_slices], axis=axis)
28
+ # TODO: Profile this approach vs modifying coords only.
29
+ return result
@@ -171,11 +171,11 @@ class CWTSettings(ez.Settings):
171
171
  Settings for :obj:`CWT`
172
172
  See :obj:`cwt` for argument details.
173
173
  """
174
-
175
- scales: list | tuple | npt.NDArray
174
+ frequencies: list | tuple | npt.NDArray | None
176
175
  wavelet: str | pywt.ContinuousWavelet | pywt.Wavelet
177
176
  min_phase: MinPhaseMode = MinPhaseMode.NONE
178
177
  axis: str = "time"
178
+ scales: list | tuple | npt.NDArray | None = None
179
179
 
180
180
 
181
181
  class CWT(GenAxisArray):
@@ -187,8 +187,9 @@ class CWT(GenAxisArray):
187
187
 
188
188
  def construct_generator(self):
189
189
  self.STATE.gen = cwt(
190
- scales=self.SETTINGS.scales,
190
+ frequencies=self.SETTINGS.frequencies,
191
191
  wavelet=self.SETTINGS.wavelet,
192
192
  min_phase=self.SETTINGS.min_phase,
193
193
  axis=self.SETTINGS.axis,
194
+ scales=self.SETTINGS.scales,
194
195
  )
@@ -1,9 +1,11 @@
1
+ import enum
1
2
  import traceback
2
3
  import typing
3
4
 
4
5
  import ezmsg.core as ez
5
6
  import numpy as np
6
7
  import numpy.typing as npt
8
+ import sparse
7
9
  from ezmsg.util.messages.axisarray import (
8
10
  AxisArray,
9
11
  slice_along_axis,
@@ -13,6 +15,13 @@ from ezmsg.util.messages.axisarray import (
13
15
  from ezmsg.util.generator import consumer
14
16
 
15
17
  from .base import GenAxisArray
18
+ from .util.sparse import sliding_win_oneaxis as sparse_sliding_win_oneaxis
19
+
20
+
21
+ class Anchor(enum.Enum):
22
+ BEGINNING = "beginning"
23
+ END = "end"
24
+ MIDDLE = "middle"
16
25
 
17
26
 
18
27
  @consumer
@@ -22,6 +31,7 @@ def windowing(
22
31
  window_dur: float | None = None,
23
32
  window_shift: float | None = None,
24
33
  zero_pad_until: str = "input",
34
+ anchor: str | Anchor = Anchor.BEGINNING,
25
35
  ) -> typing.Generator[AxisArray, AxisArray, None]:
26
36
  """
27
37
  Apply a sliding window along the specified axis to input streaming data.
@@ -49,6 +59,8 @@ def windowing(
49
59
  - "shift" fills the buffer until `window_shift`.
50
60
  No outputs will be yielded until at least `window_shift` data has been seen.
51
61
  - "none" does not pad the buffer. No outputs will be yielded until at least `window_dur` data has been seen.
62
+ anchor: Determines the entry in `axis` that gets assigned `0`, which references the
63
+ value in `newaxis`. Can be of class :obj:`Anchor` or a string representation of an :obj:`Anchor`.
52
64
 
53
65
  Returns:
54
66
  A primed generator that accepts an :obj:`AxisArray` via `.send(axis_array)`
@@ -69,10 +81,15 @@ def windowing(
69
81
  "windowing is non-deterministic with `zero_pad_until='input'` as it depends on the size "
70
82
  "of the first input. We recommend using 'shift' when `window_shift` is float-valued."
71
83
  )
84
+ try:
85
+ anchor = Anchor(anchor)
86
+ except ValueError:
87
+ raise ValueError(f"Invalid anchor: {anchor}. Valid anchor are: {', '.join([e.value for e in Anchor])}")
88
+
72
89
  msg_out = AxisArray(np.array([]), dims=[""])
73
90
 
74
91
  # State variables
75
- buffer: npt.NDArray | None = None
92
+ buffer: npt.NDArray | sparse.SparseArray | None = None
76
93
  window_samples: int | None = None
77
94
  window_shift_samples: int | None = None
78
95
  # Number of incoming samples to ignore. Only relevant when shift > window.:
@@ -83,6 +100,8 @@ def windowing(
83
100
  out_dims: list[str] | None = None
84
101
 
85
102
  check_inputs = {"samp_shape": None, "fs": None, "key": None}
103
+ concat_fun = np.concatenate
104
+ sliding_win_fun = sliding_win_oneaxis
86
105
 
87
106
  while True:
88
107
  msg_in: AxisArray = yield msg_out
@@ -116,6 +135,15 @@ def windowing(
116
135
  check_inputs["fs"] = fs
117
136
  check_inputs["key"] = msg_in.key
118
137
 
138
+ if isinstance(msg_in.data, sparse.SparseArray):
139
+ concat_fun = sparse.concatenate
140
+ sliding_win_fun = sparse_sliding_win_oneaxis
141
+ zero_fun = sparse.zeros
142
+ else:
143
+ concat_fun = np.concatenate
144
+ sliding_win_fun = sliding_win_oneaxis # Requires updated signature in ezmsg dev.
145
+ zero_fun = np.zeros
146
+
119
147
  window_samples = int(window_dur * fs)
120
148
  if not b_1to1:
121
149
  window_shift_samples = int(window_shift * fs)
@@ -131,7 +159,7 @@ def windowing(
131
159
  + (n_zero,)
132
160
  + msg_in.data.shape[axis_idx + 1 :]
133
161
  )
134
- buffer = np.zeros(init_buffer_shape, dtype=msg_in.data.dtype)
162
+ buffer = zero_fun(init_buffer_shape, dtype=msg_in.data.dtype)
135
163
 
136
164
  # Add new data to buffer.
137
165
  # Currently, we concatenate the new time samples and clip the output.
@@ -140,21 +168,21 @@ def windowing(
140
168
  # is generally faster than np.roll and slicing anyway, but this could still
141
169
  # be a performance bottleneck for large memory arrays.
142
170
  # A circular buffer might be faster.
143
- buffer = np.concatenate((buffer, msg_in.data), axis=axis_idx)
171
+ buffer = concat_fun((buffer, msg_in.data), axis=axis_idx)
144
172
 
145
173
  # Create a vector of buffer timestamps to track axis `offset` in output(s)
146
- buffer_offset = np.arange(buffer.shape[axis_idx]).astype(float)
147
- # Adjust so first _new_ sample at index 0
148
- buffer_offset -= buffer_offset[-msg_in.data.shape[axis_idx]]
174
+ buffer_tvec = np.arange(buffer.shape[axis_idx]).astype(float)
175
+ # Adjust so first _new_ sample at index 0.
176
+ buffer_tvec -= buffer_tvec[-msg_in.data.shape[axis_idx]]
149
177
  # Convert form indices to 'units' (probably seconds).
150
- buffer_offset *= axis_info.gain
151
- buffer_offset += axis_info.offset
178
+ buffer_tvec *= axis_info.gain
179
+ buffer_tvec += axis_info.offset
152
180
 
153
181
  if not b_1to1 and shift_deficit > 0:
154
182
  n_skip = min(buffer.shape[axis_idx], shift_deficit)
155
183
  if n_skip > 0:
156
184
  buffer = slice_along_axis(buffer, slice(n_skip, None), axis_idx)
157
- buffer_offset = buffer_offset[n_skip:]
185
+ buffer_tvec = buffer_tvec[n_skip:]
158
186
  shift_deficit -= n_skip
159
187
 
160
188
  # Prepare reusable parts of output
@@ -171,8 +199,12 @@ def windowing(
171
199
  out_axes = {k: v for k, v in msg_in.axes.items() if k not in [newaxis, axis]}
172
200
 
173
201
  # Update targeted (windowed) axis so that its offset is relative to the new axis
174
- # TODO: If we have `anchor_newest=True` then offset should be -win_dur
175
- out_axes[axis] = replace(axis_info, offset=0.0)
202
+ if anchor == Anchor.BEGINNING:
203
+ out_axes[axis] = replace(axis_info, offset=0.0)
204
+ elif anchor == Anchor.END:
205
+ out_axes[axis] = replace(axis_info, offset=-window_dur)
206
+ elif anchor == Anchor.MIDDLE:
207
+ out_axes[axis] = replace(axis_info, offset=-window_dur / 2)
176
208
 
177
209
  # How we update .data and .axes[newaxis] depends on the windowing mode.
178
210
  if b_1to1:
@@ -181,19 +213,14 @@ def windowing(
181
213
  out_dat = buffer.reshape(
182
214
  buffer.shape[:axis_idx] + (1,) + buffer.shape[axis_idx:]
183
215
  )
184
- out_newaxis = replace(out_newaxis, offset=buffer_offset[-window_samples])
216
+ win_offset = buffer_tvec[-window_samples]
185
217
  elif buffer.shape[axis_idx] >= window_samples:
186
218
  # Deterministic window shifts.
187
- # Note: After https://github.com/ezmsg-org/ezmsg/pull/152, add `window_shift_samples` as the last arg
188
- # to `sliding_win_oneaxis` and remove the call to `slice_along_axis`.
189
- out_dat = sliding_win_oneaxis(buffer, window_samples, axis_idx)
190
- out_dat = slice_along_axis(
191
- out_dat, slice(None, None, window_shift_samples), axis_idx
192
- )
193
- offset_view = sliding_win_oneaxis(buffer_offset, window_samples, 0)[
219
+ out_dat = sliding_win_fun(buffer, window_samples, axis_idx, step=window_shift_samples)
220
+ offset_view = sliding_win_oneaxis(buffer_tvec, window_samples, 0)[
194
221
  ::window_shift_samples
195
222
  ]
196
- out_newaxis = replace(out_newaxis, offset=offset_view[0, 0])
223
+ win_offset = offset_view[0, 0]
197
224
 
198
225
  # Drop expired beginning of buffer and update shift_deficit
199
226
  multi_shift = window_shift_samples * out_dat.shape[axis_idx]
@@ -208,7 +235,13 @@ def windowing(
208
235
  )
209
236
  out_dat = np.zeros(empty_data_shape, dtype=msg_in.data.dtype)
210
237
  # out_newaxis will have first timestamp in input... but mostly meaningless because output is size-zero.
211
- out_newaxis = replace(out_newaxis, offset=axis_info.offset)
238
+ win_offset = axis_info.offset
239
+
240
+ if anchor == Anchor.END:
241
+ win_offset += window_dur
242
+ elif anchor == Anchor.MIDDLE:
243
+ win_offset += window_dur / 2
244
+ out_newaxis = replace(out_newaxis, offset=win_offset)
212
245
 
213
246
  msg_out = replace(
214
247
  msg_in, data=out_dat, dims=out_dims, axes={**out_axes, newaxis: out_newaxis}
@@ -221,6 +254,7 @@ class WindowSettings(ez.Settings):
221
254
  window_dur: float | None = None # Sec. passthrough if None
222
255
  window_shift: float | None = None # Sec. Use "1:1 mode" if None
223
256
  zero_pad_until: str = "full" # "full", "shift", "input", "none"
257
+ anchor: str | Anchor = Anchor.BEGINNING
224
258
 
225
259
 
226
260
  class WindowState(ez.State):
@@ -243,6 +277,7 @@ class Window(GenAxisArray):
243
277
  window_dur=self.SETTINGS.window_dur,
244
278
  window_shift=self.SETTINGS.window_shift,
245
279
  zero_pad_until=self.SETTINGS.zero_pad_until,
280
+ anchor=self.SETTINGS.anchor,
246
281
  )
247
282
 
248
283
  @ez.subscriber(INPUT_SIGNAL, zero_copy=True)
@@ -261,10 +296,7 @@ class Window(GenAxisArray):
261
296
  # We need to split out_msg into multiple yields, dropping newaxis.
262
297
  axis_idx = out_msg.get_axis_idx("win")
263
298
  win_axis = out_msg.axes["win"]
264
- offsets = (
265
- np.arange(out_msg.data.shape[axis_idx]) * win_axis.gain
266
- + win_axis.offset
267
- )
299
+ offsets = win_axis.value(np.arange(out_msg.data.shape[axis_idx]))
268
300
  for msg_ix in range(out_msg.data.shape[axis_idx]):
269
301
  # Need to drop 'win' and replace self.SETTINGS.axis from axes.
270
302
  _out_axes = {
File without changes
@@ -7,6 +7,7 @@ import numpy as np
7
7
  import numpy.typing as npt
8
8
  from numpy.lib.stride_tricks import sliding_window_view
9
9
  from frozendict import frozendict
10
+ import sparse
10
11
  import ezmsg.core as ez
11
12
  from ezmsg.util.messages.axisarray import AxisArray
12
13
  from ezmsg.util.messagegate import MessageGate, MessageGateSettings
@@ -27,6 +28,7 @@ def calculate_expected_results(
27
28
  fs,
28
29
  win_shift,
29
30
  zero_pad,
31
+ anchor,
30
32
  msg_block_size,
31
33
  shift_len,
32
34
  win_len,
@@ -70,6 +72,12 @@ def calculate_expected_results(
70
72
  else:
71
73
  expected = expected[:, ::shift_len]
72
74
  tvec = tvec[::shift_len, 0]
75
+
76
+ if anchor == "middle":
77
+ tvec = tvec + win_len / (2 * fs)
78
+ elif anchor == "end":
79
+ tvec = tvec + win_len / fs
80
+
73
81
  # Transpose to put time_ax and win_ax in the correct locations.
74
82
  if win_ax == 0:
75
83
  expected = np.moveaxis(expected, 0, -1)
@@ -111,6 +119,7 @@ def test_window_gen_nodur():
111
119
  @pytest.mark.parametrize("win_shift", [None, 0.2, 1.0])
112
120
  @pytest.mark.parametrize("zero_pad", ["input", "shift", "none"])
113
121
  @pytest.mark.parametrize("fs", [10.0, 500.0])
122
+ @pytest.mark.parametrize("anchor", ["beginning", "middle", "end"])
114
123
  @pytest.mark.parametrize("time_ax", [0, 1])
115
124
  def test_window_generator(
116
125
  msg_block_size: int,
@@ -119,6 +128,7 @@ def test_window_generator(
119
128
  win_shift: float | None,
120
129
  zero_pad: str,
121
130
  fs: float,
131
+ anchor: str,
122
132
  time_ax: int,
123
133
  ):
124
134
  nchans = 3
@@ -141,6 +151,7 @@ def test_window_generator(
141
151
  window_dur=win_dur,
142
152
  window_shift=win_shift,
143
153
  zero_pad_until=zero_pad,
154
+ anchor=anchor
144
155
  )
145
156
 
146
157
  # Create inputs and send them to the generator, collecting the results along the way.
@@ -211,6 +222,7 @@ def test_window_generator(
211
222
  fs,
212
223
  win_shift,
213
224
  zero_pad,
225
+ anchor,
214
226
  msg_block_size,
215
227
  shift_len,
216
228
  win_len,
@@ -225,6 +237,62 @@ def test_window_generator(
225
237
  assert np.allclose(offsets, tvec)
226
238
 
227
239
 
240
+ @pytest.mark.parametrize("win_dur", [0.3, 1.0])
241
+ @pytest.mark.parametrize("win_shift", [0.2, 1.0, None])
242
+ @pytest.mark.parametrize("zero_pad", ["input", "shift", "none"])
243
+ def test_sparse_window(
244
+ win_dur: float,
245
+ win_shift: float | None,
246
+ zero_pad: str,
247
+ ):
248
+ fs = 100.0
249
+ n_ch = 5
250
+ n_samps = 1_000
251
+ msg_len = 100
252
+ win_len = int(win_dur * fs)
253
+ rng = np.random.default_rng()
254
+ s = sparse.random((n_samps, n_ch), density=0.1, random_state=rng) > 0
255
+ in_msgs = [
256
+ AxisArray(
257
+ data=s[msg_ix * msg_len : (msg_ix + 1) * msg_len],
258
+ dims=["time", "ch"],
259
+ axes={
260
+ "time": AxisArray.Axis.TimeAxis(fs=fs, offset=msg_ix / fs),
261
+ },
262
+ key="test_sparse_window",
263
+ )
264
+ for msg_ix in range(10)
265
+ ]
266
+
267
+ proc = windowing(
268
+ axis="time",
269
+ newaxis="win",
270
+ window_dur=win_dur,
271
+ window_shift=win_shift,
272
+ zero_pad_until=zero_pad,
273
+ )
274
+ out_msgs = [proc.send(_) for _ in in_msgs]
275
+ nwins = 0
276
+ for om in out_msgs:
277
+ assert om.dims == ["win", "time", "ch"]
278
+ assert om.data.shape[1] == win_len
279
+ assert om.data.shape[2] == n_ch
280
+ nwins += om.data.shape[0]
281
+ if win_shift is None:
282
+ # 1:1 mode
283
+ assert nwins == len(out_msgs)
284
+ else:
285
+ shift_len = int(win_shift * fs)
286
+ prepended = 0
287
+ if zero_pad == "input":
288
+ prepended = max(0, win_len - msg_len)
289
+ elif zero_pad == "shift":
290
+ prepended = max(0, win_len - shift_len)
291
+ win_offsets = np.arange(n_samps + prepended)[::shift_len]
292
+ expected_nwins = np.sum(win_offsets <= (n_samps + prepended - win_len))
293
+ assert nwins == expected_nwins
294
+
295
+
228
296
  class WindowSystemSettings(ez.Settings):
229
297
  num_msgs: int
230
298
  counter_settings: CounterSettings
@@ -374,6 +442,7 @@ def test_window_system(
374
442
  fs,
375
443
  win_shift,
376
444
  zero_pad,
445
+ "beginning",
377
446
  msg_block_size,
378
447
  shift_len,
379
448
  win_len,
@@ -115,13 +115,14 @@ wheels = [
115
115
 
116
116
  [[package]]
117
117
  name = "ezmsg-sigproc"
118
- version = "1.6.0"
118
+ version = "1.6.1.dev3+ga96f0f9.d20241223"
119
119
  source = { editable = "." }
120
120
  dependencies = [
121
121
  { name = "ezmsg" },
122
122
  { name = "numpy" },
123
123
  { name = "pywavelets" },
124
124
  { name = "scipy" },
125
+ { name = "sparse" },
125
126
  ]
126
127
 
127
128
  [package.optional-dependencies]
@@ -150,6 +151,7 @@ requires-dist = [
150
151
  { name = "pytest-cov", marker = "extra == 'test'", specifier = ">=5.0.0" },
151
152
  { name = "pywavelets", specifier = ">=1.6.0" },
152
153
  { name = "scipy", specifier = ">=1.13.1" },
154
+ { name = "sparse", specifier = ">=0.15.4" },
153
155
  ]
154
156
 
155
157
  [package.metadata.requires-dev]
@@ -218,6 +220,29 @@ wheels = [
218
220
  { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 },
219
221
  ]
220
222
 
223
+ [[package]]
224
+ name = "llvmlite"
225
+ version = "0.43.0"
226
+ source = { registry = "https://pypi.org/simple" }
227
+ sdist = { url = "https://files.pythonhosted.org/packages/9f/3d/f513755f285db51ab363a53e898b85562e950f79a2e6767a364530c2f645/llvmlite-0.43.0.tar.gz", hash = "sha256:ae2b5b5c3ef67354824fb75517c8db5fbe93bc02cd9671f3c62271626bc041d5", size = 157069 }
228
+ wheels = [
229
+ { url = "https://files.pythonhosted.org/packages/23/ff/6ca7e98998b573b4bd6566f15c35e5c8bea829663a6df0c7aa55ab559da9/llvmlite-0.43.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a289af9a1687c6cf463478f0fa8e8aa3b6fb813317b0d70bf1ed0759eab6f761", size = 31064408 },
230
+ { url = "https://files.pythonhosted.org/packages/ca/5c/a27f9257f86f0cda3f764ff21d9f4217b9f6a0d45e7a39ecfa7905f524ce/llvmlite-0.43.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d4fd101f571a31acb1559ae1af30f30b1dc4b3186669f92ad780e17c81e91bc", size = 28793153 },
231
+ { url = "https://files.pythonhosted.org/packages/7e/3c/4410f670ad0a911227ea2ecfcba9f672a77cf1924df5280c4562032ec32d/llvmlite-0.43.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7d434ec7e2ce3cc8f452d1cd9a28591745de022f931d67be688a737320dfcead", size = 42857276 },
232
+ { url = "https://files.pythonhosted.org/packages/c6/21/2ffbab5714e72f2483207b4a1de79b2eecd9debbf666ff4e7067bcc5c134/llvmlite-0.43.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6912a87782acdff6eb8bf01675ed01d60ca1f2551f8176a300a886f09e836a6a", size = 43871781 },
233
+ { url = "https://files.pythonhosted.org/packages/f2/26/b5478037c453554a61625ef1125f7e12bb1429ae11c6376f47beba9b0179/llvmlite-0.43.0-cp310-cp310-win_amd64.whl", hash = "sha256:14f0e4bf2fd2d9a75a3534111e8ebeb08eda2f33e9bdd6dfa13282afacdde0ed", size = 28123487 },
234
+ { url = "https://files.pythonhosted.org/packages/95/8c/de3276d773ab6ce3ad676df5fab5aac19696b2956319d65d7dd88fb10f19/llvmlite-0.43.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3e8d0618cb9bfe40ac38a9633f2493d4d4e9fcc2f438d39a4e854f39cc0f5f98", size = 31064409 },
235
+ { url = "https://files.pythonhosted.org/packages/ee/e1/38deed89ced4cf378c61e232265cfe933ccde56ae83c901aa68b477d14b1/llvmlite-0.43.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0a9a1a39d4bf3517f2af9d23d479b4175ead205c592ceeb8b89af48a327ea57", size = 28793149 },
236
+ { url = "https://files.pythonhosted.org/packages/2f/b2/4429433eb2dc8379e2cb582502dca074c23837f8fd009907f78a24de4c25/llvmlite-0.43.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1da416ab53e4f7f3bc8d4eeba36d801cc1894b9fbfbf2022b29b6bad34a7df2", size = 42857277 },
237
+ { url = "https://files.pythonhosted.org/packages/6b/99/5d00a7d671b1ba1751fc9f19d3b36f3300774c6eebe2bcdb5f6191763eb4/llvmlite-0.43.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:977525a1e5f4059316b183fb4fd34fa858c9eade31f165427a3977c95e3ee749", size = 43871781 },
238
+ { url = "https://files.pythonhosted.org/packages/20/ab/ed5ed3688c6ba4f0b8d789da19fd8e30a9cf7fc5852effe311bc5aefe73e/llvmlite-0.43.0-cp311-cp311-win_amd64.whl", hash = "sha256:d5bd550001d26450bd90777736c69d68c487d17bf371438f975229b2b8241a91", size = 28107433 },
239
+ { url = "https://files.pythonhosted.org/packages/0b/67/9443509e5d2b6d8587bae3ede5598fa8bd586b1c7701696663ea8af15b5b/llvmlite-0.43.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f99b600aa7f65235a5a05d0b9a9f31150c390f31261f2a0ba678e26823ec38f7", size = 31064409 },
240
+ { url = "https://files.pythonhosted.org/packages/a2/9c/24139d3712d2d352e300c39c0e00d167472c08b3bd350c3c33d72c88ff8d/llvmlite-0.43.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:35d80d61d0cda2d767f72de99450766250560399edc309da16937b93d3b676e7", size = 28793145 },
241
+ { url = "https://files.pythonhosted.org/packages/bf/f1/4c205a48488e574ee9f6505d50e84370a978c90f08dab41a42d8f2c576b6/llvmlite-0.43.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eccce86bba940bae0d8d48ed925f21dbb813519169246e2ab292b5092aba121f", size = 42857276 },
242
+ { url = "https://files.pythonhosted.org/packages/00/5f/323c4d56e8401c50185fd0e875fcf06b71bf825a863699be1eb10aa2a9cb/llvmlite-0.43.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df6509e1507ca0760787a199d19439cc887bfd82226f5af746d6977bd9f66844", size = 43871781 },
243
+ { url = "https://files.pythonhosted.org/packages/c6/94/dea10e263655ce78d777e78d904903faae39d1fc440762be4a9dc46bed49/llvmlite-0.43.0-cp312-cp312-win_amd64.whl", hash = "sha256:7a2872ee80dcf6b5dbdc838763d26554c2a18aa833d31a2635bff16aafefb9c9", size = 28107442 },
244
+ ]
245
+
221
246
  [[package]]
222
247
  name = "mccabe"
223
248
  version = "0.7.0"
@@ -236,6 +261,33 @@ wheels = [
236
261
  { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 },
237
262
  ]
238
263
 
264
+ [[package]]
265
+ name = "numba"
266
+ version = "0.60.0"
267
+ source = { registry = "https://pypi.org/simple" }
268
+ dependencies = [
269
+ { name = "llvmlite" },
270
+ { name = "numpy" },
271
+ ]
272
+ sdist = { url = "https://files.pythonhosted.org/packages/3c/93/2849300a9184775ba274aba6f82f303343669b0592b7bb0849ea713dabb0/numba-0.60.0.tar.gz", hash = "sha256:5df6158e5584eece5fc83294b949fd30b9f1125df7708862205217e068aabf16", size = 2702171 }
273
+ wheels = [
274
+ { url = "https://files.pythonhosted.org/packages/f7/cf/baa13a7e3556d73d9e38021e6d6aa4aeb30d8b94545aa8b70d0f24a1ccc4/numba-0.60.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d761de835cd38fb400d2c26bb103a2726f548dc30368853121d66201672e651", size = 2647627 },
275
+ { url = "https://files.pythonhosted.org/packages/ac/ba/4b57fa498564457c3cc9fc9e570a6b08e6086c74220f24baaf04e54b995f/numba-0.60.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:159e618ef213fba758837f9837fb402bbe65326e60ba0633dbe6c7f274d42c1b", size = 2650322 },
276
+ { url = "https://files.pythonhosted.org/packages/28/98/7ea97ee75870a54f938a8c70f7e0be4495ba5349c5f9db09d467c4a5d5b7/numba-0.60.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1527dc578b95c7c4ff248792ec33d097ba6bef9eda466c948b68dfc995c25781", size = 3407390 },
277
+ { url = "https://files.pythonhosted.org/packages/79/58/cb4ac5b8f7ec64200460aef1fed88258fb872ceef504ab1f989d2ff0f684/numba-0.60.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe0b28abb8d70f8160798f4de9d486143200f34458d34c4a214114e445d7124e", size = 3699694 },
278
+ { url = "https://files.pythonhosted.org/packages/1c/b0/c61a93ca947d12233ff45de506ddbf52af3f752066a0b8be4d27426e16da/numba-0.60.0-cp310-cp310-win_amd64.whl", hash = "sha256:19407ced081d7e2e4b8d8c36aa57b7452e0283871c296e12d798852bc7d7f198", size = 2687030 },
279
+ { url = "https://files.pythonhosted.org/packages/98/ad/df18d492a8f00d29a30db307904b9b296e37507034eedb523876f3a2e13e/numba-0.60.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a17b70fc9e380ee29c42717e8cc0bfaa5556c416d94f9aa96ba13acb41bdece8", size = 2647254 },
280
+ { url = "https://files.pythonhosted.org/packages/9a/51/a4dc2c01ce7a850b8e56ff6d5381d047a5daea83d12bad08aa071d34b2ee/numba-0.60.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3fb02b344a2a80efa6f677aa5c40cd5dd452e1b35f8d1c2af0dfd9ada9978e4b", size = 2649970 },
281
+ { url = "https://files.pythonhosted.org/packages/f9/4c/8889ac94c0b33dca80bed11564b8c6d9ea14d7f094e674c58e5c5b05859b/numba-0.60.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5f4fde652ea604ea3c86508a3fb31556a6157b2c76c8b51b1d45eb40c8598703", size = 3412492 },
282
+ { url = "https://files.pythonhosted.org/packages/57/03/2b4245b05b71c0cee667e6a0b51606dfa7f4157c9093d71c6b208385a611/numba-0.60.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4142d7ac0210cc86432b818338a2bc368dc773a2f5cf1e32ff7c5b378bd63ee8", size = 3705018 },
283
+ { url = "https://files.pythonhosted.org/packages/79/89/2d924ca60dbf949f18a6fec223a2445f5f428d9a5f97a6b29c2122319015/numba-0.60.0-cp311-cp311-win_amd64.whl", hash = "sha256:cac02c041e9b5bc8cf8f2034ff6f0dbafccd1ae9590dc146b3a02a45e53af4e2", size = 2686920 },
284
+ { url = "https://files.pythonhosted.org/packages/eb/5c/b5ec752c475e78a6c3676b67c514220dbde2725896bbb0b6ec6ea54b2738/numba-0.60.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d7da4098db31182fc5ffe4bc42c6f24cd7d1cb8a14b59fd755bfee32e34b8404", size = 2647866 },
285
+ { url = "https://files.pythonhosted.org/packages/65/42/39559664b2e7c15689a638c2a38b3b74c6e69a04e2b3019b9f7742479188/numba-0.60.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:38d6ea4c1f56417076ecf8fc327c831ae793282e0ff51080c5094cb726507b1c", size = 2650208 },
286
+ { url = "https://files.pythonhosted.org/packages/67/88/c4459ccc05674ef02119abf2888ccd3e2fed12a323f52255f4982fc95876/numba-0.60.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:62908d29fb6a3229c242e981ca27e32a6e606cc253fc9e8faeb0e48760de241e", size = 3466946 },
287
+ { url = "https://files.pythonhosted.org/packages/8b/41/ac11cf33524def12aa5bd698226ae196a1185831c05ed29dc0c56eaa308b/numba-0.60.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0ebaa91538e996f708f1ab30ef4d3ddc344b64b5227b67a57aa74f401bb68b9d", size = 3761463 },
288
+ { url = "https://files.pythonhosted.org/packages/ca/bd/0fe29fcd1b6a8de479a4ed25c6e56470e467e3611c079d55869ceef2b6d1/numba-0.60.0-cp312-cp312-win_amd64.whl", hash = "sha256:f75262e8fe7fa96db1dca93d53a194a38c46da28b112b8a4aca168f0df860347", size = 2707588 },
289
+ ]
290
+
239
291
  [[package]]
240
292
  name = "numpy"
241
293
  version = "2.0.2"
@@ -510,6 +562,20 @@ wheels = [
510
562
  { url = "https://files.pythonhosted.org/packages/3e/df/963384e90733e08eac978cd103c34df181d1fec424de383cdc443f418dd4/scipy-1.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:cdd7dacfb95fea358916410ec61bbc20440f7860333aee6d882bb8046264e949", size = 45910955 },
511
563
  ]
512
564
 
565
+ [[package]]
566
+ name = "sparse"
567
+ version = "0.15.4"
568
+ source = { registry = "https://pypi.org/simple" }
569
+ dependencies = [
570
+ { name = "numba" },
571
+ { name = "numpy" },
572
+ { name = "scipy" },
573
+ ]
574
+ sdist = { url = "https://files.pythonhosted.org/packages/26/6a/a1d00d932597c00509d333d9cde6f10d6c85470a3701455b0c48fc45886b/sparse-0.15.4.tar.gz", hash = "sha256:d4b1c57d24ff0f64f2fd5b5a95b49b7fb84ed207a26d7d58ce2764dcc5c72b84", size = 359721 }
575
+ wheels = [
576
+ { url = "https://files.pythonhosted.org/packages/1a/f2/8d5bc8cc6b822feac1cd671dac6fb0249a5202ad15ee9549d1a61d4141b5/sparse-0.15.4-py2.py3-none-any.whl", hash = "sha256:76ec76fee2aee82a84eb97155dd530a9644e3b1fdea2406bc4b454698b36d938", size = 237311 },
577
+ ]
578
+
513
579
  [[package]]
514
580
  name = "tomli"
515
581
  version = "2.1.0"
File without changes
File without changes
File without changes