csiphon 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- csiphon-0.1.0/LICENSE +21 -0
- csiphon-0.1.0/PKG-INFO +109 -0
- csiphon-0.1.0/README.md +69 -0
- csiphon-0.1.0/pyproject.toml +101 -0
- csiphon-0.1.0/setup.cfg +4 -0
- csiphon-0.1.0/src/csiphon/__init__.py +80 -0
- csiphon-0.1.0/src/csiphon/core/__init__.py +62 -0
- csiphon-0.1.0/src/csiphon/core/arrays.py +31 -0
- csiphon-0.1.0/src/csiphon/core/axes.py +162 -0
- csiphon-0.1.0/src/csiphon/core/errors.py +35 -0
- csiphon-0.1.0/src/csiphon/core/layout.py +211 -0
- csiphon-0.1.0/src/csiphon/core/profile.py +155 -0
- csiphon-0.1.0/src/csiphon/core/sampling.py +87 -0
- csiphon-0.1.0/src/csiphon/core/semantics.py +74 -0
- csiphon-0.1.0/src/csiphon/core/signal.py +158 -0
- csiphon-0.1.0/src/csiphon/graphics/__init__.py +27 -0
- csiphon-0.1.0/src/csiphon/graphics/style.py +200 -0
- csiphon-0.1.0/src/csiphon/graphics/tree.py +717 -0
- csiphon-0.1.0/src/csiphon/inspection.py +64 -0
- csiphon-0.1.0/src/csiphon/pipeline/__init__.py +49 -0
- csiphon-0.1.0/src/csiphon/pipeline/_align_ops.py +137 -0
- csiphon-0.1.0/src/csiphon/pipeline/_lines.py +11 -0
- csiphon-0.1.0/src/csiphon/pipeline/merges.py +647 -0
- csiphon-0.1.0/src/csiphon/pipeline/pipeline.py +591 -0
- csiphon-0.1.0/src/csiphon/pipeline/runners.py +267 -0
- csiphon-0.1.0/src/csiphon/pipeline/sequence.py +407 -0
- csiphon-0.1.0/src/csiphon/pipeline/step.py +222 -0
- csiphon-0.1.0/src/csiphon/py.typed +0 -0
- csiphon-0.1.0/src/csiphon/spec.py +833 -0
- csiphon-0.1.0/src/csiphon/steps/__init__.py +131 -0
- csiphon-0.1.0/src/csiphon/steps/_support/__init__.py +1 -0
- csiphon-0.1.0/src/csiphon/steps/_support/broadcasting.py +68 -0
- csiphon-0.1.0/src/csiphon/steps/_support/windowing.py +217 -0
- csiphon-0.1.0/src/csiphon/steps/baseline/__init__.py +8 -0
- csiphon-0.1.0/src/csiphon/steps/baseline/running_mean_subtract.py +110 -0
- csiphon-0.1.0/src/csiphon/steps/baseline/temporal_mean_subtract.py +59 -0
- csiphon-0.1.0/src/csiphon/steps/calibration/__init__.py +11 -0
- csiphon-0.1.0/src/csiphon/steps/calibration/axis_reference.py +198 -0
- csiphon-0.1.0/src/csiphon/steps/calibration/linear_phase_correction.py +222 -0
- csiphon-0.1.0/src/csiphon/steps/cleaning/__init__.py +8 -0
- csiphon-0.1.0/src/csiphon/steps/cleaning/nan_scrub.py +49 -0
- csiphon-0.1.0/src/csiphon/steps/cleaning/noise_floor_clip.py +54 -0
- csiphon-0.1.0/src/csiphon/steps/components/__init__.py +10 -0
- csiphon-0.1.0/src/csiphon/steps/components/magnitude.py +49 -0
- csiphon-0.1.0/src/csiphon/steps/components/phase.py +49 -0
- csiphon-0.1.0/src/csiphon/steps/components/power.py +51 -0
- csiphon-0.1.0/src/csiphon/steps/components/unit_phase.py +53 -0
- csiphon-0.1.0/src/csiphon/steps/delay/__init__.py +8 -0
- csiphon-0.1.0/src/csiphon/steps/delay/channel_impulse_response.py +113 -0
- csiphon-0.1.0/src/csiphon/steps/delay/delay_autocorrelation.py +116 -0
- csiphon-0.1.0/src/csiphon/steps/filtering/__init__.py +8 -0
- csiphon-0.1.0/src/csiphon/steps/filtering/butterworth_filter.py +197 -0
- csiphon-0.1.0/src/csiphon/steps/filtering/savitzky_golay.py +98 -0
- csiphon-0.1.0/src/csiphon/steps/normalization/__init__.py +13 -0
- csiphon-0.1.0/src/csiphon/steps/normalization/gain_normalize.py +76 -0
- csiphon-0.1.0/src/csiphon/steps/normalization/global_max_normalize.py +61 -0
- csiphon-0.1.0/src/csiphon/steps/normalization/per_frame_max_normalize.py +56 -0
- csiphon-0.1.0/src/csiphon/steps/pooling/__init__.py +9 -0
- csiphon-0.1.0/src/csiphon/steps/pooling/dyadic_frequency_bands.py +106 -0
- csiphon-0.1.0/src/csiphon/steps/pooling/fixed_size_window_sum.py +111 -0
- csiphon-0.1.0/src/csiphon/steps/pooling/mean_over_axes.py +92 -0
- csiphon-0.1.0/src/csiphon/steps/reduction/__init__.py +22 -0
- csiphon-0.1.0/src/csiphon/steps/reduction/delay_taps.py +89 -0
- csiphon-0.1.0/src/csiphon/steps/reduction/principal_components.py +178 -0
- csiphon-0.1.0/src/csiphon/steps/reduction/robust_pca.py +221 -0
- csiphon-0.1.0/src/csiphon/steps/reduction/select_axis.py +92 -0
- csiphon-0.1.0/src/csiphon/steps/resampling/__init__.py +35 -0
- csiphon-0.1.0/src/csiphon/steps/resampling/drop_samples.py +161 -0
- csiphon-0.1.0/src/csiphon/steps/resampling/resample.py +374 -0
- csiphon-0.1.0/src/csiphon/steps/resampling/subsample.py +103 -0
- csiphon-0.1.0/src/csiphon/steps/restructuring/__init__.py +8 -0
- csiphon-0.1.0/src/csiphon/steps/restructuring/fold_axes.py +139 -0
- csiphon-0.1.0/src/csiphon/steps/restructuring/stack_amp_phase.py +75 -0
- csiphon-0.1.0/src/csiphon/steps/scaling/__init__.py +8 -0
- csiphon-0.1.0/src/csiphon/steps/scaling/log_scale.py +58 -0
- csiphon-0.1.0/src/csiphon/steps/scaling/to_decibels.py +60 -0
- csiphon-0.1.0/src/csiphon/steps/statistics/__init__.py +8 -0
- csiphon-0.1.0/src/csiphon/steps/statistics/covariance_spectrum.py +197 -0
- csiphon-0.1.0/src/csiphon/steps/statistics/local_pca_bias.py +162 -0
- csiphon-0.1.0/src/csiphon/steps/temporal_features/__init__.py +9 -0
- csiphon-0.1.0/src/csiphon/steps/temporal_features/time_difference.py +160 -0
- csiphon-0.1.0/src/csiphon/steps/temporal_features/windowed_slope.py +149 -0
- csiphon-0.1.0/src/csiphon/steps/temporal_features/windowed_variance.py +149 -0
- csiphon-0.1.0/src/csiphon/steps/time_frequency/__init__.py +15 -0
- csiphon-0.1.0/src/csiphon/steps/time_frequency/complex_stft.py +193 -0
- csiphon-0.1.0/src/csiphon/steps/time_frequency/multitaper.py +213 -0
- csiphon-0.1.0/src/csiphon/steps/time_frequency/synchrosqueezed_power.py +230 -0
- csiphon-0.1.0/src/csiphon/steps/time_frequency/windowed_fft_power.py +152 -0
- csiphon-0.1.0/src/csiphon.egg-info/PKG-INFO +109 -0
- csiphon-0.1.0/src/csiphon.egg-info/SOURCES.txt +122 -0
- csiphon-0.1.0/src/csiphon.egg-info/dependency_links.txt +1 -0
- csiphon-0.1.0/src/csiphon.egg-info/requires.txt +22 -0
- csiphon-0.1.0/src/csiphon.egg-info/top_level.txt +1 -0
- csiphon-0.1.0/tests/test_alignment.py +369 -0
- csiphon-0.1.0/tests/test_branching.py +392 -0
- csiphon-0.1.0/tests/test_calibration.py +93 -0
- csiphon-0.1.0/tests/test_drop_samples.py +190 -0
- csiphon-0.1.0/tests/test_fuse.py +216 -0
- csiphon-0.1.0/tests/test_graphics.py +45 -0
- csiphon-0.1.0/tests/test_guards_filtering.py +211 -0
- csiphon-0.1.0/tests/test_guards_reduction.py +239 -0
- csiphon-0.1.0/tests/test_guards_spectral.py +178 -0
- csiphon-0.1.0/tests/test_inspection.py +91 -0
- csiphon-0.1.0/tests/test_metadata.py +55 -0
- csiphon-0.1.0/tests/test_multi_inlet.py +295 -0
- csiphon-0.1.0/tests/test_parity_cpd.py +131 -0
- csiphon-0.1.0/tests/test_pipeline.py +58 -0
- csiphon-0.1.0/tests/test_pooling.py +272 -0
- csiphon-0.1.0/tests/test_ported_misc.py +181 -0
- csiphon-0.1.0/tests/test_profile.py +73 -0
- csiphon-0.1.0/tests/test_reduce.py +79 -0
- csiphon-0.1.0/tests/test_resample.py +382 -0
- csiphon-0.1.0/tests/test_sampling.py +147 -0
- csiphon-0.1.0/tests/test_sequence_alignment.py +266 -0
- csiphon-0.1.0/tests/test_spectral_extra.py +665 -0
- csiphon-0.1.0/tests/test_statistics.py +177 -0
- csiphon-0.1.0/tests/test_step_contracts.py +578 -0
- csiphon-0.1.0/tests/test_step_edges.py +234 -0
- csiphon-0.1.0/tests/test_steps_batch.py +194 -0
- csiphon-0.1.0/tests/test_streaming.py +180 -0
- csiphon-0.1.0/tests/test_structural_axes.py +111 -0
- csiphon-0.1.0/tests/test_subsample.py +67 -0
- csiphon-0.1.0/tests/test_temporal_features.py +125 -0
- csiphon-0.1.0/tests/test_tree.py +202 -0
csiphon-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Fabian Portner
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
csiphon-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: csiphon
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: An online-first, schema-validated CSI preprocessing pipeline library.
|
|
5
|
+
Author: Fabian Portner
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/nzqo/csiphon
|
|
8
|
+
Project-URL: Repository, https://github.com/nzqo/csiphon
|
|
9
|
+
Project-URL: Issues, https://github.com/nzqo/csiphon/issues
|
|
10
|
+
Keywords: csi,wifi,channel-state-information,dsp,signal-processing,doppler,streaming
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: >=3.12
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: numpy>=2.1
|
|
23
|
+
Provides-Extra: sst
|
|
24
|
+
Requires-Dist: ssqueezepy>=0.6; extra == "sst"
|
|
25
|
+
Provides-Extra: filters
|
|
26
|
+
Requires-Dist: scipy>=1.11; extra == "filters"
|
|
27
|
+
Provides-Extra: all
|
|
28
|
+
Requires-Dist: csiphon[filters,sst]; extra == "all"
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: pytest; extra == "dev"
|
|
31
|
+
Requires-Dist: mypy; extra == "dev"
|
|
32
|
+
Requires-Dist: ruff; extra == "dev"
|
|
33
|
+
Requires-Dist: pyright; extra == "dev"
|
|
34
|
+
Requires-Dist: pylint; extra == "dev"
|
|
35
|
+
Requires-Dist: polars; extra == "dev"
|
|
36
|
+
Provides-Extra: release
|
|
37
|
+
Requires-Dist: build; extra == "release"
|
|
38
|
+
Requires-Dist: twine; extra == "release"
|
|
39
|
+
Dynamic: license-file
|
|
40
|
+
|
|
41
|
+
<p align="center">
|
|
42
|
+
<img
|
|
43
|
+
src="https://raw.githubusercontent.com/nzqo/csiphon/main/assets/ai_slop_mascot.png"
|
|
44
|
+
alt="slop Kanna doing Wi-Fi plumbing"
|
|
45
|
+
width="300"
|
|
46
|
+
>
|
|
47
|
+
</p>
|
|
48
|
+
|
|
49
|
+
# csiphon
|
|
50
|
+
|
|
51
|
+
`csiphon` is an online-first processing library for WiFi Channel State
|
|
52
|
+
Information. Compose reusable DSP steps, compile them against your capture
|
|
53
|
+
setup, then pour a complete recording or stream live chunks through the same
|
|
54
|
+
validated pipeline.
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from csiphon import AcquisitionProfile, Pipeline
|
|
58
|
+
from csiphon.steps import GainNormalize, Magnitude, WindowedVariance
|
|
59
|
+
|
|
60
|
+
profile = AcquisitionProfile(
|
|
61
|
+
n_rx_antennas=3,
|
|
62
|
+
subcarrier_indices=tuple(range(52)),
|
|
63
|
+
sampling_rate_hz=1000.0,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
siphon = (
|
|
67
|
+
Pipeline()
|
|
68
|
+
.then(Magnitude())
|
|
69
|
+
.then(GainNormalize())
|
|
70
|
+
.then(WindowedVariance(win_size_s=0.1))
|
|
71
|
+
.compile(profile)
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
features = siphon.pour(profile.raw_signal(csi, timestamps)).single()
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## What it does
|
|
78
|
+
|
|
79
|
+
- **Catches structural mistakes before execution.** Compilation checks axes,
|
|
80
|
+
shapes, value semantics, and physical representations before data starts
|
|
81
|
+
flowing.
|
|
82
|
+
- **Runs the same recipe in batch or live.** Every step states whether its
|
|
83
|
+
streaming computation matches batch, differs intentionally, or requires the
|
|
84
|
+
complete recording.
|
|
85
|
+
- **Supports branching pipelines.** Split into parallel feature paths, merge
|
|
86
|
+
them again, and expose named intermediate outlets.
|
|
87
|
+
- **Keeps real timestamps.** Non-uniform CSI sampling is expected; resampling is
|
|
88
|
+
explicit rather than silently assumed.
|
|
89
|
+
- **Describes itself.** Steps and compiled siphons expose their contracts,
|
|
90
|
+
layouts, parameters, and streaming behavior, and can save that information
|
|
91
|
+
alongside results for reproducibility.
|
|
92
|
+
|
|
93
|
+
The built-in steps cover calibration, cleaning, filtering, delay and
|
|
94
|
+
time-frequency transforms, temporal features, pooling, statistics, reduction,
|
|
95
|
+
and restructuring. Custom steps use the same contracts and inspection tools.
|
|
96
|
+
|
|
97
|
+
## Install
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
pip install -e .
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The core only depends on NumPy. Install every optional transform with:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
pip install -e ".[all]"
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Runnable recipes live in [`examples/`](examples/).
|
csiphon-0.1.0/README.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img
|
|
3
|
+
src="https://raw.githubusercontent.com/nzqo/csiphon/main/assets/ai_slop_mascot.png"
|
|
4
|
+
alt="slop Kanna doing Wi-Fi plumbing"
|
|
5
|
+
width="300"
|
|
6
|
+
>
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
# csiphon
|
|
10
|
+
|
|
11
|
+
`csiphon` is an online-first processing library for WiFi Channel State
|
|
12
|
+
Information. Compose reusable DSP steps, compile them against your capture
|
|
13
|
+
setup, then pour a complete recording or stream live chunks through the same
|
|
14
|
+
validated pipeline.
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
from csiphon import AcquisitionProfile, Pipeline
|
|
18
|
+
from csiphon.steps import GainNormalize, Magnitude, WindowedVariance
|
|
19
|
+
|
|
20
|
+
profile = AcquisitionProfile(
|
|
21
|
+
n_rx_antennas=3,
|
|
22
|
+
subcarrier_indices=tuple(range(52)),
|
|
23
|
+
sampling_rate_hz=1000.0,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
siphon = (
|
|
27
|
+
Pipeline()
|
|
28
|
+
.then(Magnitude())
|
|
29
|
+
.then(GainNormalize())
|
|
30
|
+
.then(WindowedVariance(win_size_s=0.1))
|
|
31
|
+
.compile(profile)
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
features = siphon.pour(profile.raw_signal(csi, timestamps)).single()
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## What it does
|
|
38
|
+
|
|
39
|
+
- **Catches structural mistakes before execution.** Compilation checks axes,
|
|
40
|
+
shapes, value semantics, and physical representations before data starts
|
|
41
|
+
flowing.
|
|
42
|
+
- **Runs the same recipe in batch or live.** Every step states whether its
|
|
43
|
+
streaming computation matches batch, differs intentionally, or requires the
|
|
44
|
+
complete recording.
|
|
45
|
+
- **Supports branching pipelines.** Split into parallel feature paths, merge
|
|
46
|
+
them again, and expose named intermediate outlets.
|
|
47
|
+
- **Keeps real timestamps.** Non-uniform CSI sampling is expected; resampling is
|
|
48
|
+
explicit rather than silently assumed.
|
|
49
|
+
- **Describes itself.** Steps and compiled siphons expose their contracts,
|
|
50
|
+
layouts, parameters, and streaming behavior, and can save that information
|
|
51
|
+
alongside results for reproducibility.
|
|
52
|
+
|
|
53
|
+
The built-in steps cover calibration, cleaning, filtering, delay and
|
|
54
|
+
time-frequency transforms, temporal features, pooling, statistics, reduction,
|
|
55
|
+
and restructuring. Custom steps use the same contracts and inspection tools.
|
|
56
|
+
|
|
57
|
+
## Install
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install -e .
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The core only depends on NumPy. Install every optional transform with:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pip install -e ".[all]"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Runnable recipes live in [`examples/`](examples/).
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=80"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "csiphon"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "An online-first, schema-validated CSI preprocessing pipeline library."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "Fabian Portner" }]
|
|
14
|
+
keywords = [
|
|
15
|
+
"csi",
|
|
16
|
+
"wifi",
|
|
17
|
+
"channel-state-information",
|
|
18
|
+
"dsp",
|
|
19
|
+
"signal-processing",
|
|
20
|
+
"doppler",
|
|
21
|
+
"streaming",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
# Trove classifiers for the PyPI page. No `License ::` classifier: the SPDX
|
|
25
|
+
# `license` field above supersedes it (setuptools >= 77 rejects using both).
|
|
26
|
+
classifiers = [
|
|
27
|
+
"Development Status :: 3 - Alpha",
|
|
28
|
+
"Intended Audience :: Science/Research",
|
|
29
|
+
"Operating System :: OS Independent",
|
|
30
|
+
"Programming Language :: Python :: 3",
|
|
31
|
+
"Programming Language :: Python :: 3.12",
|
|
32
|
+
"Programming Language :: Python :: 3.13",
|
|
33
|
+
"Topic :: Scientific/Engineering :: Information Analysis",
|
|
34
|
+
"Typing :: Typed",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
# Core dependencies (pretty lean)
|
|
38
|
+
dependencies = ["numpy>=2.1"]
|
|
39
|
+
|
|
40
|
+
# Optional dependencies (required for specific steps only)
|
|
41
|
+
[project.optional-dependencies]
|
|
42
|
+
sst = ["ssqueezepy>=0.6"]
|
|
43
|
+
filters = ["scipy>=1.11"]
|
|
44
|
+
|
|
45
|
+
# Every optional feature at once (a self-reference to the extras above).
|
|
46
|
+
all = ["csiphon[sst,filters]"]
|
|
47
|
+
dev = ["pytest", "mypy", "ruff", "pyright", "pylint", "polars"]
|
|
48
|
+
# Tooling for building and uploading the package to PyPI.
|
|
49
|
+
release = ["build", "twine"]
|
|
50
|
+
|
|
51
|
+
[project.urls]
|
|
52
|
+
Homepage = "https://github.com/nzqo/csiphon"
|
|
53
|
+
Repository = "https://github.com/nzqo/csiphon"
|
|
54
|
+
Issues = "https://github.com/nzqo/csiphon/issues"
|
|
55
|
+
|
|
56
|
+
[tool.setuptools.packages.find]
|
|
57
|
+
where = ["src"]
|
|
58
|
+
|
|
59
|
+
[tool.setuptools.package-data]
|
|
60
|
+
csiphon = ["py.typed"]
|
|
61
|
+
|
|
62
|
+
[tool.mypy]
|
|
63
|
+
strict = true
|
|
64
|
+
python_version = "3.12"
|
|
65
|
+
|
|
66
|
+
# Optional third-party dependencies ship no type information.
|
|
67
|
+
[[tool.mypy.overrides]]
|
|
68
|
+
module = ["ssqueezepy.*", "scipy.*"]
|
|
69
|
+
ignore_missing_imports = true
|
|
70
|
+
|
|
71
|
+
[tool.pyright]
|
|
72
|
+
venvPath = "."
|
|
73
|
+
venv = ".venv"
|
|
74
|
+
|
|
75
|
+
# mypy --strict is the library's strict type gate (see [tool.mypy]).
|
|
76
|
+
# pyright runs at basic so numpy's shape-Any typing and unannotated
|
|
77
|
+
# pytest fixtures don't drown the editor in noise while still catching
|
|
78
|
+
# real mistakes.
|
|
79
|
+
typeCheckingMode = "basic"
|
|
80
|
+
|
|
81
|
+
# scipy / ssqueezepy ship no type stubs.
|
|
82
|
+
reportMissingModuleSource = false
|
|
83
|
+
|
|
84
|
+
[tool.ruff.lint]
|
|
85
|
+
select = ["E", "F", "I", "UP", "B", "SIM", "RUF", "ANN"]
|
|
86
|
+
|
|
87
|
+
# RUF001-003: arrows / ∈ / ² in human-facing docstrings and spec strings are
|
|
88
|
+
# intentional, not ambiguous-unicode mistakes.
|
|
89
|
+
ignore = ["RUF001", "RUF002", "RUF003"]
|
|
90
|
+
|
|
91
|
+
[tool.ruff.lint.per-file-ignores]
|
|
92
|
+
# Test fixtures and parametrized args are conventionally left unannotated.
|
|
93
|
+
"tests/**" = ["ANN001", "ANN002", "ANN003"]
|
|
94
|
+
|
|
95
|
+
[tool.pylint.messages_control]
|
|
96
|
+
# Ruff (its formatter for code + E501/noqa for prose) is the single line-length
|
|
97
|
+
# authority; pylint's line-too-long would double-report and ignores ruff's noqa.
|
|
98
|
+
disable = ["line-too-long"]
|
|
99
|
+
|
|
100
|
+
[tool.pytest.ini_options]
|
|
101
|
+
testpaths = ["tests"]
|
csiphon-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""csiphon: an online-first, schema-validated CSI preprocessing pipeline.
|
|
2
|
+
|
|
3
|
+
Build an immutable Pipeline from steps (branch and merge as needed), compile it
|
|
4
|
+
against an AcquisitionProfile into a Siphon, then run it over a whole recording
|
|
5
|
+
(`siphon.pour()`) or chunk by chunk (`siphon.stream()`). A run returns Outlets,
|
|
6
|
+
the named results.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from csiphon.core import (
|
|
12
|
+
AcquisitionProfile,
|
|
13
|
+
Axis,
|
|
14
|
+
AxisName,
|
|
15
|
+
Layout,
|
|
16
|
+
Representation,
|
|
17
|
+
Signal,
|
|
18
|
+
ValueKind,
|
|
19
|
+
create_signal,
|
|
20
|
+
)
|
|
21
|
+
from csiphon.inspection import describe
|
|
22
|
+
from csiphon.pipeline import (
|
|
23
|
+
Alignment,
|
|
24
|
+
Concatenate,
|
|
25
|
+
Exact,
|
|
26
|
+
Fuse,
|
|
27
|
+
Hold,
|
|
28
|
+
Junction,
|
|
29
|
+
Mean,
|
|
30
|
+
MergeStrategy,
|
|
31
|
+
Node,
|
|
32
|
+
Outlets,
|
|
33
|
+
Pipeline,
|
|
34
|
+
PointwiseStep,
|
|
35
|
+
Sequence,
|
|
36
|
+
Siphon,
|
|
37
|
+
Stack,
|
|
38
|
+
Step,
|
|
39
|
+
Stream,
|
|
40
|
+
StreamOperator,
|
|
41
|
+
Sum,
|
|
42
|
+
Time,
|
|
43
|
+
)
|
|
44
|
+
from csiphon.spec import Category, Description, StepSpec, Streaming
|
|
45
|
+
|
|
46
|
+
__all__ = [
|
|
47
|
+
"AcquisitionProfile",
|
|
48
|
+
"Alignment",
|
|
49
|
+
"Axis",
|
|
50
|
+
"AxisName",
|
|
51
|
+
"Category",
|
|
52
|
+
"Concatenate",
|
|
53
|
+
"Description",
|
|
54
|
+
"Exact",
|
|
55
|
+
"Fuse",
|
|
56
|
+
"Hold",
|
|
57
|
+
"Junction",
|
|
58
|
+
"Layout",
|
|
59
|
+
"Mean",
|
|
60
|
+
"MergeStrategy",
|
|
61
|
+
"Node",
|
|
62
|
+
"Outlets",
|
|
63
|
+
"Pipeline",
|
|
64
|
+
"PointwiseStep",
|
|
65
|
+
"Representation",
|
|
66
|
+
"Sequence",
|
|
67
|
+
"Signal",
|
|
68
|
+
"Siphon",
|
|
69
|
+
"Stack",
|
|
70
|
+
"Step",
|
|
71
|
+
"StepSpec",
|
|
72
|
+
"Stream",
|
|
73
|
+
"StreamOperator",
|
|
74
|
+
"Streaming",
|
|
75
|
+
"Sum",
|
|
76
|
+
"Time",
|
|
77
|
+
"ValueKind",
|
|
78
|
+
"create_signal",
|
|
79
|
+
"describe",
|
|
80
|
+
]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Core data model: axes, layout, signal, acquisition profile, sampling."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from csiphon.core.arrays import (
|
|
6
|
+
ComplexArray,
|
|
7
|
+
RealArray,
|
|
8
|
+
SignalArray,
|
|
9
|
+
as_complex_array,
|
|
10
|
+
as_real_array,
|
|
11
|
+
as_signal_array,
|
|
12
|
+
)
|
|
13
|
+
from csiphon.core.axes import Axis, AxisName, Coordinate
|
|
14
|
+
from csiphon.core.errors import (
|
|
15
|
+
ClogError,
|
|
16
|
+
CompileError,
|
|
17
|
+
DataError,
|
|
18
|
+
LayoutError,
|
|
19
|
+
MissingDependencyError,
|
|
20
|
+
StreamingError,
|
|
21
|
+
)
|
|
22
|
+
from csiphon.core.layout import Layout
|
|
23
|
+
from csiphon.core.profile import AcquisitionProfile
|
|
24
|
+
from csiphon.core.sampling import (
|
|
25
|
+
JitterWarning,
|
|
26
|
+
check_jitter,
|
|
27
|
+
effective_rate_hz,
|
|
28
|
+
estimate_rate_hz,
|
|
29
|
+
jitter_ratio,
|
|
30
|
+
)
|
|
31
|
+
from csiphon.core.semantics import Representation, ValueKind
|
|
32
|
+
from csiphon.core.signal import Signal, create_signal, empty_signal
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"AcquisitionProfile",
|
|
36
|
+
"Axis",
|
|
37
|
+
"AxisName",
|
|
38
|
+
"ClogError",
|
|
39
|
+
"CompileError",
|
|
40
|
+
"ComplexArray",
|
|
41
|
+
"Coordinate",
|
|
42
|
+
"DataError",
|
|
43
|
+
"JitterWarning",
|
|
44
|
+
"Layout",
|
|
45
|
+
"LayoutError",
|
|
46
|
+
"MissingDependencyError",
|
|
47
|
+
"RealArray",
|
|
48
|
+
"Representation",
|
|
49
|
+
"Signal",
|
|
50
|
+
"SignalArray",
|
|
51
|
+
"StreamingError",
|
|
52
|
+
"ValueKind",
|
|
53
|
+
"as_complex_array",
|
|
54
|
+
"as_real_array",
|
|
55
|
+
"as_signal_array",
|
|
56
|
+
"check_jitter",
|
|
57
|
+
"create_signal",
|
|
58
|
+
"effective_rate_hz",
|
|
59
|
+
"empty_signal",
|
|
60
|
+
"estimate_rate_hz",
|
|
61
|
+
"jitter_ratio",
|
|
62
|
+
]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Concrete NumPy array types used at numerical boundaries."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import numpy.typing as npt
|
|
7
|
+
|
|
8
|
+
type RealArray = npt.NDArray[np.float64]
|
|
9
|
+
type ComplexArray = npt.NDArray[np.complex128]
|
|
10
|
+
type SignalArray = RealArray | ComplexArray
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def as_complex_array(values: npt.ArrayLike) -> ComplexArray:
|
|
14
|
+
"""Return values as the canonical complex array type."""
|
|
15
|
+
|
|
16
|
+
return np.asarray(values, dtype=np.complex128)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def as_real_array(values: npt.ArrayLike) -> RealArray:
|
|
20
|
+
"""Return values as the canonical real array type."""
|
|
21
|
+
|
|
22
|
+
return np.asarray(values, dtype=np.float64)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def as_signal_array(values: npt.ArrayLike) -> SignalArray:
|
|
26
|
+
"""Preserve real or complex values while normalizing the dtype."""
|
|
27
|
+
|
|
28
|
+
if np.iscomplexobj(values):
|
|
29
|
+
return as_complex_array(values)
|
|
30
|
+
|
|
31
|
+
return as_real_array(values)
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Named tensor axes; mostly for semantics-based processing (vs. raw axis numbers)
|
|
2
|
+
|
|
3
|
+
An Axis is a named tensor dimension. Steps address dimensions by name
|
|
4
|
+
(`AxisName.SUBCARRIER`), never by integer position, so unrelated dimensions can
|
|
5
|
+
sit in any order.
|
|
6
|
+
|
|
7
|
+
Size and coordinates come in three cases:
|
|
8
|
+
|
|
9
|
+
- Statically sized.
|
|
10
|
+
`size` and (optionally) `coordinates` are known at compile time from the
|
|
11
|
+
profile and each step's config: subcarriers, delay taps, dyadic bands,
|
|
12
|
+
the frequency bins of a windowed FFT, and so on.
|
|
13
|
+
- Runtime sized.
|
|
14
|
+
`size is None` because the length only shows up once data arrives.
|
|
15
|
+
Not just time: the number of SST frequency bins depends on the recording
|
|
16
|
+
length, so that axis is runtime sized and carries its coordinates on the
|
|
17
|
+
Signal (`coords`) instead of in the layout. Any number of axes can be
|
|
18
|
+
runtime sized.
|
|
19
|
+
- The time axis, named AxisName.TIME.
|
|
20
|
+
Its coordinates ride with the data as `Signal.times`. It is runtime sized
|
|
21
|
+
too, since you don't know a recording's or a stream's length up front. A
|
|
22
|
+
layout has at most one time axis.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
from collections.abc import Hashable, Sequence
|
|
28
|
+
from dataclasses import dataclass
|
|
29
|
+
from enum import StrEnum
|
|
30
|
+
|
|
31
|
+
from csiphon.core.errors import LayoutError
|
|
32
|
+
|
|
33
|
+
# The axis name gives a dimension its semantics.
|
|
34
|
+
# Its coordinates give each position along it a physical meaning.
|
|
35
|
+
# For example:
|
|
36
|
+
# - the exact frequency values along a `FREQUENCY` axis
|
|
37
|
+
# - the subcarrier index each `SUBCARRIER` position corresponds to.
|
|
38
|
+
# Any hashable value works; axes with no meaningful label (eg. PCA components)
|
|
39
|
+
# just use their integer index.
|
|
40
|
+
type Coordinate = Hashable
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class AxisName(StrEnum):
|
|
44
|
+
"""Semantic dimensions used by CSI preprocessing steps.
|
|
45
|
+
|
|
46
|
+
A curated, closed set (which we might extend)
|
|
47
|
+
The fixed vocabulary keeps describe() and cross-step validation type-safe.
|
|
48
|
+
The spatial names describe a general MIMO / multi-device capture,
|
|
49
|
+
such as (receivers, transmit/receive antennas, spatial streams).
|
|
50
|
+
|
|
51
|
+
When a step produces a genuinely new kind of dimension (say, reducing subcarriers
|
|
52
|
+
to abstract components), pick the closest name here (`COMPONENT`, `FEATURE`, or
|
|
53
|
+
`LATENT` for learned reductions) rather than mislabelling the output. Add a
|
|
54
|
+
member when you need a new physical dimension.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
# fmt: off
|
|
58
|
+
# time
|
|
59
|
+
TIME = "time"
|
|
60
|
+
|
|
61
|
+
# spatial (MIMO / multi-device)
|
|
62
|
+
RECEIVER = "receiver" # receiver device, for a multi-device capture
|
|
63
|
+
RX_ANTENNA = "rx_antenna" # receive antenna
|
|
64
|
+
TX_ANTENNA = "tx_antenna" # transmit antenna
|
|
65
|
+
SPATIAL_STREAM = "spatial_stream" # spatial stream (Nss)
|
|
66
|
+
|
|
67
|
+
# spectral / delay / doppler
|
|
68
|
+
SUBCARRIER = "subcarrier"
|
|
69
|
+
FREQUENCY = "frequency"
|
|
70
|
+
DELAY = "delay"
|
|
71
|
+
DOPPLER = "doppler"
|
|
72
|
+
BAND = "band"
|
|
73
|
+
WAVELET_BAND = "wavelet_band"
|
|
74
|
+
LAG = "lag"
|
|
75
|
+
|
|
76
|
+
# derived / learned
|
|
77
|
+
COMPONENT = "component" # e.g. a PCA component
|
|
78
|
+
LATENT = "latent"
|
|
79
|
+
FEATURE = "feature" # several axes folded into one
|
|
80
|
+
# fmt: on
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass(frozen=True, slots=True)
|
|
84
|
+
class Axis:
|
|
85
|
+
"""One named tensor dimension.
|
|
86
|
+
|
|
87
|
+
`size` is `None` when the length is only known at run time (the time axis, or
|
|
88
|
+
an axis like the SST frequency bins whose count depends on the data).
|
|
89
|
+
|
|
90
|
+
Statically sized axes provide a positive `size` and, optionally, matching
|
|
91
|
+
`coordinates`.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
# fmt: off
|
|
95
|
+
name : AxisName
|
|
96
|
+
size : int | None
|
|
97
|
+
coordinates : tuple[Coordinate, ...] | None = None
|
|
98
|
+
unit : str | None = None
|
|
99
|
+
# fmt: on
|
|
100
|
+
|
|
101
|
+
def __post_init__(self) -> None:
|
|
102
|
+
"""Validate the size / coordinates relationship."""
|
|
103
|
+
|
|
104
|
+
if self.size is None:
|
|
105
|
+
if self.coordinates is not None:
|
|
106
|
+
raise LayoutError(
|
|
107
|
+
f"Dynamic axis '{self.name}' must not carry coordinates."
|
|
108
|
+
)
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
if self.size < 1:
|
|
112
|
+
raise LayoutError(f"Axis '{self.name}' must have a positive size.")
|
|
113
|
+
|
|
114
|
+
if self.coordinates is not None and len(self.coordinates) != self.size:
|
|
115
|
+
raise LayoutError(
|
|
116
|
+
f"Axis '{self.name}' has {len(self.coordinates)} coordinates "
|
|
117
|
+
f"but size {self.size}."
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# -------------------------------------------------------------------------
|
|
121
|
+
# Properties
|
|
122
|
+
# -------------------------------------------------------------------------
|
|
123
|
+
@property
|
|
124
|
+
def is_dynamic(self) -> bool:
|
|
125
|
+
"""Return whether the size is only known at runtime (`size is None`)."""
|
|
126
|
+
|
|
127
|
+
return self.size is None
|
|
128
|
+
|
|
129
|
+
# -------------------------------------------------------------------------
|
|
130
|
+
# Constructors
|
|
131
|
+
# -------------------------------------------------------------------------
|
|
132
|
+
@classmethod
|
|
133
|
+
def dynamic(cls, name: AxisName = AxisName.TIME, unit: str | None = "s") -> Axis:
|
|
134
|
+
"""Create the dynamic time / window axis."""
|
|
135
|
+
|
|
136
|
+
return cls(name=name, size=None, coordinates=None, unit=unit)
|
|
137
|
+
|
|
138
|
+
@classmethod
|
|
139
|
+
def static(
|
|
140
|
+
cls,
|
|
141
|
+
name: AxisName,
|
|
142
|
+
coordinates: Sequence[Coordinate],
|
|
143
|
+
unit: str | None = None,
|
|
144
|
+
) -> Axis:
|
|
145
|
+
"""Create a static axis from explicit coordinates."""
|
|
146
|
+
|
|
147
|
+
values = tuple(coordinates)
|
|
148
|
+
return cls(name=name, size=len(values), coordinates=values, unit=unit)
|
|
149
|
+
|
|
150
|
+
@classmethod
|
|
151
|
+
def sized(cls, name: AxisName, size: int, unit: str | None = None) -> Axis:
|
|
152
|
+
"""Create a static axis of a known size without explicit coordinates."""
|
|
153
|
+
|
|
154
|
+
return cls(name=name, size=size, coordinates=None, unit=unit)
|
|
155
|
+
|
|
156
|
+
# -------------------------------------------------------------------------
|
|
157
|
+
# Transformations
|
|
158
|
+
# -------------------------------------------------------------------------
|
|
159
|
+
def relabel(self, coordinates: Sequence[Coordinate]) -> Axis:
|
|
160
|
+
"""Return this axis with new coordinates (and matching size)."""
|
|
161
|
+
|
|
162
|
+
return Axis.static(self.name, coordinates, unit=self.unit)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Exceptions raised by layouts, steps, and pipeline compilation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class LayoutError(ValueError):
|
|
7
|
+
"""A signal layout does not satisfy a step's structural requirements."""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DataError(ValueError):
|
|
11
|
+
"""Numerical values violate an assumption checked during execution."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CompileError(ValueError):
|
|
15
|
+
"""A pipeline step is incompatible with the layout before it."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class StreamingError(ValueError):
|
|
19
|
+
"""A pipeline (or one of its steps) cannot run in streaming mode."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ClogError(StreamingError):
|
|
23
|
+
"""A merge branch held too many samples without aligning (a stalled branch)."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class MissingDependencyError(ImportError):
|
|
27
|
+
"""A step needs an optional dependency that is not installed."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, package: str, extra: str) -> None:
|
|
30
|
+
"""Explain which extra to install for the missing dependency."""
|
|
31
|
+
|
|
32
|
+
super().__init__(
|
|
33
|
+
f"This step requires the optional dependency '{package}'. "
|
|
34
|
+
f"Install it with: pip install 'csiphon[{extra}]'"
|
|
35
|
+
)
|