diff --git a/.bumpversion.toml b/.bumpversion.toml index 3af2a2b..b664d15 100644 --- a/.bumpversion.toml +++ b/.bumpversion.toml @@ -3,7 +3,7 @@ parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)" serialize = ["{major}.{minor}.{patch}"] regex = false -current_version = "0.3.0" +current_version = "1.0.0" ignore_missing_version = false search = "{current_version}" replace = "{new_version}" @@ -29,4 +29,26 @@ replace = "version = \"{new_version}\"" [[tool.bumpversion.files]] filename = "spikify/version.py" search = "__version__ = \"{current_version}\"" -replace = "__version__ = \"{new_version}\"" \ No newline at end of file +replace = "__version__ = \"{new_version}\"" + +[[tool.bumpversion.files]] +filename = "codemeta.json" +search = "\"version\": \"{current_version}\"" +replace = "\"version\": \"{new_version}\"" + +[[tool.bumpversion.files]] +filename = "codemeta.json" +search = "\"downloadUrl\": \"https://github.com/neuromorphic-polito/spikify/releases/tag/{current_version}" +replace = "\"downloadUrl\": \"https://github.com/neuromorphic-polito/spikify/releases/tag/{new_version}" + +[[tool.bumpversion.files]] +filename = "codemeta.json" +search = "\"dateModified\": \"\\d{{4}}-\\d{{2}}-\\d{{2}}\"" +replace = "\"dateModified\": \"{now:%Y-%m-%d}\"" +regex = true + +[[tool.bumpversion.files]] +filename = "codemeta.json" +search = "\"datePublished\": \"\\d{{4}}-\\d{{2}}-\\d{{2}}\"" +replace = "\"datePublished\": \"{now:%Y-%m-%d}\"" +regex = true \ No newline at end of file diff --git a/.github/workflows/python-tests.yaml b/.github/workflows/python-tests.yaml index 7923546..52a33ac 100644 --- a/.github/workflows/python-tests.yaml +++ b/.github/workflows/python-tests.yaml @@ -79,4 +79,39 @@ jobs: flags: unittests name: coverage-spikify token: ${{ secrets.CODECOV_TOKEN }} - verbose: true \ No newline at end of file + verbose: true + + test-documentation: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install Poetry + run: | + python -m pip install --upgrade pip + pip install poetry + + - name: Install dependencies + run: | + poetry install --with docs + + - name: Test documentation + run: | + cd docs + poetry run make doctest + + - name: Check documentation links + run: | + cd docs + poetry run make linkcheck + + - name: Clean up documentation build files + run: | + cd docs + poetry run make clean \ No newline at end of file diff --git a/README.md b/README.md index 78dfcae..6fb3f95 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,12 @@

- Spikify Overview + + + + spikify +

-Spikify is a Python package designed to transform raw signals into spike trains that can be fed into Spiking Neural Networks (SNNs). This package implements a variety of spike encoding techniques based on recent research to facilitate the integration of time-varying signals into neuromorphic computing frameworks. +spikify is a Python package designed to transform raw signals into spike trains that can be fed into Spiking Neural Networks (SNNs). This package implements a variety of spike encoding techniques based on recent research to facilitate the integration of time-varying signals into neuromorphic computing frameworks. ## Introduction @@ -10,17 +14,9 @@ Spiking Neural Networks (SNNs) are a novel type of artificial neural network tha This package provides a suite of spike encoding techniques that convert time-varying signals into spikes, enabling seamless integration with neuromorphic computing technologies. The encoding techniques implemented in this package are based on the research article: "Spike Encoding Techniques for IoT Time-Varying Signals Benchmarked on a Neuromorphic Classification Task" (Forno et al., 2022). -## Features - -* Multiple Spike Encoding Techniques: Includes both rate-based and temporal encoding schemes -* **Signal Preprocessing:** Tools for filtering and preparing signals, including: - * **Gammatone Filter:** Mimics human auditory filtering, useful for audio and speech signals. - * **Butterworth Filter:** Smooths and removes noise from signals, ideal for general sensor data. - * Easily chain filters before encoding to improve spike train quality. - ## Installation -To install the Spikify package, use pip: +To install the spikify package, use pip: ```bash pip install innuce-spikify @@ -32,12 +28,12 @@ Here is a simple example to get started: ```python import numpy as np -from spikify.filtering import FilterBank -from spikify.encoding.rate import poisson_rate +from spikify.filters import FilterBank +from spikify.encoders.rate import poisson # Generate a sinusoidal signal -time = np.linspace(0, 2 * np.pi, 100) # Time from 0 to 2*pi -amplitude = np.sin(time) # Sinusoidal signal +time = np.linspace(0, 4 * np.pi, 200) +signal = np.sin(2 * time) + 0.5 * np.sin(4 * time) filter = FilterBank(fs=50, channels=5, f_min=0.5, f_max=5, order=4, filter_type='butterworth') @@ -46,44 +42,41 @@ filtered_signal = filter.decompose(signal) # (timesteps, channels, features) filtered_signal = np.reshape(filtered_signal, (-1, filtered_signal.shape[1] * filtered_signal.shape[2])) # Encode the filtered signal -encoded_signal = poisson_rate(filtered_signal, interval_length=2) +encoded_signal = poisson(filtered_signal, interval_length=2) ``` For more detailed examples and usage, please refer to the [documentation](https://spikify.readthedocs.io/en/latest/). ## Encoding Techniques -This package implements several spike encoding families techniques, including: - -| Encoding Family | Algorithm | Description | -|------------------------|--------------------------|--------------------------------------| -| **Rate Encoding** | Poisson Rate | Encodes intensity as firing rate | -| **Temporal Encoding** | Moving Window | Spikes on local changes | -| | Step Forward | Spikes on signal steps | -| | Threshold-Based | Spikes when crossing thresholds | -| | Zero-Cross Step Forward | Spikes on zero-crossings | -| **Deconvolution-Based**| Ben Spiker | Deconvolves spikes from signal | -| | Hough Spiker | Uses Hough transform for spikes | -| | Modified Hough Spiker | Robust Hough-based encoding | -| **Global Referenced** | Phase Encoding | Encodes phase information | -| | Time-to-Spike | Spikes at specific time delays | -| **Latency Encoding** | Burst Encoding | Encodes bursts of spikes | - -**Tip:** +spikify implements the following spike encoding families: + +| Encoding Family | Algorithm | Description | +|------------------------|--------------------------|---------------------------------------------------------------------------------------------------| +| **Rate Encoding** | Poisson Rate | Models spike generation as a Poisson process; instantaneous firing rate proportional to signal amplitude | +| **Temporal Encoding** | Threshold-Based | Fires an ON spike when the signal crosses a positive threshold, and an OFF spike when it crosses a negative one | +| | Step Forward | Fires ON or OFF spikes each time the signal accumulates enough change in either direction | +| | Zero-Cross Step Forward | Simplified version of the step-forward that encodes only positive signals | +| | Moving Window | Fires positive or negative spikes when the signal rises or drops significantly within a short local window | +| **Deconvolution-Based** | Hough Spiker | Implements an iterative subtraction procedure between the signal and a convolution filter | +| | Modified Hough Spiker | Extends Hough Spiker with outlier rejection for noise-robust encoding | +| | Bens Spiker | Extends the Hough Spiker with an additional error control threshold | +| **Global Referenced** | Phase Encoding | Use the inverse arcsin transformation of the signal to compute the binary pattern based on a quantized local mean value of the input | +| | Time-to-First Spike | Encodes amplitude as latency delay from stimulus onset to first spike | +| **Latency Encoding** | Burst Coding | Represents signal intensity via inter-spike interval within a burst | + +**Tips:** - Use **Poisson Rate** for general-purpose encoding. - Use **Temporal** or **Deconvolution** methods for signals where timing or event structure is important. ## Filters -Spikify provides preprocessing filters that can be applied to signals before encoding to improve spike train quality and remove noise. These filters help condition the raw signal data for better encoding performance. - -### Available Filters - -| Filter Type | Description | -|-------------------|----------------------------------------------------| -| **Gammatone** | Mimics human auditory filtering | -| **Butterworth** | Low-pass filter for noise reduction and smoothing | +spikify provides preprocessing filters to condition raw signals before encoding. Both filters are implemented as filter banks with configurable channels, frequency bounds, and order. +| Filter Type | Description | +|-----------------|------------------------------------------------------------------------------------------------------| +| **Gammatone** | Bandpass filterbank approximating basilar membrane response; models cochlear frequency decomposition | +| **Butterworth** | IIR low-pass filter with maximally flat passband; attenuates high-frequency noise before encoding | ## Encoded Datasets diff --git a/README_PYPI.md b/README_PYPI.md new file mode 100644 index 0000000..80d8c53 --- /dev/null +++ b/README_PYPI.md @@ -0,0 +1,111 @@ +

+ spikify logo +

+ +spikify is a Python package designed to transform raw signals into spike trains that can be fed into Spiking Neural Networks (SNNs). This package implements a variety of spike encoding techniques based on recent research to facilitate the integration of time-varying signals into neuromorphic computing frameworks. + +## Introduction + +Spiking Neural Networks (SNNs) are a novel type of artificial neural network that operates using discrete events (spikes) in time, inspired by the behavior of biological neurons. They are characterized by their potential for low energy consumption and computational cost, making them suitable for edge computing and IoT applications. However, traditional digital signals must be encoded into spike trains before they can be processed by SNNs. + +This package provides a suite of spike encoding techniques that convert time-varying signals into spikes, enabling seamless integration with neuromorphic computing technologies. The encoding techniques implemented in this package are based on the research article: "Spike Encoding Techniques for IoT Time-Varying Signals Benchmarked on a Neuromorphic Classification Task" (Forno et al., 2022). + +## Installation + +To install the spikify package, use pip: + +```bash +pip install innuce-spikify +``` + +## Usage + +Here is a simple example to get started: + +```python +import numpy as np +from spikify.filters import FilterBank +from spikify.encoders.rate import poisson + +# Generate a sinusoidal signal +time = np.linspace(0, 4 * np.pi, 200) +signal = np.sin(2 * time) + 0.5 * np.sin(4 * time) + +filter = FilterBank(fs=50, channels=5, f_min=0.5, f_max=5, order=4, filter_type='butterworth') + +filtered_signal = filter.decompose(signal) # (timesteps, channels, features) + +filtered_signal = np.reshape(filtered_signal, (-1, filtered_signal.shape[1] * filtered_signal.shape[2])) + +# Encode the filtered signal +encoded_signal = poisson(filtered_signal, interval_length=2) +``` + +For more detailed examples and usage, please refer to the [documentation](https://spikify.readthedocs.io/en/latest/). + +## Encoding Techniques + +spikify implements the following spike encoding families: + +| Encoding Family | Algorithm | Description | +|------------------------|--------------------------|---------------------------------------------------------------------------------------------------| +| **Rate Encoding** | Poisson Rate | Models spike generation as a Poisson process; instantaneous firing rate proportional to signal amplitude | +| **Temporal Encoding** | Threshold-Based | Fires an ON spike when the signal crosses a positive threshold, and an OFF spike when it crosses a negative one | +| | Step Forward | Fires ON or OFF spikes each time the signal accumulates enough change in either direction | +| | Zero-Cross Step Forward | Simplified version of the step-forward that encodes only positive signals | +| | Moving Window | Fires positive or negative spikes when the signal rises or drops significantly within a short local window | +| **Deconvolution-Based** | Hough Spiker | Implements an iterative subtraction procedure between the signal and a convolution filter | +| | Modified Hough Spiker | Extends Hough Spiker with outlier rejection for noise-robust encoding | +| | Bens Spiker | Extends the Hough Spiker with an additional error control threshold | +| **Global Referenced** | Phase Encoding | Use the inverse arcsin transformation of the signal to compute the binary pattern based on a quantized local mean value of the input | +| | Time-to-First Spike | Encodes amplitude as latency delay from stimulus onset to first spike | +| **Latency Encoding** | Burst Coding | Represents signal intensity via inter-spike interval within a burst | + +**Tips:** +- Use **Poisson Rate** for general-purpose encoding. +- Use **Temporal** or **Deconvolution** methods for signals where timing or event structure is important. + +## Filters + +spikify provides preprocessing filters to condition raw signals before encoding. Both filters are implemented as filter banks with configurable channels, frequency bounds, and order. + +| Filter Type | Description | +|-----------------|------------------------------------------------------------------------------------------------------| +| **Gammatone** | Bandpass filterbank approximating basilar membrane response; models cochlear frequency decomposition | +| **Butterworth** | IIR low-pass filter with maximally flat passband; attenuates high-frequency noise before encoding | + +## Encoded Datasets + +The following datasets have been selected to serve as examples for benchmarking spike train encoding techniques: + +* WISDM Dataset: 20 Hz recordings of human activity through mobile and wearable inertial sensors + +These datasets are preprocessed and converted into spike trains to evaluate the performance of different encoding techniques. + +## Citation + +If you use this framework in your research, please cite the following article: + +```bibtex +@ARTICLE{ + 10.3389/fnins.2022.999029, + AUTHOR={Forno, Evelina and Fra, Vittorio and Pignari, Riccardo and Macii, Enrico and Urgese, Gianvito }, + TITLE={Spike encoding techniques for IoT time-varying signals benchmarked on a neuromorphic classification task}, + JOURNAL={Frontiers in Neuroscience}, + VOLUME={16}, + YEAR={2022}, + URL={https://www.frontiersin.org/journals/neuroscience/articles/10.3389/fnins.2022.999029}, + DOI={10.3389/fnins.2022.999029}, + ISSN={1662-453X}, +} +``` + +## Contributing + +We welcome contributions from the community. Please see our CONTRIBUTING.rst file for more details on how to get involved. + +## License + +This project is licensed under the Apache 2.0 License - see the LICENSE file for details. \ No newline at end of file diff --git a/animation.py b/animation.py index d3f37c3..2073e1e 100644 --- a/animation.py +++ b/animation.py @@ -1,7 +1,7 @@ import numpy as np import matplotlib.pyplot as plt import imageio.v2 as imageio -from spikify.encoding.rate import poisson_rate +from spikify.encoders.rate import poisson import os # Set style for better visualization @@ -13,7 +13,7 @@ signal = np.sin(2 * t) + 0.5 * np.sin(4 * t) # More complex signal # Generate spikes -spikes = poisson_rate(signal=signal, interval_length=5) +spikes = poisson(signal=signal, interval_length=5) spike_times = t[spikes] # Color settings diff --git a/codecov.yml b/codecov.yml index 6a13a09..e60be6e 100644 --- a/codecov.yml +++ b/codecov.yml @@ -26,32 +26,32 @@ component_management: - component_id: temporal_encoding_deconvolution_algorithms name: TempEncDeconAlgo paths: - - "spikify/encoding/temporal/deconvolution/**" - - "tests/encoding/temporal/deconvolution/**" + - "spikify/encoders/temporal/deconvolution/**" + - "tests/encoders/temporal/deconvolution/**" - component_id: temporal_encoding_global_referenced_algorithms name: TempEncGlobAlgo paths: - - "spikify/encoding/temporal/global_referenced/**" - - "tests/encoding/temporal/global_referenced/**" + - "spikify/encoders/temporal/global_referenced/**" + - "tests/encoders/temporal/global_referenced/**" - component_id: temporal_encoding_contrast_algorithms name: TempEncContAlgo paths: - - "spikify/encoding/temporal/contrast/**" - - "tests/encoding/temporal/contrast/**" + - "spikify/encoders/temporal/contrast/**" + - "tests/encoders/temporal/contrast/**" - component_id: temporal_encoding_latency_algorithms name: TempEncLatAlgo paths: - - "spikify/encoding/temporal/latency/**" - - "tests/encoding/temporal/latency/**" + - "spikify/encoders/temporal/latency/**" + - "tests/encoders/temporal/latency/**" - component_id: temporal_encoding_rate_algorithms name: TempEncRateAlgo paths: - - "spikify/encoding/rate/**" - - "tests/encoding/rate/**" + - "spikify/encoders/rate/**" + - "tests/encoders/rate/**" - - component_id: filtering - name: Filtering + - component_id: filters + name: Filters paths: - - "spikify/filtering/**" - - "tests/filtering/**" + - "spikify/filters/**" + - "tests/filters/**" diff --git a/codemeta.json b/codemeta.json index 099f708..04686cb 100644 --- a/codemeta.json +++ b/codemeta.json @@ -56,14 +56,14 @@ "type": "Organization", "name": "Department of Control and Computer Engineering, Politecnico di Torino" }, - "email": "benedetto.letoi@polito.it", + "email": "benedetto.leto@polito.it", "familyName": "Leto", "givenName": "Benedetto" }, "dateCreated": "2025-04-11", - "dateModified": "2025-04-11", - "datePublished": "2025-04-11", - "description": "A fully Convert your data into spike-based signals for efficient and biologically-inspired spiking neural network applications, enabling faster and more energy-efficient computations.", + "dateModified": "2026-04-14", + "datePublished": "2026-04-14", + "description": "A Python package to convert your data into spike-based signals for efficient and biologically-inspired spiking neural network applications, enabling faster and more energy-efficient computations.", "funder": { "type": "Organization", "name": "NRRP-RI, M4C2, funded by the European Union - NextGenerationEU" @@ -83,12 +83,12 @@ ], "programmingLanguage": "Python", "runtimePlatform": "Miniconda", - "version": "0.1.1", + "version": "1.0.0", "developmentStatus": "active", "funding": "Project IR0000011, CUP B51E22000150006, EBRAINS-Italy", "isSourceCodeOf": "inNuCE Lab", "issueTracker": "https://github.com/neuromorphic-polito/spikify/issues", "referencePublication": "https://doi.org/10.3389/fnins.2022.999029", - "downloadUrl": "https://github.com/neuromorphic-polito/spikify/releases/tag/0.1.1" + "downloadUrl": "https://github.com/neuromorphic-polito/spikify/releases/tag/1.0.0" } \ No newline at end of file diff --git a/docs/_static/spike_encoding.gif b/docs/_static/spike_encoding.gif index bd45d9b..f9c6b28 100644 Binary files a/docs/_static/spike_encoding.gif and b/docs/_static/spike_encoding.gif differ diff --git a/docs/_static/white_logo.jpg b/docs/_static/white_logo.jpg new file mode 100644 index 0000000..f551cbb Binary files /dev/null and b/docs/_static/white_logo.jpg differ diff --git a/docs/api/python/encoding/index.rst b/docs/api/python/encoders/index.rst similarity index 87% rename from docs/api/python/encoding/index.rst rename to docs/api/python/encoders/index.rst index 1cf37f6..319122e 100644 --- a/docs/api/python/encoding/index.rst +++ b/docs/api/python/encoders/index.rst @@ -1,9 +1,9 @@ .. _encoding: -:octicon:`file-directory;0.9em;sd-mr-1 fill-primary` encoding +:octicon:`file-directory;0.9em;sd-mr-1 fill-primary` encoders ============================================================= -The ``encoding`` folder within the spikify library contains the essential components for converting raw data into spike trains, a fundamental step in spiking neural networks. This section is organized to reflect the primary structure of the encoding algorithms included in the library: +The ``encoders`` module within the spikify library contains the essential components for converting raw data into spike trains, a fundamental step in spiking neural networks. This section is organized to reflect the primary structure of the encoding algorithms included in the library: - **Rate Coding**: Contains algorithms that convert the intensity of input signals into spike frequency. - **Temporal Coding**: Encloses algorithms that encode data based on the precise timing of spikes. diff --git a/docs/api/python/encoding/rate_coding/index.rst b/docs/api/python/encoders/rate_coding/index.rst similarity index 98% rename from docs/api/python/encoding/rate_coding/index.rst rename to docs/api/python/encoders/rate_coding/index.rst index 6afe4db..5d943c5 100644 --- a/docs/api/python/encoding/rate_coding/index.rst +++ b/docs/api/python/encoders/rate_coding/index.rst @@ -14,4 +14,4 @@ Below, you will find links to the specific modules for each rate coding algorith .. toctree:: :maxdepth: 1 - poisson_rate \ No newline at end of file + poisson \ No newline at end of file diff --git a/docs/api/python/encoders/rate_coding/poisson.rst b/docs/api/python/encoders/rate_coding/poisson.rst new file mode 100644 index 0000000..79bd16e --- /dev/null +++ b/docs/api/python/encoders/rate_coding/poisson.rst @@ -0,0 +1,8 @@ +.. _poisson_function: + +.. title:: Poisson Algorithm + +.. automodule:: spikify.encoders.rate.poisson_algorithm + :members: poisson + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/api/python/encoding/temporal_coding/contrast/index.rst b/docs/api/python/encoders/temporal_coding/contrast/index.rst similarity index 100% rename from docs/api/python/encoding/temporal_coding/contrast/index.rst rename to docs/api/python/encoders/temporal_coding/contrast/index.rst diff --git a/docs/api/python/encoding/temporal_coding/contrast/moving_window.rst b/docs/api/python/encoders/temporal_coding/contrast/moving_window.rst similarity index 68% rename from docs/api/python/encoding/temporal_coding/contrast/moving_window.rst rename to docs/api/python/encoders/temporal_coding/contrast/moving_window.rst index 64b940a..bce4619 100644 --- a/docs/api/python/encoding/temporal_coding/contrast/moving_window.rst +++ b/docs/api/python/encoders/temporal_coding/contrast/moving_window.rst @@ -2,7 +2,7 @@ .. title:: Moving Window -.. automodule:: spikify.encoding.temporal.contrast.moving_window_algorithm +.. automodule:: spikify.encoders.temporal.contrast.moving_window_algorithm :members: moving_window :undoc-members: :show-inheritance: diff --git a/docs/api/python/encoding/temporal_coding/contrast/step_forward.rst b/docs/api/python/encoders/temporal_coding/contrast/step_forward.rst similarity index 67% rename from docs/api/python/encoding/temporal_coding/contrast/step_forward.rst rename to docs/api/python/encoders/temporal_coding/contrast/step_forward.rst index fcbed01..aabf4b0 100644 --- a/docs/api/python/encoding/temporal_coding/contrast/step_forward.rst +++ b/docs/api/python/encoders/temporal_coding/contrast/step_forward.rst @@ -2,7 +2,7 @@ .. title:: Step Forward -.. automodule:: spikify.encoding.temporal.contrast.step_forward_algorithm +.. automodule:: spikify.encoders.temporal.contrast.step_forward_algorithm :members: step_forward :undoc-members: :show-inheritance: diff --git a/docs/api/python/encoding/temporal_coding/contrast/threshold_based.rst b/docs/api/python/encoders/temporal_coding/contrast/threshold_based.rst similarity index 74% rename from docs/api/python/encoding/temporal_coding/contrast/threshold_based.rst rename to docs/api/python/encoders/temporal_coding/contrast/threshold_based.rst index c63a7fa..9d24a52 100644 --- a/docs/api/python/encoding/temporal_coding/contrast/threshold_based.rst +++ b/docs/api/python/encoders/temporal_coding/contrast/threshold_based.rst @@ -2,7 +2,7 @@ .. title:: Threshold Based Representation -.. automodule:: spikify.encoding.temporal.contrast.threshold_based_algorithm +.. automodule:: spikify.encoders.temporal.contrast.threshold_based_algorithm :members: threshold_based_representation :undoc-members: :show-inheritance: \ No newline at end of file diff --git a/docs/api/python/encoding/temporal_coding/contrast/zero_cross_step_forward.rst b/docs/api/python/encoders/temporal_coding/contrast/zero_cross_step_forward.rst similarity index 73% rename from docs/api/python/encoding/temporal_coding/contrast/zero_cross_step_forward.rst rename to docs/api/python/encoders/temporal_coding/contrast/zero_cross_step_forward.rst index fe13a09..e7bea65 100644 --- a/docs/api/python/encoding/temporal_coding/contrast/zero_cross_step_forward.rst +++ b/docs/api/python/encoders/temporal_coding/contrast/zero_cross_step_forward.rst @@ -2,7 +2,7 @@ .. title:: Zero Cross Step Forward -.. automodule:: spikify.encoding.temporal.contrast.zero_cross_step_forward_algorithm +.. automodule:: spikify.encoders.temporal.contrast.zero_cross_step_forward_algorithm :members: tzero_cross_step_forward :undoc-members: :show-inheritance: \ No newline at end of file diff --git a/docs/api/python/encoding/temporal_coding/deconvolution/ben_spiker.rst b/docs/api/python/encoders/temporal_coding/deconvolution/ben_spiker.rst similarity index 67% rename from docs/api/python/encoding/temporal_coding/deconvolution/ben_spiker.rst rename to docs/api/python/encoders/temporal_coding/deconvolution/ben_spiker.rst index cd8b74f..0eead44 100644 --- a/docs/api/python/encoding/temporal_coding/deconvolution/ben_spiker.rst +++ b/docs/api/python/encoders/temporal_coding/deconvolution/ben_spiker.rst @@ -2,7 +2,7 @@ .. title:: Bens Spiker -.. automodule:: spikify.encoding.temporal.deconvolution.bens_spiker_algorithm +.. automodule:: spikify.encoders.temporal.deconvolution.bens_spiker_algorithm :members: bens_spiker :undoc-members: :show-inheritance: diff --git a/docs/api/python/encoding/temporal_coding/deconvolution/hough_spiker.rst b/docs/api/python/encoders/temporal_coding/deconvolution/hough_spiker.rst similarity index 67% rename from docs/api/python/encoding/temporal_coding/deconvolution/hough_spiker.rst rename to docs/api/python/encoders/temporal_coding/deconvolution/hough_spiker.rst index 8c35823..0c4c9c2 100644 --- a/docs/api/python/encoding/temporal_coding/deconvolution/hough_spiker.rst +++ b/docs/api/python/encoders/temporal_coding/deconvolution/hough_spiker.rst @@ -2,7 +2,7 @@ .. title:: Hough Spiker -.. automodule:: spikify.encoding.temporal.deconvolution.hough_spiker_algorithm +.. automodule:: spikify.encoders.temporal.deconvolution.hough_spiker_algorithm :members: hough_spiker :undoc-members: :show-inheritance: \ No newline at end of file diff --git a/docs/api/python/encoding/temporal_coding/deconvolution/index.rst b/docs/api/python/encoders/temporal_coding/deconvolution/index.rst similarity index 100% rename from docs/api/python/encoding/temporal_coding/deconvolution/index.rst rename to docs/api/python/encoders/temporal_coding/deconvolution/index.rst diff --git a/docs/api/python/encoding/temporal_coding/deconvolution/modified_hough_spiker.rst b/docs/api/python/encoders/temporal_coding/deconvolution/modified_hough_spiker.rst similarity index 72% rename from docs/api/python/encoding/temporal_coding/deconvolution/modified_hough_spiker.rst rename to docs/api/python/encoders/temporal_coding/deconvolution/modified_hough_spiker.rst index c7bd369..68ea97d 100644 --- a/docs/api/python/encoding/temporal_coding/deconvolution/modified_hough_spiker.rst +++ b/docs/api/python/encoders/temporal_coding/deconvolution/modified_hough_spiker.rst @@ -2,7 +2,7 @@ .. title:: Modified Hough Spiker -.. automodule:: spikify.encoding.temporal.deconvolution.modified_hough_spiker_algorithm +.. automodule:: spikify.encoders.temporal.deconvolution.modified_hough_spiker_algorithm :members: modified_hough_spiker :undoc-members: :show-inheritance: \ No newline at end of file diff --git a/docs/api/python/encoding/temporal_coding/global_referenced/index.rst b/docs/api/python/encoders/temporal_coding/global_referenced/index.rst similarity index 100% rename from docs/api/python/encoding/temporal_coding/global_referenced/index.rst rename to docs/api/python/encoders/temporal_coding/global_referenced/index.rst diff --git a/docs/api/python/encoding/temporal_coding/global_referenced/phase_encoding.rst b/docs/api/python/encoders/temporal_coding/global_referenced/phase_encoding.rst similarity index 69% rename from docs/api/python/encoding/temporal_coding/global_referenced/phase_encoding.rst rename to docs/api/python/encoders/temporal_coding/global_referenced/phase_encoding.rst index 2a82ead..2e2a5a6 100644 --- a/docs/api/python/encoding/temporal_coding/global_referenced/phase_encoding.rst +++ b/docs/api/python/encoders/temporal_coding/global_referenced/phase_encoding.rst @@ -2,7 +2,7 @@ .. title:: Phase Encoding -.. automodule:: spikify.encoding.temporal.global_referenced.phase_encoding_algorithm +.. automodule:: spikify.encoders.temporal.global_referenced.phase_encoding_algorithm :members: phase_encoding :undoc-members: :show-inheritance: \ No newline at end of file diff --git a/docs/api/python/encoding/temporal_coding/global_referenced/time_to_first_spike.rst b/docs/api/python/encoders/temporal_coding/global_referenced/time_to_first_spike.rst similarity index 60% rename from docs/api/python/encoding/temporal_coding/global_referenced/time_to_first_spike.rst rename to docs/api/python/encoders/temporal_coding/global_referenced/time_to_first_spike.rst index 086d072..b67c52a 100644 --- a/docs/api/python/encoding/temporal_coding/global_referenced/time_to_first_spike.rst +++ b/docs/api/python/encoders/temporal_coding/global_referenced/time_to_first_spike.rst @@ -2,7 +2,7 @@ .. title:: Time to First Spike -.. automodule:: spikify.encoding.temporal.global_referenced.time_to_spike_algorithm +.. automodule:: spikify.encoders.temporal.global_referenced.time_to_first_spike_algorithm :members: time_to_first_spike :undoc-members: :show-inheritance: \ No newline at end of file diff --git a/docs/api/python/encoding/temporal_coding/index.rst b/docs/api/python/encoders/temporal_coding/index.rst similarity index 100% rename from docs/api/python/encoding/temporal_coding/index.rst rename to docs/api/python/encoders/temporal_coding/index.rst diff --git a/docs/api/python/encoders/temporal_coding/latency/burst_coding.rst b/docs/api/python/encoders/temporal_coding/latency/burst_coding.rst new file mode 100644 index 0000000..06b9d3d --- /dev/null +++ b/docs/api/python/encoders/temporal_coding/latency/burst_coding.rst @@ -0,0 +1,8 @@ +.. _burst_coding_function: + +.. title:: Burst Coding + +.. automodule:: spikify.encoders.temporal.latency.burst_coding_algorithm + :members: burst_coding + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/api/python/encoding/temporal_coding/latency/index.rst b/docs/api/python/encoders/temporal_coding/latency/index.rst similarity index 76% rename from docs/api/python/encoding/temporal_coding/latency/index.rst rename to docs/api/python/encoders/temporal_coding/latency/index.rst index 868f052..07f2e8b 100644 --- a/docs/api/python/encoding/temporal_coding/latency/index.rst +++ b/docs/api/python/encoders/temporal_coding/latency/index.rst @@ -7,11 +7,11 @@ The ``latency`` folder contains algorithms that encode information based on the Contents of the ``latency`` folder: -- **Burst Encoding**: A method that encodes information using the timing and intervals between multiple spikes in a burst, improving the accuracy of signal transmission. +- **Burst Coding**: A method that encodes information using the timing and intervals between multiple spikes in a burst, improving the accuracy of signal transmission. Below, you will find links to the specific modules for each latency coding algorithm: .. toctree:: :maxdepth: 1 - burst_encoding + burst_coding diff --git a/docs/api/python/encoding/rate_coding/poisson_rate.rst b/docs/api/python/encoding/rate_coding/poisson_rate.rst deleted file mode 100644 index b19bfff..0000000 --- a/docs/api/python/encoding/rate_coding/poisson_rate.rst +++ /dev/null @@ -1,8 +0,0 @@ -.. _poisson_rate_function: - -.. title:: Poisson Rate Algorithm - -.. automodule:: spikify.encoding.rate.poisson_rate_algorithm - :members: poisson_rate - :undoc-members: - :show-inheritance: \ No newline at end of file diff --git a/docs/api/python/encoding/temporal_coding/latency/burst_encoding.rst b/docs/api/python/encoding/temporal_coding/latency/burst_encoding.rst deleted file mode 100644 index a063089..0000000 --- a/docs/api/python/encoding/temporal_coding/latency/burst_encoding.rst +++ /dev/null @@ -1,8 +0,0 @@ -.. _burst_encoding_function: - -.. title:: Burst Encoding - -.. automodule:: spikify.encoding.temporal.latency.burst_encoding_algorithm - :members: burst_encoding - :undoc-members: - :show-inheritance: \ No newline at end of file diff --git a/docs/api/python/filtering/index.rst b/docs/api/python/filtering/index.rst deleted file mode 100644 index 4990210..0000000 --- a/docs/api/python/filtering/index.rst +++ /dev/null @@ -1,12 +0,0 @@ -.. _filtering: - -:octicon:`file-directory;0.9em;sd-mr-1 fill-primary` filtering -================================================================= - -The **filtering** module in the **spikify** library is designed to provide advanced filtering techniques inspired by biological systems. These filters enhance the quality of spike trains by pre-processing the raw data before encoding. - - -.. toctree:: - :maxdepth: 1 - - filterbank \ No newline at end of file diff --git a/docs/api/python/filtering/filterbank.rst b/docs/api/python/filters/filterbank.rst similarity index 64% rename from docs/api/python/filtering/filterbank.rst rename to docs/api/python/filters/filterbank.rst index 8aec94e..2c2e06b 100644 --- a/docs/api/python/filtering/filterbank.rst +++ b/docs/api/python/filters/filterbank.rst @@ -2,7 +2,7 @@ .. title:: FilterBank -.. autoclass:: spikify.filtering.filterbank.FilterBank +.. autoclass:: spikify.filters.filterbank.FilterBank :members: :undoc-members: :show-inheritance: \ No newline at end of file diff --git a/docs/api/python/filters/index.rst b/docs/api/python/filters/index.rst new file mode 100644 index 0000000..4087b90 --- /dev/null +++ b/docs/api/python/filters/index.rst @@ -0,0 +1,12 @@ +.. _filtering: + +:octicon:`file-directory;0.9em;sd-mr-1 fill-primary` filters +================================================================= + +The ``filters`` module in the **spikify** library is designed to provide advanced filtering techniques inspired by biological systems. These filters enhance the quality of spike trains by pre-processing the raw data before encoding. + + +.. toctree:: + :maxdepth: 1 + + filterbank \ No newline at end of file diff --git a/docs/api/python/index.rst b/docs/api/python/index.rst index 8f4ed28..c464368 100644 --- a/docs/api/python/index.rst +++ b/docs/api/python/index.rst @@ -19,5 +19,5 @@ Below, you will find links to the specific modules within the Python API: .. toctree:: :maxdepth: 1 - filtering/index - encoding/index + filters/index + encoders/index diff --git a/docs/changelog.rst b/docs/changelog.rst index 6e1d358..bca67e8 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -8,6 +8,36 @@ All notable changes to this project will be documented in this file. The format is based on `Keep a Changelog `__, and this project adheres to `Semantic Versioning `__. +1.0.0 +----- +:Released: 2026-04-14 +:Full Changelog: `0.3.0...1.0.0 `__ + +Improvements +............ +- Renamed ``filtering`` and ``encoding`` packages to ``filters`` and ``encoders`` for improved consistency and shorter import paths. +- Renamed ``burst encoding`` to ``burst coding`` to shorten the module path and align with conventional terminology. +- Renamed the global-referenced phase encoding and Poisson encoding functions for clarity and consistency. +- Enhanced ``FilterBank`` to enforce 2D signal processing, improve filter coefficient selection, and expand test coverage on center frequencies. +- Added automatic signal scaling and normalization to several encoding algorithms. +- Added FIR filter support to the BSA encoder along with updated parameters and documentation. +- Extended ``codemeta.json`` with auto-updating version, download link, and date fields. +- Added a PyPI-compatible README with updated logo and layout. + +Bug Fixes +......... +- Fixed the SF algorithm timestep calculation. +- Fixed the MW algorithm implementation. +- Fixed the HSA issue affecting spike generation correctness. +- Fixed the TBR output by replacing variation prepend with append to match the original algorithm setup. +- Fixed the ``codemeta.json`` version identifier. + +Documentation +............. +- Refactored and expanded documentation for TBR, BSA, HSA, SF, MW, ZCSF, burst coding, phase encoding, Poisson encoding, and contrast-based algorithms, including pseudocode and improved descriptions. +- Renamed documentation paths to reflect the new ``filters`` and ``encoders`` module structure. +- Clarified descriptions for global-referenced encoding algorithms and filter sections. + 0.3.0 ----- :Released: 2025-11-12 diff --git a/docs/citation.py b/docs/citation.py index dddee15..f588005 100644 --- a/docs/citation.py +++ b/docs/citation.py @@ -1,9 +1,7 @@ import requests -# Replace with your paper's DOI DOI = "10.3389/fnins.2022.999029" -# API endpoint for Semantic Scholar API_URL = f"https://api.semanticscholar.org/v1/paper/{DOI}" diff --git a/docs/conf.py b/docs/conf.py index 466b403..a620e98 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -18,7 +18,7 @@ now = datetime.datetime.now() author = "Benedetto Leto, Gianvito Urgese, Vittorio Fra, Riccardo Pignari" copyright = f"{now.year}, {author}." -version = "0.3.0" +version = "1.0.0" # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration diff --git a/docs/contributing.rst b/docs/contributing.rst index 26b5266..58312ed 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -8,11 +8,7 @@ This page provides a guide for developers wishing to contribute to **spikify**. Bugs, Features and PRs ----------------------- -For **bug reports** and well-described **technical feature requests**, please use our issue tracker: -|br| https://github.com/neuromorphic-polito/spikify/issues - -For **feature ideas** and **questions**, please use our discussion board: -|br| https://github.com/neuromorphic-polito/spikify/discussions +For **bug reports**, **technical feature requests**, **feature ideas**, and **questions**, please use our issue tracker: https://github.com/neuromorphic-polito/spikify/issues If you have already created a **PR**, you can send it in. Our CI workflow will check (test and code styles) and a maintainer will perform a review, before we can merge it. diff --git a/docs/index.rst b/docs/index.rst index 956efbd..5207062 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -142,8 +142,8 @@ Contents :caption: Fundamentals :maxdepth: 1 - spikify/filtering/index - spikify/encoding/index + spikify/filters/index + spikify/encoders/index .. toctree:: :caption: Development diff --git a/docs/installation.rst b/docs/installation.rst index 2b2f598..467e806 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -54,7 +54,7 @@ It's recommended to install spikify in a virtual environment to avoid conflicts python -m venv spikify-env source spikify-env/bin/activate # On Windows use `spikify-env\Scripts\activate` - pip install spikify + pip install innuce-spikify Getting code from source ------------------------ diff --git a/docs/spikify/encoding/index.rst b/docs/spikify/encoders/index.rst similarity index 69% rename from docs/spikify/encoding/index.rst rename to docs/spikify/encoders/index.rst index 4bdd836..e784814 100644 --- a/docs/spikify/encoding/index.rst +++ b/docs/spikify/encoders/index.rst @@ -3,11 +3,11 @@ Encoding ======== -The encoding techniques are essential for transforming continuous data into discrete spikes, enabling effective processing in spiking neural networks. With the increasing availability of neuromorphic, event-based sensors, such as silicon retina cameras, the need for efficient spike encoding has become more crucial. +The encoding techniques are essential for transforming continuous data into discrete spikes, enabling effective processing in spiking neural networks. With the increasing availability of neuromorphic, event-based sensors, such as silicon retina cameras, the need for efficient spike encoding has become more crucial. Two primary approaches exist for spike generation: -1. **Model-Based Encoding**: This approach relies on specific neuron models to produce spikes in response to continuous signals, aligning with the Representation Principle of the Neural Engineering Framework (NEF). +1. **Model-Based Encoding**: This approach relies on specific neuron models (e.g., leaky integrate-and-fire (LIF) neurons) to produce spikes in response to continuous signals, aligning with the Representation Principle of the Neural Engineering Framework (NEF). 2. **Algorithm-Based Encoding**: This approach, which is the focus of the **spikify** library, transforms continuous signals into discrete spikes using a variety of algorithms. This method ensures the full exploitation of neuro-inspired strategies, even in the absence of dedicated neuromorphic hardware. diff --git a/docs/spikify/encoding/rate/index.rst b/docs/spikify/encoders/rate/index.rst similarity index 100% rename from docs/spikify/encoding/rate/index.rst rename to docs/spikify/encoders/rate/index.rst diff --git a/docs/spikify/encoders/rate/poisson_rate.rst b/docs/spikify/encoders/rate/poisson_rate.rst new file mode 100644 index 0000000..25ce83b --- /dev/null +++ b/docs/spikify/encoders/rate/poisson_rate.rst @@ -0,0 +1,69 @@ +.. _poisson_algorithm_desc: + +Poisson Encoding +================ + +Poisson encoding is the classical and most widely used **rate coding** strategy for converting continuous signals or static values into discrete, irregular spike trains. It models spike generation as an **inhomogeneous Poisson process**, where the instantaneous firing rate :math:`r(t)` is directly proportional to the input intensity at time :math:`t`. + +This method is biologically inspired by the irregular, rate-modulated firing observed in many sensory neurons (e.g., retinal ganglion cells, auditory nerve fibers) and is a standard choice for benchmarking spiking neural networks (SNNs), especially when converting static datasets like made of images to spike trains. + +**Algorithm Overview** + +Spikes are generated independently with probability proportional to the input value and time bin size. The probability of observing exactly :math:`k` spikes in a small interval :math:`\Delta t` follows the Poisson distribution: + +.. math:: + + P(k; \Delta t) = \frac{\lambda^k e^{-\lambda}}{k!}, \quad \lambda = r(t) \cdot \Delta t + +where: + +- :math:`r(t)` is the instantaneous firing rate, usually scaled from the normalized input signal, +- :math:`k` is the expected number of spikes in the time window, +- :math:`\Delta t` is the discrete time step. + +**Detailed Pseudocode** + +.. code-block:: none + :linenos: + + Algorithm Poisson Encoding + input: s signal, Δt interval length + out = zeros(length(s)) + n_blocks = T // Δt + blocks = reshape(s, (n_blocks, Δt)) + rates = mean(blocks, axis=1) + for b = 0:Δt + if rate[b] > 0: + U = uniform(0,1, size=Δt) + ISI = -log(1 - U) / (rate[b] * Δt) + cumsum = cumulative_sum(ISI) + steps = searchsorted(bins, cumsum) - 1 + for s = 0:length(steps) + if steps[s] < Δt + t = b * Δt + step[s] + out[t] = 1 + end if + enf for + end if + end for + output: out + +**Advantages** + +- Biologically plausible — reproduces the irregular, rate-modulated firing observed in many cortical and sensory neurons. +- Simple, analytically tractable, and easy to implement. +- Standard for benchmarking SNNs (e.g., converting MNIST to spike trains for rate-based training). + +**Disadvantages** + +- Extremely dense spike trains — high energy consumption and synaptic events in neuromorphic hardware. +- Complete loss of precise temporal information — only average rate matters. +- Not event-driven — generates spikes even during constant input, unlike temporal contrast methods (:ref:`TBR `, :ref:`SF `, etc.). +- Less efficient than sparse, deterministic encodings (e.g., :ref:`TTFS `) for rapid processing or low-power applications. + +For details on how to implement this algorithm in Python, refer to the :ref:`Poisson Function `. + +**References** + +- Auge, D., Hille, J., Mueller, E., and Knoll, A. (2021). A survey of encoding techniques for signal processing in spiking neural networks. Neural Process. +- Liu, Q., Pineda-García, G., Stromatias, E., Serrano-Gotarredona, T., and Furber, S. B. (2016). Benchmarking spike-based visual recognition: a dataset and evaluation. Front. Neurosci. diff --git a/docs/spikify/encoding/temporal/contrast/index.rst b/docs/spikify/encoders/temporal/contrast/index.rst similarity index 100% rename from docs/spikify/encoding/temporal/contrast/index.rst rename to docs/spikify/encoders/temporal/contrast/index.rst diff --git a/docs/spikify/encoders/temporal/contrast/moving_window.rst b/docs/spikify/encoders/temporal/contrast/moving_window.rst new file mode 100644 index 0000000..f02f28f --- /dev/null +++ b/docs/spikify/encoders/temporal/contrast/moving_window.rst @@ -0,0 +1,60 @@ +.. _moving_window_algorithm_desc: + +Moving Window (MW) Encoding +================================ + +The Moving Window (MW) encoding uses a moving baseline with a set threshold value, where the baseline always equals the mean of the preceding signal values in a time window. Thus, the moving baseline is essentially the application of a moving average filter. If the signal value is above or below baseline ± threshold value, a positive or negative spike is registered. MW thus has two parameters: the threshold and the window size. Decoding is essentially the same as for TBR or SF. + +**Algorithm Overview** + +The algorithm computes a dynamic baseline as the mean of the most recent values within a fixed-size sliding window (or partial window at the beginning). The current signal value is then compared to this baseline ± a fixed threshold to determine whether to emit a spike: + +- Positive spike (+1) if s(t) > base + threshold +- Negative spike (-1) if s(t) < base - threshold + +The moving average acts as a low-pass filter, providing inherent smoothing and noise reduction. + +.. code-block:: none + :linenos: + + MW Encoding Algorithm + input: s signal, threshold, window, startpoint + startpoint = s(0) + out = zeros(length(s)) + base = zeros(length(s)) + for t = 0:(window) + if s(t) > base + threshold + out(t) = 1 + elseif s(t) < base - threshold + out(t) = -1 + end if + end for + for t = (window):length(s) + base = mean(s(t-window:t)) + if s(t) > base + threshold + out(t) = 1 + elseif s(t) < base - threshold + out(t) = -1 + end if + end for + output: out + +**Advantages** + +- Robust against white noise: the moving average baseline acts as an optimal time-domain smoothing filter for white noise. +- Can help attenuate certain artifact frequencies (e.g., power line noise) through appropriate window size selection. +- Adapts locally to signal level changes, suitable for non-stationary signals. + +**Disadvantages** + +- Trade-off between noise reduction (larger window) and preservation of high-frequency content (smaller window). +- Not recommended for strong narrowband interference (e.g., 50/60 Hz line noise); better to apply a dedicated band-stop filter before encoding. +- Requires tuning of two parameters (threshold and window size), increasing complexity compared to single-parameter methods. +- May delay response to rapid changes due to averaging over past values. + +For a practical implementation in Python, refer to the :ref:`Moving Window Function `. + +**References** + +- Kasabov, N., et al. (2016). "Neural Coding Strategies in Spiking Neural Networks." *Neural Processing Letters*. +- B. Petro, N. Kasabov and R. M. Kiss, "Selection and Optimization of Temporal Spike Encoding Methods for Spiking Neural Networks," in IEEE Transactions on Neural Networks and Learning Systems. \ No newline at end of file diff --git a/docs/spikify/encoders/temporal/contrast/step_forward.rst b/docs/spikify/encoders/temporal/contrast/step_forward.rst new file mode 100644 index 0000000..ba45c8d --- /dev/null +++ b/docs/spikify/encoders/temporal/contrast/step_forward.rst @@ -0,0 +1,64 @@ +.. _step_forward_algorithm_desc: + +Step Forward (SF) Encoding +================================ + +The Step Forward (SF) encoding utilizes an interval around a moving baseline with a set threshold. The initial baseline equals the initial signal value. If the next signal value is above or below baseline ± threshold, a positive or negative spike is registered, respectively, and the baseline is moved to the upper or lower limit of the threshold interval. The threshold is signal amplitude dependent and constitutes the only parameter of this encoding method. The decoding process reconstructs the moving baseline in a manner similar to TBR. + +**Algorithm Overview** + +SF starts with an initial baseline equal to the first signal value and uses a fixed threshold to decide when to emit spikes. For each subsequent time step, the current signal value is compared to the current baseline ± threshold: + +- If the signal exceeds **base + threshold**, a positive spike (+1) is generated and the baseline is updated to **base + threshold**. +- If the signal falls below **base - threshold**, a negative spike (-1) is generated and the baseline is updated to **base - threshold**. +- Otherwise, no spike is emitted and the baseline remains unchanged. + +This "step-forward" mechanism ensures that the baseline follows the signal in discrete jumps of size equal to the threshold, providing a staircase-like approximation of the original signal. + +**Detailed Pseudocode** + +.. code-block:: none + :linenos: + + SF Encoding Algorithm + input: s signal, threshold + base = s(0) + out = zeros(length(s)) + for t = 1 to length(s) + if s(t) > base + threshold + out(t) = 1 + base = base + threshold + elseif s(t) < base - threshold + out(t) = -1 + base = base - threshold + end if + end for + output: out + +**Advantages**: + +- Reconstructs most types of continuous signals exceptionally well in both time and frequency domains, including step-wise, smooth, and trended signals. +- Allows multiple steps to account for a single large change, enabling good representation of both small and large amplitude events (depending on threshold choice). +- Preserves the original frequency spectrum without introducing artifact frequency components. +- Introduces only minimal quantization-related noise; does not generate 1/frq ("pink") noise artifacts as seen in TBR. +- The moving baseline prevents drift in the reconstructed signal, even for longer sequences. +- No overshoot occurs, as baseline adjustments are exactly equal to the threshold. +- Exhibits wide, high-fit optimization plateaus across multiple metrics, allowing significant reduction in spike density (lower average firing rate – AFR) without major loss of accuracy. +- Lower AFR enhances data compression, reduces risk of saturation in spiking neural networks (SNN), and can improve noise suppression (though it may magnify quantization noise at low frequencies). +- Robust and straightforward parameter optimization with consistent performance across diverse signal types. + +**Disadvantages** + +- Reconstruction is inherently stepwise (piecewise constant), which may introduce quantization-like errors, particularly noticeable for very small or gradual changes. +- Small variations below the threshold are completely ignored. +- Like TBR, it encodes only signal changes, so offset errors may occur unless the initial value is correctly matched. +- Noise in the input signal is only minimally reduced by the threshold (mostly passed through rather than filtered). +- The threshold choice remains critical: too large → loss of detail; too small → excessive spikes and higher noise. + +For a practical implementation in Python, refer to the :ref:`Step Forward Function `. + +**References** + +- Kasabov, N., et al. (2016). "Neural Coding Strategies in Spiking Neural Networks." *Neural Processing Letters*. +- Delbruck, T., Lichtsteiner, P. (2007). "Artificial Retina: Applications of Image Processing with Spiking Neural Networks." *IEEE Transactions on Neural Networks*. +- B. Petro, N. Kasabov and R. M. Kiss, "Selection and Optimization of Temporal Spike Encoding Methods for Spiking Neural Networks," in IEEE Transactions on Neural Networks and Learning Systems. \ No newline at end of file diff --git a/docs/spikify/encoders/temporal/contrast/threshold_based.rst b/docs/spikify/encoders/temporal/contrast/threshold_based.rst new file mode 100644 index 0000000..5ce5184 --- /dev/null +++ b/docs/spikify/encoders/temporal/contrast/threshold_based.rst @@ -0,0 +1,79 @@ +.. _threshold_based_representation_algorithm_desc: + +Threshold-Based Representation (TBR) Encoding +============================================= + +Threshold-Based Representation (TBR) is one of the simplest and most foundational methods in temporal contrast encoding for spiking neural networks. TBR generates spikes by tracking significant temporal changes in the signal. A positive or negative spike is emitted when the variation between consecutive signal values exceeds a dynamically computed threshold, with polarity determined by the sign of the change. + +The threshold is adaptive to the signal's characteristics: it is computed over the entire sequence by taking the first-order differences, then setting the threshold as the mean of these variations plus a tunable factor multiplied by their standard deviation. This makes the encoding largely independent of absolute signal amplitude while effectively suppressing noise. The single hyperparameter factor controls sensitivity lower values preserve more variations, while higher values emit spikes only for substantial events. + +Decoding is straightforward: the original signal can be reconstructed by cumulatively summing the spikes (each scaled by the encoding threshold with appropriate sign), starting from the initial signal value. + +**Algorithm Overview** + +The TBR encoding method processes a signal (possibly with multiple channels), evaluating variations across each channel between consecutive time steps. The main steps are: + +1. **Compute Variations**: Calculate the difference between consecutive time steps for each channel. +2. **Define Threshold**: For each channel, compute the threshold using the formula: + + .. math:: + + \text{threshold} = \text{mean}(\text{diff}) + \text{factor} \cdot \text{std}(\text{diff}) + + where factor controls the noise-reduction band: + + - factor = 0: All signal variations are kept. + - 0 < factor ≤ 1: Small variations are filtered out, preserving major signal changes. + - factor > 1: Significant noise reduction; only major variations generate spikes. + +3. **Determine Spikes**: Emit +1 if diff > threshold, -1 if diff < -threshold, else 0. +4. **Construct the Spike Train**: Build the output spike train with values +1, -1, or 0. + +**Detailed Pseudocode** + +.. code-block:: none + :linenos: + + TBR Encoding Algorithm + input: s signal, f factor + startpoint = s(0) + diff = zeros(length(s)) + for t = 0:length(s)-1 + diff(t) = s(t+1) - s(t) + end for + diff(end) = diff(end-1) + threshold = mean(diff) + f*std(diff) + out = zeros(length(s)) + for t = 0:length(s) + if diff(t) > threshold + out(t) = 1 + elseif diff(t) < -threshold + out(t) = -1 + end if + end for + output: out, threshold + +**Advantages** + +- Simple and computationally efficient, originally designed for fast online/streaming processing. +- Effectively reduces small perturbations and white noise by thresholding minor variations. +- No artifacts (false spikes) at the start or end of the spike train or reconstructed signal. +- Amplitude-independent encoding; threshold adapts to signal characteristics. +- Straightforward and exact decoding via cumulative summation starting from the initial value. + +**Disadvantages** + +- Small, gradual changes are ignored; only large enough variations generate spikes. +- Poor representation of sudden step-wise changes due to uniform reconstruction step size equal to the threshold. +- Introduces scaling errors, especially prominent in trended signals. +- Trade-off in factor selection: low factor captures small events but includes noise; high factor misses small events but filters noise. +- Global threshold (computed over the entire sample) can be suboptimal for long signals with varying amplitude dynamics across different segments. +- Sensitive to strong white noise, which can mask gradual changes and introduce strong low-frequency (1/frq or "pink") artifacts during reconstruction, leading to drift in longer signals. +- Parameter optimization is challenging—multiple local minima and plateaus in error landscapes due to events of differing amplitudes, making automatic tuning unreliable without domain knowledge. + +For a practical implementation in Python, see the :ref:`Threshold Based Representation Function `. + +**References** + +- Delbruck, T., Lichtsteiner, P. (2007). "Artificial Retina: Applications of Image Processing with Spiking Neural Networks." *IEEE Transactions on Neural Networks*. +- B. Petro, N. Kasabov and R. M. Kiss, "Selection and Optimization of Temporal Spike Encoding Methods for Spiking Neural Networks," in IEEE Transactions on Neural Networks and Learning Systems. \ No newline at end of file diff --git a/docs/spikify/encoders/temporal/contrast/zero_cross_step_forward.rst b/docs/spikify/encoders/temporal/contrast/zero_cross_step_forward.rst new file mode 100644 index 0000000..7558389 --- /dev/null +++ b/docs/spikify/encoders/temporal/contrast/zero_cross_step_forward.rst @@ -0,0 +1,64 @@ +.. _zero_cross_step_forward_algorithm_desc: + +Zero-Crossing Step-Forward (ZCSF) Encoding +============================================== + +The Zero-Crossing Step-Forward (ZCSF) encoding is a minimalist temporal contrast encoding technique that focuses exclusively on significant positive excursions of the signal. It applies half-wave rectification (setting all negative values to zero) followed by simple threshold-based spike generation, producing only positive spikes (+1) when the rectified signal exceeds a predefined threshold. + +This approach draws conceptual inspiration from the rich body of work on **zero-crossing-based signal analysis**. + +**Algorithm Overview** + +The ZCSF algorithm encodes signals by generating spikes based on the condition of zero-crossing with respect to a predefined threshold. The key steps in the ZCSF encoding are: + +1. **Signal Rectification**: Convert all negative signal values to zero. This step is known as half-wave rectification and ensures that only positive parts of the signal are considered for spike generation. + + .. math:: + + \text{rectified signal} = \max(0, \text{signal}) + +2. **Threshold Definition**: Use a predefined threshold value (`threshold`) to determine when a spike should be emitted. The threshold is typically set based on the specific characteristics of the input signal or application requirements. + +3. **Spike Generation**: Emit a spike (+1) when the rectified signal exceeds the threshold. Unlike other encoding schemes, ZCSF does not consider negative spikes, and only positive spikes are generated when the signal value is higher than the threshold. + +**Detailed Pseudocode** + +.. code-block:: none + :linenos: + + ZCSF Encoding Algorithm + input: s signal, threshold + out = zeros(length(s)) + for t = 0 to length(s) + if s(t) < 0 + s(t) = 0 + end if + end for + for t = 0 to length(s) + if s(t) > threshold + out(t) = 1 + end if + end for + output: out + +**Advantages** + +- Extreme computational simplicity and speed — ideal for fast online processing and resource-constrained systems. +- Drastic data reduction: transforms dense continuous signals into highly sparse spike trains, capturing only salient positive events. +- Complete suppression of negative fluctuations and noise — excellent for applications where only upward changes or events are relevant. +- No initialization artifacts, no drift, and no boundary effects (unlike moving-window or baseline-tracking methods). +- Provides a fast, economical way to detect and represent dominant positive features or events in signals. + +**Disadvantages** + +- Total loss of all negative signal information — unsuitable for oscillatory, bipolar, or symmetric signals. +- Performance heavily depends on appropriate threshold selection; poor choice can either miss small events or include excessive noise. +- Lacks the adaptivity of baseline-updating methods (SF, MW) for signals with large amplitude variations. +- Theoretical results on zero-crossing counts (e.g., spectral equivalence) assume certain stationarity and Gaussian-like properties, though practical performance is often robust. + +For a practical implementation in Python, see the :ref:`Zero Cross Step Forward Function `. + +**References** + +- Wiren, A., Stubbs, A. (1956). "Zero-Crossing Techniques for Signal Processing." *Journal of Applied Signal Processing*. +- Kedem, B. (1986). "Spectral Analysis of Point Processes." *IEEE Transactions on Acoustics, Speech, and Signal Processing*. diff --git a/docs/spikify/encoders/temporal/deconvolution/ben_spiker.rst b/docs/spikify/encoders/temporal/deconvolution/ben_spiker.rst new file mode 100644 index 0000000..d38bf48 --- /dev/null +++ b/docs/spikify/encoders/temporal/deconvolution/ben_spiker.rst @@ -0,0 +1,75 @@ +.. _bens_spiker_algorithm_desc: + +Ben's Spiker Algorithm (BSA) Encoding +========================================= + +An analog signal can be constructed from a spike train by convolution with an FIR filter. BSA (Ben's Spiker Algorithm) is an algorithm for producing the spike train from which the original signal can be reconstructed well. BSA works only for positive-valued signals. First, an FIR filter is created. Then, two error terms are calculated at each time point: one that results from subtracting the filter coefficients from the subsequent signal values, and one that results from not changing the signal. If the subtraction error is smaller than the unchanged signal error term subtracted by a factor, a positive spike is generated and the filter coefficients are subtracted from the signal. Decoding is straightforward since it was kept in mind during the encoding: a convolution of the spike train with the filter coefficients gives the reconstructed signal. + +BSA encoding results in a unipolar (only positive) spike train. The original BSA encoding requires input with [0, 1] limits. However, BSA can be applied to any positive-valued signal if the filter coefficients are scaled up such that they appropriately match the signal boundaries. Therefore, a simple signal shift above zero is sufficient. + +**Algorithm Overview** + +BSA iteratively detects spikes by comparing two cumulative error metrics over a sliding segment of length equal to the FIR filter: + +- **error1**: sum of absolute differences between the current signal segment and the filter coefficients +- **error2**: sum of absolute values of the current signal segment + +A spike is generated if: + +.. math:: + + \text{error1} \leq \text{error2} - \text{threshold} + +When a spike is detected, the filter is subtracted from the corresponding segment of the signal, effectively removing the detected spike pattern for subsequent iterations. + +**Detailed Pseudocode** + +.. code-block:: none + :linenos: + + BSA Encoding Algorithm + input: s signal, fir filter, thr threshold + L = length(s) + F = length(fir) + out = zeros(L) + shift = min(s) + s = s - shift + for t = 0:(L-F+1) + err1 = 0 + err2 = 0 + for k = 0:F + err1 = err1 + abs(s(t+k) - fir(k)) + err2 = err2 + abs(s(t+k)) + end for + if err1 <= (err2 - thr) + out(t) = 1 + for k = 1:F + s(t+k) = s(t+k) - fir(k) + end for + end if + end for + output: out, shift + +**Advantages** + +- Designed with reconstruction in mind: the original signal can be well recovered via simple FIR convolution. +- Robust representation of continuously changing, smooth, and trended signals. +- Simultaneously performs filtering during encoding. +- Allows flexible scaling of filter coefficients to improve dynamic range and reduce saturation. +- Good SNR for signals within the designed filter bandwidth. + +**Disadvantages** + +- Only suitable for positive-valued signals (requires shifting/preprocessing for general inputs). +- Poor performance on constant plateaus and step-like changes: requires constant firing to maintain nonzero values, which can fail or saturate, especially at higher levels. +- Significant errors at signal start (catch-up from 0) and end (convolution tail equal to filter length) — can cause substantial information loss in short signals. +- Introduces offset and scaling errors in reconstruction depending on optimization. +- Can generate low-frequency artifact components, especially with large filter sizes. +- Non-smooth optimization landscape with multiple local peaks — parameter search (cutoff, filter size, threshold) is nontrivial. +- Higher plateaus are particularly problematic if filter coefficients are not scaled up appropriately (e.g., sum of coefficients ≈ 2 × signal max recommended). + +**References** + +- Schrauwen, B., Van Campenhout, J. (2003). "BSA: An Efficient Algorithm for Time-Critical Signal Processing." *Neurocomputing*. +- Petro, P., et al. (2020). "Revisiting the BSA for Modern Applications." *Signal Processing Letters*. +- B. Petro, N. Kasabov and R. M. Kiss, "Selection and Optimization of Temporal Spike Encoding Methods for Spiking Neural Networks," in IEEE Transactions on Neural Networks and Learning Systems. \ No newline at end of file diff --git a/docs/spikify/encoders/temporal/deconvolution/hough_spiker.rst b/docs/spikify/encoders/temporal/deconvolution/hough_spiker.rst new file mode 100644 index 0000000..a638078 --- /dev/null +++ b/docs/spikify/encoders/temporal/deconvolution/hough_spiker.rst @@ -0,0 +1,74 @@ +.. _hough_spiker_algorithm_desc: + +Hough Spiker (HSA) Encoding +============================ + +The Hough Spiker Algorithm (HSA) is a parameter-free technique for converting non-negative analog signals (normalized to [0, 1]) into unipolar spike trains that can be reconstructed via convolution with the same FIR filter used during encoding. At each time step, the algorithm compares the shifted FIR filter impulse response pointwise with the matching segment of the input signal. If fir filter is everywhere smaller or equal to segment signal, a positive spike is emitted, and fir filter is subtracted from the signal to remove the detected pattern. This process is repeated sequentially across the signal. + +HSA assumes a FIR filter with non-negative coefficients (e.g., low-pass filters without negative taps), as negative values would cause the algorithm to fail. The input range is limited to [0, 1], so signals are typically shifted and normalized if necessary. + +**Algorithm Overview** + +The core idea is to reverse the decoding convolution: iteratively detect and subtract filter patterns that "fit" under the signal curve. + +1. **Pointwise Comparison**: + For each possible starting time, check whether the signal values in the window are greater than or equal to the corresponding filter coefficients at every single position. + +2. **Spike Emission and Subtraction**: + If the condition holds (i.e., signal ≥ filter everywhere in the window), emit a positive spike (+1) at time t and subtract the filter coefficients from the signal segment. + +This results in a sparse unipolar spike train, but reconstruction is biased downward due to the strict "≥ filter" condition. + +**Detailed Pseudocode** + +.. code-block:: none + :linenos: + + Algorithm HSA Encoding + input: s signal, fir filter + L = length(s) + F = length(h) + out = zeros(L) + for t = 0 to L-F+1 + count = 0 + for j = 0 to F + if t+j < L + if s(t+j) >= fir(j) + count += 1 + end if + end if + end for + if count == F + out(t) = 1 + for j = 0 to F + if t+j < L + s(t+j) -= fir(j) + end if + end for + end if + end for + output: out + +**Advantages** + +- Extremely simple and parameter-free — no tuning required beyond filter design. +- Computationally lightweight and suitable for real-time encoding. +- Produces unipolar spike trains reconstructible via simple FIR convolution. +- Effective for signals within the filter's bandwidth, with minimal external parameters. +- No threshold optimization needed, unlike modified HSA or BSA. + +**Disadvantages** + +- Limited to non-negative FIR filters — cannot use steep filters with negative taps, restricting bandwidth and sharpness. +- Reconstruction is biased: converted signal always stays below the original due to strict "≥ filter" condition. +- Restricted to non-negative FIR filters — limits filter design options. +- Errors increase with higher signal values and persist over time (cumulative bias). +- Requires non-negative, [0, 1]-normalized inputs — preprocessing (shift/normalize) may be needed, potentially introducing artifacts. +- Less flexible than threshold-based variants (modified HSA) or error-minimizing methods (BSA) for diverse signals. + +For a practical implementation in Python, see the :ref:`Hough Spiker Function `. + +**References** + +- Schrauwen, B., Van Campenhout, J. (2003). "HSA: A Progressive Subtraction Technique for Spike Detection." *Neurocomputing*. +- Petro, P., et al. (2020). "Revisiting the HSA for Modern Applications." *Signal Processing Letters*. diff --git a/docs/spikify/encoding/temporal/deconvolution/index.rst b/docs/spikify/encoders/temporal/deconvolution/index.rst similarity index 74% rename from docs/spikify/encoding/temporal/deconvolution/index.rst rename to docs/spikify/encoders/temporal/deconvolution/index.rst index 2049f42..5669dc3 100644 --- a/docs/spikify/encoding/temporal/deconvolution/index.rst +++ b/docs/spikify/encoders/temporal/deconvolution/index.rst @@ -11,8 +11,6 @@ The primary methods in this category include: - **Modified-HSA and Ben's Spiker Algorithm (BSA)**: Building upon the HSA, Schrauwen and Van Campenhout (2003) introduced modifications that enhance the robustness and accuracy of the spike generation process. These algorithms refine the spike encoding by adjusting the convolutional filters in a subtractive manner. -Like the Zero-Crossing Step Forward (ZCSF) method, the deconvolution-based techniques produce unipolar spikes, where the presence of spikes indicates positive features of the original signal. This class of algorithms is particularly valuable in scenarios where the precise reconstruction of the original signal's temporal features is critical. - .. toctree:: :maxdepth: 1 diff --git a/docs/spikify/encoders/temporal/deconvolution/modified_hough_spiker.rst b/docs/spikify/encoders/temporal/deconvolution/modified_hough_spiker.rst new file mode 100644 index 0000000..30ec947 --- /dev/null +++ b/docs/spikify/encoders/temporal/deconvolution/modified_hough_spiker.rst @@ -0,0 +1,76 @@ +.. _modified_hough_spiker_algorithm_desc: + +Modified Hough Spiker (MHSA) Encoding +========================================= + +The Modified Hough Spiker Algorithm (MHSA) is a threshold-based improvement over the original Hough Spiker Algorithm (HSA). + +The original HSA is very strict: it only emits a spike if the signal is greater than or equal to the FIR filter coefficients at **every single point** in the current window. Even a tiny dip below the filter at one position prevents spike detection. + +MHSA relaxes this rule by allowing a small amount of accumulated error where the signal falls below the filter. It calculates the total shortfall (sum of the positive differences where the filter exceeds the signal) across the window. If this total error stays below a predefined threshold, a spike is still emitted, and the filter is subtracted from the signal segment as usual. + +This modification makes the algorithm more flexible and robust to noise, variations, or imperfect matches between signal and filter shape. + +Like HSA, MHSA requires: + +- Non-negative FIR filter coefficients (negative values cause failure) +- Input signal normalized to [0, 1] (automatic shifting and normalization are typically applied) + +**Algorithm Overview** + +1. **Error Accumulation** + For each possible starting time in the signal, compute the accumulated error as the sum of (filter - signal) only where filter > signal (positive differences only). + +2. **Threshold Check** + If the total accumulated error ≤ threshold, emit a positive spike at that time and subtract the filter pattern from the signal segment. + +3. **Iteration** + Continue with the updated signal until the end of the sequence. + +**Detailed Pseudocode** + +.. code-block:: none + :linenos: + + MHSA Encoding Algorithm + input: s signal, fir filter, thr threshold + L = length(s) + F = length(h) + out = zeros(L) + for t = 0 to L-F + error = 0 + for j = 0 to F + if t+j <= L + error += max(0, fir(j) - s(t+j)) + end if + end for + if error <= thr + out(t) = 1 + for j = 0 to F + if t+j <= L + s(t+j) -= fir(j) + end if + end for + end if + end for + output: out + +**Advantages** + +- Significantly better reconstruction quality than original HSA. +- Much more flexible — detects spikes even with small noise or shape variations. +- Smooth threshold optimization landscape — less sensitive to exact threshold value. + +**Disadvantages** + +- Reconstruction biased high (tends to exceed original signal) due to relaxed error tolerance. +- Errors larger at higher signal amplitudes and accumulate over time. +- Still restricted to non-negative FIR filters — limits filter design options. +- Input preprocessing (shift/normalize to [0, 1]) may introduce artifacts for arbitrary signals. + +For a practical implementation in Python, see the :ref:`Modified Hough Spiker Function `. + +**References** + +- Schrauwen, B., Van Campenhout, J. (2003). "HSA: A Progressive Subtraction Technique for Spike Detection." *Neurocomputing*. +- Petro, P., et al. (2021). "Modified HSA with Thresholding for Enhanced Spike Detection." *Signal Processing Letters*. \ No newline at end of file diff --git a/docs/spikify/encoding/temporal/global_referenced/index.rst b/docs/spikify/encoders/temporal/global_referenced/index.rst similarity index 100% rename from docs/spikify/encoding/temporal/global_referenced/index.rst rename to docs/spikify/encoders/temporal/global_referenced/index.rst diff --git a/docs/spikify/encoders/temporal/global_referenced/phase_encoding.rst b/docs/spikify/encoders/temporal/global_referenced/phase_encoding.rst new file mode 100644 index 0000000..95bd4cc --- /dev/null +++ b/docs/spikify/encoders/temporal/global_referenced/phase_encoding.rst @@ -0,0 +1,61 @@ +.. _phase_encoding_algorithm_desc: + +Phase Encoding (PE) +============================ + +Phase Encoding (PE) is a temporal coding technique that encodes information by evaluating the phase angle of an input signal. This method leverages the relative timing of spikes to the phase of these rhythms to significantly boost the information carried by spike patterns and stabilize representations against noise. + +In implementations like the one in this library, the signal is rectified, normalized, and the mean per block is mapped to a phase angle via arcsin. The phase is quantized into discrete levels (using β fractional bits as the oscillatory reference) and converted to a binary spike pattern, producing unipolar spikes. + +**Algorithm Overview** + +1. **Normalization**: Rectify and normalize the signal to [0, 1] per channel. + +2. **Phase Calculation**: Compute phase as arcsin(mean) for block mean intensity. + +3. **Quantization**: Map phase to discrete levels in [0, 2^num_bits - 1]. + +4. **Binary Representation**: Convert level to binary bits (right-shifted & masked), forming the spike pattern. + +**Detailed Pseudocode** + +.. code-block:: none + :linenos: + + Phase Encoding Algorithm + input: s signal (length T), num_bits (block size) + + out = zeros(T) + + n_blocks = T // num_bits + block_means = mean(s over blocks of size num_bits) + + bins = linspace(0, π/2, 2^num_bits + 1) + + for block_idx = 0 to n_blocks-1 + mean = block_means[block_idx] + phase = arcsin(mean) + level = searchsorted(bins, phase) + level = clip(level, 0, 2^num_bits - 1) + bits = (level >> arange(num_bits-1, -1, -1)) & 1 + global_start = block_idx * num_bits + out[global_start : global_start + num_bits] = bits + end for + + output: out + +**Advantages** + +- Stabilizes representations against sensory noise by nesting spikes in low-frequency rhythms. +- Biologically plausible for sensory cortices. + +**Disadvantages** + +- Quantization limits precision; sensitive to normalization and bit depth. + +For a practical implementation in Python, see the :ref:`Phase Encoding Function `. + +**References** + +- Montemurro, M. A., et al. (2008). "Phase-of-firing coding of natural visual stimuli in primary visual cortex." *Current Biology*. +- Kim, S., et al. (2018). "Deep neural networks with weighted spikes." *Neurocomputing*. diff --git a/docs/spikify/encoders/temporal/global_referenced/time_to_first_spike.rst b/docs/spikify/encoders/temporal/global_referenced/time_to_first_spike.rst new file mode 100644 index 0000000..6868276 --- /dev/null +++ b/docs/spikify/encoders/temporal/global_referenced/time_to_first_spike.rst @@ -0,0 +1,69 @@ +.. _time_to_first_spike_algorithm_desc: + +Time-to-First-Spike (TTFS) +====================================== + +Time-to-First-Spike (TTFS) encoding is a sparse temporal coding strategy where input intensity is represented by the **timing of a single first spike** within a fixed time window. Stronger inputs trigger earlier spikes, enabling ultra-rapid information transmission with minimal spikes (typically one per neuron or input block). + +This method is biologically plausible for fast sensory responses (e.g., tactile stimuli in milliseconds) and is highly efficient for neuromorphic hardware, as it requires very few synaptic events. It approximates integration against an exponentially decaying threshold, with spike latency inversely related to input strength. + +**Algorithm Overview** + +TTFS uses a dynamic, exponentially decaying threshold to model membrane potential: + +.. math:: + + P_{th}(t) = \theta_0 e^{-t / \tau_{th}} + +A spike is emitted at the earliest time t where the normalized input exceeds the threshold. In practice, intensity is mapped to latency via an inverse (logarithmic) function, and the signal is processed in blocks with one spike per block. + +**Detailed Pseudocode** + +.. code-block:: none + :linenos: + + Time-to-First-Spike Encoding Algorithm + input: s signal (length T), interval_length (block size) + + out = zeros(T) + + n_blocks = T // interval_length + block_means = mean(s over blocks of size interval_length) + + bins = linspace(0, 1, interval_length) + + for block_idx = 0 to n_blocks-1 + intensity = block_means[block_idx] + if intensity > 0 + latency_norm = 0.1 * log(1 / intensity) + latency_norm = clip(latency_norm, 0, 2) + + step = searchsorted(bins, latency_norm) + + t = block_idx * interval_length + clip(step, 0, interval_length-1) + out[t] = 1 + end if + end for + + output: out + +**Advantages** + +- Extremely sparse (one spike per block/neuron) — minimal energy/synaptic events. +- Biologically plausible for rapid sensory processing. +- Robust in population coding (relative latencies preserved despite jitter). + +**Disadvantages** + +- Limited information per neuron (single spike) — requires population for precision. +- Sensitive to threshold/decay parameters and block length. +- Quantization errors from discrete binning. +- Poor for slowly varying or constant inputs (late/no spike). + +For a practical implementation in Python, see the :ref:`Time-to-First-Spike Function `. + +**References** + +- Rueckauer, et al. (2017). "Conversion of analog to spiking neural networks using sparse temporal coding." *ICAS*. +- Park, S., et al. (2020). “T2FSNN: deep spiking neural networks with time-to-first-spike coding,” *DAC*. +- Lisman, J. E. (1997). Bursts as a unit of neural information: Making unreliable synapses reliable. *Trends Neurosci*. diff --git a/docs/spikify/encoding/temporal/index.rst b/docs/spikify/encoders/temporal/index.rst similarity index 100% rename from docs/spikify/encoding/temporal/index.rst rename to docs/spikify/encoders/temporal/index.rst diff --git a/docs/spikify/encoders/temporal/latency/burst_coding.rst b/docs/spikify/encoders/temporal/latency/burst_coding.rst new file mode 100644 index 0000000..26d6a8e --- /dev/null +++ b/docs/spikify/encoders/temporal/latency/burst_coding.rst @@ -0,0 +1,75 @@ +.. _burst_coding_algorithm_desc: + +Burst Coding (BC) +=================== + +Burst Coding is a biologically inspired rate-like coding strategy that represents input intensity using **two characteristics** of a spike burst: the **number of spikes** (:math:`N_s`) and the **inter-spike interval** (ISI). Stronger inputs produce bursts with more spikes and shorter ISIs, while weaker inputs produce fewer spikes with longer ISIs. + +This dual-parameter encoding increases reliability and information density, as it leverages both spike count and precise timing within short bursts. It is particularly effective for rapid, robust transmission in sensory pathways and spiking neural networks (SNNs). + +**Algorithm Overview** + +The input intensity P (normalized to [0, 1]) determines: + +- Number of spikes per burst: + + .. math:: + N_s(P) = \lceil P \cdot N_{\max} \rceil + +- Inter-spike interval (ISI): + + .. math:: + \text{ISI}(P) = + \begin{cases} + t_{\max} & \text{if } N_s = 1 \\ + \left\lceil (t_{\max} - t_{\min}) \cdot (1 - P) + t_{\min} \right\rceil & \text{otherwise} + \end{cases} + +Larger P produces bursts with more spikes and smaller ISIs. The burst is placed at regular intervals within a time window sufficient to accommodate the longest burst. + +**Detailed Pseudocode** + +.. code-block:: none + :linenos: + + Burst Coding Algorithm + input: s signal (normalized P ∈ [0,1]), N_max max spikes, t_min min ISI, t_max max ISI, interval_length output window + out = zeros(length(s)) + + n_blocks = length(s) + for block_idx = 0 to n_blocks-1 + P = mean(s[block_idx * interval_length : (block_idx+1) * interval_length]) + + N_s = ceil(P * N_max) + ISI = ceil((t_max - t_min) * (1 - P) + t_min) + + + spike_times = arange(0, N_s * (ISI + 1), ISI + 1) + spike_times = spike_times[spike_times < interval_length] + + for time in spike_times: + global_t = block_idx * interval_length + time + out[global_t] = 1 + end for + end for + + output: out + +**Advantages** + +- Higher information density and reliability in which both count and timing are used. +- Biologically plausible — mimics burst firing in sensory neurons for rapid, robust transmission. +- More robust to noise. +- Efficient for classification/recognition tasks. + +**Disadvantages** + +- Requires sufficient time window per burst. +- Sensitive to parameter choice (N_max, t_min/t_max) and block length. +- Reconstruction more complex than pure rate (needs both spike count and ISI measurement). + +For a practical implementation in Python, see the :ref:`Burst Coding Function `. + +**References** + +- Guo et al. (2021). "Neural coding in spiking neural networks: a comparative study for robust neuromorphic systems" *Front. Neurosci.* diff --git a/docs/spikify/encoding/temporal/latency/index.rst b/docs/spikify/encoders/temporal/latency/index.rst similarity index 72% rename from docs/spikify/encoding/temporal/latency/index.rst rename to docs/spikify/encoders/temporal/latency/index.rst index 1ae7b9c..168849b 100644 --- a/docs/spikify/encoding/temporal/latency/index.rst +++ b/docs/spikify/encoders/temporal/latency/index.rst @@ -7,9 +7,9 @@ Latency and inter-spike interval (ISI) are critical components in neural communi This class of encoding algorithms, known as Latency/ISI, leverages both the timing and number of spikes to improve the reliability and richness of the transmitted information. By incorporating the latency and ISI into the encoding process, these algorithms can capture more nuanced aspects of the input signal, enhancing the neural code's fidelity and precision (Izhikevich et al., 2003). -A prime example of this class is Burst Encoding, which utilizes the burst of spikes and the intervals between them to effectively encode complex information. This approach is particularly advantageous in neural systems where temporal dynamics are key to processing sensory information or triggering specific neural responses. +A prime example of this class is Burst Coding, which utilizes the burst of spikes and the intervals between them to effectively encode complex information. This approach is particularly advantageous in neural systems where temporal dynamics are key to processing sensory information or triggering specific neural responses. .. toctree:: :maxdepth: 1 - burst_encoding \ No newline at end of file + burst_coding \ No newline at end of file diff --git a/docs/spikify/encoding/rate/poisson_rate.rst b/docs/spikify/encoding/rate/poisson_rate.rst deleted file mode 100644 index d0f0d48..0000000 --- a/docs/spikify/encoding/rate/poisson_rate.rst +++ /dev/null @@ -1,52 +0,0 @@ -.. _poisson_rate_algorithm_desc: - -Poisson Rate Encoding -====================== - -Poisson rate encoding is a neural encoding strategy used to convert continuous signals into discrete spike trains using a Poisson distribution. This method is effective in neural modeling, particularly in rate coding strategies where the spike rate corresponds to the intensity of the input signal. - -**Algorithm Overview**: - -The Poisson rate encoding uses a Poisson distribution to model the probability of emitting a certain number of spikes (:math:`n`) over a given time interval (:math:`\Delta t`). The probability of having :math:`n` spikes in an interval :math:`\Delta t` is described by the formula: - -.. math:: - - P_{n}(\Delta t) = \frac{(r \Delta t)^n}{n!} e^{-r \Delta t} \quad (1) - -where: - -- :math:`r` is the spike rate, representing the real value to be encoded, -- :math:`n` is the number of spikes, -- :math:`\Delta t` is the time interval. - -**Implementation Steps**: - -The implementation of this algorithm can be performed through the following steps: - -1. **Define the Time Interval** (:math:`\Delta t`): Determine the interval within which to generate the spike train. -2. **Generate a Sequence of Random Numbers** (:math:`x \in [0, 1] \subset \mathbb{R}`): Create a series of random numbers uniformly distributed in the range `[0, 1]`. -3. **Compute Spike Times** (:math:`t_i`): Starting from :math:`t = 0`, calculate the spike times :math:`t_i` as: - - .. math:: - - t_i = t_{i-1} + ISI_i \quad \text{for } i \geq 1 \quad (2) - - where the inter-spike interval (:math:`ISI_i`) is defined as: - - .. math:: - - ISI_i = -\frac{\log(1 - x_i)}{r} \quad (3) - - This represents the :math:`i`-th inter-spike interval, the time interval in which the probability of having `n = 0` spikes equals :math:`x_i`. -4. **Generate Spikes**: A spike is emitted at each calculated spike time :math:`t_i` until :math:`t_i > \Delta t`. - -**Applications**: - -Poisson rate encoding is utilized in various neural network models that require a spike-based representation of continuous input data. This encoding method is particularly useful in simulating sensory neurons and other neural circuits where input signals need to be transformed into spike trains suitable for spiking neural networks (SNNs). - -For details on how to implement this algorithm in Python, refer to the :ref:`Poisson Rate Function `. - -**References**: - -- Auge, J., et al. (2021). "Rate Coding Strategies in Neural Networks." *Neural Computation*. -- Liu, Y., et al. (2016). "Poisson Spike Train Generation for Neural Computation." *Neural Networks*. diff --git a/docs/spikify/encoding/temporal/contrast/moving_window.rst b/docs/spikify/encoding/temporal/contrast/moving_window.rst deleted file mode 100644 index 3b17d8f..0000000 --- a/docs/spikify/encoding/temporal/contrast/moving_window.rst +++ /dev/null @@ -1,44 +0,0 @@ -.. _moving_window_algorithm_desc: - -Moving Window Encoding -======================= - -The Moving Window (MW) encoding algorithm is another strategy used in neural modeling to convert continuous signals into discrete spike trains. Unlike the Poisson rate encoding, which relies on a probabilistic approach, the Moving Window method uses a sliding window mechanism and thresholding to detect spikes. - -**Algorithm Overview**: - -The Moving Window algorithm utilizes a sliding window approach to compute the average value (referred to as the "Base") of the signal within a window of fixed length. It then uses a threshold value to determine when spikes occur based on deviations from this baseline. The formulas used in this algorithm are: - -.. math:: - - \text{Threshold} = \text{mean}(\text{Variation}) \quad (5) - -.. math:: - - \text{Base} = \text{mean}(\text{Signal}[1:\text{Window}]) \quad (6) - -where: - -- **Threshold**: The mean variation within the signal. -- **Base**: The mean of the signal values within the current sliding window. - -**Implementation Steps**: - -To implement this algorithm, follow these steps: - -1. **Set the Sliding Window Length** (:math:`\text{Window Length}`): Define the window length within which the baseline (`Base`) is computed. -2. **Calculate the Base**: For each position in the signal, compute the mean value (`Base`) of the signal values within the current window. If the window extends beyond the start of the signal, use the available values up to the current position. -3. **Apply the Threshold**: For each signal value at time :math:`t`, determine whether a spike occurs by comparing the value to `Base + Threshold` and `Base - Threshold`. - - If the signal exceeds `Base + Threshold`, a positive spike (+1) is generated. - - If the signal falls below `Base - Threshold`, a negative spike (-1) is generated. -4. **Generate the Spike Train**: Construct the spike train by marking the time points where spikes occur. - -**Advantages**: - -The Moving Window algorithm is more robust to noise compared to the TBR (Time-Based Rate) method because it directly relies on the signal's absolute value rather than its variation. This approach helps to reduce false positives and negatives in noisy conditions (Kasabov et al., 2016). - -For a practical implementation in Python, refer to the :ref:`Moving Window Function `. - -**References**: - -- Kasabov, N., et al. (2016). "Neural Coding Strategies in Spiking Neural Networks." *Neural Processing Letters*. diff --git a/docs/spikify/encoding/temporal/contrast/step_forward.rst b/docs/spikify/encoding/temporal/contrast/step_forward.rst deleted file mode 100644 index f803737..0000000 --- a/docs/spikify/encoding/temporal/contrast/step_forward.rst +++ /dev/null @@ -1,44 +0,0 @@ -.. _step_forward_algorithm_desc: - -Step Forward Encoding -===================== - -The Step-Forward (SF) encoding algorithm is a refinement of the Moving Window approach and is designed to improve the robustness and accuracy of spike generation in response to signal changes. Proposed by Kasabov et al. (2016), this method iteratively updates the baseline value (`Base`) and uses a threshold to determine spike events. - -**Algorithm Overview**: - -The Step-Forward algorithm computes an updated baseline (`Base`) for each signal value, which adjusts dynamically based on signal changes. A `Threshold` value determines when a spike should be generated. The formulas used in this algorithm are: - -.. math:: - - \text{Threshold} = \frac{\text{mean}(\text{Jump})}{\gamma} \quad (7) - -.. math:: - - \text{Base} = \text{Signal}[1] \quad (8) - -where: - -- **Threshold**: A dynamic value calculated from the mean of the "jump" (maximum-to-minimum differences in the signal) divided by a tunable parameter :math:`\gamma`. -- **Base**: The initial value of the signal used to track changes dynamically. - -**Implementation Steps**: - -To implement this algorithm, follow these steps: - -1. **Initialize the Base Value** (:math:`\text{Base}`): Set the `Base` to the first value of the input signal. -2. **Iterate Over Signal**: For each signal value at time :math:`t`, compare the current signal value to the dynamically updated `Base` plus or minus the `Threshold`. - - If the signal exceeds `Base + Threshold`, generate a positive spike (+1) and update `Base` to `Base + Threshold`. - - If the signal falls below `Base - Threshold`, generate a negative spike (-1) and update `Base` to `Base - Threshold`. -3. **Generate the Spike Train**: Construct the spike train by recording the time points where spikes occur based on the step-forward logic. - -**Advantages**: - -The Step-Forward algorithm is highly adaptable to changes in signal magnitude and direction, making it particularly effective in environments with fluctuating data. It offers better noise resistance and finer control over spike generation compared to simpler threshold-based methods (Kasabov et al., 2016). - -For a practical implementation in Python, refer to the :ref:`Step Forward Function `. - -**References**: - -- Kasabov, N., et al. (2016). "Neural Coding Strategies in Spiking Neural Networks." *Neural Processing Letters*. -- Delbruck, T., Lichtsteiner, P. (2007). "Artificial Retina: Applications of Image Processing with Spiking Neural Networks." *IEEE Transactions on Neural Networks*. diff --git a/docs/spikify/encoding/temporal/contrast/threshold_based.rst b/docs/spikify/encoding/temporal/contrast/threshold_based.rst deleted file mode 100644 index 637b90a..0000000 --- a/docs/spikify/encoding/temporal/contrast/threshold_based.rst +++ /dev/null @@ -1,49 +0,0 @@ -.. _threshold_based_representation_algorithm_desc: - -Threshold-Based Representation (TBR) Encoding -============================================= - -The Threshold-Based Representation (TBR) algorithm is a method for encoding signals by generating spikes based on absolute signal variations relative to a fixed threshold. This technique is particularly useful for reducing noise and emphasizing significant changes in the signal. - -**Algorithm Overview**: - -The TBR encoding method processes a signal composed of multiple channels, evaluating variations across each channel between consecutive time steps. A specific threshold is defined to determine when a spike should be generated. The main steps are: - -1. **Compute Variations**: For a signal with multiple channels, calculate the variation (`Variation`) along each channel between consecutive time steps. -2. **Define Threshold**: For each channel, compute a threshold using the formula: - - .. math:: - - \text{Threshold} = \text{mean}(\text{Variation}) + \gamma \cdot \text{std}(\text{Variation}) \quad (4) - - where: - - - **Variation**: The difference in signal value between consecutive time steps. - - **Threshold**: A dynamic value based on the mean and standard deviation of the signal variations, adjusted by a tunable parameter :math:`\gamma`. - - :math:`\gamma`: A parameter that controls the noise-reduction band. Depending on the noise level to be filtered out, different values for :math:`\gamma` can be selected: - - :math:`\gamma = 0`: All signal variations are kept. - - :math:`0 < \gamma \leq 1`: Small variations are filtered out, preserving major signal changes. - - :math:`\gamma > 1`: Significant noise reduction, allowing only major variations to generate spikes. - -3. **Determine Spikes**: For each time step, if the absolute value of `Variation` exceeds the `Threshold`, a spike is generated with polarity determined by the sign of both `Variation` and `Threshold`. - -4. **Construct the Spike Train**: Generate a spike train by assigning spike values (+1 or -1) based on the conditions outlined above. - -**Implementation Steps**: - -To implement the Threshold-Based Representation in Python: - -1. Compute the variation of the signal using `numpy`'s `diff` function. -2. Calculate the `Threshold` using the mean and standard deviation of the variations, adjusted by the parameter :math:`\gamma`. -3. Apply conditions to determine where spikes occur based on the computed threshold. -4. Generate the output spike train array. - -**Advantages**: - -The TBR algorithm is effective for emphasizing significant changes in a signal while filtering out minor variations, making it ideal for applications requiring robust noise reduction. - -For a practical implementation in Python, see the :ref:`Threshold Based Representation Function `. - -**References**: - -- Delbruck, T., Lichtsteiner, P. (2007). "Artificial Retina: Applications of Image Processing with Spiking Neural Networks." *IEEE Transactions on Neural Networks*. diff --git a/docs/spikify/encoding/temporal/contrast/zero_cross_step_forward.rst b/docs/spikify/encoding/temporal/contrast/zero_cross_step_forward.rst deleted file mode 100644 index 6af033a..0000000 --- a/docs/spikify/encoding/temporal/contrast/zero_cross_step_forward.rst +++ /dev/null @@ -1,39 +0,0 @@ -.. _zero_cross_step_forward_algorithm_desc: - -Zero-Crossing Step-Forward (ZCSF) Encoding -========================================== - -The Zero-Crossing Step-Forward (ZCSF) algorithm is a variant of the Step-Forward (SF) encoding technique that utilizes zero-crossings of the signal. Unlike the standard SF method, which involves a baseline (Base) value, ZCSF relies on a half-wave rectification behavior and emits spikes only for positive signal values exceeding a specified threshold. - -**Algorithm Overview**: - -The ZCSF algorithm encodes signals by generating spikes based on the condition of zero-crossing with respect to a predefined threshold. The key steps in the ZCSF encoding are: - -1. **Signal Rectification**: Convert all negative signal values to zero. This step is known as half-wave rectification and ensures that only positive parts of the signal are considered for spike generation. - - .. math:: - - \text{Rectified Signal} = \max(0, \text{Signal}) - -2. **Threshold Definition**: Use a predefined threshold value (`Threshold`) to determine when a spike should be emitted. The threshold is typically set based on the specific characteristics of the input signal or application requirements. - -3. **Spike Generation**: Emit a spike (value of 1) when the rectified signal exceeds the threshold. Unlike other encoding schemes, ZCSF does not consider negative spikes, and only positive spikes are generated when the signal value is higher than the threshold. - -**Implementation Steps**: - -To implement the Zero-Crossing Step-Forward encoding in Python: - -1. **Rectify the Signal**: Use `numpy`'s `maximum` function to zero out all negative signal values. -2. **Apply Threshold Condition**: Generate spikes by checking if the rectified signal exceeds the given threshold. -3. **Construct the Spike Train**: Create an output array that holds the spike values, where spikes occur based on the defined conditions. - -**Advantages**: - -The ZCSF encoding method is particularly useful for applications where only significant positive changes in the signal are of interest. By ignoring negative values and focusing on zero-crossings with respect to a threshold, ZCSF can efficiently represent signals in scenarios like event-based processing or certain types of sensory data interpretation. - -For a practical implementation in Python, see the :ref:`Zero Cross Step Forward Function `. - -**References**: - -- Wiren, A., Stubbs, A. (1956). "Zero-Crossing Techniques for Signal Processing." *Journal of Applied Signal Processing*. -- Kedem, B. (1986). "Spectral Analysis of Point Processes." *IEEE Transactions on Acoustics, Speech, and Signal Processing*. diff --git a/docs/spikify/encoding/temporal/deconvolution/ben_spiker.rst b/docs/spikify/encoding/temporal/deconvolution/ben_spiker.rst deleted file mode 100644 index 3bcb6ad..0000000 --- a/docs/spikify/encoding/temporal/deconvolution/ben_spiker.rst +++ /dev/null @@ -1,55 +0,0 @@ -.. _bens_spiker_algorithm_desc: - -Ben's Spiker Encoding -============================ - -Bens Spiker Algorithm is a method used to detect spikes within a signal by comparing two cumulative error metrics. These metrics are calculated over a segment of the signal, which is filtered using a boxcar (or rectangular) window of a specified length. The algorithm relies on the comparison of these errors to determine whether a spike is present at a given timestep. - -**Algorithm Overview**: - -Bens Spiker Algorithm works as follows: - -1. **Cumulative Error Calculation**: - - For each segment of the signal (of length equal to the boxcar window), two cumulative errors are calculated: - - .. math:: - \text{error1} = \sum_{j=1}^{n} \left| \text{Signal}[i+j-1] - \text{filter}[j] \right| - - - .. math:: - \text{error2} = \sum_{j=1}^{n} \left| \text{Signal}[i+j-1] \right| - - -Here, `error1` is the sum of absolute differences between the signal segment and the boxcar filter window, while `error2` is the sum of absolute values of the signal segment itself. - -2. **Spike Detection Condition**: - - After calculating `error1` and `error2`, a condition is checked to determine whether a spike should be emitted: - - .. math:: - - \text{error1} \leq \text{error2} \cdot \text{Threshold} - - If the above condition holds true, it indicates that the difference between the signal and the filter is small enough that the signal is close to the filter shape, thereby detecting a spike at that position. - -3. **Signal Adjustment**: - - Upon detection of a spike, the corresponding segment of the signal is adjusted by subtracting the filter window from it, allowing the algorithm to process subsequent spikes in the signal. - -**Implementation Steps**: - -1. **Create the Boxcar Filter Window**: A boxcar window of specified length is used to smooth the signal. -2. **Iterate Through the Signal**: Calculate the cumulative errors `error1` and `error2` for each segment of the signal. -3. **Detect and Record Spikes**: Check the spike detection condition and update the signal and spike array accordingly. - -**Advantages**: - -Ben's Spiker Algorithm is robust for detecting spikes in signals where the spike characteristics closely match the shape of the filter window. This makes it particularly suitable for signals with regular or repetitive spike-like features. - -For a practical implementation in Python, see the :ref:`Ben Spiker Function `. - -**References**: - -- Schrauwen, B., Van Campenhout, J. (2003). "BSA: An Efficient Algorithm for Time-Critical Signal Processing." *Neurocomputing*. -- Petro, P., et al. (2020). "Revisiting the BSA for Modern Applications." *Signal Processing Letters*. diff --git a/docs/spikify/encoding/temporal/deconvolution/hough_spiker.rst b/docs/spikify/encoding/temporal/deconvolution/hough_spiker.rst deleted file mode 100644 index f8ff3c9..0000000 --- a/docs/spikify/encoding/temporal/deconvolution/hough_spiker.rst +++ /dev/null @@ -1,41 +0,0 @@ -.. _hough_spiker_algorithm_desc: - -Hough Spiker Encoding -============================ - -The Hough Spiker Algorithm is a technique used to detect spikes in a signal by performing a progressive subtraction operation. The algorithm first compares the value of the analog signal to the result of a convolution operation. If the signal value exceeds the convolution result, the signal undergoes a subtraction of the convolution value. This process is repeated iteratively for each signal channel. - -**Algorithm Overview**: - -Hough Spiker algorithm follows these steps: - -1. **Progressive Subtraction**: - - For each signal segment, the algorithm compares the signal value at each time step with the result of a convolution operation (using a rectangular window in our analysis): - - .. math:: - - \text{Signal}[i+j-1] = \text{Signal}[i+j-1] - \text{filter}[j] - - Here, `Signal[i+j-1]` represents the value of the signal at a specific time step, and `filter[j]` is the value from the convolution result at the corresponding index `j`. - -2. **Spike Detection**: - - A spike is detected if the signal value surpasses the convolution result. The signal is then updated by subtracting the filter window from it. This step is performed iteratively across the entire signal, ensuring that spikes are detected and adjusted accordingly. - -**Implementation Steps**: - -1. **Create the Boxcar Filter Window**: A boxcar window of the specified length is used for the convolution operation. -2. **Iterate Through the Signal**: Compare the signal values with the filter window and update the signal if a spike is detected. -3. **Record Detected Spikes**: A spike array is maintained, where a `1` indicates a detected spike, and `0` indicates no spike. - -**Advantages**: - -The Hough Spiker Algorithm is efficient in detecting spikes in signals, particularly when the spikes closely match the convolution filter shape. It is especially useful for signals where spikes need to be detected and progressively subtracted to prevent overlap in detection. - -For a practical implementation in Python, see the :ref:`Hough Spiker Function `. - -**References**: - -- Schrauwen, B., Van Campenhout, J. (2003). "HSA: A Progressive Subtraction Technique for Spike Detection." *Neurocomputing*. -- Petro, P., et al. (2020). "Revisiting the HSA for Modern Applications." *Signal Processing Letters*. diff --git a/docs/spikify/encoding/temporal/deconvolution/modified_hough_spiker.rst b/docs/spikify/encoding/temporal/deconvolution/modified_hough_spiker.rst deleted file mode 100644 index 67b3b66..0000000 --- a/docs/spikify/encoding/temporal/deconvolution/modified_hough_spiker.rst +++ /dev/null @@ -1,47 +0,0 @@ -.. _modified_hough_spiker_algorithm_desc: - -Modified Hough Spiker Encoding -================================ - -The Modified Hough Spiker Algorithm (Modified HSA) builds upon the original Hough Spiker Algorithm by introducing a threshold mechanism to handle the detection process. While it maintains the core idea of subtractive, deconvolution-based spike detection, the modified version incorporates a threshold value to accumulate and evaluate errors. - -**Algorithm Overview**: - -Modified HSA introduces a conditional check on the error accumulation during the spike detection process. This adjustment ensures that only significant deviations from the convolution function are considered as spikes, improving detection accuracy. - -1. **Error Accumulation**: - - During the processing of each signal segment, the algorithm accumulates the error between the signal value and the convolution result: - - .. math:: - - \text{error} = \text{error} + (\text{filter}[j] - \text{Signal}[i + j - 1]) - - Here, `filter[j]` is the value from the convolution result at index `j`, and `Signal[i + j - 1]` is the corresponding signal value. - -2. **Threshold Comparison**: - - The key modification in this algorithm is the conditional check on the accumulated error. The subtraction operation from the original HSA is only performed if the accumulated error stays within a specified threshold: - - .. math:: - - \text{error} \leq \text{Threshold} - - If this condition is met, the signal is updated by subtracting the convolution value, and a spike is detected. - -**Implementation Steps**: - -1. **Compute Error**: Calculate the error by comparing the convolution filter values to the corresponding signal values. -2. **Threshold Check**: Compare the accumulated error to the predefined threshold to determine if a spike should be detected. -3. **Progressive Subtraction**: If the threshold condition is met, subtract the filter values from the signal, similar to the original HSA, but with enhanced control over detection. - -**Advantages**: - -The Modified HSA allows for more flexible spike detection by considering cumulative errors. This approach is beneficial in scenarios where spikes are not distinctly above the convolution function but still significant enough to be detected. - -For a practical implementation in Python, see the :ref:`Modified Hough Spiker Function `. - -**References**: - -- Schrauwen, B., Van Campenhout, J. (2003). "HSA: A Progressive Subtraction Technique for Spike Detection." *Neurocomputing*. -- Petro, P., et al. (2021). "Modified HSA with Thresholding for Enhanced Spike Detection." *Signal Processing Letters*. \ No newline at end of file diff --git a/docs/spikify/encoding/temporal/global_referenced/phase_encoding.rst b/docs/spikify/encoding/temporal/global_referenced/phase_encoding.rst deleted file mode 100644 index 0e2b720..0000000 --- a/docs/spikify/encoding/temporal/global_referenced/phase_encoding.rst +++ /dev/null @@ -1,40 +0,0 @@ -.. _phase_encoding_algorithm_desc: - -Phase Encoding -============================ - -The concept of encoding based on phase evaluation is rooted in the idea of using an oscillatory reference to encode information. This technique was explored by Montemurro et al. (2008), where the phase of an oscillatory reference is evaluated for encoding. In our implementation, we follow the approach proposed by Kim et al. (2018), where the binary representation of the input, using β fractional bits, serves as the oscillatory reference. The signal is first rectified and normalized for each channel into the range [0, 1]. - -**Algorithm Overview**: - -The phase encoding method works as follows: - -1. **Normalization**: - The input signal is normalized within the range [0, 1] for each channel, ensuring the signal fits within the required bounds. - -2. **Phase Calculation**: - The normalized signal is then used to compute the phase angle, which is an essential step for phase encoding. The phase is calculated using the arcsin function, which effectively maps the normalized signal to a phase value. - -3. **Quantization**: - The computed phase values are then quantized based on the number of bits specified. The phase angle is divided into discrete levels, each representing a binary value. - -4. **Binary Representation**: - Finally, the quantized phase levels are converted into a binary representation. This binary sequence represents the encoded spikes. - -**Implementation Steps**: - -1. **Normalize the Signal**: Scale the signal to fit within the range [0, 1]. -2. **Compute Phase Angles**: Use the arcsin function to determine the phase angles based on the normalized signal. -3. **Quantize the Phase**: Divide the phase values into discrete levels and assign them binary codes. -4. **Generate Spike Train**: Convert the quantized levels into a binary spike train. - -**Advantages**: - -Phase encoding is a powerful method that leverages phase information to represent the input signal. This technique is particularly effective for signals that are inherently oscillatory or when the phase information provides significant encoding advantages. - -For a practical implementation in Python, see the :ref:`Phase Encoding Function `. - -**References**: - -- Montemurro, M. A., et al. (2008). "Phase-of-Firing Coding of Natural Scenes in Primary Visual Cortex." *Current Biology*. -- Kim, S., et al. (2018). "Binary Phase Encoding for Oscillatory Reference Signals." *Journal of Computational Neuroscience*. diff --git a/docs/spikify/encoding/temporal/global_referenced/time_to_first_spike.rst b/docs/spikify/encoding/temporal/global_referenced/time_to_first_spike.rst deleted file mode 100644 index add6051..0000000 --- a/docs/spikify/encoding/temporal/global_referenced/time_to_first_spike.rst +++ /dev/null @@ -1,47 +0,0 @@ -.. _time_to_first_spike_algorithm_desc: - -Time-to-First-Spike Encoding -============================ - -The Time-to-First-Spike (TTFS) encoding method focuses on the time it takes for a neuron to fire its first spike in response to a stimulus. This technique was explored by Rueckauer and Liu (2018) and has been further developed in Park et al. (2020). In our implementation, we employ a dynamic threshold that decays exponentially over time, defined by a membrane potential. - -**Algorithm Overview**: - -The TTFS encoding process operates as follows: - -1. **Dynamic Thresholding**: - The membrane potential is modeled as an exponentially decaying function, described by the formula: - - .. math:: - - P_{th}(t) = \theta_0 e^{-t/\tau_{th}} - - where :math:`\theta_0` is a constant and :math:`\tau_{th}` is the decay time constant. For our investigation, we set :math:`\theta_0 = 1` and :math:`\tau_{th} = 0.1`. - -2. **Spike Timing Calculation**: - The input signal is normalized, and based on its intensity, the time to the first spike is determined by comparing the input signal to the dynamic threshold. The lower the signal, the earlier the spike occurs. - -3. **Quantization**: - Similar to the phase encoding approach, the spike times are quantized into discrete levels, which map the signal intensity to binary values. - -4. **Binary Representation**: - The spike times are then converted into a binary sequence, representing the encoded signal in a spike train format. - -**Implementation Steps**: - -1. **Normalize the Signal**: The input signal is rectified and normalized between 0 and 1. -2. **Compute Dynamic Threshold**: Use an exponentially decaying threshold function to model membrane potential decay. -3. **Determine Spike Timing**: Calculate the time to first spike based on the signal intensity and dynamic threshold. -4. **Quantize Spike Times**: Discretize the spike times and assign binary values. -5. **Generate Spike Train**: Convert the spike timing into a binary spike train for output. - -**Advantages**: - -TTFS encoding provides a biologically plausible way to represent the intensity of input stimuli by the timing of a single spike. The dynamic threshold approach ensures that even small differences in input intensity can result in distinguishable spike times. - -For a practical implementation in Python, see the :ref:`Time-to-First-Spike Encoding Function `. - -**References**: - -- Rueckauer, B., & Liu, S.-C. (2018). "Conversion of Continuous-Valued Deep Networks to Efficient Event-Driven Networks for Image Classification." *Frontiers in Neuroscience*. -- Park, S., et al. (2020). "Dynamic Threshold Spike Encoding for Robust Neural Coding." *Neural Computation*. diff --git a/docs/spikify/encoding/temporal/latency/burst_encoding.rst b/docs/spikify/encoding/temporal/latency/burst_encoding.rst deleted file mode 100644 index fdd8853..0000000 --- a/docs/spikify/encoding/temporal/latency/burst_encoding.rst +++ /dev/null @@ -1,54 +0,0 @@ -.. _burst_encoding_algorithm_desc: - -Burst Encoding -=============== - -Burst Encoding is a technique employed to encode information by leveraging two time-based characteristics of a single spike train: the number of spikes (`SpikeNumber`) and the inter-spike interval (`ISI`). This method is particularly effective when the goal is to encapsulate both the density of spikes and the timing between them into a unified encoding scheme. - -**Algorithm Overview**: - -Burst Encoding follows these steps: - -1. **Calculate Spike Number**: - - The algorithm first determines the number of spikes in a burst using the normalized signal rate and the maximum number of spikes: - - .. math:: - - \text{SpikeNumber} = \lceil \text{rate} \cdot N_{\text{max}} \rceil - - Here, `SpikeNumber` is the number of spikes calculated for each segment of the signal, and `rate` is derived from a normalization procedure. - -2. **Determine Inter-Spike Interval (ISI)**: - - The ISI between spikes is calculated based on the difference between `t_max` and `t_min`, scaled by the normalized signal rate: - - .. math:: - - \text{ISI} = - \begin{cases} - \left\lceil \frac{t_{\text{max}} - \text{rate}(t_{\text{max}} - t_{\text{min}})}{t_{\text{max}}} \right\rceil & \text{if SpikeNumber} > 1 \\ - t_{\text{max}} & \text{otherwise} - \end{cases} - - The `ISI` determines the timing between consecutive spikes within each burst. - -3. **Spike Train Construction**: - - Based on the calculated `SpikeNumber` and `ISI`, the algorithm constructs a spike train where bursts of spikes are placed at the calculated intervals. - -**Implementation Steps**: - -1. **Normalize the Signal**: The input signal is normalized to obtain the `rate`, which influences both the `SpikeNumber` and the `ISI`. -2. **Calculate Spike Numbers and ISI**: Using the normalized `rate`, determine the number of spikes and their inter-spike intervals. -3. **Generate the Spike Train**: Construct the spike train based on the calculated parameters, ensuring that the burst structure is maintained. - -**Advantages**: - -This encoding method is particularly useful in neural data analysis and other areas where time-based spike data needs to be efficiently encoded. - -For a practical implementation in Python, see the :ref:`Burst Encoding Function `. - -**References**: - -- Guo et al. (2021). "Burst Encoding Techniques for Neural Spike Trains." *Journal of Neuroscience Methods*. diff --git a/docs/spikify/filtering/index.rst b/docs/spikify/filtering/index.rst deleted file mode 100644 index 61b3a9c..0000000 --- a/docs/spikify/filtering/index.rst +++ /dev/null @@ -1,25 +0,0 @@ -.. _filtering_desc: - -Filtering -========= - -The **filtering** module in the **spikify** library is designed to mimic natural filtering mechanisms found in biological systems, offering advanced pre-processing techniques for time-varying signals. - -Filtering Techniques ---------------------- - -In **spikify**, the filtering approach includes: - -- **Frequency Decomposition**: The filtering techniques break down time-varying signals into multiple frequency components. -- **Advanced Filters**: Techniques such as Gammatone and Butterworth filters are used to split the signal into different channels, each corresponding to a specific frequency range. - -Filter Types Used ------------------- - -- **Gammatone Filters**: These filters are inspired by the human auditory system and are effective for modeling cochlear frequency selectivity. They are commonly used in audio and speech processing to simulate how biological systems separate sounds. -- **Butterworth Filters**: Known for their maximally flat frequency response in the passband, Butterworth filters are used to cleanly separate signal components without introducing ripples, making them suitable for general-purpose signal filtering. -- **Custom Filter Banks**: The module also supports configurable filter banks, allowing users to tailor the frequency decomposition to specific application needs. - -Each filter type can be configured with parameters such as center frequency, bandwidth, and order, providing flexibility for a wide range of signal processing tasks. - -For details on how to implement these filters in Python, including examples of configuring Gammatone, Butterworth, and custom filter banks, refer to the :ref:`FilterBank ` documentation. \ No newline at end of file diff --git a/docs/spikify/filters/index.rst b/docs/spikify/filters/index.rst new file mode 100644 index 0000000..ca234a1 --- /dev/null +++ b/docs/spikify/filters/index.rst @@ -0,0 +1,18 @@ +.. _filtering_desc: + +Filtering +========= + +Preprocessing tools for conditioning time-varying signals before spike encoding. +Frequency decomposition techniques inspired by biological sensory systems break down input signals into multiple frequency channels to improve spike train quality and encoding performance. + +Filter Types +------------ + +- **Gammatone**: Bandpass filterbank approximating the frequency selectivity of the basilar membrane. Suitable for audio and speech signals where cochlear-like decomposition is required. +- **Butterworth**: IIR filterbank with a maximally flat passband response. Suitable for general-purpose signal conditioning and noise attenuation prior to encoding. + +Each filter can be configured with parameters such as center frequency, bandwidth, and order. All filters are +implemented as filter banks, decomposing the input signal into multiple frequency channels simultaneously. + +For implementation details and usage examples, refer to the :ref:`FilterBank ` documentation. \ No newline at end of file diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 87fbccb..e7feaef 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -24,7 +24,7 @@ Before encoding, you may want to filter the signal to focus on specific frequenc .. code-block:: python - from spikify.filtering import FilterBank + from spikify.filters import FilterBank filter = FilterBank(fs=50, channels=5, f_min=0.5, f_max=5, order=4, filter_type='butterworth') @@ -35,21 +35,21 @@ Before encoding, you may want to filter the signal to focus on specific frequenc Encoding the Signal and the Filtered Signal with Poisson Rate --------------------------------------------------------------- -Now, let's encode the sinusoidal signal into spikes using the `poisson_rate` method. This method converts the signal into spike intervals based on the specified encoding interval length. +Now, let's encode the sinusoidal signal into spikes using the `poisson` method. This method converts the signal into spike intervals based on the specified encoding interval length. .. code-block:: python - from spikify.encoding.rate import poisson_rate + from spikify.encoders.rate import poisson # Set parameters for encoding np.random.seed(0) # For reproducibility interval_length = 5 # Length of the encoding interval # Encode the sinusoidal signal - encoded_signal = poisson_rate(signal, interval_length) + encoded_signal = poisson(signal, interval_length) # Encode the filtered signal - encoded_filtered_signal = poisson_rate(filtered_signal, interval_length) + encoded_filtered_signal = poisson(filtered_signal, interval_length) .. image:: _static/spike_encoding.gif diff --git a/examples/wisdm/encode.py b/examples/wisdm/encode.py index b3649d2..9fb696f 100644 --- a/examples/wisdm/encode.py +++ b/examples/wisdm/encode.py @@ -1,15 +1,15 @@ from pathlib import Path import numpy as np -from spikify.encoding.rate import poisson_rate -from spikify.encoding.temporal.latency import burst_encoding -from spikify.encoding.temporal.contrast import ( +from spikify.encoders.rate import poisson_rate +from spikify.encoders.temporal.latency import burst_encoding +from spikify.encoders.temporal.contrast import ( moving_window, step_forward, threshold_based_representation, zero_cross_step_forward, ) -from spikify.encoding.temporal.global_referenced import phase_encoding, time_to_first_spike -from spikify.encoding.temporal.deconvolution import bens_spiker, hough_spiker, modified_hough_spiker +from spikify.encoders.temporal.global_referenced import phase_encoding, time_to_first_spike +from spikify.encoders.temporal.deconvolution import bens_spiker, hough_spiker, modified_hough_spiker from datasets import load_dataset REPO_ID = "neuromorphic-polito/siddha" @@ -135,7 +135,7 @@ def convert_dataset_into_spike(dataframe): ) # # - # Apply Burst encoding and save the data + # Apply Burst Coding and save the data apply_encoding( dataframe_accel.copy(), path, diff --git a/poetry.lock b/poetry.lock index 4d233a0..51ecd7d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. [[package]] name = "alabaster" @@ -49,22 +49,22 @@ files = [ ] [package.extras] -dev = ["backports.zoneinfo", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata"] +dev = ["backports.zoneinfo ; python_version < \"3.9\"", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata ; sys_platform == \"win32\""] [[package]] name = "beautifulsoup4" -version = "4.14.2" +version = "4.14.3" description = "Screen-scraping library" optional = false python-versions = ">=3.7.0" groups = ["docs"] files = [ - {file = "beautifulsoup4-4.14.2-py3-none-any.whl", hash = "sha256:5ef6fa3a8cbece8488d66985560f97ed091e22bbc4e9c2338508a9d5de6d4515"}, - {file = "beautifulsoup4-4.14.2.tar.gz", hash = "sha256:2a98ab9f944a11acee9cc848508ec28d9228abfd522ef0fad6a02a72e0ded69e"}, + {file = "beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb"}, + {file = "beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86"}, ] [package.dependencies] -soupsieve = ">1.2" +soupsieve = ">=1.6.1" typing-extensions = ">=4.0.0" [package.extras] @@ -162,26 +162,26 @@ test = ["coverage", "freezegun", "pre-commit", "pytest", "pytest-cov", "pytest-m [[package]] name = "certifi" -version = "2025.10.5" +version = "2025.11.12" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.7" groups = ["docs"] files = [ - {file = "certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de"}, - {file = "certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43"}, + {file = "certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b"}, + {file = "certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316"}, ] [[package]] name = "cfgv" -version = "3.4.0" +version = "3.5.0" description = "Validate configuration and produce human readable error messages." optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" groups = ["code-quality"] files = [ - {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, - {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, + {file = "cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0"}, + {file = "cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132"}, ] [[package]] @@ -309,14 +309,14 @@ files = [ [[package]] name = "click" -version = "8.3.0" +version = "8.3.1" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.10" groups = ["code-quality", "release"] files = [ - {file = "click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc"}, - {file = "click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4"}, + {file = "click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6"}, + {file = "click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a"}, ] [package.dependencies] @@ -337,123 +337,111 @@ markers = {code-quality = "platform_system == \"Windows\"", dev = "sys_platform [[package]] name = "coverage" -version = "7.10.7" +version = "7.13.1" description = "Code coverage measurement for Python" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "coverage-7.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a"}, - {file = "coverage-7.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5"}, - {file = "coverage-7.10.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17"}, - {file = "coverage-7.10.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b"}, - {file = "coverage-7.10.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87"}, - {file = "coverage-7.10.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e"}, - {file = "coverage-7.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e"}, - {file = "coverage-7.10.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df"}, - {file = "coverage-7.10.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0"}, - {file = "coverage-7.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13"}, - {file = "coverage-7.10.7-cp310-cp310-win32.whl", hash = "sha256:b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b"}, - {file = "coverage-7.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807"}, - {file = "coverage-7.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59"}, - {file = "coverage-7.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a"}, - {file = "coverage-7.10.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699"}, - {file = "coverage-7.10.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d"}, - {file = "coverage-7.10.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e"}, - {file = "coverage-7.10.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23"}, - {file = "coverage-7.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab"}, - {file = "coverage-7.10.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82"}, - {file = "coverage-7.10.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2"}, - {file = "coverage-7.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61"}, - {file = "coverage-7.10.7-cp311-cp311-win32.whl", hash = "sha256:972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14"}, - {file = "coverage-7.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2"}, - {file = "coverage-7.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a"}, - {file = "coverage-7.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417"}, - {file = "coverage-7.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973"}, - {file = "coverage-7.10.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c"}, - {file = "coverage-7.10.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7"}, - {file = "coverage-7.10.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6"}, - {file = "coverage-7.10.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59"}, - {file = "coverage-7.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b"}, - {file = "coverage-7.10.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a"}, - {file = "coverage-7.10.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb"}, - {file = "coverage-7.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1"}, - {file = "coverage-7.10.7-cp312-cp312-win32.whl", hash = "sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256"}, - {file = "coverage-7.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba"}, - {file = "coverage-7.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf"}, - {file = "coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d"}, - {file = "coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b"}, - {file = "coverage-7.10.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e"}, - {file = "coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b"}, - {file = "coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49"}, - {file = "coverage-7.10.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911"}, - {file = "coverage-7.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0"}, - {file = "coverage-7.10.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f"}, - {file = "coverage-7.10.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c"}, - {file = "coverage-7.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f"}, - {file = "coverage-7.10.7-cp313-cp313-win32.whl", hash = "sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698"}, - {file = "coverage-7.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843"}, - {file = "coverage-7.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546"}, - {file = "coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c"}, - {file = "coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15"}, - {file = "coverage-7.10.7-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4"}, - {file = "coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0"}, - {file = "coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0"}, - {file = "coverage-7.10.7-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65"}, - {file = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541"}, - {file = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6"}, - {file = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999"}, - {file = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2"}, - {file = "coverage-7.10.7-cp313-cp313t-win32.whl", hash = "sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a"}, - {file = "coverage-7.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb"}, - {file = "coverage-7.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb"}, - {file = "coverage-7.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520"}, - {file = "coverage-7.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32"}, - {file = "coverage-7.10.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f"}, - {file = "coverage-7.10.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a"}, - {file = "coverage-7.10.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360"}, - {file = "coverage-7.10.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69"}, - {file = "coverage-7.10.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14"}, - {file = "coverage-7.10.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe"}, - {file = "coverage-7.10.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e"}, - {file = "coverage-7.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd"}, - {file = "coverage-7.10.7-cp314-cp314-win32.whl", hash = "sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2"}, - {file = "coverage-7.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681"}, - {file = "coverage-7.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880"}, - {file = "coverage-7.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63"}, - {file = "coverage-7.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2"}, - {file = "coverage-7.10.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d"}, - {file = "coverage-7.10.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0"}, - {file = "coverage-7.10.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699"}, - {file = "coverage-7.10.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9"}, - {file = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f"}, - {file = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1"}, - {file = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0"}, - {file = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399"}, - {file = "coverage-7.10.7-cp314-cp314t-win32.whl", hash = "sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235"}, - {file = "coverage-7.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d"}, - {file = "coverage-7.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a"}, - {file = "coverage-7.10.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fff7b9c3f19957020cac546c70025331113d2e61537f6e2441bc7657913de7d3"}, - {file = "coverage-7.10.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bc91b314cef27742da486d6839b677b3f2793dfe52b51bbbb7cf736d5c29281c"}, - {file = "coverage-7.10.7-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:567f5c155eda8df1d3d439d40a45a6a5f029b429b06648235f1e7e51b522b396"}, - {file = "coverage-7.10.7-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2af88deffcc8a4d5974cf2d502251bc3b2db8461f0b66d80a449c33757aa9f40"}, - {file = "coverage-7.10.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7315339eae3b24c2d2fa1ed7d7a38654cba34a13ef19fbcb9425da46d3dc594"}, - {file = "coverage-7.10.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:912e6ebc7a6e4adfdbb1aec371ad04c68854cd3bf3608b3514e7ff9062931d8a"}, - {file = "coverage-7.10.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f49a05acd3dfe1ce9715b657e28d138578bc40126760efb962322c56e9ca344b"}, - {file = "coverage-7.10.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cce2109b6219f22ece99db7644b9622f54a4e915dad65660ec435e89a3ea7cc3"}, - {file = "coverage-7.10.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:f3c887f96407cea3916294046fc7dab611c2552beadbed4ea901cbc6a40cc7a0"}, - {file = "coverage-7.10.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:635adb9a4507c9fd2ed65f39693fa31c9a3ee3a8e6dc64df033e8fdf52a7003f"}, - {file = "coverage-7.10.7-cp39-cp39-win32.whl", hash = "sha256:5a02d5a850e2979b0a014c412573953995174743a3f7fa4ea5a6e9a3c5617431"}, - {file = "coverage-7.10.7-cp39-cp39-win_amd64.whl", hash = "sha256:c134869d5ffe34547d14e174c866fd8fe2254918cc0a95e99052903bc1543e07"}, - {file = "coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260"}, - {file = "coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239"}, + {file = "coverage-7.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e1fa280b3ad78eea5be86f94f461c04943d942697e0dac889fa18fff8f5f9147"}, + {file = "coverage-7.13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c3d8c679607220979434f494b139dfb00131ebf70bb406553d69c1ff01a5c33d"}, + {file = "coverage-7.13.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:339dc63b3eba969067b00f41f15ad161bf2946613156fb131266d8debc8e44d0"}, + {file = "coverage-7.13.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:db622b999ffe49cb891f2fff3b340cdc2f9797d01a0a202a0973ba2562501d90"}, + {file = "coverage-7.13.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1443ba9acbb593fa7c1c29e011d7c9761545fe35e7652e85ce7f51a16f7e08d"}, + {file = "coverage-7.13.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c832ec92c4499ac463186af72f9ed4d8daec15499b16f0a879b0d1c8e5cf4a3b"}, + {file = "coverage-7.13.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:562ec27dfa3f311e0db1ba243ec6e5f6ab96b1edfcfc6cf86f28038bc4961ce6"}, + {file = "coverage-7.13.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4de84e71173d4dada2897e5a0e1b7877e5eefbfe0d6a44edee6ce31d9b8ec09e"}, + {file = "coverage-7.13.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:a5a68357f686f8c4d527a2dc04f52e669c2fc1cbde38f6f7eb6a0e58cbd17cae"}, + {file = "coverage-7.13.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:77cc258aeb29a3417062758975521eae60af6f79e930d6993555eeac6a8eac29"}, + {file = "coverage-7.13.1-cp310-cp310-win32.whl", hash = "sha256:bb4f8c3c9a9f34423dba193f241f617b08ffc63e27f67159f60ae6baf2dcfe0f"}, + {file = "coverage-7.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:c8e2706ceb622bc63bac98ebb10ef5da80ed70fbd8a7999a5076de3afaef0fb1"}, + {file = "coverage-7.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a55d509a1dc5a5b708b5dad3b5334e07a16ad4c2185e27b40e4dba796ab7f88"}, + {file = "coverage-7.13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d010d080c4888371033baab27e47c9df7d6fb28d0b7b7adf85a4a49be9298b3"}, + {file = "coverage-7.13.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d938b4a840fb1523b9dfbbb454f652967f18e197569c32266d4d13f37244c3d9"}, + {file = "coverage-7.13.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bf100a3288f9bb7f919b87eb84f87101e197535b9bd0e2c2b5b3179633324fee"}, + {file = "coverage-7.13.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef6688db9bf91ba111ae734ba6ef1a063304a881749726e0d3575f5c10a9facf"}, + {file = "coverage-7.13.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0b609fc9cdbd1f02e51f67f51e5aee60a841ef58a68d00d5ee2c0faf357481a3"}, + {file = "coverage-7.13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c43257717611ff5e9a1d79dce8e47566235ebda63328718d9b65dd640bc832ef"}, + {file = "coverage-7.13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e09fbecc007f7b6afdfb3b07ce5bd9f8494b6856dd4f577d26c66c391b829851"}, + {file = "coverage-7.13.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:a03a4f3a19a189919c7055098790285cc5c5b0b3976f8d227aea39dbf9f8bfdb"}, + {file = "coverage-7.13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3820778ea1387c2b6a818caec01c63adc5b3750211af6447e8dcfb9b6f08dbba"}, + {file = "coverage-7.13.1-cp311-cp311-win32.whl", hash = "sha256:ff10896fa55167371960c5908150b434b71c876dfab97b69478f22c8b445ea19"}, + {file = "coverage-7.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:a998cc0aeeea4c6d5622a3754da5a493055d2d95186bad877b0a34ea6e6dbe0a"}, + {file = "coverage-7.13.1-cp311-cp311-win_arm64.whl", hash = "sha256:fea07c1a39a22614acb762e3fbbb4011f65eedafcb2948feeef641ac78b4ee5c"}, + {file = "coverage-7.13.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6f34591000f06e62085b1865c9bc5f7858df748834662a51edadfd2c3bfe0dd3"}, + {file = "coverage-7.13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b67e47c5595b9224599016e333f5ec25392597a89d5744658f837d204e16c63e"}, + {file = "coverage-7.13.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e7b8bd70c48ffb28461ebe092c2345536fb18bbbf19d287c8913699735f505c"}, + {file = "coverage-7.13.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c223d078112e90dc0e5c4e35b98b9584164bea9fbbd221c0b21c5241f6d51b62"}, + {file = "coverage-7.13.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:794f7c05af0763b1bbd1b9e6eff0e52ad068be3b12cd96c87de037b01390c968"}, + {file = "coverage-7.13.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0642eae483cc8c2902e4af7298bf886d605e80f26382124cddc3967c2a3df09e"}, + {file = "coverage-7.13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f5e772ed5fef25b3de9f2008fe67b92d46831bd2bc5bdc5dd6bfd06b83b316f"}, + {file = "coverage-7.13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:45980ea19277dc0a579e432aef6a504fe098ef3a9032ead15e446eb0f1191aee"}, + {file = "coverage-7.13.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f18eca6028ffa62adbd185a8f1e1dd242f2e68164dba5c2b74a5204850b4cf"}, + {file = "coverage-7.13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8dca5590fec7a89ed6826fce625595279e586ead52e9e958d3237821fbc750c"}, + {file = "coverage-7.13.1-cp312-cp312-win32.whl", hash = "sha256:ff86d4e85188bba72cfb876df3e11fa243439882c55957184af44a35bd5880b7"}, + {file = "coverage-7.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:16cc1da46c04fb0fb128b4dc430b78fa2aba8a6c0c9f8eb391fd5103409a6ac6"}, + {file = "coverage-7.13.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d9bc218650022a768f3775dd7fdac1886437325d8d295d923ebcfef4892ad5c"}, + {file = "coverage-7.13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cb237bfd0ef4d5eb6a19e29f9e528ac67ac3be932ea6b44fb6cc09b9f3ecff78"}, + {file = "coverage-7.13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1dcb645d7e34dcbcc96cd7c132b1fc55c39263ca62eb961c064eb3928997363b"}, + {file = "coverage-7.13.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3d42df8201e00384736f0df9be2ced39324c3907607d17d50d50116c989d84cd"}, + {file = "coverage-7.13.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa3edde1aa8807de1d05934982416cb3ec46d1d4d91e280bcce7cca01c507992"}, + {file = "coverage-7.13.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9edd0e01a343766add6817bc448408858ba6b489039eaaa2018474e4001651a4"}, + {file = "coverage-7.13.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:985b7836931d033570b94c94713c6dba5f9d3ff26045f72c3e5dbc5fe3361e5a"}, + {file = "coverage-7.13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ffed1e4980889765c84a5d1a566159e363b71d6b6fbaf0bebc9d3c30bc016766"}, + {file = "coverage-7.13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8842af7f175078456b8b17f1b73a0d16a65dcbdc653ecefeb00a56b3c8c298c4"}, + {file = "coverage-7.13.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ccd7a6fca48ca9c131d9b0a2972a581e28b13416fc313fb98b6d24a03ce9a398"}, + {file = "coverage-7.13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0403f647055de2609be776965108447deb8e384fe4a553c119e3ff6bfbab4784"}, + {file = "coverage-7.13.1-cp313-cp313-win32.whl", hash = "sha256:549d195116a1ba1e1ae2f5ca143f9777800f6636eab917d4f02b5310d6d73461"}, + {file = "coverage-7.13.1-cp313-cp313-win_amd64.whl", hash = "sha256:5899d28b5276f536fcf840b18b61a9fce23cc3aec1d114c44c07fe94ebeaa500"}, + {file = "coverage-7.13.1-cp313-cp313-win_arm64.whl", hash = "sha256:868a2fae76dfb06e87291bcbd4dcbcc778a8500510b618d50496e520bd94d9b9"}, + {file = "coverage-7.13.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:67170979de0dacac3f3097d02b0ad188d8edcea44ccc44aaa0550af49150c7dc"}, + {file = "coverage-7.13.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f80e2bb21bfab56ed7405c2d79d34b5dc0bc96c2c1d2a067b643a09fb756c43a"}, + {file = "coverage-7.13.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f83351e0f7dcdb14d7326c3d8d8c4e915fa685cbfdc6281f9470d97a04e9dfe4"}, + {file = "coverage-7.13.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb3f6562e89bad0110afbe64e485aac2462efdce6232cdec7862a095dc3412f6"}, + {file = "coverage-7.13.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77545b5dcda13b70f872c3b5974ac64c21d05e65b1590b441c8560115dc3a0d1"}, + {file = "coverage-7.13.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4d240d260a1aed814790bbe1f10a5ff31ce6c21bc78f0da4a1e8268d6c80dbd"}, + {file = "coverage-7.13.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d2287ac9360dec3837bfdad969963a5d073a09a85d898bd86bea82aa8876ef3c"}, + {file = "coverage-7.13.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0d2c11f3ea4db66b5cbded23b20185c35066892c67d80ec4be4bab257b9ad1e0"}, + {file = "coverage-7.13.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:3fc6a169517ca0d7ca6846c3c5392ef2b9e38896f61d615cb75b9e7134d4ee1e"}, + {file = "coverage-7.13.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d10a2ed46386e850bb3de503a54f9fe8192e5917fcbb143bfef653a9355e9a53"}, + {file = "coverage-7.13.1-cp313-cp313t-win32.whl", hash = "sha256:75a6f4aa904301dab8022397a22c0039edc1f51e90b83dbd4464b8a38dc87842"}, + {file = "coverage-7.13.1-cp313-cp313t-win_amd64.whl", hash = "sha256:309ef5706e95e62578cda256b97f5e097916a2c26247c287bbe74794e7150df2"}, + {file = "coverage-7.13.1-cp313-cp313t-win_arm64.whl", hash = "sha256:92f980729e79b5d16d221038dbf2e8f9a9136afa072f9d5d6ed4cb984b126a09"}, + {file = "coverage-7.13.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:97ab3647280d458a1f9adb85244e81587505a43c0c7cff851f5116cd2814b894"}, + {file = "coverage-7.13.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8f572d989142e0908e6acf57ad1b9b86989ff057c006d13b76c146ec6a20216a"}, + {file = "coverage-7.13.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d72140ccf8a147e94274024ff6fd8fb7811354cf7ef88b1f0a988ebaa5bc774f"}, + {file = "coverage-7.13.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3c9f051b028810f5a87c88e5d6e9af3c0ff32ef62763bf15d29f740453ca909"}, + {file = "coverage-7.13.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f398ba4df52d30b1763f62eed9de5620dcde96e6f491f4c62686736b155aa6e4"}, + {file = "coverage-7.13.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:132718176cc723026d201e347f800cd1a9e4b62ccd3f82476950834dad501c75"}, + {file = "coverage-7.13.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e549d642426e3579b3f4b92d0431543b012dcb6e825c91619d4e93b7363c3f9"}, + {file = "coverage-7.13.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:90480b2134999301eea795b3a9dbf606c6fbab1b489150c501da84a959442465"}, + {file = "coverage-7.13.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e825dbb7f84dfa24663dd75835e7257f8882629fc11f03ecf77d84a75134b864"}, + {file = "coverage-7.13.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:623dcc6d7a7ba450bbdbeedbaa0c42b329bdae16491af2282f12a7e809be7eb9"}, + {file = "coverage-7.13.1-cp314-cp314-win32.whl", hash = "sha256:6e73ebb44dca5f708dc871fe0b90cf4cff1a13f9956f747cc87b535a840386f5"}, + {file = "coverage-7.13.1-cp314-cp314-win_amd64.whl", hash = "sha256:be753b225d159feb397bd0bf91ae86f689bad0da09d3b301478cd39b878ab31a"}, + {file = "coverage-7.13.1-cp314-cp314-win_arm64.whl", hash = "sha256:228b90f613b25ba0019361e4ab81520b343b622fc657daf7e501c4ed6a2366c0"}, + {file = "coverage-7.13.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:60cfb538fe9ef86e5b2ab0ca8fc8d62524777f6c611dcaf76dc16fbe9b8e698a"}, + {file = "coverage-7.13.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:57dfc8048c72ba48a8c45e188d811e5efd7e49b387effc8fb17e97936dde5bf6"}, + {file = "coverage-7.13.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3f2f725aa3e909b3c5fdb8192490bdd8e1495e85906af74fe6e34a2a77ba0673"}, + {file = "coverage-7.13.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ee68b21909686eeb21dfcba2c3b81fee70dcf38b140dcd5aa70680995fa3aa5"}, + {file = "coverage-7.13.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:724b1b270cb13ea2e6503476e34541a0b1f62280bc997eab443f87790202033d"}, + {file = "coverage-7.13.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:916abf1ac5cf7eb16bc540a5bf75c71c43a676f5c52fcb9fe75a2bd75fb944e8"}, + {file = "coverage-7.13.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:776483fd35b58d8afe3acbd9988d5de592ab6da2d2a865edfdbc9fdb43e7c486"}, + {file = "coverage-7.13.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b6f3b96617e9852703f5b633ea01315ca45c77e879584f283c44127f0f1ec564"}, + {file = "coverage-7.13.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd63e7b74661fed317212fab774e2a648bc4bb09b35f25474f8e3325d2945cd7"}, + {file = "coverage-7.13.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:933082f161bbb3e9f90d00990dc956120f608cdbcaeea15c4d897f56ef4fe416"}, + {file = "coverage-7.13.1-cp314-cp314t-win32.whl", hash = "sha256:18be793c4c87de2965e1c0f060f03d9e5aff66cfeae8e1dbe6e5b88056ec153f"}, + {file = "coverage-7.13.1-cp314-cp314t-win_amd64.whl", hash = "sha256:0e42e0ec0cd3e0d851cb3c91f770c9301f48647cb2877cb78f74bdaa07639a79"}, + {file = "coverage-7.13.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eaecf47ef10c72ece9a2a92118257da87e460e113b83cc0d2905cbbe931792b4"}, + {file = "coverage-7.13.1-py3-none-any.whl", hash = "sha256:2016745cb3ba554469d02819d78958b571792bb68e31302610e898f80dd3a573"}, + {file = "coverage-7.13.1.tar.gz", hash = "sha256:b7593fe7eb5feaa3fbb461ac79aac9f9fc0387a5ca8080b0c6fe2ca27b091afd"}, ] [package.dependencies] tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} [package.extras] -toml = ["tomli"] +toml = ["tomli ; python_full_version <= \"3.11.0a6\""] [[package]] name = "distlib" @@ -467,6 +455,27 @@ files = [ {file = "distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d"}, ] +[[package]] +name = "dnspython" +version = "2.8.0" +description = "DNS toolkit" +optional = false +python-versions = ">=3.10" +groups = ["docs"] +files = [ + {file = "dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af"}, + {file = "dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f"}, +] + +[package.extras] +dev = ["black (>=25.1.0)", "coverage (>=7.0)", "flake8 (>=7)", "hypercorn (>=0.17.0)", "mypy (>=1.17)", "pylint (>=3)", "pytest (>=8.4)", "pytest-cov (>=6.2.0)", "quart-trio (>=0.12.0)", "sphinx (>=8.2.0)", "sphinx-rtd-theme (>=3.0.0)", "twine (>=6.1.0)", "wheel (>=0.45.0)"] +dnssec = ["cryptography (>=45)"] +doh = ["h2 (>=4.2.0)", "httpcore (>=1.0.0)", "httpx (>=0.28.0)"] +doq = ["aioquic (>=1.2.0)"] +idna = ["idna (>=3.10)"] +trio = ["trio (>=0.30)"] +wmi = ["wmi (>=1.5.1) ; platform_system == \"Windows\""] + [[package]] name = "docformatter" version = "1.7.7" @@ -484,7 +493,7 @@ charset_normalizer = ">=3.0.0,<4.0.0" untokenize = ">=0.1.1,<0.2.0" [package.extras] -tomli = ["tomli (>=2.0.0,<3.0.0)"] +tomli = ["tomli (>=2.0.0,<3.0.0) ; python_version < \"3.11\""] [[package]] name = "docutils" @@ -500,15 +509,15 @@ files = [ [[package]] name = "exceptiongroup" -version = "1.3.0" +version = "1.3.1" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" groups = ["dev"] -markers = "python_version < \"3.11\"" +markers = "python_version == \"3.10\"" files = [ - {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, - {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, + {file = "exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598"}, + {file = "exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219"}, ] [package.dependencies] @@ -519,14 +528,14 @@ test = ["pytest (>=6)"] [[package]] name = "filelock" -version = "3.20.0" +version = "3.20.1" description = "A platform independent file lock." optional = false python-versions = ">=3.10" groups = ["code-quality"] files = [ - {file = "filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2"}, - {file = "filelock-3.20.0.tar.gz", hash = "sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4"}, + {file = "filelock-3.20.1-py3-none-any.whl", hash = "sha256:15d9e9a67306188a44baa72f569d2bfd803076269365fdea0934385da4dc361a"}, + {file = "filelock-3.20.1.tar.gz", hash = "sha256:b8360948b351b80f420878d8516519a2204b07aefcdcfd24912a5d33127f188c"}, ] [[package]] @@ -608,14 +617,14 @@ files = [ [[package]] name = "iniconfig" -version = "2.1.0" +version = "2.3.0" description = "brain-dead simple config-ini parsing" optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, - {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, + {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, + {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, ] [[package]] @@ -673,6 +682,23 @@ files = [ [package.dependencies] referencing = ">=0.31.0" +[[package]] +name = "linkchecker" +version = "10.6.0" +description = "check links in web documents or full websites" +optional = false +python-versions = ">=3.9" +groups = ["docs"] +files = [ + {file = "LinkChecker-10.6.0-py3-none-any.whl", hash = "sha256:5268587ed0b0f7e7521b75905128c96856f30f67dad49f66e2c963bc174ca92d"}, + {file = "LinkChecker-10.6.0.tar.gz", hash = "sha256:fb7e8facda7749c2fa5fa5dc241c0adc302da3d31d588964a2570db501aa49e5"}, +] + +[package.dependencies] +beautifulsoup4 = ">=4.8.1" +dnspython = ">=2.0" +requests = ">=2.20" + [[package]] name = "markdown-it-py" version = "4.0.0" @@ -834,14 +860,14 @@ files = [ [[package]] name = "nodeenv" -version = "1.9.1" +version = "1.10.0" description = "Node.js virtual environment builder" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" groups = ["code-quality"] files = [ - {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, - {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, + {file = "nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827"}, + {file = "nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb"}, ] [[package]] @@ -927,7 +953,7 @@ tabulate = ">=0.8.10" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} [package.extras] -developer = ["pre-commit (>=3.3)", "tomli"] +developer = ["pre-commit (>=3.3)", "tomli ; python_version < \"3.11\""] doc = ["intersphinx-registry", "matplotlib (>=3.5)", "numpy (>=1.22)", "pydata-sphinx-theme (>=0.13.3)", "sphinx (>=7)"] test = ["matplotlib", "pytest", "pytest-cov"] @@ -957,14 +983,14 @@ files = [ [[package]] name = "platformdirs" -version = "4.5.0" +version = "4.5.1" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.10" groups = ["code-quality"] files = [ - {file = "platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3"}, - {file = "platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312"}, + {file = "platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31"}, + {file = "platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda"}, ] [package.extras] @@ -1036,151 +1062,155 @@ files = [ [[package]] name = "pydantic" -version = "2.12.2" +version = "2.12.5" description = "Data validation using Python type hints" optional = false python-versions = ">=3.9" groups = ["release"] files = [ - {file = "pydantic-2.12.2-py3-none-any.whl", hash = "sha256:25ff718ee909acd82f1ff9b1a4acfd781bb23ab3739adaa7144f19a6a4e231ae"}, - {file = "pydantic-2.12.2.tar.gz", hash = "sha256:7b8fa15b831a4bbde9d5b84028641ac3080a4ca2cbd4a621a661687e741624fd"}, + {file = "pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d"}, + {file = "pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49"}, ] [package.dependencies] annotated-types = ">=0.6.0" -pydantic-core = "2.41.4" +pydantic-core = "2.41.5" typing-extensions = ">=4.14.1" typing-inspection = ">=0.4.2" [package.extras] email = ["email-validator (>=2.0.0)"] -timezone = ["tzdata"] +timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] [[package]] name = "pydantic-core" -version = "2.41.4" +version = "2.41.5" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.9" groups = ["release"] files = [ - {file = "pydantic_core-2.41.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2442d9a4d38f3411f22eb9dd0912b7cbf4b7d5b6c92c4173b75d3e1ccd84e36e"}, - {file = "pydantic_core-2.41.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:30a9876226dda131a741afeab2702e2d127209bde3c65a2b8133f428bc5d006b"}, - {file = "pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d55bbac04711e2980645af68b97d445cdbcce70e5216de444a6c4b6943ebcccd"}, - {file = "pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e1d778fb7849a42d0ee5927ab0f7453bf9f85eef8887a546ec87db5ddb178945"}, - {file = "pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b65077a4693a98b90ec5ad8f203ad65802a1b9b6d4a7e48066925a7e1606706"}, - {file = "pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62637c769dee16eddb7686bf421be48dfc2fae93832c25e25bc7242e698361ba"}, - {file = "pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dfe3aa529c8f501babf6e502936b9e8d4698502b2cfab41e17a028d91b1ac7b"}, - {file = "pydantic_core-2.41.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ca2322da745bf2eeb581fc9ea3bbb31147702163ccbcbf12a3bb630e4bf05e1d"}, - {file = "pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e8cd3577c796be7231dcf80badcf2e0835a46665eaafd8ace124d886bab4d700"}, - {file = "pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:1cae8851e174c83633f0833e90636832857297900133705ee158cf79d40f03e6"}, - {file = "pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a26d950449aae348afe1ac8be5525a00ae4235309b729ad4d3399623125b43c9"}, - {file = "pydantic_core-2.41.4-cp310-cp310-win32.whl", hash = "sha256:0cf2a1f599efe57fa0051312774280ee0f650e11152325e41dfd3018ef2c1b57"}, - {file = "pydantic_core-2.41.4-cp310-cp310-win_amd64.whl", hash = "sha256:a8c2e340d7e454dc3340d3d2e8f23558ebe78c98aa8f68851b04dcb7bc37abdc"}, - {file = "pydantic_core-2.41.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:28ff11666443a1a8cf2a044d6a545ebffa8382b5f7973f22c36109205e65dc80"}, - {file = "pydantic_core-2.41.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:61760c3925d4633290292bad462e0f737b840508b4f722247d8729684f6539ae"}, - {file = "pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eae547b7315d055b0de2ec3965643b0ab82ad0106a7ffd29615ee9f266a02827"}, - {file = "pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ef9ee5471edd58d1fcce1c80ffc8783a650e3e3a193fe90d52e43bb4d87bff1f"}, - {file = "pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:15dd504af121caaf2c95cb90c0ebf71603c53de98305621b94da0f967e572def"}, - {file = "pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a926768ea49a8af4d36abd6a8968b8790f7f76dd7cbd5a4c180db2b4ac9a3a2"}, - {file = "pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6916b9b7d134bff5440098a4deb80e4cb623e68974a87883299de9124126c2a8"}, - {file = "pydantic_core-2.41.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5cf90535979089df02e6f17ffd076f07237efa55b7343d98760bde8743c4b265"}, - {file = "pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7533c76fa647fade2d7ec75ac5cc079ab3f34879626dae5689b27790a6cf5a5c"}, - {file = "pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:37e516bca9264cbf29612539801ca3cd5d1be465f940417b002905e6ed79d38a"}, - {file = "pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0c19cb355224037c83642429b8ce261ae108e1c5fbf5c028bac63c77b0f8646e"}, - {file = "pydantic_core-2.41.4-cp311-cp311-win32.whl", hash = "sha256:09c2a60e55b357284b5f31f5ab275ba9f7f70b7525e18a132ec1f9160b4f1f03"}, - {file = "pydantic_core-2.41.4-cp311-cp311-win_amd64.whl", hash = "sha256:711156b6afb5cb1cb7c14a2cc2c4a8b4c717b69046f13c6b332d8a0a8f41ca3e"}, - {file = "pydantic_core-2.41.4-cp311-cp311-win_arm64.whl", hash = "sha256:6cb9cf7e761f4f8a8589a45e49ed3c0d92d1d696a45a6feaee8c904b26efc2db"}, - {file = "pydantic_core-2.41.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ab06d77e053d660a6faaf04894446df7b0a7e7aba70c2797465a0a1af00fc887"}, - {file = "pydantic_core-2.41.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c53ff33e603a9c1179a9364b0a24694f183717b2e0da2b5ad43c316c956901b2"}, - {file = "pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:304c54176af2c143bd181d82e77c15c41cbacea8872a2225dd37e6544dce9999"}, - {file = "pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:025ba34a4cf4fb32f917d5d188ab5e702223d3ba603be4d8aca2f82bede432a4"}, - {file = "pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9f5f30c402ed58f90c70e12eff65547d3ab74685ffe8283c719e6bead8ef53f"}, - {file = "pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd96e5d15385d301733113bcaa324c8bcf111275b7675a9c6e88bfb19fc05e3b"}, - {file = "pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98f348cbb44fae6e9653c1055db7e29de67ea6a9ca03a5fa2c2e11a47cff0e47"}, - {file = "pydantic_core-2.41.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec22626a2d14620a83ca583c6f5a4080fa3155282718b6055c2ea48d3ef35970"}, - {file = "pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3a95d4590b1f1a43bf33ca6d647b990a88f4a3824a8c4572c708f0b45a5290ed"}, - {file = "pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:f9672ab4d398e1b602feadcffcdd3af44d5f5e6ddc15bc7d15d376d47e8e19f8"}, - {file = "pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:84d8854db5f55fead3b579f04bda9a36461dab0730c5d570e1526483e7bb8431"}, - {file = "pydantic_core-2.41.4-cp312-cp312-win32.whl", hash = "sha256:9be1c01adb2ecc4e464392c36d17f97e9110fbbc906bcbe1c943b5b87a74aabd"}, - {file = "pydantic_core-2.41.4-cp312-cp312-win_amd64.whl", hash = "sha256:d682cf1d22bab22a5be08539dca3d1593488a99998f9f412137bc323179067ff"}, - {file = "pydantic_core-2.41.4-cp312-cp312-win_arm64.whl", hash = "sha256:833eebfd75a26d17470b58768c1834dfc90141b7afc6eb0429c21fc5a21dcfb8"}, - {file = "pydantic_core-2.41.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:85e050ad9e5f6fe1004eec65c914332e52f429bc0ae12d6fa2092407a462c746"}, - {file = "pydantic_core-2.41.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7393f1d64792763a48924ba31d1e44c2cfbc05e3b1c2c9abb4ceeadd912cced"}, - {file = "pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94dab0940b0d1fb28bcab847adf887c66a27a40291eedf0b473be58761c9799a"}, - {file = "pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:de7c42f897e689ee6f9e93c4bec72b99ae3b32a2ade1c7e4798e690ff5246e02"}, - {file = "pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:664b3199193262277b8b3cd1e754fb07f2c6023289c815a1e1e8fb415cb247b1"}, - {file = "pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d95b253b88f7d308b1c0b417c4624f44553ba4762816f94e6986819b9c273fb2"}, - {file = "pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1351f5bbdbbabc689727cb91649a00cb9ee7203e0a6e54e9f5ba9e22e384b84"}, - {file = "pydantic_core-2.41.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1affa4798520b148d7182da0615d648e752de4ab1a9566b7471bc803d88a062d"}, - {file = "pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7b74e18052fea4aa8dea2fb7dbc23d15439695da6cbe6cfc1b694af1115df09d"}, - {file = "pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:285b643d75c0e30abda9dc1077395624f314a37e3c09ca402d4015ef5979f1a2"}, - {file = "pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:f52679ff4218d713b3b33f88c89ccbf3a5c2c12ba665fb80ccc4192b4608dbab"}, - {file = "pydantic_core-2.41.4-cp313-cp313-win32.whl", hash = "sha256:ecde6dedd6fff127c273c76821bb754d793be1024bc33314a120f83a3c69460c"}, - {file = "pydantic_core-2.41.4-cp313-cp313-win_amd64.whl", hash = "sha256:d081a1f3800f05409ed868ebb2d74ac39dd0c1ff6c035b5162356d76030736d4"}, - {file = "pydantic_core-2.41.4-cp313-cp313-win_arm64.whl", hash = "sha256:f8e49c9c364a7edcbe2a310f12733aad95b022495ef2a8d653f645e5d20c1564"}, - {file = "pydantic_core-2.41.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ed97fd56a561f5eb5706cebe94f1ad7c13b84d98312a05546f2ad036bafe87f4"}, - {file = "pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a870c307bf1ee91fc58a9a61338ff780d01bfae45922624816878dce784095d2"}, - {file = "pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d25e97bc1f5f8f7985bdc2335ef9e73843bb561eb1fa6831fdfc295c1c2061cf"}, - {file = "pydantic_core-2.41.4-cp313-cp313t-win_amd64.whl", hash = "sha256:d405d14bea042f166512add3091c1af40437c2e7f86988f3915fabd27b1e9cd2"}, - {file = "pydantic_core-2.41.4-cp313-cp313t-win_arm64.whl", hash = "sha256:19f3684868309db5263a11bace3c45d93f6f24afa2ffe75a647583df22a2ff89"}, - {file = "pydantic_core-2.41.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:e9205d97ed08a82ebb9a307e92914bb30e18cdf6f6b12ca4bedadb1588a0bfe1"}, - {file = "pydantic_core-2.41.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:82df1f432b37d832709fbcc0e24394bba04a01b6ecf1ee87578145c19cde12ac"}, - {file = "pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3b4cc4539e055cfa39a3763c939f9d409eb40e85813257dcd761985a108554"}, - {file = "pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b1eb1754fce47c63d2ff57fdb88c351a6c0150995890088b33767a10218eaa4e"}, - {file = "pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6ab5ab30ef325b443f379ddb575a34969c333004fca5a1daa0133a6ffaad616"}, - {file = "pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:31a41030b1d9ca497634092b46481b937ff9397a86f9f51bd41c4767b6fc04af"}, - {file = "pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a44ac1738591472c3d020f61c6df1e4015180d6262ebd39bf2aeb52571b60f12"}, - {file = "pydantic_core-2.41.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d72f2b5e6e82ab8f94ea7d0d42f83c487dc159c5240d8f83beae684472864e2d"}, - {file = "pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c4d1e854aaf044487d31143f541f7aafe7b482ae72a022c664b2de2e466ed0ad"}, - {file = "pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b568af94267729d76e6ee5ececda4e283d07bbb28e8148bb17adad93d025d25a"}, - {file = "pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6d55fb8b1e8929b341cc313a81a26e0d48aa3b519c1dbaadec3a6a2b4fcad025"}, - {file = "pydantic_core-2.41.4-cp314-cp314-win32.whl", hash = "sha256:5b66584e549e2e32a1398df11da2e0a7eff45d5c2d9db9d5667c5e6ac764d77e"}, - {file = "pydantic_core-2.41.4-cp314-cp314-win_amd64.whl", hash = "sha256:557a0aab88664cc552285316809cab897716a372afaf8efdbef756f8b890e894"}, - {file = "pydantic_core-2.41.4-cp314-cp314-win_arm64.whl", hash = "sha256:3f1ea6f48a045745d0d9f325989d8abd3f1eaf47dd00485912d1a3a63c623a8d"}, - {file = "pydantic_core-2.41.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6c1fe4c5404c448b13188dd8bd2ebc2bdd7e6727fa61ff481bcc2cca894018da"}, - {file = "pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:523e7da4d43b113bf8e7b49fa4ec0c35bf4fe66b2230bfc5c13cc498f12c6c3e"}, - {file = "pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5729225de81fb65b70fdb1907fcf08c75d498f4a6f15af005aabb1fdadc19dfa"}, - {file = "pydantic_core-2.41.4-cp314-cp314t-win_amd64.whl", hash = "sha256:de2cfbb09e88f0f795fd90cf955858fc2c691df65b1f21f0aa00b99f3fbc661d"}, - {file = "pydantic_core-2.41.4-cp314-cp314t-win_arm64.whl", hash = "sha256:d34f950ae05a83e0ede899c595f312ca976023ea1db100cd5aa188f7005e3ab0"}, - {file = "pydantic_core-2.41.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:646e76293345954acea6966149683047b7b2ace793011922208c8e9da12b0062"}, - {file = "pydantic_core-2.41.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cc8e85a63085a137d286e2791037f5fdfff0aabb8b899483ca9c496dd5797338"}, - {file = "pydantic_core-2.41.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:692c622c8f859a17c156492783902d8370ac7e121a611bd6fe92cc71acf9ee8d"}, - {file = "pydantic_core-2.41.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d1e2906efb1031a532600679b424ef1d95d9f9fb507f813951f23320903adbd7"}, - {file = "pydantic_core-2.41.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e04e2f7f8916ad3ddd417a7abdd295276a0bf216993d9318a5d61cc058209166"}, - {file = "pydantic_core-2.41.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df649916b81822543d1c8e0e1d079235f68acdc7d270c911e8425045a8cfc57e"}, - {file = "pydantic_core-2.41.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66c529f862fdba70558061bb936fe00ddbaaa0c647fd26e4a4356ef1d6561891"}, - {file = "pydantic_core-2.41.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3b4c5a1fd3a311563ed866c2c9b62da06cb6398bee186484ce95c820db71cb"}, - {file = "pydantic_core-2.41.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6e0fc40d84448f941df9b3334c4b78fe42f36e3bf631ad54c3047a0cdddc2514"}, - {file = "pydantic_core-2.41.4-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:44e7625332683b6c1c8b980461475cde9595eff94447500e80716db89b0da005"}, - {file = "pydantic_core-2.41.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:170ee6835f6c71081d031ef1c3b4dc4a12b9efa6a9540f93f95b82f3c7571ae8"}, - {file = "pydantic_core-2.41.4-cp39-cp39-win32.whl", hash = "sha256:3adf61415efa6ce977041ba9745183c0e1f637ca849773afa93833e04b163feb"}, - {file = "pydantic_core-2.41.4-cp39-cp39-win_amd64.whl", hash = "sha256:a238dd3feee263eeaeb7dc44aea4ba1364682c4f9f9467e6af5596ba322c2332"}, - {file = "pydantic_core-2.41.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:a1b2cfec3879afb742a7b0bcfa53e4f22ba96571c9e54d6a3afe1052d17d843b"}, - {file = "pydantic_core-2.41.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:d175600d975b7c244af6eb9c9041f10059f20b8bbffec9e33fdd5ee3f67cdc42"}, - {file = "pydantic_core-2.41.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f184d657fa4947ae5ec9c47bd7e917730fa1cbb78195037e32dcbab50aca5ee"}, - {file = "pydantic_core-2.41.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ed810568aeffed3edc78910af32af911c835cc39ebbfacd1f0ab5dd53028e5c"}, - {file = "pydantic_core-2.41.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:4f5d640aeebb438517150fdeec097739614421900e4a08db4a3ef38898798537"}, - {file = "pydantic_core-2.41.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:4a9ab037b71927babc6d9e7fc01aea9e66dc2a4a34dff06ef0724a4049629f94"}, - {file = "pydantic_core-2.41.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4dab9484ec605c3016df9ad4fd4f9a390bc5d816a3b10c6550f8424bb80b18c"}, - {file = "pydantic_core-2.41.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8a5028425820731d8c6c098ab642d7b8b999758e24acae03ed38a66eca8335"}, - {file = "pydantic_core-2.41.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:1e5ab4fc177dd41536b3c32b2ea11380dd3d4619a385860621478ac2d25ceb00"}, - {file = "pydantic_core-2.41.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:3d88d0054d3fa11ce936184896bed3c1c5441d6fa483b498fac6a5d0dd6f64a9"}, - {file = "pydantic_core-2.41.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b2a054a8725f05b4b6503357e0ac1c4e8234ad3b0c2ac130d6ffc66f0e170e2"}, - {file = "pydantic_core-2.41.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0d9db5a161c99375a0c68c058e227bee1d89303300802601d76a3d01f74e258"}, - {file = "pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:6273ea2c8ffdac7b7fda2653c49682db815aebf4a89243a6feccf5e36c18c347"}, - {file = "pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:4c973add636efc61de22530b2ef83a65f39b6d6f656df97f678720e20de26caa"}, - {file = "pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b69d1973354758007f46cf2d44a4f3d0933f10b6dc9bf15cf1356e037f6f731a"}, - {file = "pydantic_core-2.41.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:3619320641fd212aaf5997b6ca505e97540b7e16418f4a241f44cdf108ffb50d"}, - {file = "pydantic_core-2.41.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:491535d45cd7ad7e4a2af4a5169b0d07bebf1adfd164b0368da8aa41e19907a5"}, - {file = "pydantic_core-2.41.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:54d86c0cada6aba4ec4c047d0e348cbad7063b87ae0f005d9f8c9ad04d4a92a2"}, - {file = "pydantic_core-2.41.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eca1124aced216b2500dc2609eade086d718e8249cb9696660ab447d50a758bd"}, - {file = "pydantic_core-2.41.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6c9024169becccf0cb470ada03ee578d7348c119a0d42af3dcf9eda96e3a247c"}, - {file = "pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:26895a4268ae5a2849269f4991cdc97236e4b9c010e51137becf25182daac405"}, - {file = "pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:ca4df25762cf71308c446e33c9b1fdca2923a3f13de616e2a949f38bf21ff5a8"}, - {file = "pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:5a28fcedd762349519276c36634e71853b4541079cab4acaaac60c4421827308"}, - {file = "pydantic_core-2.41.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c173ddcd86afd2535e2b695217e82191580663a1d1928239f877f5a1649ef39f"}, - {file = "pydantic_core-2.41.4.tar.gz", hash = "sha256:70e47929a9d4a1905a67e4b687d5946026390568a8e952b92824118063cee4d5"}, + {file = "pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146"}, + {file = "pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c"}, + {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2"}, + {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556"}, + {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49"}, + {file = "pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba"}, + {file = "pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9"}, + {file = "pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6"}, + {file = "pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b"}, + {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284"}, + {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594"}, + {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e"}, + {file = "pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b"}, + {file = "pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe"}, + {file = "pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f"}, + {file = "pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7"}, + {file = "pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5"}, + {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c"}, + {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294"}, + {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1"}, + {file = "pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d"}, + {file = "pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815"}, + {file = "pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3"}, + {file = "pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9"}, + {file = "pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d"}, + {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740"}, + {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e"}, + {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858"}, + {file = "pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36"}, + {file = "pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11"}, + {file = "pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd"}, + {file = "pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a"}, + {file = "pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553"}, + {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90"}, + {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07"}, + {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb"}, + {file = "pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23"}, + {file = "pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf"}, + {file = "pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008"}, + {file = "pydantic_core-2.41.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:8bfeaf8735be79f225f3fefab7f941c712aaca36f1128c9d7e2352ee1aa87bdf"}, + {file = "pydantic_core-2.41.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:346285d28e4c8017da95144c7f3acd42740d637ff41946af5ce6e5e420502dd5"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a75dafbf87d6276ddc5b2bf6fae5254e3d0876b626eb24969a574fff9149ee5d"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b93a4d08587e2b7e7882de461e82b6ed76d9026ce91ca7915e740ecc7855f60"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8465ab91a4bd96d36dde3263f06caa6a8a6019e4113f24dc753d79a8b3a3f82"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:299e0a22e7ae2b85c1a57f104538b2656e8ab1873511fd718a1c1c6f149b77b5"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:707625ef0983fcfb461acfaf14de2067c5942c6bb0f3b4c99158bed6fedd3cf3"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f41eb9797986d6ebac5e8edff36d5cef9de40def462311b3eb3eeded1431e425"}, + {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0384e2e1021894b1ff5a786dbf94771e2986ebe2869533874d7e43bc79c6f504"}, + {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:f0cd744688278965817fd0839c4a4116add48d23890d468bc436f78beb28abf5"}, + {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:753e230374206729bf0a807954bcc6c150d3743928a73faffee51ac6557a03c3"}, + {file = "pydantic_core-2.41.5-cp39-cp39-win32.whl", hash = "sha256:873e0d5b4fb9b89ef7c2d2a963ea7d02879d9da0da8d9d4933dee8ee86a8b460"}, + {file = "pydantic_core-2.41.5-cp39-cp39-win_amd64.whl", hash = "sha256:e4f4a984405e91527a0d62649ee21138f8e3d0ef103be488c1dc11a80d7f184b"}, + {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034"}, + {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c"}, + {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2"}, + {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad"}, + {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd"}, + {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc"}, + {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56"}, + {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51"}, + {file = "pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e"}, ] [package.dependencies] @@ -1188,14 +1218,14 @@ typing-extensions = ">=4.14.1" [[package]] name = "pydantic-settings" -version = "2.11.0" +version = "2.12.0" description = "Settings management using Pydantic" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["release"] files = [ - {file = "pydantic_settings-2.11.0-py3-none-any.whl", hash = "sha256:fe2cea3413b9530d10f3a5875adffb17ada5c1e1bab0b2885546d7310415207c"}, - {file = "pydantic_settings-2.11.0.tar.gz", hash = "sha256:d0e87a1c7d33593beb7194adb8470fc426e95ba02af83a0f23474a04c9a08180"}, + {file = "pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809"}, + {file = "pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0"}, ] [package.dependencies] @@ -1298,14 +1328,14 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] [[package]] name = "python-dotenv" -version = "1.1.1" +version = "1.2.1" description = "Read key-value pairs from a .env file and set them as environment variables" optional = false python-versions = ">=3.9" groups = ["release"] files = [ - {file = "python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc"}, - {file = "python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab"}, + {file = "python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61"}, + {file = "python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6"}, ] [package.extras] @@ -1484,14 +1514,14 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "rich-click" -version = "1.9.3" +version = "1.9.5" description = "Format click help output nicely with rich" optional = false python-versions = ">=3.8" groups = ["release"] files = [ - {file = "rich_click-1.9.3-py3-none-any.whl", hash = "sha256:8ef51bc340db4d048a846c15c035d27b88acf720cbbb9b6fecf6c8b1a297b909"}, - {file = "rich_click-1.9.3.tar.gz", hash = "sha256:60839150a935604df1378b159da340d3fff91f912903e935da7cb615b5738c1b"}, + {file = "rich_click-1.9.5-py3-none-any.whl", hash = "sha256:9b195721a773b1acf0e16ff9ec68cef1e7d237e53471e6e3f7ade462f86c403a"}, + {file = "rich_click-1.9.5.tar.gz", hash = "sha256:48120531493f1533828da80e13e839d471979ec8d7d0ca7b35f86a1379cc74b6"}, ] [package.dependencies] @@ -1502,171 +1532,131 @@ typing-extensions = {version = ">=4", markers = "python_version < \"3.11\""} [package.extras] dev = ["inline-snapshot (>=0.24)", "jsonschema (>=4)", "mypy (>=1.14.1)", "nodeenv (>=1.9.1)", "packaging (>=25)", "pre-commit (>=3.5)", "pytest (>=8.3.5)", "pytest-cov (>=5)", "rich-codex (>=1.2.11)", "ruff (>=0.12.4)", "typer (>=0.15)", "types-setuptools (>=75.8.0.20250110)"] -docs = ["markdown-include (>=0.8.1)", "mike (>=2.1.3)", "mkdocs-github-admonitions-plugin (>=0.1.1)", "mkdocs-glightbox (>=0.4)", "mkdocs-include-markdown-plugin (>=7.1.7)", "mkdocs-material-extensions (>=1.3.1)", "mkdocs-material[imaging] (>=9.5.18,<9.6.0)", "mkdocs-redirects (>=1.2.2)", "mkdocs-rss-plugin (>=1.15)", "mkdocs[docs] (>=1.6.1)", "mkdocstrings[python] (>=0.26.1)", "rich-codex (>=1.2.11)", "typer (>=0.15)"] +docs = ["markdown-include (>=0.8.1)", "mike (>=2.1.3)", "mkdocs-github-admonitions-plugin (>=0.1.1)", "mkdocs-glightbox (>=0.4)", "mkdocs-include-markdown-plugin (>=7.1.7) ; python_version >= \"3.9\"", "mkdocs-material-extensions (>=1.3.1)", "mkdocs-material[imaging] (>=9.5.18,<9.6.0)", "mkdocs-redirects (>=1.2.2)", "mkdocs-rss-plugin (>=1.15)", "mkdocs[docs] (>=1.6.1)", "mkdocstrings[python] (>=0.26.1)", "rich-codex (>=1.2.11)", "typer (>=0.15)"] [[package]] name = "rpds-py" -version = "0.27.1" +version = "0.30.0" description = "Python bindings to Rust's persistent data structures (rpds)" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["docs"] files = [ - {file = "rpds_py-0.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:68afeec26d42ab3b47e541b272166a0b4400313946871cba3ed3a4fc0cab1cef"}, - {file = "rpds_py-0.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74e5b2f7bb6fa38b1b10546d27acbacf2a022a8b5543efb06cfebc72a59c85be"}, - {file = "rpds_py-0.27.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9024de74731df54546fab0bfbcdb49fae19159ecaecfc8f37c18d2c7e2c0bd61"}, - {file = "rpds_py-0.27.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:31d3ebadefcd73b73928ed0b2fd696f7fefda8629229f81929ac9c1854d0cffb"}, - {file = "rpds_py-0.27.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2e7f8f169d775dd9092a1743768d771f1d1300453ddfe6325ae3ab5332b4657"}, - {file = "rpds_py-0.27.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d905d16f77eb6ab2e324e09bfa277b4c8e5e6b8a78a3e7ff8f3cdf773b4c013"}, - {file = "rpds_py-0.27.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50c946f048209e6362e22576baea09193809f87687a95a8db24e5fbdb307b93a"}, - {file = "rpds_py-0.27.1-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:3deab27804d65cd8289eb814c2c0e807c4b9d9916c9225e363cb0cf875eb67c1"}, - {file = "rpds_py-0.27.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8b61097f7488de4be8244c89915da8ed212832ccf1e7c7753a25a394bf9b1f10"}, - {file = "rpds_py-0.27.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8a3f29aba6e2d7d90528d3c792555a93497fe6538aa65eb675b44505be747808"}, - {file = "rpds_py-0.27.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dd6cd0485b7d347304067153a6dc1d73f7d4fd995a396ef32a24d24b8ac63ac8"}, - {file = "rpds_py-0.27.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6f4461bf931108c9fa226ffb0e257c1b18dc2d44cd72b125bec50ee0ab1248a9"}, - {file = "rpds_py-0.27.1-cp310-cp310-win32.whl", hash = "sha256:ee5422d7fb21f6a00c1901bf6559c49fee13a5159d0288320737bbf6585bd3e4"}, - {file = "rpds_py-0.27.1-cp310-cp310-win_amd64.whl", hash = "sha256:3e039aabf6d5f83c745d5f9a0a381d031e9ed871967c0a5c38d201aca41f3ba1"}, - {file = "rpds_py-0.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:be898f271f851f68b318872ce6ebebbc62f303b654e43bf72683dbdc25b7c881"}, - {file = "rpds_py-0.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:62ac3d4e3e07b58ee0ddecd71d6ce3b1637de2d373501412df395a0ec5f9beb5"}, - {file = "rpds_py-0.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4708c5c0ceb2d034f9991623631d3d23cb16e65c83736ea020cdbe28d57c0a0e"}, - {file = "rpds_py-0.27.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:abfa1171a9952d2e0002aba2ad3780820b00cc3d9c98c6630f2e93271501f66c"}, - {file = "rpds_py-0.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b507d19f817ebaca79574b16eb2ae412e5c0835542c93fe9983f1e432aca195"}, - {file = "rpds_py-0.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:168b025f8fd8d8d10957405f3fdcef3dc20f5982d398f90851f4abc58c566c52"}, - {file = "rpds_py-0.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb56c6210ef77caa58e16e8c17d35c63fe3f5b60fd9ba9d424470c3400bcf9ed"}, - {file = "rpds_py-0.27.1-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:d252f2d8ca0195faa707f8eb9368955760880b2b42a8ee16d382bf5dd807f89a"}, - {file = "rpds_py-0.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6e5e54da1e74b91dbc7996b56640f79b195d5925c2b78efaa8c5d53e1d88edde"}, - {file = "rpds_py-0.27.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ffce0481cc6e95e5b3f0a47ee17ffbd234399e6d532f394c8dce320c3b089c21"}, - {file = "rpds_py-0.27.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a205fdfe55c90c2cd8e540ca9ceba65cbe6629b443bc05db1f590a3db8189ff9"}, - {file = "rpds_py-0.27.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:689fb5200a749db0415b092972e8eba85847c23885c8543a8b0f5c009b1a5948"}, - {file = "rpds_py-0.27.1-cp311-cp311-win32.whl", hash = "sha256:3182af66048c00a075010bc7f4860f33913528a4b6fc09094a6e7598e462fe39"}, - {file = "rpds_py-0.27.1-cp311-cp311-win_amd64.whl", hash = "sha256:b4938466c6b257b2f5c4ff98acd8128ec36b5059e5c8f8372d79316b1c36bb15"}, - {file = "rpds_py-0.27.1-cp311-cp311-win_arm64.whl", hash = "sha256:2f57af9b4d0793e53266ee4325535a31ba48e2f875da81a9177c9926dfa60746"}, - {file = "rpds_py-0.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ae2775c1973e3c30316892737b91f9283f9908e3cc7625b9331271eaaed7dc90"}, - {file = "rpds_py-0.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2643400120f55c8a96f7c9d858f7be0c88d383cd4653ae2cf0d0c88f668073e5"}, - {file = "rpds_py-0.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16323f674c089b0360674a4abd28d5042947d54ba620f72514d69be4ff64845e"}, - {file = "rpds_py-0.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a1f4814b65eacac94a00fc9a526e3fdafd78e439469644032032d0d63de4881"}, - {file = "rpds_py-0.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ba32c16b064267b22f1850a34051121d423b6f7338a12b9459550eb2096e7ec"}, - {file = "rpds_py-0.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5c20f33fd10485b80f65e800bbe5f6785af510b9f4056c5a3c612ebc83ba6cb"}, - {file = "rpds_py-0.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:466bfe65bd932da36ff279ddd92de56b042f2266d752719beb97b08526268ec5"}, - {file = "rpds_py-0.27.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:41e532bbdcb57c92ba3be62c42e9f096431b4cf478da9bc3bc6ce5c38ab7ba7a"}, - {file = "rpds_py-0.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f149826d742b406579466283769a8ea448eed82a789af0ed17b0cd5770433444"}, - {file = "rpds_py-0.27.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80c60cfb5310677bd67cb1e85a1e8eb52e12529545441b43e6f14d90b878775a"}, - {file = "rpds_py-0.27.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7ee6521b9baf06085f62ba9c7a3e5becffbc32480d2f1b351559c001c38ce4c1"}, - {file = "rpds_py-0.27.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a512c8263249a9d68cac08b05dd59d2b3f2061d99b322813cbcc14c3c7421998"}, - {file = "rpds_py-0.27.1-cp312-cp312-win32.whl", hash = "sha256:819064fa048ba01b6dadc5116f3ac48610435ac9a0058bbde98e569f9e785c39"}, - {file = "rpds_py-0.27.1-cp312-cp312-win_amd64.whl", hash = "sha256:d9199717881f13c32c4046a15f024971a3b78ad4ea029e8da6b86e5aa9cf4594"}, - {file = "rpds_py-0.27.1-cp312-cp312-win_arm64.whl", hash = "sha256:33aa65b97826a0e885ef6e278fbd934e98cdcfed80b63946025f01e2f5b29502"}, - {file = "rpds_py-0.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e4b9fcfbc021633863a37e92571d6f91851fa656f0180246e84cbd8b3f6b329b"}, - {file = "rpds_py-0.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1441811a96eadca93c517d08df75de45e5ffe68aa3089924f963c782c4b898cf"}, - {file = "rpds_py-0.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55266dafa22e672f5a4f65019015f90336ed31c6383bd53f5e7826d21a0e0b83"}, - {file = "rpds_py-0.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d78827d7ac08627ea2c8e02c9e5b41180ea5ea1f747e9db0915e3adf36b62dcf"}, - {file = "rpds_py-0.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae92443798a40a92dc5f0b01d8a7c93adde0c4dc965310a29ae7c64d72b9fad2"}, - {file = "rpds_py-0.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c46c9dd2403b66a2a3b9720ec4b74d4ab49d4fabf9f03dfdce2d42af913fe8d0"}, - {file = "rpds_py-0.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2efe4eb1d01b7f5f1939f4ef30ecea6c6b3521eec451fb93191bf84b2a522418"}, - {file = "rpds_py-0.27.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:15d3b4d83582d10c601f481eca29c3f138d44c92187d197aff663a269197c02d"}, - {file = "rpds_py-0.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4ed2e16abbc982a169d30d1a420274a709949e2cbdef119fe2ec9d870b42f274"}, - {file = "rpds_py-0.27.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a75f305c9b013289121ec0f1181931975df78738cdf650093e6b86d74aa7d8dd"}, - {file = "rpds_py-0.27.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:67ce7620704745881a3d4b0ada80ab4d99df390838839921f99e63c474f82cf2"}, - {file = "rpds_py-0.27.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d992ac10eb86d9b6f369647b6a3f412fc0075cfd5d799530e84d335e440a002"}, - {file = "rpds_py-0.27.1-cp313-cp313-win32.whl", hash = "sha256:4f75e4bd8ab8db624e02c8e2fc4063021b58becdbe6df793a8111d9343aec1e3"}, - {file = "rpds_py-0.27.1-cp313-cp313-win_amd64.whl", hash = "sha256:f9025faafc62ed0b75a53e541895ca272815bec18abe2249ff6501c8f2e12b83"}, - {file = "rpds_py-0.27.1-cp313-cp313-win_arm64.whl", hash = "sha256:ed10dc32829e7d222b7d3b93136d25a406ba9788f6a7ebf6809092da1f4d279d"}, - {file = "rpds_py-0.27.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:92022bbbad0d4426e616815b16bc4127f83c9a74940e1ccf3cfe0b387aba0228"}, - {file = "rpds_py-0.27.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:47162fdab9407ec3f160805ac3e154df042e577dd53341745fc7fb3f625e6d92"}, - {file = "rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb89bec23fddc489e5d78b550a7b773557c9ab58b7946154a10a6f7a214a48b2"}, - {file = "rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e48af21883ded2b3e9eb48cb7880ad8598b31ab752ff3be6457001d78f416723"}, - {file = "rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f5b7bd8e219ed50299e58551a410b64daafb5017d54bbe822e003856f06a802"}, - {file = "rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08f1e20bccf73b08d12d804d6e1c22ca5530e71659e6673bce31a6bb71c1e73f"}, - {file = "rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dc5dceeaefcc96dc192e3a80bbe1d6c410c469e97bdd47494a7d930987f18b2"}, - {file = "rpds_py-0.27.1-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:d76f9cc8665acdc0c9177043746775aa7babbf479b5520b78ae4002d889f5c21"}, - {file = "rpds_py-0.27.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:134fae0e36022edad8290a6661edf40c023562964efea0cc0ec7f5d392d2aaef"}, - {file = "rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb11a4f1b2b63337cfd3b4d110af778a59aae51c81d195768e353d8b52f88081"}, - {file = "rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:13e608ac9f50a0ed4faec0e90ece76ae33b34c0e8656e3dceb9a7db994c692cd"}, - {file = "rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dd2135527aa40f061350c3f8f89da2644de26cd73e4de458e79606384f4f68e7"}, - {file = "rpds_py-0.27.1-cp313-cp313t-win32.whl", hash = "sha256:3020724ade63fe320a972e2ffd93b5623227e684315adce194941167fee02688"}, - {file = "rpds_py-0.27.1-cp313-cp313t-win_amd64.whl", hash = "sha256:8ee50c3e41739886606388ba3ab3ee2aae9f35fb23f833091833255a31740797"}, - {file = "rpds_py-0.27.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:acb9aafccaae278f449d9c713b64a9e68662e7799dbd5859e2c6b3c67b56d334"}, - {file = "rpds_py-0.27.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b7fb801aa7f845ddf601c49630deeeccde7ce10065561d92729bfe81bd21fb33"}, - {file = "rpds_py-0.27.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe0dd05afb46597b9a2e11c351e5e4283c741237e7f617ffb3252780cca9336a"}, - {file = "rpds_py-0.27.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b6dfb0e058adb12d8b1d1b25f686e94ffa65d9995a5157afe99743bf7369d62b"}, - {file = "rpds_py-0.27.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed090ccd235f6fa8bb5861684567f0a83e04f52dfc2e5c05f2e4b1309fcf85e7"}, - {file = "rpds_py-0.27.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf876e79763eecf3e7356f157540d6a093cef395b65514f17a356f62af6cc136"}, - {file = "rpds_py-0.27.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12ed005216a51b1d6e2b02a7bd31885fe317e45897de81d86dcce7d74618ffff"}, - {file = "rpds_py-0.27.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:ee4308f409a40e50593c7e3bb8cbe0b4d4c66d1674a316324f0c2f5383b486f9"}, - {file = "rpds_py-0.27.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b08d152555acf1f455154d498ca855618c1378ec810646fcd7c76416ac6dc60"}, - {file = "rpds_py-0.27.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:dce51c828941973a5684d458214d3a36fcd28da3e1875d659388f4f9f12cc33e"}, - {file = "rpds_py-0.27.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c1476d6f29eb81aa4151c9a31219b03f1f798dc43d8af1250a870735516a1212"}, - {file = "rpds_py-0.27.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3ce0cac322b0d69b63c9cdb895ee1b65805ec9ffad37639f291dd79467bee675"}, - {file = "rpds_py-0.27.1-cp314-cp314-win32.whl", hash = "sha256:dfbfac137d2a3d0725758cd141f878bf4329ba25e34979797c89474a89a8a3a3"}, - {file = "rpds_py-0.27.1-cp314-cp314-win_amd64.whl", hash = "sha256:a6e57b0abfe7cc513450fcf529eb486b6e4d3f8aee83e92eb5f1ef848218d456"}, - {file = "rpds_py-0.27.1-cp314-cp314-win_arm64.whl", hash = "sha256:faf8d146f3d476abfee026c4ae3bdd9ca14236ae4e4c310cbd1cf75ba33d24a3"}, - {file = "rpds_py-0.27.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:ba81d2b56b6d4911ce735aad0a1d4495e808b8ee4dc58715998741a26874e7c2"}, - {file = "rpds_py-0.27.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:84f7d509870098de0e864cad0102711c1e24e9b1a50ee713b65928adb22269e4"}, - {file = "rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9e960fc78fecd1100539f14132425e1d5fe44ecb9239f8f27f079962021523e"}, - {file = "rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:62f85b665cedab1a503747617393573995dac4600ff51869d69ad2f39eb5e817"}, - {file = "rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fed467af29776f6556250c9ed85ea5a4dd121ab56a5f8b206e3e7a4c551e48ec"}, - {file = "rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2729615f9d430af0ae6b36cf042cb55c0936408d543fb691e1a9e36648fd35a"}, - {file = "rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b207d881a9aef7ba753d69c123a35d96ca7cb808056998f6b9e8747321f03b8"}, - {file = "rpds_py-0.27.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:639fd5efec029f99b79ae47e5d7e00ad8a773da899b6309f6786ecaf22948c48"}, - {file = "rpds_py-0.27.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fecc80cb2a90e28af8a9b366edacf33d7a91cbfe4c2c4544ea1246e949cfebeb"}, - {file = "rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42a89282d711711d0a62d6f57d81aa43a1368686c45bc1c46b7f079d55692734"}, - {file = "rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:cf9931f14223de59551ab9d38ed18d92f14f055a5f78c1d8ad6493f735021bbb"}, - {file = "rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f39f58a27cc6e59f432b568ed8429c7e1641324fbe38131de852cd77b2d534b0"}, - {file = "rpds_py-0.27.1-cp314-cp314t-win32.whl", hash = "sha256:d5fa0ee122dc09e23607a28e6d7b150da16c662e66409bbe85230e4c85bb528a"}, - {file = "rpds_py-0.27.1-cp314-cp314t-win_amd64.whl", hash = "sha256:6567d2bb951e21232c2f660c24cf3470bb96de56cdcb3f071a83feeaff8a2772"}, - {file = "rpds_py-0.27.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c918c65ec2e42c2a78d19f18c553d77319119bf43aa9e2edf7fb78d624355527"}, - {file = "rpds_py-0.27.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1fea2b1a922c47c51fd07d656324531adc787e415c8b116530a1d29c0516c62d"}, - {file = "rpds_py-0.27.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbf94c58e8e0cd6b6f38d8de67acae41b3a515c26169366ab58bdca4a6883bb8"}, - {file = "rpds_py-0.27.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c2a8fed130ce946d5c585eddc7c8eeef0051f58ac80a8ee43bd17835c144c2cc"}, - {file = "rpds_py-0.27.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:037a2361db72ee98d829bc2c5b7cc55598ae0a5e0ec1823a56ea99374cfd73c1"}, - {file = "rpds_py-0.27.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5281ed1cc1d49882f9997981c88df1a22e140ab41df19071222f7e5fc4e72125"}, - {file = "rpds_py-0.27.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fd50659a069c15eef8aa3d64bbef0d69fd27bb4a50c9ab4f17f83a16cbf8905"}, - {file = "rpds_py-0.27.1-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:c4b676c4ae3921649a15d28ed10025548e9b561ded473aa413af749503c6737e"}, - {file = "rpds_py-0.27.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:079bc583a26db831a985c5257797b2b5d3affb0386e7ff886256762f82113b5e"}, - {file = "rpds_py-0.27.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4e44099bd522cba71a2c6b97f68e19f40e7d85399de899d66cdb67b32d7cb786"}, - {file = "rpds_py-0.27.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e202e6d4188e53c6661af813b46c37ca2c45e497fc558bacc1a7630ec2695aec"}, - {file = "rpds_py-0.27.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f41f814b8eaa48768d1bb551591f6ba45f87ac76899453e8ccd41dba1289b04b"}, - {file = "rpds_py-0.27.1-cp39-cp39-win32.whl", hash = "sha256:9e71f5a087ead99563c11fdaceee83ee982fd39cf67601f4fd66cb386336ee52"}, - {file = "rpds_py-0.27.1-cp39-cp39-win_amd64.whl", hash = "sha256:71108900c9c3c8590697244b9519017a400d9ba26a36c48381b3f64743a44aab"}, - {file = "rpds_py-0.27.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7ba22cb9693df986033b91ae1d7a979bc399237d45fccf875b76f62bb9e52ddf"}, - {file = "rpds_py-0.27.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5b640501be9288c77738b5492b3fd3abc4ba95c50c2e41273c8a1459f08298d3"}, - {file = "rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb08b65b93e0c6dd70aac7f7890a9c0938d5ec71d5cb32d45cf844fb8ae47636"}, - {file = "rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d7ff07d696a7a38152ebdb8212ca9e5baab56656749f3d6004b34ab726b550b8"}, - {file = "rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fb7c72262deae25366e3b6c0c0ba46007967aea15d1eea746e44ddba8ec58dcc"}, - {file = "rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b002cab05d6339716b03a4a3a2ce26737f6231d7b523f339fa061d53368c9d8"}, - {file = "rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23f6b69d1c26c4704fec01311963a41d7de3ee0570a84ebde4d544e5a1859ffc"}, - {file = "rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:530064db9146b247351f2a0250b8f00b289accea4596a033e94be2389977de71"}, - {file = "rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b90b0496570bd6b0321724a330d8b545827c4df2034b6ddfc5f5275f55da2ad"}, - {file = "rpds_py-0.27.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:879b0e14a2da6a1102a3fc8af580fc1ead37e6d6692a781bd8c83da37429b5ab"}, - {file = "rpds_py-0.27.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:0d807710df3b5faa66c731afa162ea29717ab3be17bdc15f90f2d9f183da4059"}, - {file = "rpds_py-0.27.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:3adc388fc3afb6540aec081fa59e6e0d3908722771aa1e37ffe22b220a436f0b"}, - {file = "rpds_py-0.27.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c796c0c1cc68cb08b0284db4229f5af76168172670c74908fdbd4b7d7f515819"}, - {file = "rpds_py-0.27.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cdfe4bb2f9fe7458b7453ad3c33e726d6d1c7c0a72960bcc23800d77384e42df"}, - {file = "rpds_py-0.27.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:8fabb8fd848a5f75a2324e4a84501ee3a5e3c78d8603f83475441866e60b94a3"}, - {file = "rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eda8719d598f2f7f3e0f885cba8646644b55a187762bec091fa14a2b819746a9"}, - {file = "rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c64d07e95606ec402a0a1c511fe003873fa6af630bda59bac77fac8b4318ebc"}, - {file = "rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:93a2ed40de81bcff59aabebb626562d48332f3d028ca2036f1d23cbb52750be4"}, - {file = "rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:387ce8c44ae94e0ec50532d9cb0edce17311024c9794eb196b90e1058aadeb66"}, - {file = "rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaf94f812c95b5e60ebaf8bfb1898a7d7cb9c1af5744d4a67fa47796e0465d4e"}, - {file = "rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:4848ca84d6ded9b58e474dfdbad4b8bfb450344c0551ddc8d958bf4b36aa837c"}, - {file = "rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2bde09cbcf2248b73c7c323be49b280180ff39fadcfe04e7b6f54a678d02a7cf"}, - {file = "rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:94c44ee01fd21c9058f124d2d4f0c9dc7634bec93cd4b38eefc385dabe71acbf"}, - {file = "rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:df8b74962e35c9249425d90144e721eed198e6555a0e22a563d29fe4486b51f6"}, - {file = "rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:dc23e6820e3b40847e2f4a7726462ba0cf53089512abe9ee16318c366494c17a"}, - {file = "rpds_py-0.27.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:aa8933159edc50be265ed22b401125c9eebff3171f570258854dbce3ecd55475"}, - {file = "rpds_py-0.27.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a50431bf02583e21bf273c71b89d710e7a710ad5e39c725b14e685610555926f"}, - {file = "rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78af06ddc7fe5cc0e967085a9115accee665fb912c22a3f54bad70cc65b05fe6"}, - {file = "rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:70d0738ef8fee13c003b100c2fbd667ec4f133468109b3472d249231108283a3"}, - {file = "rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2f6fd8a1cea5bbe599b6e78a6e5ee08db434fc8ffea51ff201c8765679698b3"}, - {file = "rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8177002868d1426305bb5de1e138161c2ec9eb2d939be38291d7c431c4712df8"}, - {file = "rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:008b839781d6c9bf3b6a8984d1d8e56f0ec46dc56df61fd669c49b58ae800400"}, - {file = "rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:a55b9132bb1ade6c734ddd2759c8dc132aa63687d259e725221f106b83a0e485"}, - {file = "rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a46fdec0083a26415f11d5f236b79fa1291c32aaa4a17684d82f7017a1f818b1"}, - {file = "rpds_py-0.27.1-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:8a63b640a7845f2bdd232eb0d0a4a2dd939bcdd6c57e6bb134526487f3160ec5"}, - {file = "rpds_py-0.27.1-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:7e32721e5d4922deaaf963469d795d5bde6093207c52fec719bd22e5d1bedbc4"}, - {file = "rpds_py-0.27.1-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:2c426b99a068601b5f4623573df7a7c3d72e87533a2dd2253353a03e7502566c"}, - {file = "rpds_py-0.27.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4fc9b7fe29478824361ead6e14e4f5aed570d477e06088826537e202d25fe859"}, - {file = "rpds_py-0.27.1.tar.gz", hash = "sha256:26a1c73171d10b7acccbded82bf6a586ab8203601e565badc74bbbf8bc5a10f8"}, + {file = "rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288"}, + {file = "rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7"}, + {file = "rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff"}, + {file = "rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7"}, + {file = "rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139"}, + {file = "rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464"}, + {file = "rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169"}, + {file = "rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425"}, + {file = "rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038"}, + {file = "rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7"}, + {file = "rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed"}, + {file = "rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85"}, + {file = "rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c"}, + {file = "rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825"}, + {file = "rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229"}, + {file = "rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad"}, + {file = "rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51"}, + {file = "rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5"}, + {file = "rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e"}, + {file = "rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394"}, + {file = "rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf"}, + {file = "rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b"}, + {file = "rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e"}, + {file = "rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2"}, + {file = "rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d"}, + {file = "rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7"}, + {file = "rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31"}, + {file = "rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95"}, + {file = "rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d"}, + {file = "rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15"}, + {file = "rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1"}, + {file = "rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a"}, + {file = "rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0"}, + {file = "rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94"}, + {file = "rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08"}, + {file = "rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27"}, + {file = "rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6"}, + {file = "rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d"}, + {file = "rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0"}, + {file = "rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f"}, + {file = "rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65"}, + {file = "rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f"}, + {file = "rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53"}, + {file = "rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed"}, + {file = "rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950"}, + {file = "rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6"}, + {file = "rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb"}, + {file = "rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5"}, + {file = "rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404"}, + {file = "rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856"}, + {file = "rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40"}, + {file = "rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0"}, + {file = "rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e"}, + {file = "rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84"}, ] [[package]] @@ -1731,7 +1721,7 @@ numpy = ">=1.23.5,<2.5" [package.extras] dev = ["cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy (==1.10.0)", "pycodestyle", "pydevtool", "rich-click", "ruff (>=0.0.292)", "types-psutil", "typing_extensions"] doc = ["intersphinx_registry", "jupyterlite-pyodide-kernel", "jupyterlite-sphinx (>=0.19.1)", "jupytext", "matplotlib (>=3.5)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (>=0.15.2)", "sphinx (>=5.0.0,<8.0.0)", "sphinx-copybutton", "sphinx-design (>=0.4.0)"] -test = ["Cython", "array-api-strict (>=2.0,<2.1.1)", "asv", "gmpy2", "hypothesis (>=6.30)", "meson", "mpmath", "ninja", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] +test = ["Cython", "array-api-strict (>=2.0,<2.1.1)", "asv", "gmpy2", "hypothesis (>=6.30)", "meson", "mpmath", "ninja ; sys_platform != \"emscripten\"", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] [[package]] name = "snowballstemmer" @@ -1747,14 +1737,14 @@ files = [ [[package]] name = "soupsieve" -version = "2.8" +version = "2.8.1" description = "A modern CSS selector implementation for Beautiful Soup." optional = false python-versions = ">=3.9" groups = ["docs"] files = [ - {file = "soupsieve-2.8-py3-none-any.whl", hash = "sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c"}, - {file = "soupsieve-2.8.tar.gz", hash = "sha256:e2dd4a40a628cb5f28f6d4b0db8800b8f581b65bb380b97de22ba5ca8d72572f"}, + {file = "soupsieve-2.8.1-py3-none-any.whl", hash = "sha256:a11fe2a6f3d76ab3cf2de04eb339c1be5b506a8a47f2ceb6d139803177f85434"}, + {file = "soupsieve-2.8.1.tar.gz", hash = "sha256:4cf733bc50fa805f5df4b8ef4740fc0e0fa6218cf3006269afd3f9d6d80fd350"}, ] [[package]] @@ -2123,7 +2113,7 @@ files = [ {file = "tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b"}, {file = "tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549"}, ] -markers = {code-quality = "python_version < \"3.11\"", dev = "python_full_version <= \"3.11.0a6\"", docs = "python_version < \"3.11\""} +markers = {code-quality = "python_version == \"3.10\"", dev = "python_full_version <= \"3.11.0a6\"", docs = "python_version == \"3.10\""} [[package]] name = "tomlkit" @@ -2148,7 +2138,7 @@ files = [ {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, ] -markers = {code-quality = "python_version < \"3.11\"", dev = "python_version < \"3.11\""} +markers = {code-quality = "python_version == \"3.10\"", dev = "python_version == \"3.10\""} [[package]] name = "typing-inspection" @@ -2178,32 +2168,32 @@ files = [ [[package]] name = "urllib3" -version = "2.5.0" +version = "2.6.2" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.9" groups = ["docs"] files = [ - {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}, - {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"}, + {file = "urllib3-2.6.2-py3-none-any.whl", hash = "sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd"}, + {file = "urllib3-2.6.2.tar.gz", hash = "sha256:016f9c98bb7e98085cb2b4b17b87d2c702975664e4f060c6532e64d1c1a5e797"}, ] [package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["zstandard (>=0.18.0)"] +zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] [[package]] name = "virtualenv" -version = "20.35.3" +version = "20.35.4" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.8" groups = ["code-quality"] files = [ - {file = "virtualenv-20.35.3-py3-none-any.whl", hash = "sha256:63d106565078d8c8d0b206d48080f938a8b25361e19432d2c9db40d2899c810a"}, - {file = "virtualenv-20.35.3.tar.gz", hash = "sha256:4f1a845d131133bdff10590489610c98c168ff99dc75d6c96853801f7f67af44"}, + {file = "virtualenv-20.35.4-py3-none-any.whl", hash = "sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b"}, + {file = "virtualenv-20.35.4.tar.gz", hash = "sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c"}, ] [package.dependencies] @@ -2214,7 +2204,7 @@ typing-extensions = {version = ">=4.13.2", markers = "python_version < \"3.11\"" [package.extras] docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] -test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"GraalVM\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] [[package]] name = "wcmatch" @@ -2252,4 +2242,4 @@ release = [] [metadata] lock-version = "2.1" python-versions = "^3.10" -content-hash = "54bff2b98b00542ece8b935729d33fec3d8a7311bc784961d2e013cdedcce53c" +content-hash = "8d8ac6570e700e1fa392472475b24e601ec5477cc91d1fcc2983912eedfb6f6d" diff --git a/pyproject.toml b/pyproject.toml index 4d1faa1..fc3c4c0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,9 +1,9 @@ [tool.poetry] name = "innuce-spikify" -version = "0.3.0" +version = "1.0.0" description = "A Python package to convert signal data to spike trains" authors = ["Benedetto Leto ", "Gianvito Urgese ", "Vittorio Fra ", "Riccardo Pignari "] -readme = "README.md" +readme = "README_PYPI.md" # homepage = "" repository = "https://github.com/neuromorphic-polito/spikify" documentation = "https://spikify.readthedocs.io/en/latest/" @@ -71,6 +71,7 @@ sphinx_design = '0.6.1' sphinxcontrib-plantuml = '0.30' sphinxcontrib-programoutput = '0.18' numpydoc = '1.8.0' +linkchecker = '10.6.0' @@ -133,7 +134,7 @@ output = "coverage/reports/coverage.xml" dev = ["pytest", "pytest-cov"] code-quality = ["pyproject-flake8", "black", "docformatter", "pre-commit"] release = ["bump-my-version"] -docs = ["furo", "sphinx", "sphinx-copybutton", "sphinx-data-viewer", "sphinx-needs", "sphinx_design", "sphinxcontrib-plantuml", "sphinxcontrib-programoutput", "numpydoc"] +docs = ["furo", "sphinx", "sphinx-copybutton", "sphinx-data-viewer", "sphinx-needs", "sphinx_design", "sphinxcontrib-plantuml", "sphinxcontrib-programoutput", "numpydoc", "linkchecker"] [build-system] diff --git a/spikify/encoders/__init__.py b/spikify/encoders/__init__.py new file mode 100644 index 0000000..a33f0d2 --- /dev/null +++ b/spikify/encoders/__init__.py @@ -0,0 +1 @@ +"""Encoders package.""" diff --git a/spikify/encoders/rate/__init__.py b/spikify/encoders/rate/__init__.py new file mode 100644 index 0000000..6e13f0c --- /dev/null +++ b/spikify/encoders/rate/__init__.py @@ -0,0 +1,5 @@ +"""Rate package.""" + +from .poisson_algorithm import poisson + +__all__ = ["poisson"] diff --git a/spikify/encoders/rate/poisson_algorithm.py b/spikify/encoders/rate/poisson_algorithm.py new file mode 100644 index 0000000..aa1a8fa --- /dev/null +++ b/spikify/encoders/rate/poisson_algorithm.py @@ -0,0 +1,124 @@ +""" +.. raw:: html + +

Poisson Algorithm

+""" + +import numpy as np + + +def poisson(signal: np.ndarray, interval_length: int, seed: int = 0) -> np.ndarray: + """ + Perform Poisson encoding on the input signal. + + This function generates a spike train using a Poisson distribution, where the probability of emitting + a spike in a given interval is determined by the normalized rate of the signal. + + See the :ref:`poisson_algorithm_desc` description for a detailed explanation of the Poisson encoding + algorithm. + + .. note:: + - If ``interval_length == 1``, each input value is treated directly as the firing rate + (after normalization/clipping), resulting in a standard bin-by-bin + approximation of Poisson encoding (at most one spike per time step). + - If ``interval_length > 1``, the average value over each interval block determines + the constant firing rate for that block. Spikes are placed within the block using + exponentially distributed inter-spike intervals, allowing multiple spikes per block + (exact Poisson generation for the block's constant rate). + + **Code Example:** + + .. code-block:: python + + import numpy as np + from spikify.encoders.rate import poisson + signal = np.array([0.2, 0.5, 0.8, 1.0]) + np.random.seed(0) + interval_length = 2 + encoded_signal = poisson(signal, interval_length) + + .. doctest:: + :hide: + + >>> import numpy as np + >>> from spikify.encoders.rate import poisson + >>> # Example with numpy array + >>> signal = np.array([0.2, 0.5, 0.8, 1.0]) + >>> interval_length = 2 + >>> encoded_signal = poisson(signal, interval_length) + >>> encoded_signal + array([0, 0, 0, 1], dtype=int8) + + :param signal: Input signal to encode (1D or 2D: timestamps × features). + :type signal: numpy.ndarray + :param interval_length: Length of each time block for rate computation and spike placement. + Must evenly divide the signal length. ``interval_length=1`` uses the + instantaneous value as rate (bin-by-bin); larger values use the block + mean as constant rate (allows multiple spikes per block). + :type interval_length: int + :param seed: Random seed for reproducibility. Default is 0. + :type seed: int + :return: A numpy array representing the encoded spike train. + :rtype: numpy.ndarray + :raises ValueError: If the input signal is empty or if signal length is not divisible for the interval length + :raises TypeError: If the signal is not a numpy.ndarray + + """ + + # Check for empty signal + if len(signal) == 0: + raise ValueError("Signal cannot be empty.") + + # Ensure 2D processing (T, F) + if signal.ndim == 1: + signal = signal.reshape(-1, 1) + + T, F = signal.shape + + if T % interval_length != 0: + raise ValueError( + f"The interval ({interval_length}) must evenly divide the signal length ({T}). " + "Consider trimming or padding the signal to make its length a multiple of the interval." + ) + + # Set seed + np.random.seed(seed) + + signal_copy = np.copy(signal) + + # Ensure non-negative signal values + signal_copy = np.clip(signal_copy, 0, None) + + max_amp = signal_copy.max(axis=0) + + # Find features that require scaling + features_to_scale = np.where(max_amp > 1)[0] + + for f in features_to_scale: + signal_copy[:, f] /= max_amp[f] + + # Compute mean over the signal reshaped to interval-sized chunks + interval_rate_mean = np.mean(signal_copy.reshape(T // interval_length, interval_length, F), axis=1) + + spikes = np.zeros((T // interval_length, interval_length, F), dtype=np.int8) + + # Create bins for Poisson encoding + bins = np.linspace(0, 1, interval_length + 1) + + for feat in range(F): + for idx, rate in enumerate(interval_rate_mean[:, feat]): + if rate > 0: + ISI = -np.log(1 - np.random.random(interval_length)) / ( # inter-spike intervals where probability of + rate * interval_length # having k=0 spikes is equal to rate (time + ) # amount to wait to see the next spike) + spike_times = np.searchsorted(bins, np.cumsum(ISI)) - 1 # find spike times + spike_times = spike_times[spike_times < interval_length] # clip times within interval + spikes[idx, spike_times, feat] = 1 + + spikes = spikes.reshape(T, F) + + # Flatten if input was 1D + if F == 1: + spikes = spikes.flatten() + + return spikes diff --git a/spikify/encoding/temporal/__init__.py b/spikify/encoders/temporal/__init__.py similarity index 100% rename from spikify/encoding/temporal/__init__.py rename to spikify/encoders/temporal/__init__.py diff --git a/spikify/encoding/temporal/contrast/__init__.py b/spikify/encoders/temporal/contrast/__init__.py similarity index 100% rename from spikify/encoding/temporal/contrast/__init__.py rename to spikify/encoders/temporal/contrast/__init__.py diff --git a/spikify/encoders/temporal/contrast/moving_window_algorithm.py b/spikify/encoders/temporal/contrast/moving_window_algorithm.py new file mode 100644 index 0000000..549361d --- /dev/null +++ b/spikify/encoders/temporal/contrast/moving_window_algorithm.py @@ -0,0 +1,107 @@ +""" +.. raw:: html + +

Moving Window Algorithm

+""" + +import numpy as np + + +def moving_window( + signal: np.ndarray, window_length: int, threshold: float | int | list[float | int] | np.ndarray +) -> np.ndarray: + """ + Perform Moving Window (MW) encoding on the input signal. + + This function takes a continuous signal and converts it into a spike train using a moving window and + threshold-based approach. A spike is generated when the signal exceeds the calculated `base` plus or minus a + specified `threshold`. + + Refer to the :ref:`moving_window_algorithm_desc` for a detailed explanation of the Moving Window encoding + algorithm. + + **Code Example:** + + .. code-block:: python + + import numpy as np + from spikify.encoders.temporal.contrast import moving_window + signal = np.array([0.1, 0.3, 0.2, 0.5, 0.8, 1.0]) + window_length = 3 + threshold = 0.2 + encoded_signal = moving_window(signal, window_length, threshold) + encoded_signal + + .. doctest:: + :hide: + + >>> import numpy as np + >>> from spikify.encoders.temporal.contrast import moving_window + >>> signal = np.array([0.1, 0.3, 0.2, 0.5, 0.8, 1.0]) + >>> window_length = 3 + >>> threshold = 0.2 + >>> encoded_signal = moving_window(signal, window_length, threshold) + >>> encoded_signal + array([0, 0, 0, 1, 1, 1], dtype=int8) + + :param signal: The input signal to be encoded. This should be a numpy ndarray. + :type signal: numpy.ndarray + :param window_length: The size of the sliding window for calculating the signal base mean. + :type window_length: int + :param threshold: Threshold(s) for spike generation; scalar or 1D sequence matching features. + :type threshold: float | int | list[float | int] | numpy.ndarray + :return: A numpy array representing the encoded spike train. + :rtype: numpy.ndarray + :raises ValueError: If the input signal is empty or if the threshold dimensions do not match the signal features. + :raises TypeError: If the threshold parameter is of invalid dimension. + + """ + + # Check for empty signal + if len(signal) == 0: + raise ValueError("Signal cannot be empty.") + + # Ensure 2D processing (T, F) + if signal.ndim == 1: + signal = signal.reshape(-1, 1) + + T, F = signal.shape + + # Handle threshold + if np.isscalar(threshold): + thresholds = np.full(F, float(threshold)) + else: + thresholds = np.asarray(threshold, dtype=float) + if thresholds.ndim != 1: + raise TypeError("Threshold must be a scalar or a 1D sequence of numbers.") + if thresholds.size != F: + raise ValueError("Threshold must match the number of features in the signal.") + + spikes = np.zeros_like(signal, dtype=np.int8) + + # First loop: t = 0 : window_length + # For the first window_length samples, use the mean of available samples as base signal otherwise + # the first window_length samples will not be encoded since there are not enough samples to fill the window + for f in range(F): + base = np.mean(signal[:window_length, f]) + for t in range(window_length): + if signal[t, f] > base + thresholds[f]: + spikes[t, f] = 1 + elif signal[t, f] < base - thresholds[f]: + spikes[t, f] = -1 + + # Second loop: t = window_length : T + # For the rest of the signal, use the moving window to calculate the base signal + for f in range(F): + for t in range(window_length, T): + base = np.mean(signal[t - window_length : t, f]) + if signal[t, f] > base + thresholds[f]: + spikes[t, f] = 1 + elif signal[t, f] < base - thresholds[f]: + spikes[t, f] = -1 + + # Flatten if input was 1D + if F == 1: + spikes = spikes.flatten() + + return spikes diff --git a/spikify/encoders/temporal/contrast/step_forward_algorithm.py b/spikify/encoders/temporal/contrast/step_forward_algorithm.py new file mode 100644 index 0000000..8dfc6ef --- /dev/null +++ b/spikify/encoders/temporal/contrast/step_forward_algorithm.py @@ -0,0 +1,93 @@ +""" +.. raw:: html + +

Step Forward Algorithm

+""" + +import numpy as np + + +def step_forward(signal: np.ndarray, threshold: float | int | list[float | int] | np.ndarray) -> np.ndarray: + """ + Perform Step-Forward (SF) encoding on the input signal. + + This function takes a continuous signal and converts it into a spike train using a dynamically updated baseline + signal and threshold-based approach. A spike is generated when the signal exceeds or drops below the dynamically + adjusted baseline (`base`) by the specified `threshold`. + + Refer to the :ref:`step_forward_algorithm_desc` for a detailed explanation of the SF encoding algorithm. + + **Code Example:** + + .. code-block:: python + + import numpy as np + from spikify.encoders.temporal.contrast import step_forward + signal = np.array([0.1, 0.3, 0.4, 0.2, 0.5, 0.6]) + threshold = 0.2 + encoded_signal = step_forward(signal, threshold) + + .. doctest:: + :hide: + + >>> import numpy as np + >>> from spikify.encoders.temporal.contrast import step_forward + >>> signal = np.array([0.1, 0.3, 0.4, 0.2, 0.5, 0.6]) + >>> threshold = 0.2 + >>> encoded_signal = step_forward(signal, threshold) + >>> encoded_signal + array([0, 0, 1, 0, 0, 1], dtype=int8) + + :param signal: The input signal to be encoded. This should be a numpy ndarray. + :type signal: numpy.ndarray + :param threshold: Threshold(s) for spike generation; scalar or 1D sequence matching features. + :type threshold: float | int | list[float | int] | numpy.ndarray + :return: A numpy array representing the encoded spike train. + :rtype: numpy.ndarray + :raises ValueError: If the input signal is empty or if the threshold dimensions do not match the signal features. + :raises TypeError: If the threshold parameter is of invalid dimension. + + """ + + # Input validation + if len(signal) == 0: + raise ValueError("Signal cannot be empty.") + + # Ensure 2D processing (T, F) + if signal.ndim == 1: + signal = signal.reshape(-1, 1) + + T, F = signal.shape + + # Handle threshold + if np.isscalar(threshold): + thresholds = np.full(F, float(threshold)) + else: + thresholds = np.asarray(threshold, dtype=float) + if thresholds.ndim != 1: + raise TypeError("Threshold must be a scalar or a 1D sequence of numbers.") + if thresholds.size != F: + raise ValueError("Threshold must match the number of features in the signal.") + + spike = np.zeros_like(signal, dtype=np.int8) + + # base signal initialized at the start of the signal + base = signal[0, :] + + # Iterate over signal values skipping the first timestep since it's used for initialization of the base signal + for feat in range(F): + base = signal[0, feat] + for t in range(1, T): + value = signal[t, feat] + if value > base + thresholds[feat]: + spike[t, feat] = 1 + base += thresholds[feat] + elif value < base - thresholds[feat]: + spike[t, feat] = -1 + base -= thresholds[feat] + + # Flatten if input was 1D + if F == 1: + spike = spike.flatten() + + return spike diff --git a/spikify/encoders/temporal/contrast/threshold_based_algorithm.py b/spikify/encoders/temporal/contrast/threshold_based_algorithm.py new file mode 100644 index 0000000..1862b06 --- /dev/null +++ b/spikify/encoders/temporal/contrast/threshold_based_algorithm.py @@ -0,0 +1,94 @@ +""" +.. raw:: html + +

Threshold Based Representation Algorithm

+""" + +import numpy as np + + +def threshold_based_representation( + signal: np.ndarray, factor: float | int | list[float | int] | np.ndarray +) -> tuple[np.ndarray, np.ndarray]: + """ + Perform Threshold-Based Representation (TBR) encoding on the input signal. + + This function takes a continuous signal and converts it into a spike train using a fixed threshold based on + the signal's variations. A spike is generated when the variation exceeds the computed threshold. + + Refer to the :ref:`threshold_based_representation_algorithm_desc` for a detailed explanation of the TBR + encoding algorithm. + + **Code Example:** + + .. code-block:: python + + import numpy as np + from spikify.encoders.temporal.contrast import threshold_based_representation + signal = np.array([0.1, 0.3, 0.4, 0.2, 0.5, 0.6]) + factor = 0.5 + encoded_signal, threshold = threshold_based_representation(signal, factor) + + .. doctest:: + :hide: + + >>> import numpy as np + >>> from spikify.encoders.temporal.contrast import threshold_based_representation + >>> signal = np.array([0.1, 0.3, 0.4, 0.2, 0.5, 0.6]) + >>> factor = 0.5 + >>> encoded_signal, threshold = threshold_based_representation(signal, factor) + >>> encoded_signal + array([ 1, 0, -1, 1, 0, 0], dtype=int8) + + :param signal: The input signal to be encoded. This should be a numpy ndarray. + :type signal: numpy.ndarray + :param factor: The factor value (`factor`) that controls the noise-reduction threshold. + Can be a float, an integer, or a list of floats or integers. + :type factor: float | int | list[float | int] | numpy.ndarray + :return: A tuple containing the encoded spike train and the computed threshold for each feature. + :rtype: tuple[numpy.ndarray, numpy.ndarray] + :raises ValueError: If the input signal is empty or if the factor length does not match the number of features. + :raises TypeError: If the factor parameter is of invalid dimension. + + """ + + # Input validation + if len(signal) == 0: + raise ValueError("Signal cannot be empty.") + + # Ensure 2D processing (T, F) + if signal.ndim == 1: + signal = signal.reshape(-1, 1) + + T, F = signal.shape + + # Handle factor + if np.isscalar(factor): + factors = np.full(F, float(factor)) + else: + factors = np.asarray(factor, dtype=float) + if factors.ndim != 1: + raise TypeError("Factor must be a scalar or a 1D sequence of numbers.") + if factors.size != F: + raise ValueError("Factor must match the number of features in the signal.") + spike = np.zeros_like(signal, dtype=np.int8) + + # Compute variation exactly as in the original code + # diff[t] = s[t+1] - s[t] for t = 0 to T-2 + # diff[T-1] = diff[T-2] (last value set to second-last) + diff = np.diff(signal, axis=0, append=signal[[0], :]) # append first value of signal to maintain shape + diff[-1, :] = diff[-2, :] # force last to equal second-last + + # Compute threshold per feature (over all T variations, including the duplicated last) + threshold = np.mean(diff, axis=0) + factors * np.std(diff, axis=0) + + # Generate spikes: compare on the full diff array (length S) + threshold = threshold.reshape(1, threshold.shape[0]) + spike[diff > threshold] = 1 + spike[diff < -threshold] = -1 + + # Flatten if input was 1D + if F == 1: + spike = spike.flatten() + + return spike, threshold.flatten() diff --git a/spikify/encoding/temporal/contrast/zero_cross_step_forward_algorithm.py b/spikify/encoders/temporal/contrast/zero_cross_step_forward_algorithm.py similarity index 63% rename from spikify/encoding/temporal/contrast/zero_cross_step_forward_algorithm.py rename to spikify/encoders/temporal/contrast/zero_cross_step_forward_algorithm.py index 1313aa8..1ce4b36 100644 --- a/spikify/encoding/temporal/contrast/zero_cross_step_forward_algorithm.py +++ b/spikify/encoders/temporal/contrast/zero_cross_step_forward_algorithm.py @@ -7,7 +7,7 @@ import numpy as np -def zero_cross_step_forward(signal: np.ndarray, threshold: float | list[float]) -> np.ndarray: +def zero_cross_step_forward(signal: np.ndarray, threshold: float | int | list[float | int] | np.ndarray) -> np.ndarray: """ Perform Zero-Crossing Step-Forward (ZCSF) encoding on the input signal. @@ -23,7 +23,7 @@ def zero_cross_step_forward(signal: np.ndarray, threshold: float | list[float]) .. code-block:: python import numpy as np - from spikify.encoding.temporal.contrast import zero_cross_step_forward + from spikify.encoders.temporal.contrast import zero_cross_step_forward signal = np.array([-0.2, 0.1, 0.5, 0.0, 1.2, 0.3]) threshold = 0.4 encoded_signal = zero_cross_step_forward(signal, threshold) @@ -32,7 +32,7 @@ def zero_cross_step_forward(signal: np.ndarray, threshold: float | list[float]) :hide: >>> import numpy as np - >>> from spikify.encoding.temporal.contrast import zero_cross_step_forward + >>> from spikify.encoders.temporal.contrast import zero_cross_step_forward >>> signal = np.array([-0.2, 0.1, 0.5, 0.0, 1.2, 0.3]) >>> threshold = 0.4 >>> encoded_signal = zero_cross_step_forward(signal, threshold) @@ -41,34 +41,34 @@ def zero_cross_step_forward(signal: np.ndarray, threshold: float | list[float]) :param signal: The input signal to be encoded. This should be a numpy ndarray. :type signal: numpy.ndarray - :param threshold: The threshold value used to determine spike generation. Can be a float or a list of floats. - :type threshold: float | list[float] + :param threshold: Threshold(s) for spike generation; scalar or 1D sequence matching features. + :type threshold: float | int | list[float | int] | numpy.ndarray :return: A numpy array representing the encoded spike train. :rtype: numpy.ndarray - :raises ValueError: If the input signal is empty. - :raises TypeError: If the signal is not a numpy ndarray. + :raises ValueError: If the input signal is empty or if the threshold dimensions do not match the signal features. + :raises TypeError: If the threshold parameter is of invalid dimension. """ + # Check for empty signal if len(signal) == 0: raise ValueError("Signal cannot be empty.") + # Ensure 2D processing (T, F) if signal.ndim == 1: signal = signal.reshape(-1, 1) S, F = signal.shape - if isinstance(threshold, float): - thresholds = [threshold] * F - elif isinstance(threshold, list): - if not all(isinstance(w, float) for w in threshold): - raise TypeError("All elements in threshold list must be float.") - thresholds = threshold + # Handle threshold + if np.isscalar(threshold): + thresholds = np.full(F, float(threshold)) else: - raise TypeError("Threshold must be a float or a list of floats.") - - if len(thresholds) != F: - raise ValueError("Threshold must match the number of features in the signal.") + thresholds = np.asarray(threshold, dtype=float) + if thresholds.ndim != 1: + raise TypeError("Threshold must be a scalar or a 1D sequence of numbers.") + if thresholds.size != F: + raise ValueError("Threshold must match the number of features in the signal.") spike = np.zeros_like(signal, dtype=np.int8) @@ -78,6 +78,7 @@ def zero_cross_step_forward(signal: np.ndarray, threshold: float | list[float]) # Apply threshold condition spike[signal > thresholds] = 1 + # Flatten if input was 1D if F == 1: spike = spike.flatten() diff --git a/spikify/encoding/temporal/deconvolution/__init__.py b/spikify/encoders/temporal/deconvolution/__init__.py similarity index 100% rename from spikify/encoding/temporal/deconvolution/__init__.py rename to spikify/encoders/temporal/deconvolution/__init__.py diff --git a/spikify/encoders/temporal/deconvolution/bens_spiker_algorithm.py b/spikify/encoders/temporal/deconvolution/bens_spiker_algorithm.py new file mode 100644 index 0000000..943f384 --- /dev/null +++ b/spikify/encoders/temporal/deconvolution/bens_spiker_algorithm.py @@ -0,0 +1,168 @@ +""" +.. raw:: html + +

Bens Spiker Algorithm

+""" + +import numpy as np +from scipy.signal import firwin +from .utils import WindowType + + +def bens_spiker( + signal: np.ndarray, + window_length: int, + cutoff: float | np.ndarray, + threshold: float | int | list[float, int] | np.ndarray, + width: int | None = None, + window_type: WindowType = "hann", + pass_zero: bool | str = True, + scale: bool = True, + fs: float | None = None, +) -> tuple[np.ndarray, np.ndarray, np.ndarray]: + """ + Perform Ben's Spiker (BSA) encoding on the input signal. + + BSA is an efficient algorithm for converting an analog (positive-valued) signal into a unipolar spike train + from which the original signal can be well reconstructed via convolution with the same FIR filter used during + encoding. The method iteratively detects spike locations by comparing two cumulative error metrics over a + sliding segment of length equal to the FIR filter: + + - error1: sum of absolute differences between the current signal segment and the filter coefficients + - error2: sum of absolute values of the current signal segment + + A positive spike (+1) is emitted at time t if error1 ≤ error2 - threshold. When a spike is detected, the filter + is subtracted from the corresponding signal segment, removing the detected pattern for subsequent iterations. + + .. note:: + - BSA requires non-negative input signals. The function automatically shifts the signal by its minimum value + (per feature) if negative values are present. + - The FIR filter is designed using `scipy.signal.firwin` with the specified cutoff, window type, etc. + - When the maximum amplitute of the signal is greater than 1, filter coefficients are automatically scaled up + (sum ≈ 2 × max amplitude) for improved dynamic range and reduced saturation (recommended practice). + - For multi-feature signals, the same filter shape is applied across all features, but scaling is performed + independently per feature based on its amplitude. + + Refer to the :ref:`bens_spiker_algorithm_desc` for a detailed explanation of the Ben's Spiker algorithm. + + **Code Example:** + + .. code-block:: python + + import numpy as np + from spikify.encoders.temporal.deconvolution import bens_spiker + signal = np.array([0.1, 0.2, 0.8, 0.95, 0.5, 0.3, 0.1]) + window_length = 3 + threshold = 0.1 + cutoff = 0.1 + encoded_signal, shift, fir_coeffs = bens_spiker(signal, window_length, cutoff, threshold) + + .. doctest:: + :hide: + + >>> import numpy as np + >>> from spikify.encoders.temporal.deconvolution import bens_spiker + >>> signal = np.array([0.1, 0.2, 0.8, 0.95, 0.5, 0.3, 0.1]) + >>> window_length = 3 + >>> threshold = 0.1 + >>> cutoff = 0.1 + >>> encoded_signal, shift, fir_coeffs = bens_spiker(signal, window_length, cutoff, threshold) + >>> encoded_signal + array([0, 1, 1, 0, 0, 0, 0], dtype=int8) + + :param signal: Input signal to encode (1D or 2D: time × features). + :type signal: numpy.ndarray + :param window_length: Length of the FIR filter (number of coefficients). + :type window_length: int + :param cutoff: Cutoff frequency(ies) for the FIR filter design (normalized 0 to 1, where 1 = Nyquist). + Scalar or an array of cutoff frequencies (that is, band edges). + :type cutoff: float | numpy.ndarray + :param threshold: Threshold factor for spike detection. + Scalar or per-feature sequence. + :type threshold: float | int | list[float | int] | numpy.ndarray + :param width: Transition width for FIR filter design (optional, used with certain window types). + :type width: int | None + :param window_type: Window function for FIR filter design (e.g., 'hann', 'hamming', 'blackman', 'boxcar'). + :type window_type: str + :param pass_zero: Whether the filter should be low-pass (True) or high-pass (False/'highpass'). + :type pass_zero: bool | str + :param scale: Set to True to scale the coefficients so that the frequency response is exactly unity at a + certain frequency. + :type scale: bool + :param fs: Sampling frequency (used for physical frequency units in cutoff; optional). + :type fs: float | None + :return: + - spikes: Spike train (same shape as input, dtype=int8, values 0 or 1) + - shift: Per-feature shift values subtracted to make signal non-negative (1D array) + - fir_bank: Final filter coefficients used (after scaling), shape (window_length, n_features) + :rtype: tuple[numpy.ndarray, numpy.ndarray, numpy.ndarray] + :raises ValueError: If the input signal is empty or if the threshold dimensions do not match the signal + features or if the window_length is greater than the signal lenght. + :raises TypeError: If the threshold parameter is of invalid dimension. + + """ + + # Check for empty signal + if len(signal) == 0: + raise ValueError("Signal cannot be empty.") + + # Ensure 2D processing (T, F) + if signal.ndim == 1: + signal = signal.reshape(-1, 1) + + T, F = signal.shape + + # Handle window_length + if window_length > T: + raise ValueError("window_length must be less than the number of time steps in the signal.") + + # Handle threshold + if np.isscalar(threshold): + thresholds = np.full(F, float(threshold)) + else: + thresholds = np.asarray(threshold, dtype=float) + if thresholds.ndim != 1: + raise TypeError("Threshold must be a scalar or a 1D sequence of numbers.") + if thresholds.size != F: + raise ValueError("Threshold must match the number of features in the signal.") + + spikes = np.zeros_like(signal, dtype=np.int8) + + # Generate filter coefficient values according to their window length for each feature + fir = firwin(window_length, cutoff, width=width, window=window_type, pass_zero=pass_zero, scale=scale, fs=fs) + + # Stack the same filter for all features in case we need to modify coeffiecient due to signal + # amplitude for certain features + fir_bank = np.stack([fir] * F, axis=0).T + + signal_copy = np.copy(signal) + + # Normalize signal if signal has negative values + shift = signal_copy.min(axis=0) + shift[shift > 0] = 0 # only shift if negative values are present + signal_copy -= shift + + # Compute max amplitude per feature to be used for scaling if max amplitude is grater than 1 + max_amp = signal_copy.max(axis=0) + + # Find features that require scaling + features_to_scale = np.where(max_amp > 1)[0] + + for f in features_to_scale: + s = fir_bank[:, f].sum() + fir_bank[:, f] *= 2 * max_amp[f] / s + + for f in range(F): + for t in range(0, T - window_length + 1): + seg = signal_copy[t : t + window_length, f] # segment of the signal + error1 = np.abs(seg - fir_bank[:, f]).sum() # error between segment and filter + error2 = np.abs(seg).sum() # error between segment and zero signal + if error1 <= (error2 - thresholds[f]): # spike condition + spikes[t, f] = 1 + signal_copy[t : t + window_length, f] -= fir_bank[:, f] # update signal by removing filter effect + + # Flatten if input was 1D + if F == 1: + spikes = spikes.flatten() + + return spikes, shift, fir_bank diff --git a/spikify/encoders/temporal/deconvolution/hough_spiker_algorithm.py b/spikify/encoders/temporal/deconvolution/hough_spiker_algorithm.py new file mode 100644 index 0000000..b2f6438 --- /dev/null +++ b/spikify/encoders/temporal/deconvolution/hough_spiker_algorithm.py @@ -0,0 +1,136 @@ +""" +.. raw:: html + +

Hough Spiker Algorithm

+""" + +import numpy as np +from scipy.signal import firwin +from .utils import WindowType + + +def hough_spiker( + signal: np.ndarray, + window_length: int, + cutoff: float | np.ndarray, + width: int | None = None, + window_type: WindowType = "hann", + pass_zero: bool | str = True, + scale: bool = True, + fs: float | None = None, +) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: + """ + Perform Hough Spiker Algorithm (HSA) encoding on the input signal. + + HSA is a simple, parameter-free technique for converting non-negative analog signals (scaled to [0, 1]) into + unipolar spike trains that can be accurately reconstructed via convolution with the same FIR filter used during + encoding. It iteratively scans the signal with a sliding window of length equal to the FIR filter and emits a + positive spike (+1) if the entire signal segment is greater than or equal to the filter coefficients pointwise. + Upon detection, the filter is subtracted from the segment, effectively removing the detected pattern for + subsequent scans. + + .. note:: + - HSA requires non-negative inputs; automatic shifting and normalization to [0, 1] is applied per feature. + - The FIR filter is designed using scipy.signal.firwin with the specified cutoff, window type, etc. + - For multi-feature signals, the same filter shape is applied across all features, but scaling is performed + independently per feature based on its amplitude. + + Refer to the :ref:`hough_spiker_algorithm_desc` for a detailed explanation of the HSA. + + **Code Example** + + .. code-block:: python + + import numpy as np + from spikify.encoders.temporal.deconvolution import hough_spiker + signal = np.array([0.1, 0.2, 4.1, 1.0, 3.0, 0.3, 0.1]) + window_length = 3 + cutoff = 0.1 + encoded_signal, shift, norm, fir_coeffs = hough_spiker(signal, window_length, cutoff) + + .. doctest:: + :hide: + + >>> import numpy as np + >>> from spikify.encoders.temporal.deconvolution import hough_spiker + >>> signal = np.array([0.1, 0.2, 4.1, 1.0, 3.0, 0.3, 0.1]) + >>> window_length = 3 + >>> cutoff = 0.1 + >>> encoded_signal, shift, norm, fir_coeffs = hough_spiker(signal, window_length, cutoff) + >>> encoded_signal + array([0, 1, 0, 0, 0, 0, 0], dtype=int8) + + :param signal: Input signal to encode (1D or 2D: time × features). + :type signal: numpy.ndarray + :param window_length: Length of the FIR filter (number of coefficients). + :type window_length: int + :param cutoff: Cutoff frequency(ies) for the FIR filter design (normalized 0 to 1, where 1 = Nyquist). + Scalar or an array of cutoff frequencies (that is, band edges). + :type cutoff: float | numpy.ndarray + :param width: Transition width for FIR filter design (optional, used with certain window types). + :type width: int | None + :param window_type: Window function for FIR filter design (e.g., 'hann', 'hamming', 'blackman', 'boxcar'). + :type window_type: str + :param pass_zero: Whether the filter should be low-pass (True) or high-pass (False/'highpass'). + :type pass_zero: bool | str + :param scale: Set to True to scale the coefficients so that the frequency response is exactly unity at a + certain frequency. + :type scale: bool + :param fs: Sampling frequency (used for physical frequency units in cutoff; optional). + :type fs: float | None + :return: + - spikes: Spike train (same shape as input, dtype=int8, values 0 or 1) + - shift: Per-feature shift values subtracted to make signal non-negative (1D array) + - norm: Per-feature normalization values used to scale signal to [0, 1] (1D array) + - fir_bank: Final filter coefficients used, shape (window_length, n_features) + :rtype: tuple[numpy.ndarray, numpy.ndarray, numpy.ndarray, numpy.ndarray] + :raises ValueError: If the input signal is empty or if the window_length is greater than the signal lenght. + + """ + # Check for empty signal + if len(signal) == 0: + raise ValueError("Signal cannot be empty.") + + # Ensure 2D processing (T, F) + if signal.ndim == 1: + signal = signal.reshape(-1, 1) + + T, F = signal.shape + + # Handle window_length + if window_length > T: + raise ValueError("window_length must be less than the number of time steps in the signal.") + + spikes = np.zeros_like(signal, dtype=np.int8) + + # Generate filter coefficient values according to their window length for each feature + fir = firwin(window_length, cutoff, width=width, window=window_type, pass_zero=pass_zero, scale=scale, fs=fs) + + fir_bank = np.stack([fir] * F, axis=0).T + + signal_copy = np.copy(np.array(signal, dtype=np.float64)) + + # Normalize signal if signal has negative values + shift = signal_copy.min(axis=0) + shift[shift > 0] = 0 # only shift if negative values are present + signal_copy -= shift + + norm = signal_copy.max(axis=0) + norm[norm <= 1] = 1 # only normalize if max is greater than 1 + signal_copy /= norm + + for f in range(F): + for t in range(0, T - window_length + 1): + # Count how many values match or exceed the filter window values + match_count = np.sum(signal_copy[t : t + window_length, f] >= fir) + + # If all values match or exceed, a spike is detected + if match_count == window_length: + signal_copy[t : t + window_length, f] -= fir_bank[:, f] + spikes[t, f] = 1 + + # Flatten if input was 1D + if F == 1: + spikes = spikes.flatten() + + return spikes, shift, norm, fir_bank diff --git a/spikify/encoders/temporal/deconvolution/modified_hough_spiker_algorithm.py b/spikify/encoders/temporal/deconvolution/modified_hough_spiker_algorithm.py new file mode 100644 index 0000000..fdc78e7 --- /dev/null +++ b/spikify/encoders/temporal/deconvolution/modified_hough_spiker_algorithm.py @@ -0,0 +1,170 @@ +""" +.. raw:: html + +

Modified Hough Spiker Algorithm

+""" + +import numpy as np +from scipy.signal import firwin +from .utils import WindowType + + +def modified_hough_spiker( + signal: np.ndarray, + window_length: int, + cutoff: float | np.ndarray, + threshold: float | int | list[float, int] | np.ndarray, + width: int | None = None, + window_type: WindowType = "hann", + pass_zero: bool | str = True, + scale: bool = True, + fs: float | None = None, +) -> np.ndarray: + """ + Perform Modified Hough Spiker Algorithm (MHSA) encoding on the input signal. + + The Modified Hough Spiker Algorithm (MHSA) is basically an improved version of the original Hough Spiker + Algorithm (HSA). The original HSA is very strict: it only allows a spike to be created if the current piece of + the signal is exactly as big as or bigger than the filter pattern at every single point in that window. If even + one tiny spot dips below the filter, no spike is detected. + + MHSA relaxes this rule a little bit to make it more practical and flexible. Instead of demanding perfection + everywhere, it allows a small amount of "shortfall" — places where the signal is slightly below the filter. + It measures how much the signal falls short in those spots (by adding up the differences where the filter + is higher than the signal), and if that total shortfall is small enough (below a chosen limit called the + threshold), it still decides to create a spike there. Then, just like in the original, it subtracts the filter + pattern from the signal to remove the detected spike pattern so the algorithm can keep looking for the next one. + + .. note:: + - MHSA requires non-negative inputs; automatic shifting and normalization to [0, 1] is applied per feature. + - The FIR filter is designed using `scipy.signal.firwin` with the specified cutoff, window type, etc. + - For multi-feature signals, the same filter shape is applied across all features, but scaling is performed + independently per feature based on its amplitude. + + Refer to the :ref:`modified_hough_spiker_algorithm_desc` for a detailed explanation of the Modified Hough Spiker + Algorithm. + + **Code Example:** + + .. code-block:: python + + import numpy as np + from spikify.encoders.temporal.deconvolution import modified_hough_spiker + signal = np.array([0.1, 0.2, 0.3, 1.0, 0.5, 0.3, 0.1]) + window_length = 3 + threshold = 0.5 + cutoff = 0.1 + encoded_signal, shift, norm, fir_coeffs = modified_hough_spiker(signal, window_length, cutoff, threshold) + + .. doctest:: + :hide: + + >>> import numpy as np + >>> from spikify.encoders.temporal.deconvolution import modified_hough_spiker + >>> signal = np.array([0.1, 0.2, 0.3, 1.0, 0.5, 0.3, 0.1]) + >>> window_length = 3 + >>> threshold = 0.5 + >>> cutoff = 0.1 + >>> encoded_signal, shift, norm, fir_coeffs = modified_hough_spiker(signal, window_length, cutoff, threshold) + >>> encoded_signal + array([0, 0, 1, 1, 0, 0, 1], dtype=int8) + + :param signal: Input signal to encode (1D or 2D: time × features). + :type signal: numpy.ndarray + :param window_length: Length of the FIR filter (number of coefficients). + :type window_length: int + :param cutoff: Cutoff frequency(ies) for the FIR filter design (normalized 0 to 1, where 1 = Nyquist). + Scalar or an array of cutoff frequencies (that is, band edges). + :type cutoff: float | numpy.ndarray + :param threshold: Threshold factor for spike detection. + Scalar or per-feature sequence. + :type threshold: float | int | list[float | int] | numpy.ndarray + :param width: Transition width for FIR filter design (optional, used with certain window types). + :type width: int | None + :param window_type: Window function for FIR filter design (e.g., 'hann', 'hamming', 'blackman', 'boxcar'). + :type window_type: str + :param pass_zero: Whether the filter should be low-pass (True) or high-pass (False/'highpass'). + :type pass_zero: bool | str + :param scale: Set to True to scale the coefficients so that the frequency response is exactly unity at a + certain frequency. + :type scale: bool + :param fs: Sampling frequency (used for physical frequency units in cutoff; optional). + :type fs: float | None + :return: + - spikes: Spike train (same shape as input, dtype=int8, values 0 or 1) + - shift: Per-feature shift values subtracted to make signal non-negative (1D array) + - norm: Per-feature normalization values used to scale signal to [0, 1] (1D array) + - fir_bank: Final filter coefficients used, shape (window_length, n_features) + :rtype: tuple[numpy.ndarray, numpy.ndarray, numpy.ndarray, numpy.ndarray] + :raises ValueError: If the input signal is empty or if the threshold dimensions do not match the signal + features or if the window_length is greater than the signal lenght. + :raises TypeError: If the threshold parameter is of invalid dimension. + + """ + # Check for empty signal + if len(signal) == 0: + raise ValueError("Signal cannot be empty.") + + # Ensure 2D processing (T, F) + if signal.ndim == 1: + signal = signal.reshape(-1, 1) + + T, F = signal.shape + + # Handle window_length + if window_length > T: + raise ValueError("window_length must be less than the number of time steps in the signal.") + + # Handle threshold + if np.isscalar(threshold): + thresholds = np.full(F, float(threshold)) + else: + thresholds = np.asarray(threshold, dtype=float) + if thresholds.ndim != 1: + raise TypeError("Threshold must be a scalar or a 1D sequence of numbers.") + if thresholds.size != F: + raise ValueError("Threshold must match the number of features in the signal.") + + spikes = np.zeros_like(signal, dtype=np.int8) + + # Generate filter coefficient values according to their window length for each feature + fir = firwin(window_length, cutoff, width=width, window=window_type, pass_zero=pass_zero, scale=scale, fs=fs) + + # Stack the same filter for all features in case we need to modify coeffiecient due to signal + # amplitude for certain features + fir_bank = np.stack([fir] * F, axis=0).T + + signal_copy = np.copy(np.array(signal, dtype=np.float64)) + + # Normalize signal if signal has negative values + shift = signal_copy.min(axis=0) + shift[shift > 0] = 0 # only shift if negative values are present + signal_copy -= shift + + norm = signal_copy.max(axis=0) + norm[norm <= 1] = 1 # only normalize if max is greater than 1 + signal_copy /= norm + + for f in range(F): + for t in range(0, T): + + # Determine the end index for the current window + end_index = min(t + window_length, T) + + # Extract the relevant segment of the signal and the corresponding filter window + signal_segment = signal_copy[t:end_index, f] + filter_segment = fir_bank[: end_index - t, f] + + # Calculate the error for this segment + error = np.sum(np.maximum(filter_segment - signal_segment, 0)) + + # If the cumulative error is within the threshold, a spike is detected + if error <= thresholds[f]: + signal_copy[t:end_index, f] -= filter_segment + spikes[t, f] = 1 + + # Flatten if input was 1D + if F == 1: + spikes = spikes.flatten() + + return spikes, shift, norm, fir_bank diff --git a/spikify/encoding/temporal/deconvolution/utils.py b/spikify/encoders/temporal/deconvolution/utils.py similarity index 100% rename from spikify/encoding/temporal/deconvolution/utils.py rename to spikify/encoders/temporal/deconvolution/utils.py diff --git a/spikify/encoders/temporal/global_referenced/__init__.py b/spikify/encoders/temporal/global_referenced/__init__.py new file mode 100644 index 0000000..7dc3994 --- /dev/null +++ b/spikify/encoders/temporal/global_referenced/__init__.py @@ -0,0 +1,6 @@ +"""GlobalReferenced package.""" + +from .phase_encoding_algorithm import phase +from .time_to_first_spike_algorithm import time_to_first_spike + +__all__ = ["phase", "time_to_first_spike"] diff --git a/spikify/encoders/temporal/global_referenced/phase_encoding_algorithm.py b/spikify/encoders/temporal/global_referenced/phase_encoding_algorithm.py new file mode 100644 index 0000000..2aaf40d --- /dev/null +++ b/spikify/encoders/temporal/global_referenced/phase_encoding_algorithm.py @@ -0,0 +1,118 @@ +""" +.. raw:: html + +

Phase Encoding Algorithm

+""" + +import numpy as np + + +def phase(signal: np.ndarray, num_bits: int) -> np.ndarray: + """ + Perform Phase Encoding (PE) on the input signal. + + This function encodes the input signal by calculating the phase angles + of the normalized signal and quantizing these angles into a binary + spike train representation. The encoding process uses a specified number + of bits to determine the level of quantization. + + .. note:: + - PE requires normalized input signals between 0 and 1. If the input signal contains negative + values, they are shifted to be non-negative and then normalized. + + Refer to the :ref:`phase_encoding_algorithm_desc` for a detailed explanation of the Phase Encoding Algorithm. + + **Code Example:** + + .. code-block:: python + + import numpy as np + from spikify.encoders.temporal.global_referenced import phase + signal = np.array([0.1, 0.2, 0.3, 1.0, 0.5, 0.3, 0.1, 0.2]) + num_bits = 4 + encoded_signal = phase(signal, num_bits) + + + .. doctest:: + :hide: + + >>> import numpy as np + >>> from spikify.encoders.temporal.global_referenced import phase + >>> signal = np.array([0.1, 0.2, 0.3, 1.0, 0.5, 0.3, 0.1, 0.2]) + >>> num_bits = 4 + >>> encoded_signal = phase(signal, num_bits) + >>> encoded_signal + array([1, 1, 1, 1, 1, 0, 0, 0], dtype=uint8) + + :param signal: The input signal to be encoded. This should be a numpy ndarray. + :type signal: numpy.ndarray + :param num_bits: The number of bits to use for encoding. + :type num_bits: int + :return: A 1D numpy array representing the phase-encoded spike train. + :rtype: numpy.ndarray + :raises ValueError: If the input signal is empty or if the number of bits is not appropriate for the signal length. + + """ + + # Check for invalid inputs + if len(signal) == 0: + raise ValueError("Signal cannot be empty.") + + # Ensure 2D processing (T, F) + if signal.ndim == 1: + signal = signal.reshape(-1, 1) + + T, F = signal.shape + + if T % num_bits != 0: + raise ValueError(f"The phase_encoding num_bits ({num_bits}) is not a factor of the signal length ({T}).") + + signal_copy = signal.copy() + + # Normalize signal if signal has negative values + shift = signal_copy.min(axis=0) + shift[shift > 0] = 0 # only shift if negative values are present + signal_copy -= shift + + # Compute max amplitude per feature to be used for scaling if max amplitude is grater than 1 + max_amp = signal_copy.max(axis=0) + + # Find features that require scaling + features_to_scale = np.where(max_amp > 1)[0] + + for f in features_to_scale: + signal_copy[:, f] /= max_amp[f] + + # Compute mean over the signal reshaped to bit-sized chunks + interval_bit_mean = np.mean(signal_copy.reshape(T // num_bits, num_bits, F), axis=1) + + max_amp = interval_bit_mean.max(axis=0) + + # Find features that require scaling + features_to_scale = np.where(max_amp > 0)[0] + + for f in features_to_scale: + interval_bit_mean[:, f] /= max_amp[f] + + phase = np.arcsin(interval_bit_mean) + + bins = np.linspace(0, np.pi / 2, 2**num_bits + 1) + levels = np.searchsorted(bins, phase) + + # Adjust levels to avoid out-of-range values + levels = np.clip(levels, 0, 2**num_bits - 1) + + spikes = np.zeros((T, F), dtype=np.uint8) + + # Shift and extract bits + # Each integer is represented in binary using `num_bits` bits. + # The signal (levels) is right-shifted bit-by-bit to bring each bit position to the least significant bit, + # then masked with &1 to extract it (1 if set, 0 otherwise). + bits_arr = ((levels[..., None] >> np.arange(num_bits - 1, -1, -1)) & 1).astype(np.uint8) + spikes = bits_arr.transpose(0, 2, 1).reshape(T, F) + + # Flatten if input was 1D + if F == 1: + spikes = spikes.flatten() + + return spikes diff --git a/spikify/encoders/temporal/global_referenced/time_to_first_spike_algorithm.py b/spikify/encoders/temporal/global_referenced/time_to_first_spike_algorithm.py new file mode 100644 index 0000000..ccc430a --- /dev/null +++ b/spikify/encoders/temporal/global_referenced/time_to_first_spike_algorithm.py @@ -0,0 +1,112 @@ +""" +.. raw:: html + +

Time To First Spike Algorithm

+""" + +import numpy as np + + +def time_to_first_spike(signal: np.ndarray, interval_length: int) -> np.ndarray: + """ + Perform Time To First Spike (TTFS) encoding on the input signal. + + This function implements a sparse temporal coding scheme where + input intensity is encoded in the **timing of the first spike** within a fixed + time window (``interval_length``). Stronger inputs produce earlier spikes. + + The signal is divided into non-overlapping blocks of length ``interval_length``. + For each block, the mean intensity is computed and mapped to a spike latency using + a logarithmic function (approximating an exponential decaying threshold). A single + spike is placed at the corresponding time step within the block. + + .. note:: + - TTFS requires normalized input signals between 0 and 1. If the input signal contains negative + values, they are shifted to be non-negative and then normalized. + + Refer to the :ref:`time_to_first_spike_algorithm_desc` + for a detailed explanation of the Time-to-First-Spike Encoding Algorithm. + + **Code Example:** + + .. code-block:: python + + import numpy as np + from spikify.encoders.temporal.global_referenced import time_to_first_spike + signal = np.array([0.1, 0.2, 0.3, 1.0, 0.5, 0.3, 0.1, 0.2]) + interval_length = 4 + encoded_signal = time_to_first_spike(signal, interval_length) + + .. doctest:: + :hide: + + >>> import numpy as np + >>> from spikify.encoders.temporal.global_referenced import time_to_first_spike + >>> signal = np.array([0.1, 0.2, 0.3, 1.0, 0.5, 0.3, 0.1, 0.2]) + >>> interval_length = 4 + >>> encoded_signal = time_to_first_spike(signal, interval_length) + >>> encoded_signal + array([0, 1, 0, 0, 0, 1, 0, 0], dtype=int8) + + :param signal: Input signal to encode (1D or 2D: timestamps × features). + :type signal: numpy.ndarray + :param interval_length: Length of each time window (block) for latency mapping. + Must evenly divide the signal length. Larger values give + coarser temporal resolution but allow longer latency range. + :type interval_length: int + :return: A numpy array representing the encoded spike train. + :rtype: numpy.ndarray + :raises ValueError: If signal is empty or interval_length does not divide signal length. + + """ + + # Check for empty signal + if signal.shape[0] == 0: + raise ValueError("Signal cannot be empty.") + + # Ensure 2D processing (T, F) + if signal.ndim == 1: + signal = signal.reshape(-1, 1) + + T, F = signal.shape + + if T % interval_length != 0: + raise ValueError(f"The interval_length ({interval_length}) is not a factor of the signal length ({T}).") + + signal_copy = signal.copy() + + # Normalize signal if signal has negative values + shift = signal_copy.min(axis=0) + shift[shift > 0] = 0 # only shift if negative values are present + signal_copy -= shift + + # Compute max amplitude per feature to be used for scaling if max amplitude is grater than 1 + max_amp = signal_copy.max(axis=0) + + # Find features that require scaling + features_to_scale = np.where(max_amp > 1)[0] + + for f in features_to_scale: + signal_copy[:, f] /= max_amp[f] + + # Compute mean over the signal reshaped to interval-sized chunks + signal_copy = np.mean(signal_copy.reshape(T // interval_length, interval_length, F), axis=1) + + intensity = np.full_like(signal_copy, 2.0) + mask = signal_copy > 0.0 + intensity[mask] = 0.1 * np.log(1 / signal_copy[mask]) + + bins = np.linspace(0, 1, interval_length) + levels = np.searchsorted(bins, intensity) + + spikes = np.zeros((T // interval_length, interval_length, F), dtype=np.int8) + for f in range(F): + spikes[np.arange(T // interval_length), np.clip(levels[:, f], 0, interval_length - 1), f] = 1 + + spikes = spikes.reshape(T, F) + + # Flatten if input was 1D + if F == 1: + spikes = spikes.flatten() + + return spikes diff --git a/spikify/encoders/temporal/latency/__init__.py b/spikify/encoders/temporal/latency/__init__.py new file mode 100644 index 0000000..4fb1548 --- /dev/null +++ b/spikify/encoders/temporal/latency/__init__.py @@ -0,0 +1,5 @@ +"""Latency package.""" + +from .burst_coding_algorithm import burst_coding + +__all__ = ["burst_coding"] diff --git a/spikify/encoders/temporal/latency/burst_coding_algorithm.py b/spikify/encoders/temporal/latency/burst_coding_algorithm.py new file mode 100644 index 0000000..09dd822 --- /dev/null +++ b/spikify/encoders/temporal/latency/burst_coding_algorithm.py @@ -0,0 +1,133 @@ +""" +.. raw:: html + +

Burst Coding Algorithm

+""" + +import numpy as np + + +def burst_coding(signal: np.ndarray, n_max: int, t_min: int, t_max: int, interval_length: int) -> np.ndarray: + """ + Perform Burst Coding (BC) on the input signal. + + This function implements a biologically inspired burst coding scheme where each input + intensity (normalized P ∈ [0, 1]) is converted into a burst of spikes: + + - Number of spikes N_s(P) = ceil(P * n_max) + - ISI(P): Fixed at t_max when N_s = 1; otherwise linearly mapped between t_min (strong input) + and t_max (weak input) for N_s > 1. + + The signal is divided into non-overlapping blocks of length ``interval_length``. The mean + intensity per block determines the burst parameters (N_s and ISI). The burst is placed at + regular intervals starting from the beginning of the output block. + + Higher intensities produce bursts with more spikes and smaller ISIs. The ``interval_length`` + must be large enough to fit the longest burst. + + .. note:: + - BC requires normalized input signals between 0 and 1. If the input signal contains negative values, they are + shifted to be non-negative and then normalized. + + Refer to the :ref:`burst_coding_algorithm_desc` for a detailed explanation of the Burst Coding Algorithm. + + **Code Example:** + + .. code-block:: python + + import numpy as np + from spikify.encoders.temporal.latency import burst_coding + np.random.seed(42) + signal = np.random.rand(16) + n_max = 4 + t_min = 2 + t_max = 6 + length = 16 + encoded_signal = burst_coding(signal, n_max, t_min, t_max, length) + + + .. doctest:: + :hide: + + >>> import numpy as np + >>> from spikify.encoders.temporal.latency import burst_coding + >>> np.random.seed(42) + >>> signal = np.random.rand(16) + >>> n_max = 4 + >>> t_min = 2 + >>> t_max = 6 + >>> length = 16 + >>> encoded_signal = burst_coding(signal, n_max, t_min, t_max, length) + >>> encoded_signal + array([1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0], dtype=int8) + + :param signal: Input signal to encode (1D or 2D: timestamps × features). + :type signal: numpy.ndarray + :param n_max: Maximum number of spikes in a burst (for P = 1). Must be ≥ 1. + :type n_max: int + :param t_min: Minimum inter-spike interval (ISI) in time steps (for strong inputs with N_s > 1). + :type t_min: int + :param t_max: Maximum inter-spike interval (ISI) in time steps (for weak inputs or N_s = 1). + :type t_max: int + :param interval_length: Length of each output block corresponding to one input block. + Must divide the signal length evenly and be large enough to fit + the longest burst (approximately n_max * (t_min + 1)). + :type interval_length: int + :return: A numpy array representing the encoded spike train. + :rtype: numpy.ndarray + :raises ValueError: If signal is empty, interval_length does not divide signal length, + or interval_length is too small for the longest possible burst. + + """ + # Check for empty signal + if len(signal) == 0: + raise ValueError("Signal cannot be empty.") + + # Ensure 2D processing (T, F) + if signal.ndim == 1: + signal = signal.reshape(-1, 1) + + T, F = signal.shape + + if T % interval_length != 0: + raise ValueError(f"The interval_length ({interval_length}) is not a factor of the signal length ({T}).") + + signal_copy = signal.copy() + + # signal_copy = np.clip(signal_copy, 0, None) + shift = signal_copy.min(axis=0) + shift[shift > 0] = 0 # only shift if negative values are present + signal_copy -= shift + + # Compute max amplitude per feature to be used for scaling if max amplitude is grater than 1 + max_amp = signal_copy.max(axis=0) + + # Find features that require scaling + features_to_scale = np.where(max_amp > 1)[0] + + for f in features_to_scale: + signal_copy[:, f] /= max_amp[f] + + signal_copy = np.mean(signal_copy.reshape(-1, interval_length, F), axis=1) + + spike_num = np.ceil(signal_copy * n_max).astype(int) + ISI = np.ceil(t_max - signal_copy * (t_max - t_min)).astype(int) + + required_length = np.max(spike_num * (ISI + 1)) + if interval_length < required_length: + raise ValueError(f"Invalid stream length, the min length is {required_length}") + + spikes = np.zeros((T // interval_length, interval_length, F), dtype=np.int8) + + for i in range(signal_copy.shape[0]): + for f in range(F): + spike_times = np.arange(0, spike_num[i, f] * (ISI[i, f] + 1), ISI[i, f] + 1) + spikes[i, spike_times[spike_times < interval_length], f] = 1 + + spikes = spikes.reshape(-1, F) + + # Flatten if input was 1D + if F == 1: + spikes = spikes.flatten() + + return spikes diff --git a/spikify/encoding/__init__.py b/spikify/encoding/__init__.py deleted file mode 100644 index 85ff2cb..0000000 --- a/spikify/encoding/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Encoding package.""" diff --git a/spikify/encoding/rate/__init__.py b/spikify/encoding/rate/__init__.py deleted file mode 100644 index 28a21d8..0000000 --- a/spikify/encoding/rate/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Rate package.""" - -from .poisson_rate_algorithm import poisson_rate - -__all__ = ["poisson_rate"] diff --git a/spikify/encoding/rate/poisson_rate_algorithm.py b/spikify/encoding/rate/poisson_rate_algorithm.py deleted file mode 100644 index a02ce5a..0000000 --- a/spikify/encoding/rate/poisson_rate_algorithm.py +++ /dev/null @@ -1,106 +0,0 @@ -""" -.. raw:: html - -

Poisson Rate Algorithm

-""" - -import numpy as np - - -def poisson_rate(signal: np.ndarray, interval_length: int, seed: int = 0) -> np.ndarray: - """ - Perform Poisson rate encoding on the input signal. - - This function generates a spike train using a Poisson distribution, - where the probability of emitting a spike in a given interval is determined by the normalized rate of the signal. - - See the :ref:`poisson_rate_algorithm_desc` description for a detailed explanation of the Poisson rate encoding - algorithm. - - **Code Example:** - - .. code-block:: python - - import numpy as np - from spikify.encoding.rate import poisson_rate - signal = np.array([0.2, 0.5, 0.8, 1.0]) - np.random.seed(0) - interval_length = 2 - encoded_signal = poisson_rate(signal, interval_length) - - .. doctest:: - :hide: - - >>> import numpy as np - >>> from spikify.encoding.rate import poisson_rate - >>> # Example with numpy array - >>> signal = np.array([0.2, 0.5, 0.8, 1.0]) - >>> # Set seed for reproducibility (optional) - >>> np.random.seed(0) - >>> interval_length = 2 - >>> encoded_signal = poisson_rate(signal, interval_length) - >>> encoded_signal - array([0, 0, 1, 1], dtype=int8) - - :param signal: The input signal to be encoded. This should be a numpy ndarray. - :type signal: numpy.ndarray - :param interval_length: The size of the interval for encoding the spike train. - :type interval_length: int - :param seed: Random seed for reproducibility. Default is 0. - :type seed: int - :return: A numpy array of encoded spike data after Poisson rate encoding. - :rtype: numpy.ndarray - :raises ValueError: If the input signal is empty. - :raises ValueError: If the interval is not a factor of the signal length. - :raises TypeError: If the signal is not a numpy.ndarray - - """ - # Check for invalid inputs - if signal.shape[0] == 0: - raise ValueError("Signal cannot be empty.") - - if signal.shape[0] % interval_length != 0: - raise ValueError( - f"The interval ({interval_length}) is not a factor of the signal length ({signal.shape[0]}). " - "To resolve this, consider trimming or padding the signal to ensure its length is a multiple of the " - "interval." - ) - if signal.ndim == 1: - signal = signal.reshape(-1, 1) - - S, F = signal.shape - - np.random.seed(seed) - - # Ensure non-negative signal values - signal = np.clip(signal, 0, None) - - # Compute mean over the signal reshaped to interval-sized chunks - signal = np.mean(signal.reshape(S // interval_length, interval_length, F), axis=1) - - # Normalize the signal - signal_max = np.max(signal, axis=0, keepdims=True) - if np.any(signal_max > 0): - safe_max = np.where(signal_max > 0, signal_max, 1) - signal /= safe_max - - # Initialize the spike array - spikes = np.zeros((S // interval_length, interval_length, F), dtype=np.int8) - - # Create bins for Poisson rate encoding - bins = np.linspace(0, 1, interval_length + 1) - - # Generate Poisson spike trains - for feat in range(F): - for idx, rate in enumerate(signal[:, feat]): - if rate > 0: - ISI = -np.log(1 - np.random.random(interval_length)) / (rate * interval_length) # Inter-spike intervals - spike_times = np.searchsorted(bins, np.cumsum(ISI)) - 1 # Find spike times - spike_times = spike_times[spike_times < interval_length] # Clip times within interval - spikes[idx, spike_times, feat] = 1 - - spikes = spikes.reshape(S, F) - - if F == 1: - spikes = spikes.flatten() - return spikes diff --git a/spikify/encoding/temporal/contrast/moving_window_algorithm.py b/spikify/encoding/temporal/contrast/moving_window_algorithm.py deleted file mode 100644 index 0c5632f..0000000 --- a/spikify/encoding/temporal/contrast/moving_window_algorithm.py +++ /dev/null @@ -1,83 +0,0 @@ -""" -.. raw:: html - -

Moving Window Algorithm

-""" - -import numpy as np - - -def moving_window(signal: np.ndarray, window_length: int) -> np.ndarray: - """ - Perform Moving Window encoding on the input signal. - - This function takes a continuous signal and converts it into a spike train using a moving window and - threshold-based approach. A spike is generated when the signal exceeds the calculated `Base` plus or minus a - specified `Threshold`. - - Refer to the :ref:`moving_window_algorithm_desc` for a detailed explanation of the Moving Window encoding - algorithm. - - **Code Example:** - - .. code-block:: python - - import numpy as np - from spikify.encoding.temporal.contrast import moving_window - signal = np.array([0.1, 0.3, 0.2, 0.5, 0.8, 1.0]) - window_length = 3 - encoded_signal = moving_window(signal, window_length) - encoded_signal - - .. doctest:: - :hide: - - >>> import numpy as np - >>> from spikify.encoding.temporal.contrast import moving_window - >>> signal = np.array([0.1, 0.3, 0.2, 0.5, 0.8, 1.0]) - >>> window_length = 3 - >>> encoded_signal = moving_window(signal, window_length) - >>> encoded_signal - array([0, 0, 0, 1, 1, 1], dtype=int8) - - :param signal: The input signal to be encoded. This should be a numpy ndarray. - :type signal: numpy.ndarray - :param window_length: The size of the sliding window for calculating the base mean. - :type window_length: int - :return: A numpy array representing the encoded spike train. - :rtype: numpy.ndarray - :raises ValueError: If the input signal is empty. - :raises ValueError: If the window length is greater than the length of the signal. - :raises TypeError: If the signal is not a numpy ndarray. - - """ - - # Check for empty signal - if len(signal) == 0: - raise ValueError("Signal cannot be empty.") - - if signal.ndim == 1: - signal = signal.reshape(-1, 1) - - S, F = signal.shape - - variation = np.diff(signal[1:, :], prepend=signal[[0], :], axis=0) - threshold = np.mean(np.abs(variation), axis=0) - spikes = np.zeros_like(signal, dtype=np.int8) - - # Compute the moving window mean and apply thresholds - for feat in range(F): - for seq_idx in range(len(signal[:, feat])): - base = ( - np.mean(signal[:window_length, feat]) - if seq_idx < window_length - else np.mean(signal[seq_idx - window_length : seq_idx, feat]) - ) - if signal[seq_idx, feat] > base + threshold[feat]: - spikes[seq_idx, feat] = 1 - elif signal[seq_idx, feat] < base - threshold[feat]: - spikes[seq_idx, feat] = -1 - - if F == 1: - spikes = spikes.flatten() - return spikes diff --git a/spikify/encoding/temporal/contrast/step_forward_algorithm.py b/spikify/encoding/temporal/contrast/step_forward_algorithm.py deleted file mode 100644 index 33c930b..0000000 --- a/spikify/encoding/temporal/contrast/step_forward_algorithm.py +++ /dev/null @@ -1,86 +0,0 @@ -""" -.. raw:: html - -

Step Forward Algorithm

-""" - -import numpy as np - - -def step_forward(signal: np.ndarray, threshold: float | list[float]) -> np.ndarray: - """ - Perform Step-Forward encoding on the input signal. - - This function takes a continuous signal and converts it into a spike train using a dynamically updated baseline - and threshold-based approach. A spike is generated when the signal exceeds or drops below the dynamically - adjusted baseline (`Base`) by the specified `Threshold`. - - Refer to the :ref:`step_forward_algorithm_desc` for a detailed explanation of the Step-Forward encoding algorithm. - - **Code Example:** - - .. code-block:: python - - import numpy as np - from spikify.encoding.temporal.contrast import step_forward - signal = np.array([0.1, 0.3, 0.4, 0.2, 0.5, 0.6]) - threshold = 0.2 - encoded_signal = step_forward(signal, threshold) - - .. doctest:: - :hide: - - >>> import numpy as np - >>> from spikify.encoding.temporal.contrast import step_forward - >>> signal = np.array([0.1, 0.3, 0.4, 0.2, 0.5, 0.6]) - >>> threshold = 0.2 - >>> encoded_signal = step_forward(signal, threshold) - >>> encoded_signal - array([0, 0, 1, 0, 0, 1], dtype=int8) - - :param signal: The input signal to be encoded. This should be a numpy ndarray. - :type signal: numpy.ndarray - :param threshold: The threshold value(s) for spike detection. Can be a float or a list of floats. - :type threshold: float | list[float] - :return: A numpy array representing the encoded spike train. - :rtype: numpy.ndarray - :raises ValueError: If the input signal is empty. - :raises TypeError: If the signal is not a numpy ndarray. - - """ - if len(signal) == 0: - raise ValueError("Signal cannot be empty.") - - if signal.ndim == 1: - signal = signal.reshape(-1, 1) - - S, F = signal.shape - - if isinstance(threshold, float): - thresholds = [threshold] * F - elif isinstance(threshold, list): - if not all(isinstance(w, float) for w in threshold): - raise TypeError("All elements in threshold list must be float.") - thresholds = threshold - else: - raise TypeError("Threshold must be a float or a list of floats.") - - if len(thresholds) != F: - raise ValueError("Threshold must match the number of features in the signal.") - - spike = np.zeros_like(signal, dtype=np.int8) - - # Base value initialized at the start of the signal - for feat in range(F): - base = signal[0, feat] - for value_idx, value in enumerate(signal[:, feat]): - if value > base + thresholds[feat]: - spike[value_idx, feat] = 1 - base += thresholds[feat] - elif value < base - thresholds[feat]: - spike[value_idx, feat] = -1 - base -= thresholds[feat] - - if F == 1: - spike = spike.flatten() - return spike diff --git a/spikify/encoding/temporal/contrast/threshold_based_algorithm.py b/spikify/encoding/temporal/contrast/threshold_based_algorithm.py deleted file mode 100644 index 831be66..0000000 --- a/spikify/encoding/temporal/contrast/threshold_based_algorithm.py +++ /dev/null @@ -1,84 +0,0 @@ -""" -.. raw:: html - -

Threshold Based Representation Algorithm

-""" - -import numpy as np - - -def threshold_based_representation(signal: np.ndarray, factor: float | list[float]) -> np.ndarray: - """ - Perform Threshold-Based Representation (TBR) encoding on the input signal. - - This function takes a continuous signal and converts it into a spike train using a fixed threshold based on - the signal's variations. A spike is generated when the variation exceeds the computed threshold. - - Refer to the :ref:`threshold_based_representation_algorithm_desc` for a detailed explanation of the TBR - encoding algorithm. - - **Code Example:** - - .. code-block:: python - - import numpy as np - from spikify.encoding.temporal.contrast import threshold_based_representation - signal = np.array([0.1, 0.3, 0.4, 0.2, 0.5, 0.6]) - factor = 0.5 - encoded_signal = threshold_based_representation(signal, factor) - - .. doctest:: - :hide: - - >>> import numpy as np - >>> from spikify.encoding.temporal.contrast import threshold_based_representation - >>> signal = np.array([0.1, 0.3, 0.4, 0.2, 0.5, 0.6]) - >>> factor = 0.5 - >>> encoded_signal = threshold_based_representation(signal, factor) - >>> encoded_signal - array([ 0, 1, 0, -1, 1, 0], dtype=int8) - - :param signal: The input signal to be encoded. This should be a numpy ndarray. - :type signal: numpy.ndarray - :param factor: The factor value (`γ`) that controls the noise-reduction threshold. - Can be a float or a list of floats. - :type factor: float | list[float] - :return: A numpy array representing the encoded spike train. - :rtype: numpy.ndarray - :raises ValueError: If the input signal is empty. - :raises TypeError: If the signal is not a numpy ndarray. - - """ - if len(signal) == 0: - raise ValueError("Signal cannot be empty.") - - if signal.ndim == 1: - signal = signal.reshape(-1, 1) - - S, F = signal.shape - - if isinstance(factor, float): - factors = [factor] * F - elif isinstance(factor, list): - if not all(isinstance(w, float) for w in factor): - raise TypeError("All elements in factor list must be float.") - factors = factor - else: - raise TypeError("factor must be a float or a list of floats.") - if len(factors) != F: - raise ValueError("Factor must match the number of features in the signal.") - - spike = np.zeros_like(signal, dtype=np.int8) - variation = np.diff(signal[1:, :], prepend=signal[[0], :], axis=0) - - threshold = np.mean(variation, axis=0) + factors * np.std(variation, axis=0) - variation = np.insert(variation, 0, variation[1, :], axis=0) - - # Apply threshold conditions - threshold = threshold.reshape(1, threshold.shape[0]) - spike[variation > threshold] = 1 - spike[variation < -threshold] = -1 - - if F == 1: - spike = spike.flatten() - return spike diff --git a/spikify/encoding/temporal/deconvolution/bens_spiker_algorithm.py b/spikify/encoding/temporal/deconvolution/bens_spiker_algorithm.py deleted file mode 100644 index dd149a1..0000000 --- a/spikify/encoding/temporal/deconvolution/bens_spiker_algorithm.py +++ /dev/null @@ -1,123 +0,0 @@ -""" -.. raw:: html - -

Bens Spiker Algorithm

-""" - -import numpy as np -from scipy.signal.windows import get_window -from .utils import WindowType - - -def bens_spiker( - signal: np.ndarray, - window_length: int | list[int], - threshold: float | list[float], - window_type: WindowType = "boxcar", -) -> np.ndarray: - """ - Perform spike detection using Bens Spiker Algorithm. - - This function detects spikes in an input signal based on the comparison of cumulative errors calculated over a - segment of the signal, which is filtered using a boxcar window. A spike is detected if the cumulative error between - the filtered signal and the raw signal is below a certain threshold. - - Refer to the :ref:`bens_spiker_algorithm_desc` for a detailed explanation of the Ben's Spiker algorithm. - - **Code Example:** - - .. code-block:: python - - import numpy as np - from spikify.encoding.temporal.deconvolution import bens_spiker - signal = np.array([0.1, 0.2, 0.3, 1.0, 0.5, 0.3, 0.1]) - window_length = 3 - threshold = 0.5 - spikes = bens_spiker(signal, window_length, threshold) - - .. doctest:: - :hide: - - >>> import numpy as np - >>> from spikify.encoding.temporal.deconvolution import bens_spiker - >>> signal = np.array([0.1, 0.2, 0.3, 1.0, 0.5, 0.3, 0.1]) - >>> window_length = 3 - >>> threshold = 0.5 - >>> spikes = bens_spiker(signal, window_length, threshold) - >>> spikes - array([0, 0, 1, 0, 0, 0, 0], dtype=int8) - - :param signal: The input signal to be analyzed. This should be a numpy ndarray. - :type signal: numpy.ndarray - :param window_length: The length of the window type filter window. Can be a int or a list of ints. - :type window_length: int | list[int] - :param threshold: Threshold value used to detect spikes. Can be a float or a list of floats. - :type threshold: float | list[float] - :return: A numpy array representing the detected spikes. - :rtype: numpy.ndarray - :raises ValueError: If the input signal is empty or if the window length is greater than the signal length. - :raises TypeError: If the signal is not a numpy ndarray. - - """ - # Check for invalid inputs - if len(signal) == 0: - raise ValueError("Signal cannot be empty.") - - if signal.ndim == 1: - signal = signal.reshape(-1, 1) - - S, F = signal.shape - - if isinstance(window_length, int): - window_lengths = [window_length] * F - elif isinstance(window_length, list): - if not all(isinstance(w, int) for w in window_length): - raise TypeError("All elements in window_length list must be integers.") - window_lengths = window_length - else: - raise TypeError("Window lengths must be an int or a list of ints.") - - if len(window_lengths) != F: - raise ValueError("Window lengths must match the number of features in the signal.") - - if np.any(np.array(window_lengths) > S): - raise ValueError("All filter window sizes must be less than the length of the signal.") - - if isinstance(threshold, float): - thresholds = [threshold] * F - elif isinstance(threshold, list): - if not all(isinstance(w, float) for w in threshold): - raise TypeError("All elements in threshold list must be float.") - thresholds = threshold - else: - raise TypeError("Threshold must be a float or a list of floats.") - - if len(thresholds) != F: - raise ValueError("Threshold must match the number of features in the signal.") - - # Initialize the spike array - spikes = np.zeros_like(signal, dtype=np.int8) - - # Create the boxcar filter window - filter_window = [get_window(window_type, w) for w in window_lengths] - - # Copy of the signal to avoid modifying the original input - signal_copy = np.copy(np.array(signal, dtype=np.float64)) - - # Iterate over the signal to detect spikes - for feat in range(F): - for seq_idx in range(len(signal[:, feat]) - window_lengths[feat] + 1): - # Calculate errors using the filter window - segment = signal_copy[seq_idx : seq_idx + window_lengths[feat], feat] - error1 = np.sum(np.abs(segment - filter_window[feat]), axis=0) - error2 = np.sum(np.abs(segment), axis=0) - - # Update signal and spike array if a spike is detected - if error1 <= (error2 - thresholds[feat]): - signal_copy[seq_idx : seq_idx + window_lengths[feat], feat] -= filter_window[feat] - spikes[seq_idx, feat] = 1 - - if F == 1: - spikes = spikes.flatten() - - return spikes diff --git a/spikify/encoding/temporal/deconvolution/hough_spiker_algorithm.py b/spikify/encoding/temporal/deconvolution/hough_spiker_algorithm.py deleted file mode 100644 index 7e163ad..0000000 --- a/spikify/encoding/temporal/deconvolution/hough_spiker_algorithm.py +++ /dev/null @@ -1,98 +0,0 @@ -""" -.. raw:: html - -

Hough Spiker Algorithm

-""" - -import numpy as np -from scipy.signal.windows import get_window -from .utils import WindowType - - -def hough_spiker(signal: np.ndarray, window_length: int | list[int], window_type: WindowType = "boxcar") -> np.ndarray: - """ - Perform spike detection using the Hough Spiker Algorithm (HSA). - - This function detects spikes in an input signal by performing a progressive subtraction operation, - where the signal is compared with a convolution result using a boxcar filter. If the signal value - exceeds the filter result, the signal is modified by subtracting the filter, and a spike is recorded. - - Refer to the :ref:`hough_spiker_algorithm_desc` for a detailed explanation of the HSA. - - **Code Example** - - .. code-block:: python - - import numpy as np - from spikify.encoding.temporal.deconvolution import hough_spiker - signal = np.array([0.1, 0.2, 4.1, 1.0, 3.0, 0.3, 0.1]) - window_length = 3 - spikes = hough_spiker(signal, window_length) - - .. doctest:: - :hide: - - >>> import numpy as np - >>> from spikify.encoding.temporal.deconvolution import hough_spiker - >>> signal = np.array([0.1, 0.2, 4.1, 1.0, 3.0, 0.3, 0.1]) - >>> window_length = 3 - >>> spikes = hough_spiker(signal, window_length) - >>> spikes - array([0, 0, 1, 0, 0, 0, 0], dtype=int8) - - :param signal: The input signal to be analyzed. This should be a numpy ndarray. - :type signal: numpy.ndarray - :param window_length: The length of the boxcar filter window. Can be a int or a list of ints. - :type window_length: int | list[int] - :return: A 1D numpy array representing the detected spikes. - :rtype: numpy.ndarray - :raises ValueError: If the input signal is empty or if the window length is greater than the signal length. - :raises TypeError: If the signal is not a numpy ndarray. - - """ - if len(signal) == 0: - raise ValueError("Signal cannot be empty.") - - if signal.ndim == 1: - signal = signal.reshape(-1, 1) - - S, F = signal.shape - - if isinstance(window_length, int): - window_lengths = [window_length] * F - elif isinstance(window_length, list): - if not all(isinstance(w, int) for w in window_length): - raise TypeError("All elements in window_length list must be integers.") - window_lengths = window_length - else: - raise TypeError("Window lengths must be an int or a list of ints.") - - if len(window_lengths) != F: - raise ValueError("Window lengths must match the number of features in the signal.") - - if np.any(np.array(window_lengths) > S): - raise ValueError("All filter window sizes must be less than the length of the signal.") - - # Initialize the spike array - spikes = np.zeros_like(signal, dtype=np.int8) - - # Create the boxcar filter window - filter_window = [get_window(window_type, w) for w in window_lengths] - - # Copy the signal for modification - signal_copy = np.copy(np.array(signal, dtype=np.float64)) - - for feature in range(F): - # Iterate over the signal to detect spikes - for t in range(len(signal_copy[:, feature]) - window_lengths[feature] + 1): - # Count how many values match or exceed the filter window values - match_count = np.sum(signal_copy[t : t + window_lengths[feature], feature] >= filter_window[feature]) - - # If all values match or exceed, a spike is detected - if match_count == window_lengths[feature]: - signal_copy[t : t + window_lengths[feature], feature] -= filter_window[feature] - spikes[t, feature] = 1 - - if spikes.shape[-1] == 1: - spikes = spikes.flatten() - return spikes diff --git a/spikify/encoding/temporal/deconvolution/modified_hough_spiker_algorithm.py b/spikify/encoding/temporal/deconvolution/modified_hough_spiker_algorithm.py deleted file mode 100644 index 94ff6c5..0000000 --- a/spikify/encoding/temporal/deconvolution/modified_hough_spiker_algorithm.py +++ /dev/null @@ -1,126 +0,0 @@ -""" -.. raw:: html - -

Modified Hough Spiker Algorithm

-""" - -import numpy as np -from scipy.signal.windows import get_window -from .utils import WindowType - - -def modified_hough_spiker( - signal: np.ndarray, - window_length: int | list[int], - threshold: float | list[float], - window_type: WindowType = "boxcar", -) -> np.ndarray: - """ - Detect spikes in a signal using the Modified Hough Spiker Algorithm. - - This function detects spikes in an input signal by incorporating a threshold-based - error accumulation mechanism. The signal is compared with a convolution result - using a boxcar filter, and the error is accumulated over time. If the error remains - within a specified threshold, a spike is detected, and the signal is modified. - - Refer to the :ref:`modified_hough_spiker_algorithm_desc` for a detailed explanation of the Modified Hough Spiker - Algorithm. - - **Code Example:** - - .. code-block:: python - - import numpy as np - from spikify.encoding.temporal.deconvolution import modified_hough_spiker - signal = np.array([0.1, 0.2, 0.3, 1.0, 0.5, 0.3, 0.1]) - window_length = 3 - threshold = 0.5 - spikes = modified_hough_spiker(signal, window_length, threshold) - - .. doctest:: - :hide: - - >>> import numpy as np - >>> from spikify.encoding.temporal.deconvolution import modified_hough_spiker - >>> signal = np.array([0.1, 0.2, 0.3, 1.0, 0.5, 0.3, 0.1]) - >>> window_length = 3 - >>> threshold = 0.5 - >>> spikes = modified_hough_spiker(signal, window_length, threshold) - >>> spikes - array([0, 0, 0, 0, 0, 0, 0], dtype=int8) - - :param signal: The input signal to be analyzed. This should be a numpy ndarray. - :type signal: numpy.ndarray - :param window_length: The length of the boxcar filter window. Can be a int or a list of ints. - :type window_length: int | list[int] - :param threshold: The threshold value for error accumulation. Can be a float or a list/array of floats. - :type threshold: float or list of float - :return: A 1D numpy array representing the detected spikes. - :rtype: numpy.ndarray - :raises ValueError: If the input signal is empty or if the window length is greater than the signal length. - :raises TypeError: If the signal is not a numpy ndarray. - - """ - # Check for invalid inputs - if len(signal) == 0: - raise ValueError("Signal cannot be empty.") - - if signal.ndim == 1: - signal = signal.reshape(-1, 1) - - S, F = signal.shape - - if isinstance(threshold, float): - thresholds = [threshold] * F - elif isinstance(threshold, list): - if not all(isinstance(w, float) for w in threshold): - raise TypeError("All elements in threshold list must be float.") - thresholds = threshold - else: - raise TypeError("Threshold must be a float or a list of floats.") - - if len(thresholds) != F: - raise ValueError("Thresholds must match the number of features in the signal.") - - if isinstance(window_length, int): - window_lengths = [window_length] * F - elif isinstance(window_length, list): - window_lengths = window_length - else: - raise TypeError("Window length must be an int or a list of ints.") - - if len(window_lengths) != F: - raise ValueError("Window lengths must match the number of features in the signal.") - - if np.any(np.array(window_lengths) > S): - raise ValueError("All filter window sizes must be less than the length of the signal.") - - # Initialize the spikes array - spikes = np.zeros_like(signal, dtype=np.int8) - - # Create the boxcar filter window - filter_window = [get_window(window_type, w) for w in window_lengths] - # Copy the signal for modification - signal_copy = np.copy(np.array(signal, dtype=np.float64)) - - for feature in range(F): - # Iterate over the signal to detect spikes - for t in range(len(signal[:, feature])): - # Determine the end index for the current window - end_index = min(t + window_lengths[feature], S) - - # Extract the relevant segment of the signal and the corresponding filter window - signal_segment = signal_copy[t:end_index, feature] - filter_segment = filter_window[feature][: end_index - t] - - # Calculate the error for this segment - error = np.sum(np.maximum(filter_segment - signal_segment, 0)) - - # If the cumulative error is within the threshold, a spike is detected - if error <= thresholds[feature]: - signal_copy[t:end_index, feature] -= filter_segment - spikes[t, feature] = 1 - - if F == 1: - spikes = spikes.flatten() - return spikes diff --git a/spikify/encoding/temporal/global_referenced/__init__.py b/spikify/encoding/temporal/global_referenced/__init__.py deleted file mode 100644 index dd82bcf..0000000 --- a/spikify/encoding/temporal/global_referenced/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -"""GlobalReferenced package.""" - -from .phase_encoding_algorithm import phase_encoding -from .time_to_spike_algorithm import time_to_first_spike - -__all__ = ["phase_encoding", "time_to_first_spike"] diff --git a/spikify/encoding/temporal/global_referenced/phase_encoding_algorithm.py b/spikify/encoding/temporal/global_referenced/phase_encoding_algorithm.py deleted file mode 100644 index 9273632..0000000 --- a/spikify/encoding/temporal/global_referenced/phase_encoding_algorithm.py +++ /dev/null @@ -1,96 +0,0 @@ -""" -.. raw:: html - -

Phase Encoding Algorithm

-""" - -import numpy as np - - -def phase_encoding(signal: np.ndarray, num_bits: int) -> np.ndarray: - """ - Perform phase encoding on the input signal based on the given settings. - - This function encodes the input signal by calculating the phase angles - of the normalized signal and quantizing these angles into a binary - spike train representation. The encoding process uses a specified number - of bits to determine the level of quantization. - - Refer to the :ref:`phase_encoding_algorithm_desc` for a detailed explanation of the Phase Encoding Algorithm. - - **Code Example:** - - .. code-block:: python - - import numpy as np - from spikify.encoding.temporal.global_referenced import phase_encoding - signal = np.array([0.1, 0.2, 0.3, 1.0, 0.5, 0.3, 0.1, 0.2]) - num_bits = 4 - encoded_signal = phase_encoding(signal, num_bits) - - - .. doctest:: - :hide: - - >>> import numpy as np - >>> from spikify.encoding.temporal.global_referenced import phase_encoding - >>> signal = np.array([0.1, 0.2, 0.3, 1.0, 0.5, 0.3, 0.1, 0.2]) - >>> num_bits = 4 - >>> encoded_signal = phase_encoding(signal, num_bits) - >>> encoded_signal - array([1, 1, 1, 1, 1, 0, 0, 0], dtype=uint8) - - :param signal: The input signal to be encoded. This should be a numpy ndarray. - :type signal: numpy.ndarray - :param num_bits: The number of bits to use for encoding. - :type num_bits: int - :return: A 1D numpy array representing the phase-encoded spike train. - :rtype: numpy.ndarray - :raises ValueError: If the input signal is empty or if the number of bits is not appropriate for the signal length. - - """ - # Check for invalid inputs - if len(signal) == 0: - raise ValueError("Signal cannot be empty.") - - if len(signal) % num_bits != 0: - raise ValueError( - f"The phase_encoding num_bits ({num_bits}) is not a multiple of the signal length ({len(signal)})." - ) - - if signal.ndim == 1: - signal = signal.reshape(-1, 1) - - S, F = signal.shape - # Ensure non-negative signal values - signal = np.clip(signal, 0, None) - - # Compute mean over the signal reshaped to bit-sized chunks - signal = np.mean(signal.reshape(signal.shape[0] // num_bits, num_bits, signal.shape[1]), axis=1) - signal_max = signal.max(axis=0) # shape (F,) - - for i in range(signal.shape[1]): # per ogni feature - if signal_max[i] > 0: - signal[:, i] /= signal_max[i] - - # Compute the phase angles based on the signal - phase = np.arcsin(signal) - - # Create phase bins and quantize the phase - bins = np.linspace(0, np.pi / 2, 2**num_bits + 1) - levels = np.searchsorted(bins, phase) - - # Adjust levels to avoid out-of-range values - levels = np.clip(levels, 0, 2**num_bits - 1) - - N, F = levels.shape - spikes = np.zeros((N * num_bits, F), dtype=np.uint8) - - # Vectorized bit extraction for all features and samples - bits_arr = ((levels[..., None] >> np.arange(num_bits - 1, -1, -1)) & 1).astype(np.uint8) - spikes = bits_arr.transpose(0, 2, 1).reshape(N * num_bits, F) - - if F == 1: - spikes = spikes.flatten() - - return spikes diff --git a/spikify/encoding/temporal/global_referenced/time_to_spike_algorithm.py b/spikify/encoding/temporal/global_referenced/time_to_spike_algorithm.py deleted file mode 100644 index caa6cc6..0000000 --- a/spikify/encoding/temporal/global_referenced/time_to_spike_algorithm.py +++ /dev/null @@ -1,96 +0,0 @@ -""" -.. raw:: html - -

Time To First Spike Algorithm

-""" - -import numpy as np - - -def time_to_first_spike(signal: np.ndarray, interval: int) -> np.ndarray: - """ - Perform time-to-first-spike encoding on the input signal. - - This function encodes the input signal by computing the time to the first spike - based on a dynamically decaying threshold, following an exponential function. - The time to the first spike is determined by the value of the signal relative - to this threshold. - - Refer to the :ref:`time_to_first_spike_algorithm_desc` - for a detailed explanation of the Time-to-First-Spike Encoding Algorithm. - - **Code Example:** - - .. code-block:: python - - import numpy as np - from spikify.encoding.temporal.global_referenced import time_to_first_spike - signal = np.array([0.1, 0.2, 0.3, 1.0, 0.5, 0.3, 0.1, 0.2]) - interval = 4 - encoded_signal = time_to_first_spike(signal, interval) - - .. doctest:: - :hide: - - >>> import numpy as np - >>> from spikify.encoding.temporal.global_referenced import time_to_first_spike - >>> signal = np.array([0.1, 0.2, 0.3, 1.0, 0.5, 0.3, 0.1, 0.2]) - >>> interval = 4 - >>> encoded_signal = time_to_first_spike(signal, interval) - >>> encoded_signal - array([1, 0, 0, 0, 0, 1, 0, 0], dtype=int8) - - :param signal: The input signal to be encoded.This should be a numpy ndarray. - :type signal: numpy.ndarray - :param interval: The size of the interval used for encoding. - :type interval: int - :return: A 1D numpy array representing the time-to-first-spike encoded spike train. - :rtype: numpy.ndarray - :raises ValueError: If the input signal is empty or if the interval is not a multiple of the signal length. - - """ - - if signal.ndim == 1: - signal = signal.reshape(-1, 1) - - S, F = signal.shape - # Check for invalid inputs - if signal.shape[0] == 0: - raise ValueError("Signal cannot be empty.") - - if signal.shape[0] % interval != 0: - raise ValueError( - f"The time_to_spike interval ({interval}) is not a multiple of the signal length ({len(signal)})." - ) - - # Ensure non-negative signal values - signal = np.clip(signal, 0, None) - - # Compute mean over the signal reshaped to interval-sized chunks - signal = np.mean(signal.reshape(signal.shape[0] // interval, interval, signal.shape[1]), axis=1) - - signal_max = signal.max(axis=0) # shape (F,) - - for feature in range(F): - if signal_max[feature] > 0: - signal[:, feature] /= signal_max[feature] - - # Calculate intensity based on the signal - with np.errstate(divide="ignore"): # Avoid division warnings - intensity = np.where(signal > 0, 0.1 * np.log(1 / signal), 2) - - # Create bins and quantize the intensity - bins = np.linspace(0, 1, interval) - levels = np.searchsorted(bins, intensity) - - # Create the spike matrix and set spikes - spike = np.zeros((signal.shape[0], interval, signal.shape[1]), dtype=np.int8) - for feature in range(signal.shape[1]): - spike[np.arange(signal.shape[0]), np.clip(levels[:, feature], 0, interval - 1), feature] = 1 - - # Reshape the spike array into 1D - if spike.shape[2] == 1: - spike = spike.reshape(-1) - else: - spike = spike.reshape(spike.shape[0] * spike.shape[1], spike.shape[2]) - return spike diff --git a/spikify/encoding/temporal/latency/__init__.py b/spikify/encoding/temporal/latency/__init__.py deleted file mode 100644 index 3281eb9..0000000 --- a/spikify/encoding/temporal/latency/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Latency package.""" - -from .burst_encoding_algorithm import burst_encoding - -__all__ = ["burst_encoding"] diff --git a/spikify/encoding/temporal/latency/burst_encoding_algorithm.py b/spikify/encoding/temporal/latency/burst_encoding_algorithm.py deleted file mode 100644 index 3b38ca8..0000000 --- a/spikify/encoding/temporal/latency/burst_encoding_algorithm.py +++ /dev/null @@ -1,104 +0,0 @@ -""" -.. raw:: html - -

Burst Encoding Algorithm

-""" - -import numpy as np - - -def burst_encoding(signal: np.ndarray, n_max: int, t_min: int, t_max: int, length: int) -> np.ndarray: - """ - Perform burst encoding on the input signal. - - This function encodes the input signal by generating bursts of spikes - based on the number of spikes and the inter-spike interval (ISI). - The encoding process takes into account the maximum number of spikes (n_max), - the minimum (t_min) and maximum (t_max) ISI, and the desired length of the encoded signal. - - Refer to the :ref:`burst_encoding_algorithm_desc` for a detailed explanation of the Burst Encoding Algorithm. - - **Code Example:** - - .. code-block:: python - - import numpy as np - from spikify.encoding.temporal.latency import burst_encoding - np.random.seed(42) - signal = np.random.rand(16) - n_max = 4 - t_min = 2 - t_max = 6 - length = 16 - encoded_signal = burst_encoding(signal, n_max, t_min, t_max, length) - - - .. doctest:: - :hide: - - >>> import numpy as np - >>> from spikify.encoding.temporal.latency import burst_encoding - >>> np.random.seed(42) - >>> signal = np.random.rand(16) - >>> n_max = 4 - >>> t_min = 2 - >>> t_max = 6 - >>> length = 16 - >>> encoded_signal = burst_encoding(signal, n_max, t_min, t_max, length) - >>> encoded_signal - array([1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0], dtype=int8) - - :param signal: The input signal to be encoded. This should be a numpy ndarray. - :type signal: numpy.ndarray - :param n_max: The maximum number of spikes in each burst. - :type n_max: int - :param t_min: The minimum inter-spike interval (ISI). - :type t_min: int - :param t_max: The maximum inter-spike interval (ISI). - :type t_max: int - :param length: The total length of the encoded signal. - :type length: int - :return: A 1D numpy array representing the burst-encoded spike train. - :rtype: numpy.ndarray - :raises ValueError: If the input signal is empty or the length is not a multiple of the signal length. - :raises ValueError: If the required spike train length exceeds the provided length. - - """ - is_1d = signal.ndim == 1 - if is_1d: - signal = signal[:, None] - - S, F = signal.shape - - if S == 0: - raise ValueError("Signal cannot be empty.") - - if S % length != 0: - raise ValueError( - f"The burst_encoding length ({length}) is not a multiple of the signal length ({len(signal)})." - ) - - signal = np.clip(signal, 0, None) - signal = np.mean(signal.reshape(-1, length, F), axis=1) - - signal_max = signal.max(axis=0) - signal_max[signal_max == 0] = 1 - signal /= signal_max - - spike_num = np.ceil(signal * n_max).astype(int) - ISI = np.ceil(t_max - signal * (t_max - t_min)).astype(int) - - required_length = np.max(spike_num * (ISI + 1)) - if length < required_length: - raise ValueError(f"Invalid stream length, the min length is {required_length}") - - spikes = np.zeros((signal.shape[0], length, signal.shape[1]), dtype=np.int8) - - for i in range(signal.shape[0]): - for f in range(signal.shape[1]): - spike_times = np.arange(0, spike_num[i, f] * (ISI[i, f] + 1), ISI[i, f] + 1) - spikes[i, spike_times[spike_times < length], f] = 1 - - spikes = spikes.reshape(-1, signal.shape[1]) - - return spikes.ravel() if is_1d else spikes diff --git a/spikify/filtering/__init__.py b/spikify/filters/__init__.py similarity index 71% rename from spikify/filtering/__init__.py rename to spikify/filters/__init__.py index 9c4e0e4..4ec3fbf 100644 --- a/spikify/filtering/__init__.py +++ b/spikify/filters/__init__.py @@ -1,4 +1,4 @@ -"""Filtering package.""" +"""Filters package.""" from .filterbank import FilterBank diff --git a/spikify/filtering/filterbank.py b/spikify/filters/filterbank.py similarity index 71% rename from spikify/filtering/filterbank.py rename to spikify/filters/filterbank.py index 25ea0f9..b721c61 100644 --- a/spikify/filtering/filterbank.py +++ b/spikify/filters/filterbank.py @@ -17,7 +17,7 @@ class FilterBank(ABC): .. code-block:: python import numpy as np - from spikify.filtering import FilterBank + from spikify.filters import FilterBank fs = 1000 signal = np.random.randn(1000) filterbank = FilterBank(fs=fs, channels=4, f_min=100, f_max=800, order=2) @@ -27,7 +27,7 @@ class FilterBank(ABC): :hide: >>> import numpy as np - >>> from spikify.filtering.filterbank import FilterBank + >>> from spikify.filters.filterbank import FilterBank >>> fs = 1000 >>> signal = np.random.randn(1000) >>> filterbank = FilterBank(fs=fs, channels=4, f_min=100, f_max=800, order=2) @@ -61,7 +61,7 @@ def __init__( f_max: float, order: int, filter_type: Literal["butterworth", "gammatone", "sos"] = "butterworth", - **kwargs + **kwargs, ): """Constructor method.""" super().__init__() @@ -76,11 +76,6 @@ def __init__( ) self.freq_poles[-1, 1] = self.fs / 2 * 0.99999 - # Validate inputs - if self.filter_type not in ["butterworth", "gammatone", "sos"]: - raise ValueError("filter_type must be 'butterworth', 'gammatone', or 'sos'") - - # Build filter coefficients self._build_filters(**kwargs) def _build_filters(self, **kwargs): @@ -92,28 +87,36 @@ def _build_filters(self, **kwargs): :param kwargs: Additional filter parameters for specific filter types. :type kwargs: dict + :raises ValueError: If filter type is not supported. """ - self.filters = [] + self.filter_coeffs = [] self.channel_frequencies = [] - if self.filter_type == "butterworth": - for low_freq, high_freq in self.freq_poles: - num, den = butter(N=self.order, Wn=[low_freq, high_freq], btype="band", fs=self.fs) - self.filters.append((num, den)) - self.channel_frequencies.append((low_freq, high_freq)) + match self.filter_type: + + case "butterworth": + for low_freq, high_freq in self.freq_poles: + num, den = butter(N=self.order, Wn=[low_freq, high_freq], btype="band", fs=self.fs, **kwargs) + self.filter_coeffs.append((num, den)) + self.channel_frequencies.append((low_freq, high_freq)) + + case "gammatone": + for freq in self.freq_centers: + num, den = gammatone(order=self.order, freq=freq, ftype="fir", fs=self.fs, **kwargs) + self.filter_coeffs.append((num, den)) + self.channel_frequencies.append(freq) - elif self.filter_type == "gammatone": - for freq in self.freq_centers: - num, den = gammatone(order=self.order, freq=freq, ftype="fir", fs=self.fs) - self.filters.append((num, den)) - self.channel_frequencies.append(freq) + case "sos": + for low_freq, high_freq in self.freq_poles: + sos = butter( + N=self.order, Wn=[low_freq, high_freq], btype="band", output="sos", fs=self.fs, **kwargs + ) + self.filter_coeffs.append(sos) + self.channel_frequencies.append((low_freq, high_freq)) - elif self.filter_type == "sos": - for low_freq, high_freq in self.freq_poles: - sos = butter(N=self.order, Wn=[low_freq, high_freq], btype="band", output="sos", fs=self.fs) - self.filters.append(sos) - self.channel_frequencies.append((low_freq, high_freq)) + case _: + raise ValueError(f"Filter {self.filter_type} is not supported") def decompose(self, signal: np.ndarray) -> np.ndarray: """ @@ -127,30 +130,28 @@ def decompose(self, signal: np.ndarray) -> np.ndarray: :type signal: numpy.ndarray :return: Array of filtered signals with shape (timestamps, channels, features). :rtype: numpy.ndarray - :raises ValueError: If signal is not 1D or 2D. """ - if len(signal.shape) == 1: + # Ensure 2D processing (T, F) + if signal.ndim == 1: signal = signal.reshape(-1, 1) - elif len(signal.shape) != 2: - raise ValueError("Signal must be 1D or 2D array") - n_timestamps, n_features = signal.shape - n_channels = len(self.filters) + T, F = signal.shape + n_channels = len(self.filter_coeffs) # Initialize output - freq_components = np.zeros((n_timestamps, n_channels, n_features)) + freq_components = np.zeros((T, n_channels, F)) for ch in range(n_channels): - filter_coeffs = self.filters[ch] + filter_coeffs = self.filter_coeffs[ch] if self.filter_type == "sos": # Use sosfilt for second-order sections - for feat in range(n_features): + for feat in range(F): freq_components[:, ch, feat] = sosfilt(filter_coeffs, signal[:, feat]) else: # Use lfilter for b,a coefficients - for feat in range(n_features): + for feat in range(F): num, den = filter_coeffs freq_components[:, ch, feat] = lfilter(num, den, signal[:, feat]) diff --git a/spikify/version.py b/spikify/version.py index 493f741..5becc17 100644 --- a/spikify/version.py +++ b/spikify/version.py @@ -1 +1 @@ -__version__ = "0.3.0" +__version__ = "1.0.0" diff --git a/tests/encoding/__init__.py b/tests/encoders/__init__.py similarity index 100% rename from tests/encoding/__init__.py rename to tests/encoders/__init__.py diff --git a/tests/encoding/rate/__init__.py b/tests/encoders/rate/__init__.py similarity index 100% rename from tests/encoding/rate/__init__.py rename to tests/encoders/rate/__init__.py diff --git a/tests/encoding/rate/test_poisson_rate.py b/tests/encoders/rate/test_poisson.py similarity index 81% rename from tests/encoding/rate/test_poisson_rate.py rename to tests/encoders/rate/test_poisson.py index 0c9b1aa..6320575 100644 --- a/tests/encoding/rate/test_poisson_rate.py +++ b/tests/encoders/rate/test_poisson.py @@ -1,16 +1,16 @@ import unittest import numpy as np -from spikify.encoding.rate.poisson_rate_algorithm import poisson_rate +from spikify.encoders.rate.poisson_algorithm import poisson class TestPoissonRateEncoding(unittest.TestCase): - """Tests for the poisson_rate function.""" + """Tests for the poisson function.""" def test_basic_functionality(self): """Ensure the function correctly encodes a simple signal.""" signal = np.array([0, 0.5, 1, 1.5, 2]) interval = 5 - result = poisson_rate(signal, interval) + result = poisson(signal, interval) self.assertEqual(len(result), len(signal)) self.assertTrue(np.all((result == 0) | (result == 1))) # All values should be 0 or 1 @@ -19,20 +19,20 @@ def test_empty_signal(self): signal = np.array([]) interval = 3 with self.assertRaises(ValueError): - poisson_rate(signal, interval) + poisson(signal, interval) def test_invalid_interval(self): """Ensure the function raises an error if the interval is not a multiple of the signal length.""" signal = np.array([0, 1, 2, 3, 4]) interval = 3 with self.assertRaises(ValueError): - poisson_rate(signal, interval) + poisson(signal, interval) def test_signal_normalization(self): """Ensure the function normalizes the signal correctly.""" - signal = np.array([0, 1, 2, 3, 4]) + signal = np.array([0, 1, 2, 3, 4], dtype=np.float32) interval = 5 - result = poisson_rate(signal, interval) + result = poisson(signal, interval) self.assertTrue(np.all(result <= 1)) def test_poisson_spike_generation(self): @@ -40,7 +40,7 @@ def test_poisson_spike_generation(self): np.random.rand(42) signal = np.random.rand(100) interval = 5 - result = poisson_rate(signal, interval) + result = poisson(signal, interval) self.assertEqual(len(result), len(signal)) for i in range(0, len(result), interval): spike_count = np.sum(result[i : i + interval]) @@ -50,7 +50,7 @@ def test_large_signal(self): """Test the function's performance on a large signal.""" signal = np.random.rand(1000) interval = 10 - result = poisson_rate(signal, interval) + result = poisson(signal, interval) self.assertEqual(len(result), interval * (len(signal) // interval)) self.assertTrue(np.all((result == 0) | (result == 1))) @@ -59,7 +59,7 @@ def test_all_zero_signal(self): signal = np.array([0, 0, 0, 0]) interval = 4 expected_output = np.zeros(interval) - result = poisson_rate(signal, interval) + result = poisson(signal, interval) np.testing.assert_array_equal(result, expected_output) def test_non_numeric_input(self): @@ -67,14 +67,14 @@ def test_non_numeric_input(self): signal = np.array(["a", "b", "c"]) interval = 2 with self.assertRaises(ValueError): - poisson_rate(signal, interval) + poisson(signal, interval) def test_shape_with_multiple_features(self): """Test the function with a signal containing multiple features.""" np.random.seed(42) signal = np.random.rand(10, 2) interval = 2 - encoded_signal = poisson_rate(signal, interval) + encoded_signal = poisson(signal, interval) self.assertEqual(encoded_signal.shape, signal.shape) diff --git a/tests/encoding/temporal/contrast/test_moving_window.py b/tests/encoders/temporal/contrast/test_moving_window.py similarity index 53% rename from tests/encoding/temporal/contrast/test_moving_window.py rename to tests/encoders/temporal/contrast/test_moving_window.py index 1ca68fd..2e8476a 100644 --- a/tests/encoding/temporal/contrast/test_moving_window.py +++ b/tests/encoders/temporal/contrast/test_moving_window.py @@ -1,6 +1,6 @@ import unittest import numpy as np -from spikify.encoding.temporal.contrast.moving_window_algorithm import moving_window +from spikify.encoders.temporal.contrast.moving_window_algorithm import moving_window class TestMovingWindow(unittest.TestCase): @@ -10,46 +10,51 @@ def test_basic_functionality(self): """Ensure the function correctly generates spikes using a moving window.""" signal = np.array([0, 1, 2, 3, 4, 3, 2, 1, 0]) window_length = 3 - expected_spikes = np.array([0, 0, 0, 1, 1, 0, -1, -1, -1]) - result = moving_window(signal, window_length) + threshold = 0.5 + expected_spikes = np.array([-1, 0, 1, 1, 1, 0, -1, -1, -1]) + result = moving_window(signal, window_length, threshold) np.testing.assert_array_equal(result, expected_spikes) def test_empty_signal(self): """Test the function with an empty signal.""" signal = np.array([]) window_length = 3 + threshold = 0.5 with self.assertRaises(ValueError): - moving_window(signal, window_length) + moving_window(signal, window_length, threshold) def test_window_length_greater_than_signal(self): - """Ensure the function works when the window length is greater than the signal length.""" + """Test the function when the window length is greater than the signal length.""" signal = np.array([1, 2, 3]) window_length = 5 - expected_spikes = np.array([0, 0, 0]) - result = moving_window(signal, window_length) - np.testing.assert_array_equal(result, expected_spikes) + threshold = 0.5 + with self.assertRaises(IndexError): + moving_window(signal, window_length, threshold) def test_all_zero_signal(self): """Test the function with a signal that is all zeros.""" signal = np.array([0, 0, 0, 0, 0]) window_length = 3 + threshold = 0.5 expected_spikes = np.array([0, 0, 0, 0, 0]) - result = moving_window(signal, window_length) + result = moving_window(signal, window_length, threshold) np.testing.assert_array_equal(result, expected_spikes) def test_large_signal(self): """Test the function's performance and correctness on a large signal.""" signal = np.random.randn(1000) window_length = 50 - result = moving_window(signal, window_length) + threshold = 0.3 + result = moving_window(signal, window_length, threshold) self.assertEqual(len(result), len(signal)) def test_no_spikes(self): """Test the function with a signal that should produce no spikes.""" signal = np.array([1, 1, 1, 1, 1]) window_length = 3 + threshold = 5 expected_spikes = np.array([0, 0, 0, 0, 0]) - result = moving_window(signal, window_length) + result = moving_window(signal, window_length, threshold) np.testing.assert_array_equal(result, expected_spikes) def test_variable_window_length(self): @@ -58,26 +63,45 @@ def test_variable_window_length(self): # Test case for window length of 3 window_length_3 = 3 - expected_spikes_3 = np.array([0, 0, 0, 1, 1, 1, 1, 0, -1, -1, -1, -1, -1]) - result_3 = moving_window(signal, window_length_3) + threshold_3 = 0.5 + expected_spikes_3 = np.array([-1, 0, 1, 1, 1, 1, 1, 0, -1, -1, -1, -1, -1]) + result_3 = moving_window(signal, window_length_3, threshold_3) np.testing.assert_array_equal(result_3, expected_spikes_3) # Test case for window length of 5 window_length_5 = 5 - expected_spikes_5 = np.array([-1, 0, 0, 0, 1, 1, 1, 0, 0, -1, -1, -1, -1]) - result_5 = moving_window(signal, window_length_5) + threshold_5 = 0.5 + expected_spikes_5 = np.array([-1, -1, 0, 1, 1, 1, 1, 1, -1, -1, -1, -1, -1]) + result_5 = moving_window(signal, window_length_5, threshold_5) np.testing.assert_array_equal(result_5, expected_spikes_5) def test_with_multiple_features(self): """Test the function with a signal containing multiple features.""" np.random.seed(42) signal = np.random.rand(10, 2) - interval = 2 - encoded_signal = moving_window(signal, interval) + window_length = 2 + encoded_signal = moving_window(signal, window_length, threshold=0.1) self.assertEqual(encoded_signal.shape, signal.shape) signal_f1 = signal[:, 0] signal_f2 = signal[:, 1] - encoded_signal_f1 = moving_window(signal_f1, interval) - encoded_signal_f2 = moving_window(signal_f2, interval) + encoded_signal_f1 = moving_window(signal_f1, window_length, threshold=0.1) + encoded_signal_f2 = moving_window(signal_f2, window_length, threshold=0.1) np.testing.assert_array_equal(encoded_signal[:, 0], encoded_signal_f1) np.testing.assert_array_equal(encoded_signal[:, 1], encoded_signal_f2) + + def test_threshold_dimension(self): + """Test that the function raises TypeError when threshold is of invalid dimension.""" + signal = np.random.rand(10, 2) + window_length = 2 + threshold = np.array([[0.1, 0.2], [0.3, 0.4]]) + with self.assertRaises(TypeError): + moving_window(signal, window_length, threshold) + + def test_threshold_dims_different_from_features(self): + """Test the function with a signal containing multiple features.""" + np.random.seed(42) + signal = np.random.rand(10, 2) + window_length = 2 + threshold = [0.1, 0.3, 0.4] + with self.assertRaises(ValueError): + moving_window(signal, window_length, threshold) diff --git a/tests/encoding/temporal/contrast/test_step_forward.py b/tests/encoders/temporal/contrast/test_step_forward.py similarity index 85% rename from tests/encoding/temporal/contrast/test_step_forward.py rename to tests/encoders/temporal/contrast/test_step_forward.py index 2de98a0..7e81030 100644 --- a/tests/encoding/temporal/contrast/test_step_forward.py +++ b/tests/encoders/temporal/contrast/test_step_forward.py @@ -1,6 +1,6 @@ import unittest import numpy as np -from spikify.encoding.temporal.contrast.step_forward_algorithm import step_forward +from spikify.encoders.temporal.contrast.step_forward_algorithm import step_forward class TestStepForward(unittest.TestCase): @@ -8,7 +8,7 @@ class TestStepForward(unittest.TestCase): def test_basic_functionality(self): """Ensure the function correctly generates spikes based on the threshold logic.""" - signal = np.array([0, 2, 4, 6, 4, 2, 0]) + signal = np.array([0, 2, 4, 6, 4, 2, 0], dtype=float) threshold = 2.0 expected_spikes = np.array([0, 0, 1, 1, 0, 0, -1]) result = step_forward(signal, threshold) @@ -31,7 +31,7 @@ def test_no_spikes(self): def test_single_positive_spike(self): """Test the function’s ability to detect a single positive spike.""" - signal = np.array([0, 0, 5, 0, 0]) + signal = np.array([0, 0, 5, 0, 0], dtype=float) threshold = 4.0 expected_spikes = np.array([0, 0, 1, 0, 0]) result = step_forward(signal, threshold) @@ -39,7 +39,7 @@ def test_single_positive_spike(self): def test_single_negative_spike(self): """Test the function’s ability to detect a single negative spike.""" - signal = np.array([0, 0, -5, 0, 0]) + signal = np.array([0, 0, -5, 0, 0], dtype=float) threshold = 4.0 expected_spikes = np.array([0, 0, -1, 0, 0]) result = step_forward(signal, threshold) @@ -47,7 +47,7 @@ def test_single_negative_spike(self): def test_alternating_spikes(self): """Test the function with alternating spikes.""" - signal = np.array([0, 5, 0, -5, 0]) + signal = np.array([0, 5, 0, -5, 0], dtype=float) threshold = 4.0 expected_spikes = np.array([0, 1, 0, -1, 0]) result = step_forward(signal, threshold) @@ -70,7 +70,7 @@ def test_large_signal(self): def test_boundary_conditions(self): """Test how the function handles boundary conditions where spikes occur at the start or end of the signal.""" - signal = np.array([10, 0, -10]) + signal = np.array([10, 0, -10], dtype=float) threshold = 5.0 expected_spikes = np.array([0, -1, -1]) result = step_forward(signal, threshold) @@ -80,27 +80,30 @@ def test_with_multiple_features(self): """Test the function with a signal containing multiple features.""" np.random.seed(42) signal = np.random.rand(10, 2) + signal_f1 = signal[:, 0] + signal_f2 = signal[:, 1] + threshold = [0.1, 0.3] encoded_signal = step_forward(signal, threshold) self.assertEqual(encoded_signal.shape, signal.shape) - signal_f1 = signal[:, 0] - signal_f2 = signal[:, 1] + encoded_signal_f1 = step_forward(signal_f1, threshold[0]) encoded_signal_f2 = step_forward(signal_f2, threshold[1]) + np.testing.assert_array_equal(encoded_signal[:, 0], encoded_signal_f1) np.testing.assert_array_equal(encoded_signal[:, 1], encoded_signal_f2) - def test_features_list(self): - """Test the function with a integer thrshold.""" - signal = np.array([0, 1, 2, 3, 2, 1, 0]) - thresholds = 1 - with self.assertRaises(TypeError): - step_forward(signal, thresholds) - - def test_thrsholds_different_from_features(self): + def test_threshold_dims_different_from_features(self): """Test the function with a signal containing multiple features.""" np.random.seed(42) signal = np.random.rand(10, 2) threshold = [0.1, 0.3, 0.4] with self.assertRaises(ValueError): step_forward(signal, threshold) + + def test_threshold_dimension(self): + """Test that the function raises TypeError when threshold is of invalid dimension.""" + signal = np.random.rand(10, 2) + threshold = np.array([[0.1, 0.2], [0.3, 0.4]]) + with self.assertRaises(TypeError): + step_forward(signal, threshold) diff --git a/tests/encoding/temporal/contrast/test_threshold_based.py b/tests/encoders/temporal/contrast/test_threshold_based.py similarity index 69% rename from tests/encoding/temporal/contrast/test_threshold_based.py rename to tests/encoders/temporal/contrast/test_threshold_based.py index d853bef..7a2d2ec 100644 --- a/tests/encoding/temporal/contrast/test_threshold_based.py +++ b/tests/encoders/temporal/contrast/test_threshold_based.py @@ -1,6 +1,6 @@ import unittest import numpy as np -from spikify.encoding.temporal.contrast.threshold_based_algorithm import threshold_based_representation +from spikify.encoders.temporal.contrast.threshold_based_algorithm import threshold_based_representation class TestThresholdBasedRepresentation(unittest.TestCase): @@ -10,8 +10,8 @@ def test_basic_functionality(self): """Test the function with a basic signal where spikes should be generated based on threshold logic.""" signal = np.array([0, 1, 2, 3, 2, 1, 0]) factor = 1.0 - expected_spikes = np.array([0, 0, 0, 0, 0, 0, 0]) - result = threshold_based_representation(signal, factor) + expected_spikes = np.array([1, 1, 1, -1, -1, -1, -1]) + result, _ = threshold_based_representation(signal, factor) np.testing.assert_array_equal(result, expected_spikes) def test_empty_signal(self): @@ -26,7 +26,7 @@ def test_no_variation(self): signal = np.array([1, 1, 1, 1, 1]) factor = 1.0 expected_spikes = np.array([0, 0, 0, 0, 0]) - result = threshold_based_representation(signal, factor) + result, _ = threshold_based_representation(signal, factor) np.testing.assert_array_equal(result, expected_spikes) def test_signal_with_noise(self): @@ -34,14 +34,14 @@ def test_signal_with_noise(self): np.random.seed(0) signal = np.array([0, 1, 2, 3, 2, 1, 0]) + np.random.randn(7) * 0.1 factor = 0.5 - result = threshold_based_representation(signal, factor) + result, _ = threshold_based_representation(signal, factor) self.assertTrue(np.any(result)) # Expect some spikes def test_large_signal(self): """Test the function's performance and correctness on a large signal.""" signal = np.random.randn(1000) * 10 # Random signal with mean 0 and standard deviation 10 factor = 1.0 - result = threshold_based_representation(signal, factor) + result, _ = threshold_based_representation(signal, factor) self.assertEqual(len(result), len(signal)) def test_varying_factor(self): @@ -51,13 +51,13 @@ def test_varying_factor(self): # Higher threshold factor, less sensitivity factor_high = 2.0 expected_spikes_high = np.array([0, 0, 0, 0, 0, 0, 0]) - result_high = threshold_based_representation(signal, factor_high) + result_high, _ = threshold_based_representation(signal, factor_high) np.testing.assert_array_equal(result_high, expected_spikes_high) # Lower threshold factor, more sensitivity factor_low = 0.1 - expected_spikes_low = np.array([1, 1, 1, 1, -1, -1, -1]) - result_low = threshold_based_representation(signal, factor_low) + expected_spikes_low = np.array([1, 1, 1, -1, -1, -1, -1]) + result_low, _ = threshold_based_representation(signal, factor_low) np.testing.assert_array_equal(result_low, expected_spikes_low) def test_with_multiple_features(self): @@ -65,32 +65,33 @@ def test_with_multiple_features(self): np.random.seed(42) signal = np.random.rand(10, 2) threshold = [0.5, 0.3] - encoded_signal = threshold_based_representation(signal, threshold) + encoded_signal, _ = threshold_based_representation(signal, threshold) self.assertEqual(encoded_signal.shape, signal.shape) signal_f1 = signal[:, 0] signal_f2 = signal[:, 1] - encoded_signal_f1 = threshold_based_representation(signal_f1, threshold[0]) - encoded_signal_f2 = threshold_based_representation(signal_f2, threshold[1]) + encoded_signal_f1, _ = threshold_based_representation(signal_f1, threshold[0]) + encoded_signal_f2, _ = threshold_based_representation(signal_f2, threshold[1]) np.testing.assert_array_equal(encoded_signal[:, 0], encoded_signal_f1) np.testing.assert_array_equal(encoded_signal[:, 1], encoded_signal_f2) - def test_factor_type(self): - """Test the function with an Integer factor.""" - signal = np.random.rand(10) - factor = 3 - with self.assertRaises(TypeError): - threshold_based_representation(signal, factor) - - def test_factors_type(self): - """Test the function with a list of Integer factors.""" - signal = np.random.rand(10, 2) - factors = [5, 1] - with self.assertRaises(TypeError): - threshold_based_representation(signal, factors) - def test_factors_length(self): """Test the function with a list of factors with dimension different from feature.""" signal = np.random.rand(10, 2) factors = [5.0, 1.0, 3.0] with self.assertRaises(ValueError): threshold_based_representation(signal, factors) + + def test_return_type(self): + """Test that the function returns a tuple of numpy ndarrays.""" + signal = np.random.rand(10) + factor = 1.0 + encoded_signal, thresholds = threshold_based_representation(signal, factor) + self.assertIsInstance(encoded_signal, np.ndarray) + self.assertIsInstance(thresholds, np.ndarray) + + def test_factor_dimension(self): + """Test that the function raises TypeError when factor is of invalid dimension.""" + signal = np.random.rand(10, 2) + factor = np.array([[1.0, 2.0], [3.0, 4.0]]) + with self.assertRaises(TypeError): + threshold_based_representation(signal, factor) diff --git a/tests/encoding/temporal/contrast/test_zero_cross_step_forward.py b/tests/encoders/temporal/contrast/test_zero_cross_step_forward.py similarity index 83% rename from tests/encoding/temporal/contrast/test_zero_cross_step_forward.py rename to tests/encoders/temporal/contrast/test_zero_cross_step_forward.py index a1df1ed..866c515 100644 --- a/tests/encoding/temporal/contrast/test_zero_cross_step_forward.py +++ b/tests/encoders/temporal/contrast/test_zero_cross_step_forward.py @@ -1,6 +1,6 @@ import unittest import numpy as np -from spikify.encoding.temporal.contrast.zero_cross_step_forward_algorithm import zero_cross_step_forward +from spikify.encoders.temporal.contrast.zero_cross_step_forward_algorithm import zero_cross_step_forward class TestZeroCrossStepForward(unittest.TestCase): @@ -55,7 +55,7 @@ def test_no_spikes(self): def test_large_signal(self): """Test the function's performance and correctness on a large signal.""" - signal = np.random.randint(-10, 20, size=1000) # Random signal with values between -10 and 20 + signal = np.random.randint(-10, 20, size=1000) threshold = 10.0 result = zero_cross_step_forward(signal, threshold) self.assertEqual(len(result), len(signal)) @@ -82,23 +82,17 @@ def test_shape_with_multiple_features(self): np.testing.assert_array_equal(encoded_signal[:, 0], encoded_signal_f1) np.testing.assert_array_equal(encoded_signal[:, 1], encoded_signal_f2) - def test_factor_type(self): - """Test the function with an Integer factor.""" - signal = np.random.rand(10) - threshold = 3 - with self.assertRaises(TypeError): + def test_threshold_dims_different_from_features(self): + """Test the function with a signal containing multiple features.""" + np.random.seed(42) + signal = np.random.rand(10, 2) + threshold = [0.1, 0.3, 0.4] + with self.assertRaises(ValueError): zero_cross_step_forward(signal, threshold) - def test_factors_type(self): - """Test the function with a list of Integer factors.""" + def test_threshold_dimension(self): + """Test that the function raises TypeError when threshold is of invalid dimension.""" signal = np.random.rand(10, 2) - thresholds = [5, 1] + threshold = np.array([[0.1, 0.2], [0.3, 0.4]]) with self.assertRaises(TypeError): - zero_cross_step_forward(signal, thresholds) - - def test_factors_length(self): - """Test the function with a list of factors with dimension different from feature.""" - signal = np.random.rand(10, 2) - thresholds = [5.0, 1.0, 3.0] - with self.assertRaises(ValueError): - zero_cross_step_forward(signal, thresholds) + zero_cross_step_forward(signal, threshold) diff --git a/tests/encoders/temporal/deconvolution/test_ben_spiker.py b/tests/encoders/temporal/deconvolution/test_ben_spiker.py new file mode 100644 index 0000000..efada42 --- /dev/null +++ b/tests/encoders/temporal/deconvolution/test_ben_spiker.py @@ -0,0 +1,194 @@ +from spikify.encoders.temporal.deconvolution.bens_spiker_algorithm import bens_spiker +import unittest +import numpy as np + + +class TestBenSpikerAlgorithm(unittest.TestCase): + """Tests ben_spiker function.""" + + def test_basic_functionality(self): + """Ensure the function correctly generates spikes when the signal contains patterns that match the filter window + and threshold conditions.""" + + signal = np.array([0.3, 1.5, 2.8, 3.4, 4.6, 5.2, 6.8, 3.5, 2.4, 1.3, 0], dtype=np.float32) + window_length = 5 + threshold = 3.0 + cutoff = 0.2 + expected_spikes = np.array([0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0]) + result, _, _ = bens_spiker(signal, window_length, cutoff, threshold) + np.testing.assert_array_equal(result, expected_spikes) + + def test_threshold_sensitivity(self): + """Verify that the function respects the threshold value by changing the threshold and observing the output.""" + + signal = np.array([0, 1, 2, 3, 5, 3, 2, 1, 0], dtype=np.float32) + window_length = 5 + threshold_low = 1.0 + threshold_high = 10.0 + cutoff = 0.2 + result_low, _, _ = bens_spiker(signal, window_length, cutoff, threshold_low) + print(result_low) + result_high, _, _ = bens_spiker(signal, window_length, cutoff, threshold_high) + print(result_high) + self.assertTrue(np.any(result_low)) + self.assertFalse(np.any(result_high)) + + def test_empty_signal(self): + """Test the function with an empty signal.""" + + signal = np.array([]) + window_length = 0 + cutoff = 0.2 + threshold = 1.0 + with self.assertRaises(ValueError): + bens_spiker(signal, window_length, cutoff, threshold) + + def test_filter_window_greater_than_signal_length(self): + """Ensure the function raises an appropriate error when the filter window size is greater than the signal + length.""" + + signal = np.array([0, 1, 2, 3, 4, 5]) + window_length = 7 + threshold = 1.0 + cutoff = 0.2 + with self.assertRaises(ValueError): + bens_spiker(signal, window_length, cutoff, threshold) + + def test_threshold_dims_different_from_features(self): + """Test the function with a signal containing multiple features.""" + np.random.seed(42) + signal = np.random.rand(10, 2) + window_length = 3 + cutoff = 0.2 + threshold = [0.1, 0.3, 0.4] + with self.assertRaises(ValueError): + bens_spiker(signal, window_length, cutoff, threshold) + + def test_threshold_dimension(self): + """Test that the function raises TypeError when threshold is of invalid dimension.""" + np.random.seed(42) + signal = np.random.rand(10, 2) + threshold = np.array([[0.1, 0.2], [0.3, 0.4]]) + window_length = 3 + cutoff = 0.2 + with self.assertRaises(TypeError): + bens_spiker(signal, window_length, cutoff, threshold) + + def test_no_matching_pattern(self): + """Ensure the function correctly identifies when there are no generated spikes when the signal that lacks any + pattern matching.""" + + signal = np.array([0, 0, 0, 0, 0]) + window_length = 3 + threshold = 1.0 + cutoff = 0.2 + expected_spikes = np.array([0, 0, 0, 0, 0]) + result, _, _ = bens_spiker(signal, window_length, cutoff, threshold) + np.testing.assert_array_equal(result, expected_spikes) + + def test_varying_filter_window_size(self): + """Ensure the function works correctly with different filter window sizes.""" + + # Test case for filter window size of 3 + signal = np.array([0, 1, 2, 3, 4, 5, 2, 1, 0], dtype=np.float32) + threshold = 1.0 + cutoff = 0.2 + + window_length_3 = 3 + expected_spikes_3 = np.array([0, 0, 0, 0, 0, 0, 0, 0, 0]) + result_3, _, _ = bens_spiker(signal, window_length_3, cutoff, threshold) + np.testing.assert_array_equal(result_3, expected_spikes_3) + + # Test case for filter window size of 5 + window_length_5 = 5 + expected_spikes_5 = np.array([0, 1, 0, 1, 0, 0, 0, 0, 0]) + result_5, _, _ = bens_spiker(signal, window_length_5, cutoff, threshold) + np.testing.assert_array_equal(result_5, expected_spikes_5) + + # Test case for filter window size of 7 + + window_length_7 = 7 + expected_spikes_7 = np.array([1, 0, 1, 0, 0, 0, 0, 0, 0]) + result_7, _, _ = bens_spiker(signal, window_length_7, cutoff, threshold) + np.testing.assert_array_equal(result_7, expected_spikes_7) + + def test_large_signal(self): + """Test the function’s performance and correctness on a large signal.""" + + signal = np.random.randn(10000) + window_length = 10 + threshold = 5.0 + cutoff = 0.2 + result, _, _ = bens_spiker(signal, window_length, cutoff, threshold) + self.assertEqual(len(result), len(signal)) + + def test_with_multiple_features(self): + """Test the function with a signal containing multiple features.""" + np.random.seed(42) + signal = np.random.rand(10, 2) + threshold = [0.5, 0.3] + window_length = 3 + cutoff = 0.2 + encoded_signal, _, _ = bens_spiker(signal, window_length, cutoff, threshold) + self.assertEqual(encoded_signal.shape, signal.shape) + signal_f1 = signal[:, 0] + signal_f2 = signal[:, 1] + encoded_signal_f1, _, _ = bens_spiker(signal_f1, window_length, cutoff, threshold[0]) + encoded_signal_f2, _, _ = bens_spiker(signal_f2, window_length, cutoff, threshold[1]) + np.testing.assert_array_equal(encoded_signal[:, 0], encoded_signal_f1) + np.testing.assert_array_equal(encoded_signal[:, 1], encoded_signal_f2) + + def test_list_threshold_differnt_features(self): + """Test the function with a size window length major of signal length.""" + np.random.seed(42) + signal = np.random.rand(10, 2) + threshold = [0.5, 0.3, 0.1] + cutoff = 0.2 + window_length = 3 + with self.assertRaises(ValueError): + bens_spiker(signal, window_length, cutoff, threshold, window_type="lanczos") + + def test_signal_with_negative_values(self): + """Test the function with a signal containing negative values.""" + + signal = np.array([-0.1, 0.1, 0.2, 0.3, 0.4, 0.3, 0.2, 0.1], dtype=np.float32) + window_length = 3 + cutoff = 0.4 + threshold = 0.95 + _, shift, fir_coeff = bens_spiker(signal, window_length, cutoff, threshold) + + expected_shift = np.array([-0.1], dtype=np.float32) + expected_fir_sum = 1.0 + + np.testing.assert_array_equal(shift, expected_shift) + self.assertAlmostEqual(fir_coeff.sum(), expected_fir_sum) + + def test_signal_with_positive_values_lower_than_one(self): + """Test the function with a signal containing positive values lower than one.""" + + signal = np.array([0.1, 0.3, 0.5, 0.7, 0.9, 0.6, 0.4, 0.2], dtype=np.float32) + window_length = 3 + cutoff = 0.4 + threshold = 0.95 + _, shift, fir_coeff = bens_spiker(signal, window_length, cutoff, threshold) + + expected_shift = np.array([0.0], dtype=np.float32) + expected_fir_sum = 1.0 + + np.testing.assert_array_equal(shift, expected_shift) + self.assertAlmostEqual(fir_coeff.sum(), expected_fir_sum) + + def test_signal_with_positive_values_greater_than_one(self): + """Test the function with a signal containing positive values greater than one.""" + + signal = np.array([0.1, 0.3, 0.5, 0.7, 1.9, 1.6, 1.4, 0.2], dtype=np.float32) + window_length = 3 + cutoff = 0.4 + threshold = 0.95 + _, shift, fir_coeff = bens_spiker(signal, window_length, cutoff, threshold) + + expected_shift = np.array([0.0], dtype=np.float32) + expected_fir_sum = 3.8 + + np.testing.assert_array_equal(shift, expected_shift) + self.assertAlmostEqual(fir_coeff.sum(), expected_fir_sum) diff --git a/tests/encoders/temporal/deconvolution/test_hough_spiker.py b/tests/encoders/temporal/deconvolution/test_hough_spiker.py new file mode 100644 index 0000000..b60075a --- /dev/null +++ b/tests/encoders/temporal/deconvolution/test_hough_spiker.py @@ -0,0 +1,153 @@ +from spikify.encoders.temporal.deconvolution.hough_spiker_algorithm import hough_spiker +import unittest +import numpy as np + + +class TestHoughSpikerAlgorithm(unittest.TestCase): + """Tests hough_spiker function.""" + + def test_basic_functionality(self): + """Ensure the function correctly generates spikes when the signal contains patterns that match the filter window + and threshold conditions.""" + + signal = np.array([0, 1.5, 2, 3, 4, 5, 6, 3, 2, 1, 0]) + window_length = 5 + cutoff = 0.1 + expected_spikes = np.array([0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0]) + result, _, _, _ = hough_spiker(signal, window_length, cutoff) + np.testing.assert_array_equal(result, expected_spikes) + + def test_empty_signal(self): + """Test the function with an empty signal.""" + + signal = np.array([]) + window_length = 0 + cutoff = 0.1 + with self.assertRaises(ValueError): + hough_spiker(signal, window_length, cutoff) + + def test_filter_window_greater_than_signal_length(self): + """Ensure the function raises an appropriate error when the filter window size is greater than the signal + length.""" + + signal = np.array([0, 1, 2, 3, 4, 5]) + window_length = 7 + cutoff = 0.1 + with self.assertRaises(ValueError): + hough_spiker(signal, window_length, cutoff) + + def test_no_matching_pattern(self): + """Ensure the function correctly identifies when there are no generated spikes when the signal that lacks any + pattern matching.""" + + signal = np.array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) + window_length = 5 + cutoff = 0.1 + result, _, _, _ = hough_spiker(signal, window_length, cutoff) + self.assertEqual(np.sum(result), 0) + + def test_single_point_spike(self): + """Test the function’s ability to detect a spike when the signal contains a single sharp value.""" + + signal = np.array([0, 0, 4.6, 0, 0, 0, 0, 0]) + window_length = 3 + cutoff = 0.2 + expected_spikes = np.array([0, 1, 0, 0, 0, 0, 0, 0]) + result, _, _, _ = hough_spiker(signal, window_length, cutoff) + np.testing.assert_array_equal(result, expected_spikes) + + def test_varying_filter_window_size(self): + """Ensure the function works correctly with different filter window sizes.""" + + # Test case for filter window size of 3 + signal = np.array([0.1, 1.2, 2.3, 2.4, 3.2, 3.1, 2.3, 1.2, 0.1, 0.5, 0.4]) + cutoff = 0.1 + + window_length_3 = 3 + expected_spikes_3 = np.array([0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0]) + result_3, _, _, _ = hough_spiker(signal, window_length_3, cutoff) + np.testing.assert_array_equal(result_3, expected_spikes_3) + + # Test case for filter window size of 5 + window_length_5 = 5 + expected_spikes_5 = np.array([1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0]) + result_5, _, _, _ = hough_spiker(signal, window_length_5, cutoff) + np.testing.assert_array_equal(result_5, expected_spikes_5) + + # Test case for filter window size of 7 + window_length_7 = 7 + expected_spikes_7 = np.array([1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0]) + result_7, _, _, _ = hough_spiker(signal, window_length_7, cutoff) + np.testing.assert_array_equal(result_7, expected_spikes_7) + + def test_large_signal(self): + """Test the function’s performance and correctness on a large signal.""" + + signal = np.random.randn(10000) + window_length = 10 + cutoff = 0.1 + result, _, _, _ = hough_spiker(signal, window_length, cutoff) + self.assertEqual(len(result), len(signal)) + + def test_with_multiple_features(self): + """Test the function with a signal containing multiple features.""" + np.random.seed(42) + signal = np.random.rand(10, 2) + cutoff = 0.1 + window_length = 3 + encoded_signal = hough_spiker(signal, window_length, cutoff)[0] + self.assertEqual(encoded_signal.shape, signal.shape) + signal_f1 = signal[:, 0] + signal_f2 = signal[:, 1] + encoded_signal_f1 = hough_spiker(signal_f1, window_length, cutoff)[0] + encoded_signal_f2 = hough_spiker(signal_f2, window_length, cutoff)[0] + np.testing.assert_array_equal(encoded_signal[:, 0], encoded_signal_f1) + np.testing.assert_array_equal(encoded_signal[:, 1], encoded_signal_f2) + + def test_signal_with_negative_values(self): + """Test the function with a signal containing negative values.""" + + signal = np.array([-0.1, 0.1, 0.2, 0.3, 0.4, 0.3, 0.2, 0.1], dtype=np.float32) + window_length = 3 + cutoff = 0.4 + _, shift, norm, fir_coeff = hough_spiker(signal, window_length, cutoff) + + expected_shift = np.array([-0.1], dtype=np.float32) + expected_norm = np.array([1.0], dtype=np.float32) + expected_fir_sum = 1.0 + + np.testing.assert_array_equal(shift, expected_shift) + np.testing.assert_array_equal(norm, expected_norm) + self.assertAlmostEqual(fir_coeff.sum(), expected_fir_sum) + + def test_signal_with_positive_values_lower_than_one(self): + """Test the function with a signal containing positive values lower than one.""" + + signal = np.array([0.1, 0.3, 0.5, 0.7, 0.9, 0.6, 0.4, 0.2], dtype=np.float32) + window_length = 3 + cutoff = 0.4 + _, shift, norm, fir_coeff = hough_spiker(signal, window_length, cutoff) + + expected_shift = np.array([0.0], dtype=np.float32) + expected_norm = np.array([1.0], dtype=np.float32) + expected_fir_sum = 1.0 + + np.testing.assert_array_equal(shift, expected_shift) + np.testing.assert_array_equal(norm, expected_norm) + self.assertAlmostEqual(fir_coeff.sum(), expected_fir_sum) + + def test_signal_with_positive_values_greater_than_one(self): + """Test the function with a signal containing positive values greater than one.""" + + signal = np.array([0.1, 0.3, 0.5, 0.7, 1.9, 1.6, 1.4, 0.2], dtype=np.float32) + window_length = 3 + cutoff = 0.4 + _, shift, norm, fir_coeff = hough_spiker(signal, window_length, cutoff) + + expected_shift = np.array([0.0], dtype=np.float32) + expected_norm = np.array([1.9], dtype=np.float32) + expected_fir_sum = 1.0 + + np.testing.assert_array_equal(shift, expected_shift) + np.testing.assert_array_equal(norm, expected_norm) + self.assertAlmostEqual(fir_coeff.sum(), expected_fir_sum) diff --git a/tests/encoders/temporal/deconvolution/test_modified_hough_spiker.py b/tests/encoders/temporal/deconvolution/test_modified_hough_spiker.py new file mode 100644 index 0000000..1f88642 --- /dev/null +++ b/tests/encoders/temporal/deconvolution/test_modified_hough_spiker.py @@ -0,0 +1,184 @@ +from spikify.encoders.temporal.deconvolution.modified_hough_spiker_algorithm import modified_hough_spiker +import unittest +import numpy as np + + +class TestModifiedHoughSpikerAlgorithm(unittest.TestCase): + """Tests modified_hough_spiker function.""" + + def test_basic_functionality(self): + """Ensure the function correctly generates spikes when the signal contains patterns that match the filter window + and threshold conditions.""" + + signal = np.array([0, 1.5, 2, 3, 4, 5, 6, 3, 2, 1, 0]) + window_length = 3 + threshold = 0.3 + cutoff = 0.2 + expected_spikes = np.array([0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1]) + result, _, _, _ = modified_hough_spiker(signal, window_length, cutoff, threshold) + np.testing.assert_array_equal(result, expected_spikes) + + def test_empty_signal(self): + """Test the function with an empty signal.""" + + signal = np.array([]) + window_length = 0 + cutoff = 0.2 + threshold = 1.0 + with self.assertRaises(ValueError): + modified_hough_spiker(signal, window_length, cutoff, threshold) + + def test_filter_window_greater_than_signal_length(self): + """Ensure the function raises an appropriate error when the filter window size is greater than the signal + length.""" + + signal = np.array([0, 1, 2, 3, 4, 5]) + window_length = 7 + cutoff = 0.2 + threshold = 1.0 + with self.assertRaises(ValueError): + modified_hough_spiker(signal, window_length, cutoff, threshold) + + def test_no_matching_pattern(self): + """Ensure the function correctly identifies when there are no generated spikes when the signal that lacks any + pattern matching.""" + + signal = np.array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) + window_length = 5 + cutoff = 0.2 + threshold = -0.1 + expected_spikes = np.array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) + result, _, _, _ = modified_hough_spiker(signal, window_length, cutoff, threshold) + np.testing.assert_array_equal(result, expected_spikes) + + def test_single_point_spike(self): + """Test the function’s ability to detect a spike when the signal contains a single sharp value.""" + + signal = np.array([0, 0, 5, 0, 0]) + window_length = 1 + cutoff = 0.1 + threshold = 0.1 + expected_spikes = np.array([0, 0, 1, 0, 0]) + result, _, _, _ = modified_hough_spiker(signal, window_length, cutoff, threshold) + np.testing.assert_array_equal(result, expected_spikes) + + def test_varying_filter_window_size(self): + """Ensure the function works correctly with different filter window sizes.""" + + # Test case for filter window size of 3 + signal = np.array([0.1, 1.2, 2.3, 2.4, 3.2, 3.1, 2.3, 1.2, 0.1, 0.5, 0.4]) + cutoff = 0.1 + threshold = 0.1 + window_length_3 = 3 + expected_spikes_3 = np.array([0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1]) + result_3, _, _, _ = modified_hough_spiker(signal, window_length_3, cutoff, threshold) + np.testing.assert_array_equal(result_3, expected_spikes_3) + + # Test case for filter window size of 5 + window_length_5 = 5 + expected_spikes_5 = np.array([1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1]) + result_5, _, _, _ = modified_hough_spiker(signal, window_length_5, cutoff, threshold) + np.testing.assert_array_equal(result_5, expected_spikes_5) + + # Test case for filter window size of 7 + window_length_7 = 7 + expected_spikes_7 = np.array([1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1]) + result_7, _, _, _ = modified_hough_spiker(signal, window_length_7, cutoff, threshold) + np.testing.assert_array_equal(result_7, expected_spikes_7) + + def test_large_signal(self): + """Test the function’s performance and correctness on a large signal.""" + + signal = np.random.randn(10000) + window_length = 10 + cutoff = 0.1 + threshold = 1.0 + result, _, _, _ = modified_hough_spiker(signal, window_length, cutoff, threshold) + self.assertEqual(len(result), len(signal)) + + def test_with_multiple_features(self): + """Test the function with a signal containing multiple features.""" + np.random.seed(42) + signal = np.random.rand(10, 2) + window_length = 3 + cutoff = 0.1 + threshold = [3.0, 5.0] + encoded_signal, _, _, _ = modified_hough_spiker(signal, window_length, cutoff, threshold) + self.assertEqual(encoded_signal.shape, signal.shape) + signal_f1 = signal[:, 0] + signal_f2 = signal[:, 1] + encoded_signal_f1, _, _, _ = modified_hough_spiker(signal_f1, window_length, cutoff, threshold[0]) + encoded_signal_f2, _, _, _ = modified_hough_spiker(signal_f2, window_length, cutoff, threshold[1]) + np.testing.assert_array_equal(encoded_signal[:, 0], encoded_signal_f1) + np.testing.assert_array_equal(encoded_signal[:, 1], encoded_signal_f2) + + def test_threshold_dims_different_from_features(self): + """Test the function with a signal containing multiple features.""" + np.random.seed(42) + signal = np.random.rand(10, 2) + window_length = 3 + cutoff = 0.2 + threshold = [0.1, 0.3, 0.4] + with self.assertRaises(ValueError): + modified_hough_spiker(signal, window_length, cutoff, threshold) + + def test_threshold_dimension(self): + """Test that the function raises TypeError when threshold is of invalid dimension.""" + np.random.seed(42) + signal = np.random.rand(10, 2) + threshold = np.array([[0.1, 0.2], [0.3, 0.4]]) + window_length = 3 + cutoff = 0.2 + with self.assertRaises(TypeError): + modified_hough_spiker(signal, window_length, cutoff, threshold) + + def test_signal_with_negative_values(self): + """Test the function with a signal containing negative values.""" + + signal = np.array([-0.1, 0.1, 0.2, 0.3, 0.4, 0.3, 0.2, 0.1], dtype=np.float32) + window_length = 3 + cutoff = 0.4 + threshold = 0.1 + _, shift, norm, fir_coeff = modified_hough_spiker(signal, window_length, cutoff, threshold) + + expected_shift = np.array([-0.1], dtype=np.float32) + expected_norm = np.array([1.0], dtype=np.float32) + expected_fir_sum = 1.0 + + np.testing.assert_array_equal(shift, expected_shift) + np.testing.assert_array_equal(norm, expected_norm) + self.assertAlmostEqual(fir_coeff.sum(), expected_fir_sum) + + def test_signal_with_positive_values_lower_than_one(self): + """Test the function with a signal containing positive values lower than one.""" + + signal = np.array([0.1, 0.3, 0.5, 0.7, 0.9, 0.6, 0.4, 0.2], dtype=np.float32) + window_length = 3 + cutoff = 0.4 + threshold = 0.1 + _, shift, norm, fir_coeff = modified_hough_spiker(signal, window_length, cutoff, threshold) + + expected_shift = np.array([0.0], dtype=np.float32) + expected_norm = np.array([1.0], dtype=np.float32) + expected_fir_sum = 1.0 + + np.testing.assert_array_equal(shift, expected_shift) + np.testing.assert_array_equal(norm, expected_norm) + self.assertAlmostEqual(fir_coeff.sum(), expected_fir_sum) + + def test_signal_with_positive_values_greater_than_one(self): + """Test the function with a signal containing positive values greater than one.""" + + signal = np.array([0.1, 0.3, 0.5, 0.7, 1.9, 1.6, 1.4, 0.2], dtype=np.float32) + window_length = 3 + cutoff = 0.4 + threshold = 0.1 + _, shift, norm, fir_coeff = modified_hough_spiker(signal, window_length, cutoff, threshold) + + expected_shift = np.array([0.0], dtype=np.float32) + expected_norm = np.array([1.9], dtype=np.float32) + expected_fir_sum = 1.0 + + np.testing.assert_array_equal(shift, expected_shift) + np.testing.assert_array_equal(norm, expected_norm) + self.assertAlmostEqual(fir_coeff.sum(), expected_fir_sum) diff --git a/tests/encoding/temporal/global_referenced/test_phase_encoding.py b/tests/encoders/temporal/global_referenced/test_phase_encoding.py similarity index 71% rename from tests/encoding/temporal/global_referenced/test_phase_encoding.py rename to tests/encoders/temporal/global_referenced/test_phase_encoding.py index a1beb89..8c24bad 100644 --- a/tests/encoding/temporal/global_referenced/test_phase_encoding.py +++ b/tests/encoders/temporal/global_referenced/test_phase_encoding.py @@ -1,19 +1,19 @@ import unittest import numpy as np -from spikify.encoding.temporal.global_referenced.phase_encoding_algorithm import ( - phase_encoding, +from spikify.encoders.temporal.global_referenced.phase_encoding_algorithm import ( + phase, ) class TestPhaseEncoding(unittest.TestCase): - """Tests phase_encoding function.""" + """Tests phase function.""" def test_basic_functionality(self): """Test the function's ability to encode a basic signal into phase encoding.""" - signal = np.array([0, 1, 2, 3, 4, 5, 6, 3, 2, 1]) + signal = np.array([0, 1, 2, 3, 4, 5, 6, 3, 2, 1], dtype=np.float32) num_bits = 2 - result = phase_encoding(signal, num_bits) + result = phase(signal, num_bits) self.assertEqual(len(result), len(signal)) def test_empty_signal(self): @@ -22,7 +22,7 @@ def test_empty_signal(self): signal = np.array([]) num_bits = 3 with self.assertRaises(ValueError): - phase_encoding(signal, num_bits) + phase(signal, num_bits) def test_non_multiple_signal_length(self): """Test that a signal with length not a multiple of num_bits raises a ValueError.""" @@ -30,22 +30,22 @@ def test_non_multiple_signal_length(self): signal = np.array([0, 1, 2, 3, 4]) num_bits = 3 with self.assertRaises(ValueError): - phase_encoding(signal, num_bits) + phase(signal, num_bits) def test_normalization(self): """Test that the signal is normalized when the maximum value is greater than 0.""" - signal = np.array([1, 2, 3, 4]) + signal = np.array([1, 2, 3, 4], dtype=np.float32) num_bits = 2 - result = phase_encoding(signal, num_bits) + result = phase(signal, num_bits) self.assertTrue(np.all(np.logical_or(result == 0, result == 1))) def test_single_value_signal(self): """Test the function with a signal that has all identical values.""" - signal = np.array([3, 3, 3, 3, 3, 3]) + signal = np.array([3, 3, 3, 3, 3, 3], dtype=np.float32) num_bits = 2 - result = phase_encoding(signal, num_bits) + result = phase(signal, num_bits) self.assertTrue(np.all(result == 1)) def test_large_signal(self): @@ -53,7 +53,7 @@ def test_large_signal(self): signal = np.random.randn(10000) num_bits = 2 - result = phase_encoding(signal, num_bits) + result = phase(signal, num_bits) self.assertEqual(len(result), len(signal)) def test_with_multiple_features(self): @@ -61,11 +61,11 @@ def test_with_multiple_features(self): np.random.seed(42) signal = np.random.rand(10, 2) num_bit = 2 - encoded_signal = phase_encoding(signal, num_bit) + encoded_signal = phase(signal, num_bit) self.assertEqual(encoded_signal.shape, signal.shape) signal_f1 = signal[:, 0] signal_f2 = signal[:, 1] - encoded_signal_f1 = phase_encoding(signal_f1, num_bit) - encoded_signal_f2 = phase_encoding(signal_f2, num_bit) + encoded_signal_f1 = phase(signal_f1, num_bit) + encoded_signal_f2 = phase(signal_f2, num_bit) np.testing.assert_array_equal(encoded_signal[:, 0], encoded_signal_f1) np.testing.assert_array_equal(encoded_signal[:, 1], encoded_signal_f2) diff --git a/tests/encoding/temporal/global_referenced/test_time_to_spike.py b/tests/encoders/temporal/global_referenced/test_time_to_first_spike.py similarity index 89% rename from tests/encoding/temporal/global_referenced/test_time_to_spike.py rename to tests/encoders/temporal/global_referenced/test_time_to_first_spike.py index 6f6ed2d..affc689 100644 --- a/tests/encoding/temporal/global_referenced/test_time_to_spike.py +++ b/tests/encoders/temporal/global_referenced/test_time_to_first_spike.py @@ -1,6 +1,6 @@ import unittest import numpy as np -from spikify.encoding.temporal.global_referenced.time_to_spike_algorithm import ( +from spikify.encoders.temporal.global_referenced.time_to_first_spike_algorithm import ( time_to_first_spike, ) @@ -11,7 +11,7 @@ class TestTimeToFirstSpike(unittest.TestCase): def test_basic_functionality(self): """Test the function's ability to encode a basic signal into time-to-first-spike encoding.""" - signal = np.array([0, 1, 2, 3, 4, 5, 6, 3, 2, 1, 0, 4]) + signal = np.array([0, 1, 2, 3, 4, 5, 6, 3, 2, 1, 0, 4], dtype=np.float32) interval = 4 result = time_to_first_spike(signal, interval) self.assertEqual(len(result), len(signal)) @@ -35,7 +35,7 @@ def test_non_multiple_signal_length(self): def test_normalization(self): """Test that the signal is normalized when the maximum value is greater than 0.""" - signal = np.array([1, 2, 3, 4, 5, 7]) + signal = np.array([1, 2, 3, 4, 5, 7], np.float32) interval = 2 result = time_to_first_spike(signal, interval) self.assertTrue(np.all(np.logical_or(result == 0, result == 1))) @@ -43,7 +43,7 @@ def test_normalization(self): def test_single_value_signal(self): """Test the function with a signal that has all identical values.""" - signal = np.array([3, 3, 3, 3, 3, 3]) + signal = np.array([3, 3, 3, 3, 3, 3], dtype=np.float32) interval = 2 result = time_to_first_spike(signal, interval) expected_result = np.array([1, 0, 1, 0, 1, 0]) @@ -60,7 +60,7 @@ def test_large_signal(self): def test_intensity_computation(self): """Test that the function correctly computes intensity based on the signal.""" - signal = np.array([0, 1, 2, 3, 4, 5]) + signal = np.array([0, 1, 2, 3, 4, 5], dtype=np.float32) interval = 2 result = time_to_first_spike(signal, interval) self.assertEqual(result.shape[0], len(signal)) @@ -78,7 +78,7 @@ def test_zero_signal(self): def test_boundary_conditions(self): """Test how the function handles boundary conditions where spikes are expected at the extremes of the signal.""" - signal = np.array([0, 1, 0, 1, 0, 2]) + signal = np.array([0, 1, 0, 1, 0, 2], dtype=np.float32) interval = 3 result = time_to_first_spike(signal, interval) self.assertTrue(np.any(result)) diff --git a/tests/encoding/temporal/latency/test_burst_encoding.py b/tests/encoders/temporal/latency/test_burst_coding.py similarity index 76% rename from tests/encoding/temporal/latency/test_burst_encoding.py rename to tests/encoders/temporal/latency/test_burst_coding.py index 3762a7d..bf51645 100644 --- a/tests/encoding/temporal/latency/test_burst_encoding.py +++ b/tests/encoders/temporal/latency/test_burst_coding.py @@ -1,9 +1,9 @@ import unittest import numpy as np -from spikify.encoding.temporal.latency.burst_encoding_algorithm import burst_encoding +from spikify.encoders.temporal.latency.burst_coding_algorithm import burst_coding -class TestBurstEncoding(unittest.TestCase): +class TestBurstCoding(unittest.TestCase): """Tests for the burst_encoding function.""" def test_basic_functionality(self): @@ -14,7 +14,7 @@ def test_basic_functionality(self): t_max = 5 length = 6 expected_output_length = length - result = burst_encoding(signal, n_max, t_min, t_max, length) + result = burst_coding(signal, n_max, t_min, t_max, length) self.assertEqual(len(result), expected_output_length) self.assertTrue(np.all(result <= 1)) @@ -26,7 +26,7 @@ def test_empty_signal(self): t_max = 10 length = 10 with self.assertRaises(ValueError): - burst_encoding(signal, n_max, t_min, t_max, length) + burst_coding(signal, n_max, t_min, t_max, length) def test_invalid_length_multiple(self): """Ensure the function raises an error if the length is not a multiple of the signal length.""" @@ -36,7 +36,7 @@ def test_invalid_length_multiple(self): t_max = 10 length = 5 with self.assertRaises(ValueError): - burst_encoding(signal, n_max, t_min, t_max, length) + burst_coding(signal, n_max, t_min, t_max, length) def test_signal_normalization(self): """Ensure the function normalizes the signal correctly.""" @@ -45,7 +45,7 @@ def test_signal_normalization(self): t_min = 1 t_max = 5 length = 12 - result = burst_encoding(signal, n_max, t_min, t_max, length) + result = burst_coding(signal, n_max, t_min, t_max, length) self.assertTrue(np.all(result <= 1)) def test_spike_number_calculation(self): @@ -56,8 +56,8 @@ def test_spike_number_calculation(self): t_min = 2 t_max = 6 length = 16 - result = burst_encoding(signal, n_max, t_min, t_max, length) - expected_spike_counts = 53 + result = burst_coding(signal, n_max, t_min, t_max, length) + expected_spike_counts = 35 actual_spike_counts = np.sum(result) self.assertEqual(actual_spike_counts, expected_spike_counts) @@ -69,7 +69,7 @@ def test_invalid_stream_length(self): t_max = 6 length = 10 with self.assertRaises(ValueError): - burst_encoding(signal, n_max, t_min, t_max, length) + burst_coding(signal, n_max, t_min, t_max, length) def test_insufficient_length_for_spikes(self): """Test that the function raises an error if the length is insufficient for the spikes and ISI.""" @@ -79,7 +79,7 @@ def test_insufficient_length_for_spikes(self): t_max = 5 length = 3 with self.assertRaises(ValueError): - burst_encoding(signal, n_max, t_min, t_max, length) + burst_coding(signal, n_max, t_min, t_max, length) def test_large_signal(self): """Test the function’s performance on a large signal.""" @@ -88,7 +88,7 @@ def test_large_signal(self): t_min = 1 t_max = 10 length = 100 - result = burst_encoding(signal, n_max, t_min, t_max, length) + result = burst_coding(signal, n_max, t_min, t_max, length) self.assertEqual(len(result), len(signal)) self.assertTrue(np.all(result <= 1)) @@ -100,11 +100,11 @@ def test_with_multiple_features(self): t_min = 1 t_max = 10 length = 100 - encoded_signal = burst_encoding(signal, n_max, t_min, t_max, length) + encoded_signal = burst_coding(signal, n_max, t_min, t_max, length) self.assertEqual(encoded_signal.shape, signal.shape) signal_f1 = signal[:, 0] signal_f2 = signal[:, 1] - encoded_signal_f1 = burst_encoding(signal_f1, n_max, t_min, t_max, length) - encoded_signal_f2 = burst_encoding(signal_f2, n_max, t_min, t_max, length) + encoded_signal_f1 = burst_coding(signal_f1, n_max, t_min, t_max, length) + encoded_signal_f2 = burst_coding(signal_f2, n_max, t_min, t_max, length) np.testing.assert_array_equal(encoded_signal[:, 0], encoded_signal_f1) np.testing.assert_array_equal(encoded_signal[:, 1], encoded_signal_f2) diff --git a/tests/encoding/temporal/deconvolution/test_ben_spiker.py b/tests/encoding/temporal/deconvolution/test_ben_spiker.py deleted file mode 100644 index fac2940..0000000 --- a/tests/encoding/temporal/deconvolution/test_ben_spiker.py +++ /dev/null @@ -1,229 +0,0 @@ -from spikify.encoding.temporal.deconvolution.bens_spiker_algorithm import bens_spiker -import unittest -import numpy as np - - -class TestBenSpikerAlgorithm(unittest.TestCase): - """Tests ben_spiker function.""" - - def test_basic_functionality(self): - """Ensure the function correctly generates spikes when the signal contains patterns that match the filter window - and threshold conditions.""" - - signal = np.array([0, 1.5, 2, 3, 4, 5, 6, 3, 2, 1, 0]) - window_length = 3 - threshold = 2.0 - expected_spikes = np.array([0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0]) - result = bens_spiker(signal, window_length, threshold) - np.testing.assert_array_equal(result, expected_spikes) - - def test_threshold_sensitivity(self): - """Verify that the function respects the threshold value by changing the threshold and observing the output.""" - - signal = np.array([0, 1, 2, 3, 5, 3, 2, 1, 0]) - window_length = 3 - threshold_low = 1.0 - threshold_high = 5.0 - result_low = bens_spiker(signal, window_length, threshold_low) - result_high = bens_spiker(signal, window_length, threshold_high) - self.assertTrue(np.any(result_low)) - self.assertFalse(np.any(result_high)) - - def test_empty_signal(self): - """Test the function with an empty signal.""" - - signal = np.array([]) - window_length = 0 - threshold = 1.0 - with self.assertRaises(ValueError): - bens_spiker(signal, window_length, threshold) - - def test_filter_window_greater_than_signal_length(self): - """Ensure the function raises an appropriate error when the filter window size is greater than the signal - length.""" - - signal = np.array([0, 1, 2, 3, 4, 5]) - window_length = 7 - threshold = 1.0 - with self.assertRaises(ValueError): - bens_spiker(signal, window_length, threshold) - - def test_no_matching_pattern(self): - """Ensure the function correctly identifies when there are no generated spikes when the signal that lacks any - pattern matching.""" - - signal = np.array([0, 0, 0, 0, 0]) - window_length = 3 - threshold = 1.0 - expected_spikes = np.array([0, 0, 0, 0, 0]) - result = bens_spiker(signal, window_length, threshold) - np.testing.assert_array_equal(result, expected_spikes) - - def test_single_point_spike(self): - """Test the function’s ability to detect a spike when the signal contains a single sharp value.""" - - signal = np.array([0, 0, 5, 0, 0]) - window_length = 1 - threshold = 1.0 - expected_spikes = np.array([0, 0, 1, 0, 0]) - result = bens_spiker(signal, window_length, threshold) - np.testing.assert_array_equal(result, expected_spikes) - - def test_varying_filter_window_size(self): - """Ensure the function works correctly with different filter window sizes.""" - - # Test case for filter window size of 3 - signal_3 = np.array([0, 1, 2, 3, 4, 5, 2, 1, 0]) - window_length_3 = 3 - threshold_3 = 1.0 - expected_spikes_3 = np.array([1, 1, 1, 1, 1, 1, 0, 0, 0]) - result_3 = bens_spiker(signal_3, window_length_3, threshold_3) - np.testing.assert_array_equal(result_3, expected_spikes_3) - - # Test case for filter window size of 5 - signal_5 = np.array([0, 1, 2, 3, 4, 5, 4, 3, 2, 1, 0]) - window_length_5 = 5 - threshold_5 = 1.0 - expected_spikes_5 = np.array([1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0]) - result_5 = bens_spiker(signal_5, window_length_5, threshold_5) - np.testing.assert_array_equal(result_5, expected_spikes_5) - - # Test case for filter window size of 7 - signal_7 = np.array([0, 1, 2, 3, 4, 5, 6, 5, 4, 3, 2, 1, 0]) - window_length_7 = 7 - threshold_7 = 1.0 - expected_spikes_7 = np.array([1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0]) - result_7 = bens_spiker(signal_7, window_length_7, threshold_7) - np.testing.assert_array_equal(result_7, expected_spikes_7) - - def test_boundary_conditions(self): - """Test how the function handles boundary conditions where the spikes generated occur at the beginning or end of - the signal.""" - - signal = np.array([5, 5, 5, 0, 0]) - window_length = 3 - threshold = 1.0 - expected_spikes = np.array([1, 1, 0, 0, 0]) - result = bens_spiker(signal, window_length, threshold) - np.testing.assert_array_equal(result, expected_spikes) - - def test_large_signal(self): - """Test the function’s performance and correctness on a large signal.""" - - signal = np.random.randn(10000) - window_length = 10 - threshold = 5.0 - result = bens_spiker(signal, window_length, threshold) - self.assertEqual(len(result), len(signal)) - - def test_non_numeric_input(self): - """Ensure the function raises an appropriate error when provided with non-numeric input.""" - - signal = np.array(["a", "b", "c"]) - window_length = 2 - threshold = 1.0 - with self.assertRaises(ValueError): - bens_spiker(signal, window_length, threshold) - - def test_noise(self): - """Test the function's robustness against random noise in the signal.""" - - np.random.seed(0) - signal = np.array([0, 1, 2, 3, 4, 5, 6, 3, 2, 1, 0]) + np.random.randn(11) - window_length = 3 - threshold = 1.0 - result = bens_spiker(signal, window_length, threshold) - self.assertTrue(np.any(result)) - - def test_with_multiple_features(self): - """Test the function with a signal containing multiple features.""" - np.random.seed(42) - signal = np.random.rand(10, 2) - threshold = [0.5, 0.3] - window_length = 3 - encoded_signal = bens_spiker(signal, window_length, threshold) - self.assertEqual(encoded_signal.shape, signal.shape) - signal_f1 = signal[:, 0] - signal_f2 = signal[:, 1] - encoded_signal_f1 = bens_spiker(signal_f1, window_length, threshold[0]) - encoded_signal_f2 = bens_spiker(signal_f2, window_length, threshold[1]) - np.testing.assert_array_equal(encoded_signal[:, 0], encoded_signal_f1) - np.testing.assert_array_equal(encoded_signal[:, 1], encoded_signal_f2) - - def test_with_multiple_features_mutiple_window_length(self): - """Test the function with a signal containing multiple features and multiple window length.""" - np.random.seed(42) - signal = np.random.rand(10, 2) - threshold = [0.5, 0.3] - window_length = [3, 4] - encoded_signal = bens_spiker(signal, window_length, threshold, "lanczos") - self.assertEqual(encoded_signal.shape, signal.shape) - signal_f1 = signal[:, 0] - signal_f2 = signal[:, 1] - encoded_signal_f1 = bens_spiker(signal_f1, window_length[0], threshold[0], "lanczos") - encoded_signal_f2 = bens_spiker(signal_f2, window_length[1], threshold[1], "lanczos") - np.testing.assert_array_equal(encoded_signal[:, 0], encoded_signal_f1) - np.testing.assert_array_equal(encoded_signal[:, 1], encoded_signal_f2) - - def test_list_window_length_float(self): - """Test the function with a list of of float in window length.""" - np.random.seed(42) - signal = np.random.rand(10, 2) - threshold = [0.5, 0.3] - window_length = [3, 4.0] - with self.assertRaises(TypeError): - bens_spiker(signal, window_length, threshold, "lanczos") - - def test_window_length_float(self): - """Test the function with a float in window length.""" - np.random.seed(42) - signal = np.random.rand(10) - threshold = 0.5 - window_length = 4.0 - with self.assertRaises(TypeError): - bens_spiker(signal, window_length, threshold, "lanczos") - - def test_list_window_length_len_signal(self): - """Test the function with a size window length major of signal length.""" - np.random.seed(42) - signal = np.random.rand(10, 2) - threshold = [0.5, 0.3] - window_length = [3, 20] - with self.assertRaises(ValueError): - bens_spiker(signal, window_length, threshold, "lanczos") - - def test_list_window_length_len_features(self): - """Test the function with a window length different from features.""" - np.random.seed(42) - signal = np.random.rand(10, 2) - threshold = [0.5, 0.3] - window_length = [3, 5, 7] - with self.assertRaises(ValueError): - bens_spiker(signal, window_length, threshold, "lanczos") - - def test_threshold_int(self): - """Test the function with a ineteger in threshold.""" - np.random.seed(42) - signal = np.random.rand(10) - threshold = 2 - window_length = 4.0 - with self.assertRaises(TypeError): - bens_spiker(signal, window_length, threshold, "lanczos") - - def test_list_threshold_differnt_features(self): - """Test the function with a size window length major of signal length.""" - np.random.seed(42) - signal = np.random.rand(10, 2) - threshold = [0.5, 0.3, 0.1] - window_length = [3, 20] - with self.assertRaises(ValueError): - bens_spiker(signal, window_length, threshold, "lanczos") - - def test_list_threshold_integer(self): - """Test the function with a window length different from features.""" - np.random.seed(42) - signal = np.random.rand(10, 2) - threshold = [0.5, 1] - window_length = [3, 5] - with self.assertRaises(TypeError): - bens_spiker(signal, window_length, threshold, "lanczos") diff --git a/tests/encoding/temporal/deconvolution/test_hough_spiker.py b/tests/encoding/temporal/deconvolution/test_hough_spiker.py deleted file mode 100644 index e7a3cb8..0000000 --- a/tests/encoding/temporal/deconvolution/test_hough_spiker.py +++ /dev/null @@ -1,158 +0,0 @@ -from spikify.encoding.temporal.deconvolution.hough_spiker_algorithm import hough_spiker -import unittest -import numpy as np - - -class TestHoughSpikerAlgorithm(unittest.TestCase): - """Tests hough_spiker function.""" - - def test_basic_functionality(self): - """Ensure the function correctly generates spikes when the signal contains patterns that match the filter window - and threshold conditions.""" - - signal = np.array([0, 1.5, 2, 3, 4, 5, 6, 3, 2, 1, 0]) - window_length = 3 - expected_spikes = np.array([0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0]) - result = hough_spiker(signal, window_length) - np.testing.assert_array_equal(result, expected_spikes) - - def test_empty_signal(self): - """Test the function with an empty signal.""" - - signal = np.array([]) - window_length = 0 - with self.assertRaises(ValueError): - hough_spiker(signal, window_length) - - def test_filter_window_greater_than_signal_length(self): - """Ensure the function raises an appropriate error when the filter window size is greater than the signal - length.""" - - signal = np.array([0, 1, 2, 3, 4, 5]) - window_length = 7 - with self.assertRaises(ValueError): - hough_spiker(signal, window_length) - - def test_no_matching_pattern(self): - """Ensure the function correctly identifies when there are no generated spikes when the signal that lacks any - pattern matching.""" - - signal = np.array([0, 0, 0, 0, 0]) - window_length = 3 - expected_spikes = np.array([0, 0, 0, 0, 0]) - result = hough_spiker(signal, window_length) - np.testing.assert_array_equal(result, expected_spikes) - - def test_single_point_spike(self): - """Test the function’s ability to detect a spike when the signal contains a single sharp value.""" - - signal = np.array([0, 0, 5, 0, 0]) - window_length = 1 - expected_spikes = np.array([0, 0, 1, 0, 0]) - result = hough_spiker(signal, window_length) - np.testing.assert_array_equal(result, expected_spikes) - - def test_varying_filter_window_size(self): - """Ensure the function works correctly with different filter window sizes.""" - - # Test case for filter window size of 3 - signal_3 = np.array([0, 1, 2, 3, 4, 5, 2, 1, 0]) - window_length_3 = 3 - expected_spikes_3 = np.array([0, 1, 1, 1, 1, 1, 0, 0, 0]) - result_3 = hough_spiker(signal_3, window_length_3) - np.testing.assert_array_equal(result_3, expected_spikes_3) - - # Test case for filter window size of 5 - signal_5 = np.array([0, 1, 2, 3, 4, 5, 4, 3, 2, 1, 0]) - window_length_5 = 5 - expected_spikes_5 = np.array([0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0]) - result_5 = hough_spiker(signal_5, window_length_5) - np.testing.assert_array_equal(result_5, expected_spikes_5) - - # Test case for filter window size of 7 - signal_7 = np.array([0, 1, 2, 3, 4, 5, 6, 5, 4, 3, 2, 1, 0]) - window_length_7 = 7 - expected_spikes_7 = np.array([0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0]) - result_7 = hough_spiker(signal_7, window_length_7) - np.testing.assert_array_equal(result_7, expected_spikes_7) - - def test_boundary_conditions(self): - """Test how the function handles boundary conditions where the spikes generated occur at the beginning or end of - the signal.""" - - signal = np.array([5, 5, 5, 0, 0]) - window_length = 3 - expected_spikes = np.array([1, 0, 0, 0, 0]) - result = hough_spiker(signal, window_length) - np.testing.assert_array_equal(result, expected_spikes) - - def test_large_signal(self): - """Test the function’s performance and correctness on a large signal.""" - - signal = np.random.randn(10000) - window_length = 10 - result = hough_spiker(signal, window_length) - self.assertEqual(len(result), len(signal)) - - def test_non_numeric_input(self): - """Ensure the function raises an appropriate error when provided with non-numeric input.""" - - signal = np.array(["a", "b", "c"]) - window_length = 2 - with self.assertRaises(ValueError): - hough_spiker(signal, window_length) - - def test_noise(self): - """Test the function's robustness against random noise in the signal.""" - - np.random.seed(0) - signal = np.array([0, 1, 2, 3, 4, 5, 6, 3, 2, 1, 0]) + np.random.randn(11) - window_length = 3 - result = hough_spiker(signal, window_length) - self.assertTrue(np.any(result)) - - def test_with_multiple_features(self): - """Test the function with a signal containing multiple features.""" - np.random.seed(42) - signal = np.random.rand(10, 2) - window_length = 3 - encoded_signal = hough_spiker(signal, window_length) - self.assertEqual(encoded_signal.shape, signal.shape) - signal_f1 = signal[:, 0] - signal_f2 = signal[:, 1] - encoded_signal_f1 = hough_spiker(signal_f1, window_length) - encoded_signal_f2 = hough_spiker(signal_f2, window_length) - np.testing.assert_array_equal(encoded_signal[:, 0], encoded_signal_f1) - np.testing.assert_array_equal(encoded_signal[:, 1], encoded_signal_f2) - - def test_list_window_length_float(self): - """Test the function with a list of of float in window length.""" - np.random.seed(42) - signal = np.random.rand(10, 2) - window_length = [3, 4.0] - with self.assertRaises(TypeError): - hough_spiker(signal, window_length) - - def test_window_length_float(self): - """Test the function with a float in window length.""" - np.random.seed(42) - signal = np.random.rand(10) - window_length = 4.0 - with self.assertRaises(TypeError): - hough_spiker(signal, window_length) - - def test_list_window_length_len_signal(self): - """Test the function with a size window length major of signal length.""" - np.random.seed(42) - signal = np.random.rand(10, 2) - window_length = [3, 20] - with self.assertRaises(ValueError): - hough_spiker(signal, window_length) - - def test_list_window_length_len_features(self): - """Test the function with a window length different from features.""" - np.random.seed(42) - signal = np.random.rand(10, 2) - window_length = [3, 5, 7] - with self.assertRaises(ValueError): - hough_spiker(signal, window_length) diff --git a/tests/encoding/temporal/deconvolution/test_modified_hough_spiker.py b/tests/encoding/temporal/deconvolution/test_modified_hough_spiker.py deleted file mode 100644 index 00bb3d4..0000000 --- a/tests/encoding/temporal/deconvolution/test_modified_hough_spiker.py +++ /dev/null @@ -1,231 +0,0 @@ -from spikify.encoding.temporal.deconvolution.modified_hough_spiker_algorithm import modified_hough_spiker -import unittest -import numpy as np - - -class TestModifiedHoughSpikerAlgorithm(unittest.TestCase): - """Tests modified_hough_spiker function.""" - - def test_basic_functionality(self): - """Ensure the function correctly generates spikes when the signal contains patterns that match the filter window - and threshold conditions.""" - - signal = np.array([0, 1.5, 2, 3, 4, 5, 6, 3, 2, 1, 0]) - window_length = 3 - threshold = 2.0 - expected_spikes = np.array([1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1]) - result = modified_hough_spiker(signal, window_length, threshold) - np.testing.assert_array_equal(result, expected_spikes) - - def test_empty_signal(self): - """Test the function with an empty signal.""" - - signal = np.array([]) - window_length = 0 - threshold = 1.0 - with self.assertRaises(ValueError): - modified_hough_spiker(signal, window_length, threshold) - - def test_filter_window_greater_than_signal_length(self): - """Ensure the function raises an appropriate error when the filter window size is greater than the signal - length.""" - - signal = np.array([0, 1, 2, 3, 4, 5]) - window_length = 7 - threshold = 1.0 - with self.assertRaises(ValueError): - modified_hough_spiker(signal, window_length, threshold) - - def test_no_matching_pattern(self): - """Ensure the function correctly identifies when there are no generated spikes when the signal that lacks any - pattern matching.""" - - signal = np.array([0, 0, 0, 0]) - window_length = 3 - expected_spikes = np.array( - [ - 0, - 0, - 0, - 0, - ] - ) - threshold = 0.5 - result = modified_hough_spiker(signal, window_length, threshold) - np.testing.assert_array_equal(result, expected_spikes) - - def test_single_point_spike(self): - """Test the function’s ability to detect a spike when the signal contains a single sharp value.""" - - signal = np.array([0, 0, 5, 0, 0]) - window_length = 1 - threshold = 0.1 - expected_spikes = np.array([0, 0, 1, 0, 0]) - result = modified_hough_spiker(signal, window_length, threshold) - np.testing.assert_array_equal(result, expected_spikes) - - def test_varying_filter_window_size(self): - """Ensure the function works correctly with different filter window sizes.""" - - # Test case for filter window size of 3 - signal_3 = np.array([0, 1, 2, 3, 4, 5, 2, 1, 0]) - window_length_3 = 3 - threshold = 1.0 - expected_spikes_3 = np.array([1, 1, 1, 1, 1, 1, 0, 0, 1]) - result_3 = modified_hough_spiker(signal_3, window_length_3, threshold) - np.testing.assert_array_equal(result_3, expected_spikes_3) - - # Test case for filter window size of 5 - signal_5 = np.array([0, 1, 2, 3, 4, 5, 4, 3, 2, 1, 0]) - window_length_5 = 5 - expected_spikes_5 = np.array([1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1]) - result_5 = modified_hough_spiker(signal_5, window_length_5, threshold) - np.testing.assert_array_equal(result_5, expected_spikes_5) - - # Test case for filter window size of 7 - signal_7 = np.array([0, 1, 2, 3, 4, 5, 6, 5, 4, 3, 2, 1, 0]) - window_length_7 = 7 - expected_spikes_7 = np.array([1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1]) - result_7 = modified_hough_spiker(signal_7, window_length_7, threshold) - np.testing.assert_array_equal(result_7, expected_spikes_7) - - def test_boundary_conditions(self): - """Test how the function handles boundary conditions where the spikes generated occur at the beginning or end of - the signal.""" - - signal = np.array([5, 5, 5, 0, 0]) - window_length = 3 - threshold = 1.0 - expected_spikes = np.array([1, 1, 0, 0, 1]) - result = modified_hough_spiker(signal, window_length, threshold) - np.testing.assert_array_equal(result, expected_spikes) - - def test_large_signal(self): - """Test the function’s performance and correctness on a large signal.""" - - signal = np.random.randn(10000) - window_length = 10 - threshold = 1.0 - result = modified_hough_spiker(signal, window_length, threshold) - self.assertEqual(len(result), len(signal)) - - def test_non_numeric_input(self): - """Ensure the function raises an appropriate error when provided with non-numeric input.""" - - signal = np.array(["a", "b", "c"]) - window_length = 2 - threshold = 1.0 - with self.assertRaises(ValueError): - modified_hough_spiker(signal, window_length, threshold) - - def test_noise(self): - """Test the function's robustness against random noise in the signal.""" - - np.random.seed(0) - signal = np.array([0, 1, 2, 3, 4, 5, 6, 3, 2, 1, 0]) + np.random.randn(11) - window_length = 3 - threshold = 2.0 - result = modified_hough_spiker(signal, window_length, threshold) - self.assertTrue(np.any(result)) - - def test_with_multiple_features(self): - """Test the function with a signal containing multiple features.""" - np.random.seed(42) - signal = np.random.rand(10, 2) - window_length = 3 - threshold = [3.0, 5.0] - encoded_signal = modified_hough_spiker(signal, window_length, threshold) - self.assertEqual(encoded_signal.shape, signal.shape) - signal_f1 = signal[:, 0] - signal_f2 = signal[:, 1] - encoded_signal_f1 = modified_hough_spiker(signal_f1, window_length, threshold[0]) - encoded_signal_f2 = modified_hough_spiker(signal_f2, window_length, threshold[1]) - np.testing.assert_array_equal(encoded_signal[:, 0], encoded_signal_f1) - np.testing.assert_array_equal(encoded_signal[:, 1], encoded_signal_f2) - - def test_with_multiple_features_multiple_windows(self): - """Test the function with a signal containing multiple features and multiple window length.""" - np.random.seed(42) - signal = np.random.rand(10, 2) - window_length = [3, 2] - threshold = [3.0, 5.0] - encoded_signal = modified_hough_spiker(signal, window_length, threshold) - self.assertEqual(encoded_signal.shape, signal.shape) - signal_f1 = signal[:, 0] - signal_f2 = signal[:, 1] - encoded_signal_f1 = modified_hough_spiker(signal_f1, window_length[0], threshold[0]) - encoded_signal_f2 = modified_hough_spiker(signal_f2, window_length[1], threshold[1]) - np.testing.assert_array_equal(encoded_signal[:, 0], encoded_signal_f1) - np.testing.assert_array_equal(encoded_signal[:, 1], encoded_signal_f2) - - def test_list_window_length_float(self): - """Test the function with a list of of float in window length.""" - np.random.seed(42) - signal = np.random.rand(10, 2) - window_length = [3, 4.0] - thresholds = [3.0, 5.0] - with self.assertRaises(TypeError): - modified_hough_spiker(signal, window_length, thresholds) - - def test_window_length_float(self): - """Test the function with a float in window length.""" - np.random.seed(42) - signal = np.random.rand(10) - window_length = 4.0 - thresholds = 5.0 - with self.assertRaises(TypeError): - modified_hough_spiker(signal, window_length, thresholds) - - def test_list_window_length_len_signal(self): - """Test the function with a size window length major of signal length.""" - np.random.seed(42) - signal = np.random.rand(10, 2) - window_length = [3, 20] - thresholds = [3.0, 5.0] - with self.assertRaises(ValueError): - modified_hough_spiker(signal, window_length, thresholds) - - def test_list_window_length_len_features(self): - """Test the function with a window length different from features.""" - np.random.seed(42) - signal = np.random.rand(10, 2) - window_length = [3, 5, 7] - thresholds = [3.0, 5.0] - with self.assertRaises(ValueError): - modified_hough_spiker(signal, window_length, thresholds) - - def test_list_window_lenth_float(self): - """Test the function with a list of of int in threshold.""" - np.random.seed(42) - signal = np.random.rand(10, 2) - window_length = [3, 4.0] - thresholds = [3.0, 5.0] - with self.assertRaises(TypeError): - modified_hough_spiker(signal, window_length, thresholds) - - def test_list_thresholds_int(self): - """Test the function with a list of of int in threshold.""" - np.random.seed(42) - signal = np.random.rand(10, 2) - window_length = [3, 4] - thresholds = [3.0, 5] - with self.assertRaises(TypeError): - modified_hough_spiker(signal, window_length, thresholds) - - def test_threshold_int(self): - """Test the function with a int in threshold.""" - np.random.seed(42) - signal = np.random.rand(10) - window_length = 4 - thresholds = 5 - with self.assertRaises(TypeError): - modified_hough_spiker(signal, window_length, thresholds) - - def test_list_thresholds_len_features(self): - """Test the function with a len of threshold different from features.""" - np.random.seed(42) - signal = np.random.rand(10, 2) - window_length = [3, 5] - thresholds = [3.0, 5.0, 1.0] - with self.assertRaises(ValueError): - modified_hough_spiker(signal, window_length, thresholds) diff --git a/tests/filtering/__init__.py b/tests/filters/__init__.py similarity index 100% rename from tests/filtering/__init__.py rename to tests/filters/__init__.py diff --git a/tests/filtering/test_filterbank.py b/tests/filters/test_filterbank.py similarity index 67% rename from tests/filtering/test_filterbank.py rename to tests/filters/test_filterbank.py index f2c056f..b62744e 100644 --- a/tests/filtering/test_filterbank.py +++ b/tests/filters/test_filterbank.py @@ -1,6 +1,6 @@ import unittest import numpy as np -from spikify.filtering import FilterBank +from spikify.filters import FilterBank class TestFilterBank(unittest.TestCase): @@ -77,18 +77,6 @@ def test_sos_decomposition(self): freq_components = filterbank.decompose(self.signal) self.assertEqual(freq_components.shape, (self.signal_length, self.channels, 1)) - def test_invalid_filter_type(self): - """Test that an invalid filter type raises a ValueError.""" - with self.assertRaises(ValueError): - FilterBank( - fs=self.fs, - channels=self.channels, - f_min=self.f_min, - f_max=self.f_max, - filter_type="invalid_type", - order=self.order, - ) - def test_signal_multiple_shape_decomposition(self): """Test that decomposing a too short signal raises a ValueError.""" filterbank = FilterBank( @@ -99,7 +87,7 @@ def test_signal_multiple_shape_decomposition(self): filter_type="butterworth", order=self.order, ) - signal = np.random.randn(10, 5, 3) # Invalid shape + signal = np.random.randn(10, 5, 3) with self.assertRaises(ValueError): filterbank.decompose(signal) @@ -113,11 +101,23 @@ def test_signal_with_multiple_features(self): filter_type="butterworth", order=self.order, ) - multi_feature_signal = np.random.randn(self.signal_length, 3) # 3 features + multi_feature_signal = np.random.randn(self.signal_length, 3) freq_components = filterbank.decompose(multi_feature_signal) self.assertEqual(freq_components.shape, (self.signal_length, self.channels, 3)) - def test_center_frequencies(self): + def test_unsupported_filter(self): + + with self.assertRaises(ValueError): + FilterBank( + fs=self.fs, + channels=self.channels, + f_min=self.f_min, + f_max=self.f_max, + filter_type="gammachirp", + order=self.order, + ) + + def test_center_frequencies_butterworth(self): """Test that center frequencies are computed correctly.""" filterbank = FilterBank( fs=self.fs, @@ -127,6 +127,49 @@ def test_center_frequencies(self): filter_type="butterworth", order=self.order, ) - expected_octave = (self.channels - 0.5) * np.log10(2) / np.log10(self.f_max / self.f_min) - expected_freq_centers = np.array([self.f_min * (2 ** (ch / expected_octave)) for ch in range(self.channels)]) - np.testing.assert_array_almost_equal(filterbank.freq_centers, expected_freq_centers) + + octave = (self.channels - 0.5) * np.log10(2) / np.log10(self.f_max / self.f_min) + freq_centers = np.array([self.f_min * (2 ** (ch / octave)) for ch in range(self.channels)]) + freq_poles = np.array( + [(freq * (2 ** (-1 / (2 * octave))), (freq * (2 ** (1 / (2 * octave))))) for freq in freq_centers] + ) + freq_poles[-1, 1] = self.fs / 2 * 0.99999 + + expected_freq_centers = np.array([np.mean(freqs) for freqs in freq_poles]) + np.testing.assert_array_almost_equal(filterbank.center_frequencies, expected_freq_centers) + + def test_center_frequencies_gammatone(self): + """Test that center frequencies are computed correctly.""" + filterbank = FilterBank( + fs=self.fs, + channels=self.channels, + f_min=self.f_min, + f_max=self.f_max, + filter_type="gammatone", + order=self.order, + ) + + octave = (self.channels - 0.5) * np.log10(2) / np.log10(self.f_max / self.f_min) + expected_freq_centers = np.array([self.f_min * (2 ** (ch / octave)) for ch in range(self.channels)]) + np.testing.assert_array_almost_equal(filterbank.center_frequencies, expected_freq_centers) + + def test_center_frequencies_sos(self): + """Test that center frequencies are computed correctly.""" + filterbank = FilterBank( + fs=self.fs, + channels=self.channels, + f_min=self.f_min, + f_max=self.f_max, + filter_type="sos", + order=self.order, + ) + + octave = (self.channels - 0.5) * np.log10(2) / np.log10(self.f_max / self.f_min) + freq_centers = np.array([self.f_min * (2 ** (ch / octave)) for ch in range(self.channels)]) + freq_poles = np.array( + [(freq * (2 ** (-1 / (2 * octave))), (freq * (2 ** (1 / (2 * octave))))) for freq in freq_centers] + ) + freq_poles[-1, 1] = self.fs / 2 * 0.99999 + + expected_freq_centers = np.array([np.mean(freqs) for freqs in freq_poles]) + np.testing.assert_array_almost_equal(filterbank.center_frequencies, expected_freq_centers)