csiphon 0.2.0__tar.gz → 0.3.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.2.0/src/csiphon.egg-info → csiphon-0.3.0}/PKG-INFO +31 -17
- csiphon-0.3.0/README.md +92 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/pyproject.toml +1 -1
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/__init__.py +2 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/pipeline/__init__.py +2 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/pipeline/measure.py +10 -1
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/pipeline/merges.py +35 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/pipeline/runners.py +31 -1
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/__init__.py +10 -1
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/components/__init__.py +4 -2
- csiphon-0.3.0/src/csiphon/steps/components/imag_part.py +53 -0
- csiphon-0.3.0/src/csiphon/steps/components/real_part.py +53 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/filtering/butterworth_filter.py +13 -6
- {csiphon-0.2.0 → csiphon-0.3.0/src/csiphon.egg-info}/PKG-INFO +31 -17
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon.egg-info/SOURCES.txt +3 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_branching.py +18 -0
- csiphon-0.3.0/tests/test_complex_parts.py +131 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_measure.py +18 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_steps_batch.py +14 -0
- csiphon-0.2.0/README.md +0 -78
- {csiphon-0.2.0 → csiphon-0.3.0}/LICENSE +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/setup.cfg +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/core/__init__.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/core/arrays.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/core/axes.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/core/errors.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/core/layout.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/core/profile.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/core/sampling.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/core/semantics.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/core/signal.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/graphics/__init__.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/graphics/style.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/graphics/tree.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/inspection.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/pipeline/_align_ops.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/pipeline/_lines.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/pipeline/pipeline.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/pipeline/sequence.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/pipeline/step.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/py.typed +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/spec.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/_support/__init__.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/_support/broadcasting.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/_support/windowing.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/baseline/__init__.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/baseline/running_mean_subtract.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/baseline/temporal_mean_subtract.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/calibration/__init__.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/calibration/axis_reference.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/calibration/linear_phase_correction.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/cleaning/__init__.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/cleaning/nan_scrub.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/cleaning/noise_floor_clip.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/components/magnitude.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/components/phase.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/components/power.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/components/unit_phase.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/delay/__init__.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/delay/channel_impulse_response.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/delay/delay_autocorrelation.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/filtering/__init__.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/filtering/savitzky_golay.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/normalization/__init__.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/normalization/gain_normalize.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/normalization/global_max_normalize.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/normalization/per_frame_max_normalize.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/pooling/__init__.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/pooling/dyadic_frequency_bands.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/pooling/fixed_size_window_sum.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/pooling/mean_over_axes.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/reduction/__init__.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/reduction/delay_taps.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/reduction/principal_components.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/reduction/robust_pca.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/reduction/select_axis.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/resampling/__init__.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/resampling/drop_samples.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/resampling/resample.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/resampling/subsample.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/restructuring/__init__.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/restructuring/fold_axes.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/restructuring/stack_amp_phase.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/scaling/__init__.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/scaling/log_scale.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/scaling/to_decibels.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/statistics/__init__.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/statistics/covariance_spectrum.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/statistics/local_pca_bias.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/temporal_features/__init__.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/temporal_features/time_difference.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/temporal_features/windowed_slope.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/temporal_features/windowed_variance.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/time_frequency/__init__.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/time_frequency/complex_stft.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/time_frequency/multitaper.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/time_frequency/synchrosqueezed_power.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/time_frequency/windowed_fft_power.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon.egg-info/dependency_links.txt +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon.egg-info/requires.txt +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon.egg-info/top_level.txt +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_alignment.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_calibration.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_drop_samples.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_fuse.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_golden_sst_pipeline.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_graphics.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_guards_filtering.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_guards_reduction.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_guards_spectral.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_inspection.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_metadata.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_multi_inlet.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_pipeline.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_pooling.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_ported_misc.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_profile.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_reduce.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_resample.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_sampling.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_sequence_alignment.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_spectral_extra.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_statistics.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_step_contracts.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_step_edges.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_streaming.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_structural_axes.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_subsample.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_temporal_features.py +0 -0
- {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_tree.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: csiphon
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: An online-first, schema-validated CSI preprocessing pipeline library.
|
|
5
5
|
Author: Fabian Portner
|
|
6
6
|
License-Expression: MIT
|
|
@@ -49,16 +49,17 @@ Dynamic: license-file
|
|
|
49
49
|
|
|
50
50
|
# csiphon
|
|
51
51
|
|
|
52
|
-
`csiphon` is
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
`csiphon` is a Python library for preprocessing WiFi Channel State Information.
|
|
53
|
+
You plug DSP steps together into a pipeline, csiphon checks that the pieces fit
|
|
54
|
+
before you run anything, and the finished pipeline handles recordings and live
|
|
55
|
+
streams alike. Every signal carries its axes by name, so you never have to
|
|
56
|
+
wonder which one was the subcarrier again.
|
|
56
57
|
|
|
57
58
|
```python
|
|
58
59
|
from csiphon import AcquisitionProfile, Pipeline
|
|
59
60
|
from csiphon.steps import GainNormalize, Magnitude, WindowedVariance
|
|
60
61
|
|
|
61
|
-
#
|
|
62
|
+
# Describe the capture setup
|
|
62
63
|
profile = AcquisitionProfile(
|
|
63
64
|
n_rx_antennas=3,
|
|
64
65
|
subcarrier_indices=tuple(range(52)),
|
|
@@ -81,6 +82,10 @@ print(siphon.describe())
|
|
|
81
82
|
features = siphon.pour(profile.raw_signal(csi, timestamps)).single()
|
|
82
83
|
```
|
|
83
84
|
|
|
85
|
+
A compiled pipeline is a `Siphon`. You `pour()` a recording through it or
|
|
86
|
+
`stream()` live data, and collect results from named `Outlets`, which you can tap
|
|
87
|
+
anywhere along the pipeline.
|
|
88
|
+
|
|
84
89
|
## What it does
|
|
85
90
|
|
|
86
91
|
- **Catches structural mistakes before execution.** Compilation checks axes,
|
|
@@ -91,29 +96,38 @@ features = siphon.pour(profile.raw_signal(csi, timestamps)).single()
|
|
|
91
96
|
complete recording.
|
|
92
97
|
- **Supports branching pipelines.** Split into parallel feature paths, merge
|
|
93
98
|
them again, and expose named intermediate outlets.
|
|
99
|
+
- **Merges receivers.** Feed several receivers through their own inlets and line
|
|
100
|
+
them up on timestamps or on packet sequence numbers, which survive clock drift.
|
|
94
101
|
- **Keeps real timestamps.** Non-uniform CSI sampling is expected; resampling is
|
|
95
|
-
explicit rather than silently assumed.
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
102
|
+
explicit rather than silently assumed. You can also drop packets on purpose,
|
|
103
|
+
with independent or bursty loss, to test how a pipeline copes.
|
|
104
|
+
- **Describes itself.** `describe()` draws a compiled siphon as a data-flow graph
|
|
105
|
+
in the terminal. Steps and siphons expose their contracts, layouts, parameters,
|
|
106
|
+
and streaming behavior, and can save that information alongside results for
|
|
107
|
+
reproducibility.
|
|
99
108
|
- **Measures itself.** `siphon.measure(signal)` pours once and reports each
|
|
100
109
|
step's time and output shape (memory on request), per step or for a named
|
|
101
110
|
group of consecutive steps.
|
|
102
111
|
|
|
103
|
-
The built-in steps cover
|
|
104
|
-
|
|
105
|
-
|
|
112
|
+
The built-in steps cover components, scaling, cleaning, calibration,
|
|
113
|
+
normalization, baseline removal, filtering, temporal features, delay and
|
|
114
|
+
time-frequency transforms, pooling, statistics, reduction, restructuring, and
|
|
115
|
+
resampling. The
|
|
116
|
+
[steps README](https://github.com/nzqo/csiphon/blob/main/src/csiphon/steps/README.md)
|
|
117
|
+
lists the categories. Custom steps use the same contracts and inspection tools.
|
|
106
118
|
|
|
107
119
|
## Install
|
|
108
120
|
|
|
109
121
|
```bash
|
|
110
|
-
pip install
|
|
122
|
+
pip install csiphon
|
|
111
123
|
```
|
|
112
124
|
|
|
113
|
-
The core only depends on NumPy.
|
|
125
|
+
The core only depends on NumPy. A few steps (filters, STFT, multitaper,
|
|
126
|
+
cubic-spline resampling) need the `[filters]` extra (SciPy), and the
|
|
127
|
+
synchrosqueezed transform needs `[sst]`. Install everything with:
|
|
114
128
|
|
|
115
129
|
```bash
|
|
116
|
-
pip install
|
|
130
|
+
pip install "csiphon[all]"
|
|
117
131
|
```
|
|
118
132
|
|
|
119
|
-
Runnable recipes live in [`examples/`](examples
|
|
133
|
+
Runnable recipes live in [`examples/`](https://github.com/nzqo/csiphon/tree/main/examples).
|
csiphon-0.3.0/README.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
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="400"
|
|
6
|
+
>
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
# csiphon
|
|
10
|
+
|
|
11
|
+
`csiphon` is a Python library for preprocessing WiFi Channel State Information.
|
|
12
|
+
You plug DSP steps together into a pipeline, csiphon checks that the pieces fit
|
|
13
|
+
before you run anything, and the finished pipeline handles recordings and live
|
|
14
|
+
streams alike. Every signal carries its axes by name, so you never have to
|
|
15
|
+
wonder which one was the subcarrier again.
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from csiphon import AcquisitionProfile, Pipeline
|
|
19
|
+
from csiphon.steps import GainNormalize, Magnitude, WindowedVariance
|
|
20
|
+
|
|
21
|
+
# Describe the capture setup
|
|
22
|
+
profile = AcquisitionProfile(
|
|
23
|
+
n_rx_antennas=3,
|
|
24
|
+
subcarrier_indices=tuple(range(52)),
|
|
25
|
+
sampling_rate_hz=1000.0,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Define the pipeline steps
|
|
29
|
+
siphon = (
|
|
30
|
+
Pipeline()
|
|
31
|
+
.then(Magnitude())
|
|
32
|
+
.then(GainNormalize())
|
|
33
|
+
.then(WindowedVariance(win_size_s=0.1))
|
|
34
|
+
.compile(profile)
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Generate a summary of the pipeline
|
|
38
|
+
print(siphon.describe())
|
|
39
|
+
|
|
40
|
+
# Push some data through it to run
|
|
41
|
+
features = siphon.pour(profile.raw_signal(csi, timestamps)).single()
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
A compiled pipeline is a `Siphon`. You `pour()` a recording through it or
|
|
45
|
+
`stream()` live data, and collect results from named `Outlets`, which you can tap
|
|
46
|
+
anywhere along the pipeline.
|
|
47
|
+
|
|
48
|
+
## What it does
|
|
49
|
+
|
|
50
|
+
- **Catches structural mistakes before execution.** Compilation checks axes,
|
|
51
|
+
shapes, value semantics, and physical representations before data starts
|
|
52
|
+
flowing.
|
|
53
|
+
- **Runs the same recipe in batch or live.** Every step states whether its
|
|
54
|
+
streaming computation matches batch, differs intentionally, or requires the
|
|
55
|
+
complete recording.
|
|
56
|
+
- **Supports branching pipelines.** Split into parallel feature paths, merge
|
|
57
|
+
them again, and expose named intermediate outlets.
|
|
58
|
+
- **Merges receivers.** Feed several receivers through their own inlets and line
|
|
59
|
+
them up on timestamps or on packet sequence numbers, which survive clock drift.
|
|
60
|
+
- **Keeps real timestamps.** Non-uniform CSI sampling is expected; resampling is
|
|
61
|
+
explicit rather than silently assumed. You can also drop packets on purpose,
|
|
62
|
+
with independent or bursty loss, to test how a pipeline copes.
|
|
63
|
+
- **Describes itself.** `describe()` draws a compiled siphon as a data-flow graph
|
|
64
|
+
in the terminal. Steps and siphons expose their contracts, layouts, parameters,
|
|
65
|
+
and streaming behavior, and can save that information alongside results for
|
|
66
|
+
reproducibility.
|
|
67
|
+
- **Measures itself.** `siphon.measure(signal)` pours once and reports each
|
|
68
|
+
step's time and output shape (memory on request), per step or for a named
|
|
69
|
+
group of consecutive steps.
|
|
70
|
+
|
|
71
|
+
The built-in steps cover components, scaling, cleaning, calibration,
|
|
72
|
+
normalization, baseline removal, filtering, temporal features, delay and
|
|
73
|
+
time-frequency transforms, pooling, statistics, reduction, restructuring, and
|
|
74
|
+
resampling. The
|
|
75
|
+
[steps README](https://github.com/nzqo/csiphon/blob/main/src/csiphon/steps/README.md)
|
|
76
|
+
lists the categories. Custom steps use the same contracts and inspection tools.
|
|
77
|
+
|
|
78
|
+
## Install
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
pip install csiphon
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The core only depends on NumPy. A few steps (filters, STFT, multitaper,
|
|
85
|
+
cubic-spline resampling) need the `[filters]` extra (SciPy), and the
|
|
86
|
+
synchrosqueezed transform needs `[sst]`. Install everything with:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
pip install "csiphon[all]"
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Runnable recipes live in [`examples/`](https://github.com/nzqo/csiphon/tree/main/examples).
|
|
@@ -19,6 +19,7 @@ from csiphon.core import (
|
|
|
19
19
|
from csiphon.inspection import describe
|
|
20
20
|
from csiphon.pipeline import (
|
|
21
21
|
Alignment,
|
|
22
|
+
ComplexFromParts,
|
|
22
23
|
Concatenate,
|
|
23
24
|
Exact,
|
|
24
25
|
Fuse,
|
|
@@ -50,6 +51,7 @@ __all__ = [
|
|
|
50
51
|
"Axis",
|
|
51
52
|
"AxisName",
|
|
52
53
|
"Category",
|
|
54
|
+
"ComplexFromParts",
|
|
53
55
|
"Concatenate",
|
|
54
56
|
"Description",
|
|
55
57
|
"Exact",
|
|
@@ -6,6 +6,7 @@ from csiphon.pipeline._align_ops import align_on_time
|
|
|
6
6
|
from csiphon.pipeline.measure import GroupCost, MeasuredRun, StepCost
|
|
7
7
|
from csiphon.pipeline.merges import (
|
|
8
8
|
Alignment,
|
|
9
|
+
ComplexFromParts,
|
|
9
10
|
Concatenate,
|
|
10
11
|
Exact,
|
|
11
12
|
Fuse,
|
|
@@ -24,6 +25,7 @@ from csiphon.pipeline.step import PointwiseStep, Step, StreamOperator
|
|
|
24
25
|
|
|
25
26
|
__all__ = [
|
|
26
27
|
"Alignment",
|
|
28
|
+
"ComplexFromParts",
|
|
27
29
|
"Concatenate",
|
|
28
30
|
"Exact",
|
|
29
31
|
"Fuse",
|
|
@@ -16,7 +16,13 @@ from typing import TYPE_CHECKING
|
|
|
16
16
|
|
|
17
17
|
from csiphon.core.signal import Signal
|
|
18
18
|
from csiphon.pipeline.merges import Junction
|
|
19
|
-
from csiphon.pipeline.runners import
|
|
19
|
+
from csiphon.pipeline.runners import (
|
|
20
|
+
Outlets,
|
|
21
|
+
_run_node,
|
|
22
|
+
_seed_inlets,
|
|
23
|
+
last_readers,
|
|
24
|
+
release_spent_lines,
|
|
25
|
+
)
|
|
20
26
|
|
|
21
27
|
if TYPE_CHECKING:
|
|
22
28
|
from csiphon.pipeline.pipeline import Node, Siphon
|
|
@@ -188,11 +194,14 @@ def measure(
|
|
|
188
194
|
tracemalloc.start()
|
|
189
195
|
try:
|
|
190
196
|
env = _seed_inlets(siphon, signals, allow_partial=False)
|
|
197
|
+
last_reader = last_readers(siphon)
|
|
191
198
|
steps: list[StepCost] = []
|
|
192
199
|
for number, node in enumerate(siphon.nodes, start=1):
|
|
193
200
|
inputs = [env[line] for line in node.inputs]
|
|
194
201
|
env[node.output], cost = _measure_node(number, node, inputs, memory)
|
|
195
202
|
steps.append(cost)
|
|
203
|
+
del inputs
|
|
204
|
+
release_spent_lines(env, siphon, node, number - 1, last_reader)
|
|
196
205
|
finally:
|
|
197
206
|
if tracing:
|
|
198
207
|
tracemalloc.stop()
|
|
@@ -211,6 +211,41 @@ class Sum(MergeStrategy):
|
|
|
211
211
|
return as_signal_array(np.sum(np.stack(arrays, axis=0), axis=0))
|
|
212
212
|
|
|
213
213
|
|
|
214
|
+
@dataclass(frozen=True, slots=True)
|
|
215
|
+
class ComplexFromParts(MergeStrategy):
|
|
216
|
+
"""Join a real-part branch and an imaginary-part branch into complex values.
|
|
217
|
+
|
|
218
|
+
The first branch is the real part, the second the imaginary part. Name them at
|
|
219
|
+
the merge so the order is explicit:
|
|
220
|
+
`.merge(["re", "im"], using=ComplexFromParts())`.
|
|
221
|
+
"""
|
|
222
|
+
|
|
223
|
+
def output_layout(self, inputs: SequenceABC[Layout]) -> Layout:
|
|
224
|
+
"""Require two matching real-valued branches; the result is complex."""
|
|
225
|
+
|
|
226
|
+
if len(inputs) != 2:
|
|
227
|
+
raise LayoutError(
|
|
228
|
+
"ComplexFromParts needs exactly two branches (real part, imaginary "
|
|
229
|
+
f"part), got {len(inputs)}."
|
|
230
|
+
)
|
|
231
|
+
_require_matching(inputs)
|
|
232
|
+
if inputs[0].values != ValueKind.REAL:
|
|
233
|
+
raise LayoutError(
|
|
234
|
+
"ComplexFromParts needs real-valued branches, got "
|
|
235
|
+
f"{inputs[0].values.value}."
|
|
236
|
+
)
|
|
237
|
+
return inputs[0].with_values(ValueKind.COMPLEX)
|
|
238
|
+
|
|
239
|
+
def combine(
|
|
240
|
+
self, arrays: SequenceABC[SignalArray], inputs: SequenceABC[Layout]
|
|
241
|
+
) -> SignalArray:
|
|
242
|
+
"""Return `real + 1j * imaginary`."""
|
|
243
|
+
|
|
244
|
+
del inputs
|
|
245
|
+
real, imaginary = arrays
|
|
246
|
+
return as_signal_array(real + 1j * imaginary)
|
|
247
|
+
|
|
248
|
+
|
|
214
249
|
def _unify_values(kinds: SequenceABC[ValueKind]) -> ValueKind:
|
|
215
250
|
"""The value kind of fused branches: keep it if shared, else a common numeric kind.
|
|
216
251
|
|
|
@@ -150,12 +150,42 @@ def pour(siphon: Siphon, signals: Signal | Mapping[str, Signal]) -> Outlets:
|
|
|
150
150
|
"""Run a whole recording through a siphon and return every outlet."""
|
|
151
151
|
|
|
152
152
|
env = _seed_inlets(siphon, signals, allow_partial=False)
|
|
153
|
-
|
|
153
|
+
last_reader = last_readers(siphon)
|
|
154
|
+
for index, node in enumerate(siphon.nodes):
|
|
154
155
|
inputs = [env[line] for line in node.inputs]
|
|
155
156
|
env[node.output] = _run_node(node, inputs)
|
|
157
|
+
del inputs
|
|
158
|
+
release_spent_lines(env, siphon, node, index, last_reader)
|
|
156
159
|
return Outlets({display: env[line] for display, line in siphon.outlets.items()})
|
|
157
160
|
|
|
158
161
|
|
|
162
|
+
def last_readers(siphon: Siphon) -> dict[str, int]:
|
|
163
|
+
"""For every line, the index of the last node that reads it."""
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
line: index for index, node in enumerate(siphon.nodes) for line in node.inputs
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def release_spent_lines(
|
|
171
|
+
env: dict[str, Signal],
|
|
172
|
+
siphon: Siphon,
|
|
173
|
+
node: Node,
|
|
174
|
+
index: int,
|
|
175
|
+
last_reader: Mapping[str, int],
|
|
176
|
+
) -> None:
|
|
177
|
+
"""Drop this node's input lines that no later node reads and no outlet shows.
|
|
178
|
+
|
|
179
|
+
A whole-recording run would otherwise hold every intermediate signal until the
|
|
180
|
+
end; for wide signals (many channels, spectrograms) that multiplies the memory.
|
|
181
|
+
"""
|
|
182
|
+
|
|
183
|
+
outlet_lines = set(siphon.outlets.values())
|
|
184
|
+
for line in node.inputs:
|
|
185
|
+
if last_reader[line] == index and line not in outlet_lines:
|
|
186
|
+
env.pop(line, None)
|
|
187
|
+
|
|
188
|
+
|
|
159
189
|
def _run_node(node: Node, inputs: list[Signal]) -> Signal:
|
|
160
190
|
"""Run one node in batch: a junction takes all inputs, a step takes the one."""
|
|
161
191
|
|
|
@@ -15,7 +15,14 @@ numpy-only.
|
|
|
15
15
|
from csiphon.steps.baseline import RunningMeanSubtract, TemporalMeanSubtract
|
|
16
16
|
from csiphon.steps.calibration import AxisReference, LinearPhaseCorrection
|
|
17
17
|
from csiphon.steps.cleaning import NanScrub, NoiseFloorClip
|
|
18
|
-
from csiphon.steps.components import
|
|
18
|
+
from csiphon.steps.components import (
|
|
19
|
+
ImagPart,
|
|
20
|
+
Magnitude,
|
|
21
|
+
Phase,
|
|
22
|
+
Power,
|
|
23
|
+
RealPart,
|
|
24
|
+
UnitPhase,
|
|
25
|
+
)
|
|
19
26
|
from csiphon.steps.delay import (
|
|
20
27
|
ChannelImpulseResponse,
|
|
21
28
|
DelayAutocorrelation,
|
|
@@ -89,6 +96,7 @@ __all__ = [
|
|
|
89
96
|
"GainNormalize",
|
|
90
97
|
"GlobalMaxNormalize",
|
|
91
98
|
"Hold",
|
|
99
|
+
"ImagPart",
|
|
92
100
|
"Independent",
|
|
93
101
|
"Linear",
|
|
94
102
|
"LinearPhaseCorrection",
|
|
@@ -107,6 +115,7 @@ __all__ = [
|
|
|
107
115
|
"PolarLinear",
|
|
108
116
|
"Power",
|
|
109
117
|
"PrincipalComponents",
|
|
118
|
+
"RealPart",
|
|
110
119
|
"Resample",
|
|
111
120
|
"RobustPca",
|
|
112
121
|
"RunningMeanSubtract",
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
"""Component steps: a direct per-value component (magnitude, phase, power)."""
|
|
1
|
+
"""Component steps: a direct per-value component (magnitude, phase, power, parts)."""
|
|
2
2
|
|
|
3
|
+
from csiphon.steps.components.imag_part import ImagPart
|
|
3
4
|
from csiphon.steps.components.magnitude import Magnitude
|
|
4
5
|
from csiphon.steps.components.phase import Phase
|
|
5
6
|
from csiphon.steps.components.power import Power
|
|
7
|
+
from csiphon.steps.components.real_part import RealPart
|
|
6
8
|
from csiphon.steps.components.unit_phase import UnitPhase
|
|
7
9
|
|
|
8
|
-
__all__ = ["Magnitude", "Phase", "Power", "UnitPhase"]
|
|
10
|
+
__all__ = ["ImagPart", "Magnitude", "Phase", "Power", "RealPart", "UnitPhase"]
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Element-wise imaginary part."""
|
|
2
|
+
|
|
3
|
+
# Same spec and method skeleton as RealPart and the other one-component steps by
|
|
4
|
+
# design; only the part taken differs.
|
|
5
|
+
# pylint: disable=duplicate-code
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import ClassVar
|
|
9
|
+
|
|
10
|
+
from csiphon.core.arrays import SignalArray, as_real_array
|
|
11
|
+
from csiphon.core.layout import Layout
|
|
12
|
+
from csiphon.core.profile import AcquisitionProfile
|
|
13
|
+
from csiphon.core.semantics import ValueKind
|
|
14
|
+
from csiphon.core.signal import Signal
|
|
15
|
+
from csiphon.pipeline.step import PointwiseStep
|
|
16
|
+
from csiphon.spec import Category, LayoutEffect, StepSpec, Streaming
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True, slots=True)
|
|
20
|
+
class ImagPart(PointwiseStep):
|
|
21
|
+
"""Take the element-wise imaginary part.
|
|
22
|
+
|
|
23
|
+
With `RealPart` and the `ComplexFromParts` merge, this runs a real-only step
|
|
24
|
+
on complex CSI part by part.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
spec: ClassVar[StepSpec] = StepSpec(
|
|
28
|
+
name="imag-part",
|
|
29
|
+
summary="take the imaginary part",
|
|
30
|
+
category=Category.COMPONENTS,
|
|
31
|
+
admissible_values=(ValueKind.COMPLEX,),
|
|
32
|
+
admissible_reprs=None,
|
|
33
|
+
requires_axes=(),
|
|
34
|
+
layout_effect=LayoutEffect(value_kind=ValueKind.REAL),
|
|
35
|
+
streaming=Streaming.BATCH_EQUIVALENT,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def output_layout(self, layout: Layout, profile: AcquisitionProfile) -> Layout:
|
|
39
|
+
"""Complex input becomes real-valued."""
|
|
40
|
+
|
|
41
|
+
self.require_inputs(layout)
|
|
42
|
+
return layout.with_values(ValueKind.REAL)
|
|
43
|
+
|
|
44
|
+
def transform_values(
|
|
45
|
+
self,
|
|
46
|
+
values: SignalArray,
|
|
47
|
+
signal: Signal,
|
|
48
|
+
out_layout: Layout,
|
|
49
|
+
profile: AcquisitionProfile,
|
|
50
|
+
) -> SignalArray:
|
|
51
|
+
"""Return `values.imag`."""
|
|
52
|
+
|
|
53
|
+
return as_real_array(values.imag)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Element-wise real part."""
|
|
2
|
+
|
|
3
|
+
# Same spec and method skeleton as ImagPart and the other one-component steps by
|
|
4
|
+
# design; only the part taken differs.
|
|
5
|
+
# pylint: disable=duplicate-code
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import ClassVar
|
|
9
|
+
|
|
10
|
+
from csiphon.core.arrays import SignalArray, as_real_array
|
|
11
|
+
from csiphon.core.layout import Layout
|
|
12
|
+
from csiphon.core.profile import AcquisitionProfile
|
|
13
|
+
from csiphon.core.semantics import ValueKind
|
|
14
|
+
from csiphon.core.signal import Signal
|
|
15
|
+
from csiphon.pipeline.step import PointwiseStep
|
|
16
|
+
from csiphon.spec import Category, LayoutEffect, StepSpec, Streaming
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True, slots=True)
|
|
20
|
+
class RealPart(PointwiseStep):
|
|
21
|
+
"""Take the element-wise real part.
|
|
22
|
+
|
|
23
|
+
With `ImagPart` and the `ComplexFromParts` merge, this runs a real-only step
|
|
24
|
+
on complex CSI part by part.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
spec: ClassVar[StepSpec] = StepSpec(
|
|
28
|
+
name="real-part",
|
|
29
|
+
summary="take the real part",
|
|
30
|
+
category=Category.COMPONENTS,
|
|
31
|
+
admissible_values=(ValueKind.COMPLEX,),
|
|
32
|
+
admissible_reprs=None,
|
|
33
|
+
requires_axes=(),
|
|
34
|
+
layout_effect=LayoutEffect(value_kind=ValueKind.REAL),
|
|
35
|
+
streaming=Streaming.BATCH_EQUIVALENT,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def output_layout(self, layout: Layout, profile: AcquisitionProfile) -> Layout:
|
|
39
|
+
"""Complex input becomes real-valued."""
|
|
40
|
+
|
|
41
|
+
self.require_inputs(layout)
|
|
42
|
+
return layout.with_values(ValueKind.REAL)
|
|
43
|
+
|
|
44
|
+
def transform_values(
|
|
45
|
+
self,
|
|
46
|
+
values: SignalArray,
|
|
47
|
+
signal: Signal,
|
|
48
|
+
out_layout: Layout,
|
|
49
|
+
profile: AcquisitionProfile,
|
|
50
|
+
) -> SignalArray:
|
|
51
|
+
"""Return `values.real`."""
|
|
52
|
+
|
|
53
|
+
return as_real_array(values.real)
|
|
@@ -15,7 +15,7 @@ from typing import ClassVar, cast
|
|
|
15
15
|
|
|
16
16
|
import numpy as np
|
|
17
17
|
|
|
18
|
-
from csiphon.core.arrays import RealArray,
|
|
18
|
+
from csiphon.core.arrays import RealArray, SignalArray, as_signal_array
|
|
19
19
|
from csiphon.core.axes import AxisName
|
|
20
20
|
from csiphon.core.errors import LayoutError, MissingDependencyError
|
|
21
21
|
from csiphon.core.layout import Layout
|
|
@@ -71,7 +71,9 @@ class ButterworthFilter(Step):
|
|
|
71
71
|
"""Butterworth filter along the time axis.
|
|
72
72
|
|
|
73
73
|
`cutoff_hz` is a scalar for `low` / `high` and a `(low, high)` pair
|
|
74
|
-
for `band`. Batch is zero-phase; streaming is causal.
|
|
74
|
+
for `band`. Batch is zero-phase; streaming is causal. Complex input is filtered
|
|
75
|
+
on its real and imaginary parts, the same filter on each. The explicit form is
|
|
76
|
+
`RealPart` / `ImagPart` branches merged with `ComplexFromParts`.
|
|
75
77
|
"""
|
|
76
78
|
|
|
77
79
|
cutoff_hz: float | tuple[float, float] = field(
|
|
@@ -87,7 +89,12 @@ class ButterworthFilter(Step):
|
|
|
87
89
|
name="butterworth-filter",
|
|
88
90
|
summary="apply a Butterworth filter along time (zero-phase batch, causal stream)", # noqa: E501
|
|
89
91
|
category=Category.FILTERING,
|
|
90
|
-
admissible_values=(
|
|
92
|
+
admissible_values=(
|
|
93
|
+
ValueKind.COMPLEX,
|
|
94
|
+
ValueKind.REAL,
|
|
95
|
+
ValueKind.MAGNITUDE,
|
|
96
|
+
ValueKind.POWER,
|
|
97
|
+
),
|
|
91
98
|
admissible_reprs=None,
|
|
92
99
|
requires_axes=(AxisName.TIME,),
|
|
93
100
|
layout_effect=LayoutEffect(),
|
|
@@ -138,7 +145,7 @@ class ButterworthFilter(Step):
|
|
|
138
145
|
self.order, self.cutoff_hz, self.btype, rate_hz
|
|
139
146
|
)
|
|
140
147
|
filtered = scipy_signal.filtfilt(
|
|
141
|
-
numerator, denominator,
|
|
148
|
+
numerator, denominator, as_signal_array(signal.values), axis=time_index
|
|
142
149
|
)
|
|
143
150
|
return signal.with_values(filtered, out_layout)
|
|
144
151
|
|
|
@@ -166,7 +173,7 @@ class _CausalFilter(StreamOperator):
|
|
|
166
173
|
self._numerator = numerator
|
|
167
174
|
self._denominator = denominator
|
|
168
175
|
self._out_layout = out_layout
|
|
169
|
-
self._state:
|
|
176
|
+
self._state: SignalArray | None = None
|
|
170
177
|
|
|
171
178
|
def push(self, chunk: Signal) -> Signal:
|
|
172
179
|
"""Filter this chunk causally, updating the filter state."""
|
|
@@ -176,7 +183,7 @@ class _CausalFilter(StreamOperator):
|
|
|
176
183
|
return empty_signal(self._out_layout)
|
|
177
184
|
|
|
178
185
|
scipy_signal = _scipy_signal()
|
|
179
|
-
data = np.moveaxis(
|
|
186
|
+
data = np.moveaxis(as_signal_array(chunk.values), time_index, 0)
|
|
180
187
|
flat = data.reshape(data.shape[0], -1)
|
|
181
188
|
|
|
182
189
|
if self._state is None:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: csiphon
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: An online-first, schema-validated CSI preprocessing pipeline library.
|
|
5
5
|
Author: Fabian Portner
|
|
6
6
|
License-Expression: MIT
|
|
@@ -49,16 +49,17 @@ Dynamic: license-file
|
|
|
49
49
|
|
|
50
50
|
# csiphon
|
|
51
51
|
|
|
52
|
-
`csiphon` is
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
`csiphon` is a Python library for preprocessing WiFi Channel State Information.
|
|
53
|
+
You plug DSP steps together into a pipeline, csiphon checks that the pieces fit
|
|
54
|
+
before you run anything, and the finished pipeline handles recordings and live
|
|
55
|
+
streams alike. Every signal carries its axes by name, so you never have to
|
|
56
|
+
wonder which one was the subcarrier again.
|
|
56
57
|
|
|
57
58
|
```python
|
|
58
59
|
from csiphon import AcquisitionProfile, Pipeline
|
|
59
60
|
from csiphon.steps import GainNormalize, Magnitude, WindowedVariance
|
|
60
61
|
|
|
61
|
-
#
|
|
62
|
+
# Describe the capture setup
|
|
62
63
|
profile = AcquisitionProfile(
|
|
63
64
|
n_rx_antennas=3,
|
|
64
65
|
subcarrier_indices=tuple(range(52)),
|
|
@@ -81,6 +82,10 @@ print(siphon.describe())
|
|
|
81
82
|
features = siphon.pour(profile.raw_signal(csi, timestamps)).single()
|
|
82
83
|
```
|
|
83
84
|
|
|
85
|
+
A compiled pipeline is a `Siphon`. You `pour()` a recording through it or
|
|
86
|
+
`stream()` live data, and collect results from named `Outlets`, which you can tap
|
|
87
|
+
anywhere along the pipeline.
|
|
88
|
+
|
|
84
89
|
## What it does
|
|
85
90
|
|
|
86
91
|
- **Catches structural mistakes before execution.** Compilation checks axes,
|
|
@@ -91,29 +96,38 @@ features = siphon.pour(profile.raw_signal(csi, timestamps)).single()
|
|
|
91
96
|
complete recording.
|
|
92
97
|
- **Supports branching pipelines.** Split into parallel feature paths, merge
|
|
93
98
|
them again, and expose named intermediate outlets.
|
|
99
|
+
- **Merges receivers.** Feed several receivers through their own inlets and line
|
|
100
|
+
them up on timestamps or on packet sequence numbers, which survive clock drift.
|
|
94
101
|
- **Keeps real timestamps.** Non-uniform CSI sampling is expected; resampling is
|
|
95
|
-
explicit rather than silently assumed.
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
102
|
+
explicit rather than silently assumed. You can also drop packets on purpose,
|
|
103
|
+
with independent or bursty loss, to test how a pipeline copes.
|
|
104
|
+
- **Describes itself.** `describe()` draws a compiled siphon as a data-flow graph
|
|
105
|
+
in the terminal. Steps and siphons expose their contracts, layouts, parameters,
|
|
106
|
+
and streaming behavior, and can save that information alongside results for
|
|
107
|
+
reproducibility.
|
|
99
108
|
- **Measures itself.** `siphon.measure(signal)` pours once and reports each
|
|
100
109
|
step's time and output shape (memory on request), per step or for a named
|
|
101
110
|
group of consecutive steps.
|
|
102
111
|
|
|
103
|
-
The built-in steps cover
|
|
104
|
-
|
|
105
|
-
|
|
112
|
+
The built-in steps cover components, scaling, cleaning, calibration,
|
|
113
|
+
normalization, baseline removal, filtering, temporal features, delay and
|
|
114
|
+
time-frequency transforms, pooling, statistics, reduction, restructuring, and
|
|
115
|
+
resampling. The
|
|
116
|
+
[steps README](https://github.com/nzqo/csiphon/blob/main/src/csiphon/steps/README.md)
|
|
117
|
+
lists the categories. Custom steps use the same contracts and inspection tools.
|
|
106
118
|
|
|
107
119
|
## Install
|
|
108
120
|
|
|
109
121
|
```bash
|
|
110
|
-
pip install
|
|
122
|
+
pip install csiphon
|
|
111
123
|
```
|
|
112
124
|
|
|
113
|
-
The core only depends on NumPy.
|
|
125
|
+
The core only depends on NumPy. A few steps (filters, STFT, multitaper,
|
|
126
|
+
cubic-spline resampling) need the `[filters]` extra (SciPy), and the
|
|
127
|
+
synchrosqueezed transform needs `[sst]`. Install everything with:
|
|
114
128
|
|
|
115
129
|
```bash
|
|
116
|
-
pip install
|
|
130
|
+
pip install "csiphon[all]"
|
|
117
131
|
```
|
|
118
132
|
|
|
119
|
-
Runnable recipes live in [`examples/`](examples
|
|
133
|
+
Runnable recipes live in [`examples/`](https://github.com/nzqo/csiphon/tree/main/examples).
|