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.
Files changed (130) hide show
  1. {csiphon-0.2.0/src/csiphon.egg-info → csiphon-0.3.0}/PKG-INFO +31 -17
  2. csiphon-0.3.0/README.md +92 -0
  3. {csiphon-0.2.0 → csiphon-0.3.0}/pyproject.toml +1 -1
  4. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/__init__.py +2 -0
  5. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/pipeline/__init__.py +2 -0
  6. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/pipeline/measure.py +10 -1
  7. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/pipeline/merges.py +35 -0
  8. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/pipeline/runners.py +31 -1
  9. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/__init__.py +10 -1
  10. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/components/__init__.py +4 -2
  11. csiphon-0.3.0/src/csiphon/steps/components/imag_part.py +53 -0
  12. csiphon-0.3.0/src/csiphon/steps/components/real_part.py +53 -0
  13. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/filtering/butterworth_filter.py +13 -6
  14. {csiphon-0.2.0 → csiphon-0.3.0/src/csiphon.egg-info}/PKG-INFO +31 -17
  15. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon.egg-info/SOURCES.txt +3 -0
  16. {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_branching.py +18 -0
  17. csiphon-0.3.0/tests/test_complex_parts.py +131 -0
  18. {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_measure.py +18 -0
  19. {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_steps_batch.py +14 -0
  20. csiphon-0.2.0/README.md +0 -78
  21. {csiphon-0.2.0 → csiphon-0.3.0}/LICENSE +0 -0
  22. {csiphon-0.2.0 → csiphon-0.3.0}/setup.cfg +0 -0
  23. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/core/__init__.py +0 -0
  24. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/core/arrays.py +0 -0
  25. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/core/axes.py +0 -0
  26. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/core/errors.py +0 -0
  27. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/core/layout.py +0 -0
  28. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/core/profile.py +0 -0
  29. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/core/sampling.py +0 -0
  30. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/core/semantics.py +0 -0
  31. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/core/signal.py +0 -0
  32. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/graphics/__init__.py +0 -0
  33. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/graphics/style.py +0 -0
  34. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/graphics/tree.py +0 -0
  35. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/inspection.py +0 -0
  36. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/pipeline/_align_ops.py +0 -0
  37. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/pipeline/_lines.py +0 -0
  38. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/pipeline/pipeline.py +0 -0
  39. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/pipeline/sequence.py +0 -0
  40. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/pipeline/step.py +0 -0
  41. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/py.typed +0 -0
  42. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/spec.py +0 -0
  43. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/_support/__init__.py +0 -0
  44. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/_support/broadcasting.py +0 -0
  45. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/_support/windowing.py +0 -0
  46. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/baseline/__init__.py +0 -0
  47. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/baseline/running_mean_subtract.py +0 -0
  48. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/baseline/temporal_mean_subtract.py +0 -0
  49. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/calibration/__init__.py +0 -0
  50. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/calibration/axis_reference.py +0 -0
  51. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/calibration/linear_phase_correction.py +0 -0
  52. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/cleaning/__init__.py +0 -0
  53. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/cleaning/nan_scrub.py +0 -0
  54. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/cleaning/noise_floor_clip.py +0 -0
  55. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/components/magnitude.py +0 -0
  56. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/components/phase.py +0 -0
  57. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/components/power.py +0 -0
  58. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/components/unit_phase.py +0 -0
  59. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/delay/__init__.py +0 -0
  60. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/delay/channel_impulse_response.py +0 -0
  61. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/delay/delay_autocorrelation.py +0 -0
  62. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/filtering/__init__.py +0 -0
  63. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/filtering/savitzky_golay.py +0 -0
  64. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/normalization/__init__.py +0 -0
  65. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/normalization/gain_normalize.py +0 -0
  66. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/normalization/global_max_normalize.py +0 -0
  67. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/normalization/per_frame_max_normalize.py +0 -0
  68. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/pooling/__init__.py +0 -0
  69. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/pooling/dyadic_frequency_bands.py +0 -0
  70. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/pooling/fixed_size_window_sum.py +0 -0
  71. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/pooling/mean_over_axes.py +0 -0
  72. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/reduction/__init__.py +0 -0
  73. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/reduction/delay_taps.py +0 -0
  74. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/reduction/principal_components.py +0 -0
  75. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/reduction/robust_pca.py +0 -0
  76. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/reduction/select_axis.py +0 -0
  77. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/resampling/__init__.py +0 -0
  78. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/resampling/drop_samples.py +0 -0
  79. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/resampling/resample.py +0 -0
  80. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/resampling/subsample.py +0 -0
  81. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/restructuring/__init__.py +0 -0
  82. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/restructuring/fold_axes.py +0 -0
  83. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/restructuring/stack_amp_phase.py +0 -0
  84. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/scaling/__init__.py +0 -0
  85. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/scaling/log_scale.py +0 -0
  86. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/scaling/to_decibels.py +0 -0
  87. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/statistics/__init__.py +0 -0
  88. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/statistics/covariance_spectrum.py +0 -0
  89. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/statistics/local_pca_bias.py +0 -0
  90. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/temporal_features/__init__.py +0 -0
  91. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/temporal_features/time_difference.py +0 -0
  92. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/temporal_features/windowed_slope.py +0 -0
  93. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/temporal_features/windowed_variance.py +0 -0
  94. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/time_frequency/__init__.py +0 -0
  95. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/time_frequency/complex_stft.py +0 -0
  96. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/time_frequency/multitaper.py +0 -0
  97. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/time_frequency/synchrosqueezed_power.py +0 -0
  98. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon/steps/time_frequency/windowed_fft_power.py +0 -0
  99. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon.egg-info/dependency_links.txt +0 -0
  100. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon.egg-info/requires.txt +0 -0
  101. {csiphon-0.2.0 → csiphon-0.3.0}/src/csiphon.egg-info/top_level.txt +0 -0
  102. {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_alignment.py +0 -0
  103. {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_calibration.py +0 -0
  104. {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_drop_samples.py +0 -0
  105. {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_fuse.py +0 -0
  106. {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_golden_sst_pipeline.py +0 -0
  107. {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_graphics.py +0 -0
  108. {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_guards_filtering.py +0 -0
  109. {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_guards_reduction.py +0 -0
  110. {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_guards_spectral.py +0 -0
  111. {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_inspection.py +0 -0
  112. {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_metadata.py +0 -0
  113. {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_multi_inlet.py +0 -0
  114. {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_pipeline.py +0 -0
  115. {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_pooling.py +0 -0
  116. {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_ported_misc.py +0 -0
  117. {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_profile.py +0 -0
  118. {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_reduce.py +0 -0
  119. {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_resample.py +0 -0
  120. {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_sampling.py +0 -0
  121. {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_sequence_alignment.py +0 -0
  122. {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_spectral_extra.py +0 -0
  123. {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_statistics.py +0 -0
  124. {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_step_contracts.py +0 -0
  125. {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_step_edges.py +0 -0
  126. {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_streaming.py +0 -0
  127. {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_structural_axes.py +0 -0
  128. {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_subsample.py +0 -0
  129. {csiphon-0.2.0 → csiphon-0.3.0}/tests/test_temporal_features.py +0 -0
  130. {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.2.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 an online-first processing library for WiFi Channel State
53
- Information. Compose reusable DSP steps, compile them against your capture
54
- setup, then pour a complete recording or stream live chunks through the same
55
- validated pipeline.
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
- # Define the hardware setup
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
- - **Describes itself.** Steps and compiled siphons expose their contracts,
97
- layouts, parameters, and streaming behavior, and can save that information
98
- alongside results for reproducibility.
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 calibration, cleaning, filtering, delay and
104
- time-frequency transforms, temporal features, pooling, statistics, reduction,
105
- and restructuring. Custom steps use the same contracts and inspection tools.
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 -e .
122
+ pip install csiphon
111
123
  ```
112
124
 
113
- The core only depends on NumPy. Install every optional transform with:
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 -e ".[all]"
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).
@@ -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).
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "csiphon"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  description = "An online-first, schema-validated CSI preprocessing pipeline library."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -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 Outlets, _run_node, _seed_inlets
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
- for node in siphon.nodes:
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 Magnitude, Phase, Power, UnitPhase
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, as_real_array
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=(ValueKind.REAL, ValueKind.MAGNITUDE, ValueKind.POWER),
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, as_real_array(signal.values), axis=time_index
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: RealArray | None = None
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(as_real_array(chunk.values), time_index, 0)
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.2.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 an online-first processing library for WiFi Channel State
53
- Information. Compose reusable DSP steps, compile them against your capture
54
- setup, then pour a complete recording or stream live chunks through the same
55
- validated pipeline.
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
- # Define the hardware setup
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
- - **Describes itself.** Steps and compiled siphons expose their contracts,
97
- layouts, parameters, and streaming behavior, and can save that information
98
- alongside results for reproducibility.
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 calibration, cleaning, filtering, delay and
104
- time-frequency transforms, temporal features, pooling, statistics, reduction,
105
- and restructuring. Custom steps use the same contracts and inspection tools.
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 -e .
122
+ pip install csiphon
111
123
  ```
112
124
 
113
- The core only depends on NumPy. Install every optional transform with:
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 -e ".[all]"
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).