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.
Files changed (124) hide show
  1. csiphon-0.1.0/LICENSE +21 -0
  2. csiphon-0.1.0/PKG-INFO +109 -0
  3. csiphon-0.1.0/README.md +69 -0
  4. csiphon-0.1.0/pyproject.toml +101 -0
  5. csiphon-0.1.0/setup.cfg +4 -0
  6. csiphon-0.1.0/src/csiphon/__init__.py +80 -0
  7. csiphon-0.1.0/src/csiphon/core/__init__.py +62 -0
  8. csiphon-0.1.0/src/csiphon/core/arrays.py +31 -0
  9. csiphon-0.1.0/src/csiphon/core/axes.py +162 -0
  10. csiphon-0.1.0/src/csiphon/core/errors.py +35 -0
  11. csiphon-0.1.0/src/csiphon/core/layout.py +211 -0
  12. csiphon-0.1.0/src/csiphon/core/profile.py +155 -0
  13. csiphon-0.1.0/src/csiphon/core/sampling.py +87 -0
  14. csiphon-0.1.0/src/csiphon/core/semantics.py +74 -0
  15. csiphon-0.1.0/src/csiphon/core/signal.py +158 -0
  16. csiphon-0.1.0/src/csiphon/graphics/__init__.py +27 -0
  17. csiphon-0.1.0/src/csiphon/graphics/style.py +200 -0
  18. csiphon-0.1.0/src/csiphon/graphics/tree.py +717 -0
  19. csiphon-0.1.0/src/csiphon/inspection.py +64 -0
  20. csiphon-0.1.0/src/csiphon/pipeline/__init__.py +49 -0
  21. csiphon-0.1.0/src/csiphon/pipeline/_align_ops.py +137 -0
  22. csiphon-0.1.0/src/csiphon/pipeline/_lines.py +11 -0
  23. csiphon-0.1.0/src/csiphon/pipeline/merges.py +647 -0
  24. csiphon-0.1.0/src/csiphon/pipeline/pipeline.py +591 -0
  25. csiphon-0.1.0/src/csiphon/pipeline/runners.py +267 -0
  26. csiphon-0.1.0/src/csiphon/pipeline/sequence.py +407 -0
  27. csiphon-0.1.0/src/csiphon/pipeline/step.py +222 -0
  28. csiphon-0.1.0/src/csiphon/py.typed +0 -0
  29. csiphon-0.1.0/src/csiphon/spec.py +833 -0
  30. csiphon-0.1.0/src/csiphon/steps/__init__.py +131 -0
  31. csiphon-0.1.0/src/csiphon/steps/_support/__init__.py +1 -0
  32. csiphon-0.1.0/src/csiphon/steps/_support/broadcasting.py +68 -0
  33. csiphon-0.1.0/src/csiphon/steps/_support/windowing.py +217 -0
  34. csiphon-0.1.0/src/csiphon/steps/baseline/__init__.py +8 -0
  35. csiphon-0.1.0/src/csiphon/steps/baseline/running_mean_subtract.py +110 -0
  36. csiphon-0.1.0/src/csiphon/steps/baseline/temporal_mean_subtract.py +59 -0
  37. csiphon-0.1.0/src/csiphon/steps/calibration/__init__.py +11 -0
  38. csiphon-0.1.0/src/csiphon/steps/calibration/axis_reference.py +198 -0
  39. csiphon-0.1.0/src/csiphon/steps/calibration/linear_phase_correction.py +222 -0
  40. csiphon-0.1.0/src/csiphon/steps/cleaning/__init__.py +8 -0
  41. csiphon-0.1.0/src/csiphon/steps/cleaning/nan_scrub.py +49 -0
  42. csiphon-0.1.0/src/csiphon/steps/cleaning/noise_floor_clip.py +54 -0
  43. csiphon-0.1.0/src/csiphon/steps/components/__init__.py +10 -0
  44. csiphon-0.1.0/src/csiphon/steps/components/magnitude.py +49 -0
  45. csiphon-0.1.0/src/csiphon/steps/components/phase.py +49 -0
  46. csiphon-0.1.0/src/csiphon/steps/components/power.py +51 -0
  47. csiphon-0.1.0/src/csiphon/steps/components/unit_phase.py +53 -0
  48. csiphon-0.1.0/src/csiphon/steps/delay/__init__.py +8 -0
  49. csiphon-0.1.0/src/csiphon/steps/delay/channel_impulse_response.py +113 -0
  50. csiphon-0.1.0/src/csiphon/steps/delay/delay_autocorrelation.py +116 -0
  51. csiphon-0.1.0/src/csiphon/steps/filtering/__init__.py +8 -0
  52. csiphon-0.1.0/src/csiphon/steps/filtering/butterworth_filter.py +197 -0
  53. csiphon-0.1.0/src/csiphon/steps/filtering/savitzky_golay.py +98 -0
  54. csiphon-0.1.0/src/csiphon/steps/normalization/__init__.py +13 -0
  55. csiphon-0.1.0/src/csiphon/steps/normalization/gain_normalize.py +76 -0
  56. csiphon-0.1.0/src/csiphon/steps/normalization/global_max_normalize.py +61 -0
  57. csiphon-0.1.0/src/csiphon/steps/normalization/per_frame_max_normalize.py +56 -0
  58. csiphon-0.1.0/src/csiphon/steps/pooling/__init__.py +9 -0
  59. csiphon-0.1.0/src/csiphon/steps/pooling/dyadic_frequency_bands.py +106 -0
  60. csiphon-0.1.0/src/csiphon/steps/pooling/fixed_size_window_sum.py +111 -0
  61. csiphon-0.1.0/src/csiphon/steps/pooling/mean_over_axes.py +92 -0
  62. csiphon-0.1.0/src/csiphon/steps/reduction/__init__.py +22 -0
  63. csiphon-0.1.0/src/csiphon/steps/reduction/delay_taps.py +89 -0
  64. csiphon-0.1.0/src/csiphon/steps/reduction/principal_components.py +178 -0
  65. csiphon-0.1.0/src/csiphon/steps/reduction/robust_pca.py +221 -0
  66. csiphon-0.1.0/src/csiphon/steps/reduction/select_axis.py +92 -0
  67. csiphon-0.1.0/src/csiphon/steps/resampling/__init__.py +35 -0
  68. csiphon-0.1.0/src/csiphon/steps/resampling/drop_samples.py +161 -0
  69. csiphon-0.1.0/src/csiphon/steps/resampling/resample.py +374 -0
  70. csiphon-0.1.0/src/csiphon/steps/resampling/subsample.py +103 -0
  71. csiphon-0.1.0/src/csiphon/steps/restructuring/__init__.py +8 -0
  72. csiphon-0.1.0/src/csiphon/steps/restructuring/fold_axes.py +139 -0
  73. csiphon-0.1.0/src/csiphon/steps/restructuring/stack_amp_phase.py +75 -0
  74. csiphon-0.1.0/src/csiphon/steps/scaling/__init__.py +8 -0
  75. csiphon-0.1.0/src/csiphon/steps/scaling/log_scale.py +58 -0
  76. csiphon-0.1.0/src/csiphon/steps/scaling/to_decibels.py +60 -0
  77. csiphon-0.1.0/src/csiphon/steps/statistics/__init__.py +8 -0
  78. csiphon-0.1.0/src/csiphon/steps/statistics/covariance_spectrum.py +197 -0
  79. csiphon-0.1.0/src/csiphon/steps/statistics/local_pca_bias.py +162 -0
  80. csiphon-0.1.0/src/csiphon/steps/temporal_features/__init__.py +9 -0
  81. csiphon-0.1.0/src/csiphon/steps/temporal_features/time_difference.py +160 -0
  82. csiphon-0.1.0/src/csiphon/steps/temporal_features/windowed_slope.py +149 -0
  83. csiphon-0.1.0/src/csiphon/steps/temporal_features/windowed_variance.py +149 -0
  84. csiphon-0.1.0/src/csiphon/steps/time_frequency/__init__.py +15 -0
  85. csiphon-0.1.0/src/csiphon/steps/time_frequency/complex_stft.py +193 -0
  86. csiphon-0.1.0/src/csiphon/steps/time_frequency/multitaper.py +213 -0
  87. csiphon-0.1.0/src/csiphon/steps/time_frequency/synchrosqueezed_power.py +230 -0
  88. csiphon-0.1.0/src/csiphon/steps/time_frequency/windowed_fft_power.py +152 -0
  89. csiphon-0.1.0/src/csiphon.egg-info/PKG-INFO +109 -0
  90. csiphon-0.1.0/src/csiphon.egg-info/SOURCES.txt +122 -0
  91. csiphon-0.1.0/src/csiphon.egg-info/dependency_links.txt +1 -0
  92. csiphon-0.1.0/src/csiphon.egg-info/requires.txt +22 -0
  93. csiphon-0.1.0/src/csiphon.egg-info/top_level.txt +1 -0
  94. csiphon-0.1.0/tests/test_alignment.py +369 -0
  95. csiphon-0.1.0/tests/test_branching.py +392 -0
  96. csiphon-0.1.0/tests/test_calibration.py +93 -0
  97. csiphon-0.1.0/tests/test_drop_samples.py +190 -0
  98. csiphon-0.1.0/tests/test_fuse.py +216 -0
  99. csiphon-0.1.0/tests/test_graphics.py +45 -0
  100. csiphon-0.1.0/tests/test_guards_filtering.py +211 -0
  101. csiphon-0.1.0/tests/test_guards_reduction.py +239 -0
  102. csiphon-0.1.0/tests/test_guards_spectral.py +178 -0
  103. csiphon-0.1.0/tests/test_inspection.py +91 -0
  104. csiphon-0.1.0/tests/test_metadata.py +55 -0
  105. csiphon-0.1.0/tests/test_multi_inlet.py +295 -0
  106. csiphon-0.1.0/tests/test_parity_cpd.py +131 -0
  107. csiphon-0.1.0/tests/test_pipeline.py +58 -0
  108. csiphon-0.1.0/tests/test_pooling.py +272 -0
  109. csiphon-0.1.0/tests/test_ported_misc.py +181 -0
  110. csiphon-0.1.0/tests/test_profile.py +73 -0
  111. csiphon-0.1.0/tests/test_reduce.py +79 -0
  112. csiphon-0.1.0/tests/test_resample.py +382 -0
  113. csiphon-0.1.0/tests/test_sampling.py +147 -0
  114. csiphon-0.1.0/tests/test_sequence_alignment.py +266 -0
  115. csiphon-0.1.0/tests/test_spectral_extra.py +665 -0
  116. csiphon-0.1.0/tests/test_statistics.py +177 -0
  117. csiphon-0.1.0/tests/test_step_contracts.py +578 -0
  118. csiphon-0.1.0/tests/test_step_edges.py +234 -0
  119. csiphon-0.1.0/tests/test_steps_batch.py +194 -0
  120. csiphon-0.1.0/tests/test_streaming.py +180 -0
  121. csiphon-0.1.0/tests/test_structural_axes.py +111 -0
  122. csiphon-0.1.0/tests/test_subsample.py +67 -0
  123. csiphon-0.1.0/tests/test_temporal_features.py +125 -0
  124. 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/).
@@ -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"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+ )