commkit 1.0.0__py3-none-any.whl

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 (84) hide show
  1. commkit/__init__.py +74 -0
  2. commkit/_cuda/__init__.py +321 -0
  3. commkit/_cuda/compiler.py +88 -0
  4. commkit/_cuda/src/bps_min_d2.cu +104 -0
  5. commkit/_cuda/src/cs_block.cu +119 -0
  6. commkit/_cuda/src/selftest.cu +14 -0
  7. commkit/analysis/__init__.py +55 -0
  8. commkit/analysis/_common.py +236 -0
  9. commkit/analysis/allan.py +108 -0
  10. commkit/analysis/drift.py +213 -0
  11. commkit/analysis/interferometry.py +887 -0
  12. commkit/analysis/linewidth.py +480 -0
  13. commkit/analysis/trajectory.py +91 -0
  14. commkit/backend.py +507 -0
  15. commkit/coding/__init__.py +23 -0
  16. commkit/coding/base.py +17 -0
  17. commkit/coding/bch.py +6 -0
  18. commkit/coding/convolutional.py +7 -0
  19. commkit/coding/crc.py +7 -0
  20. commkit/coding/galois.py +8 -0
  21. commkit/coding/hamming.py +6 -0
  22. commkit/coding/interleaving.py +7 -0
  23. commkit/coding/ldpc.py +8 -0
  24. commkit/coding/polar.py +8 -0
  25. commkit/coding/ratematch.py +6 -0
  26. commkit/coding/reed_solomon.py +6 -0
  27. commkit/coding/turbo.py +8 -0
  28. commkit/core/__init__.py +32 -0
  29. commkit/core/frame.py +992 -0
  30. commkit/core/generation.py +581 -0
  31. commkit/core/signal.py +725 -0
  32. commkit/equalization/__init__.py +49 -0
  33. commkit/equalization/_block.py +1855 -0
  34. commkit/equalization/_common.py +606 -0
  35. commkit/equalization/_kernels_jax.py +1720 -0
  36. commkit/equalization/_kernels_numba.py +1704 -0
  37. commkit/equalization/blind.py +223 -0
  38. commkit/equalization/linear.py +365 -0
  39. commkit/equalization/polarization.py +790 -0
  40. commkit/equalization/result.py +191 -0
  41. commkit/equalization/sequential.py +2805 -0
  42. commkit/filtering.py +1120 -0
  43. commkit/frequency.py +1191 -0
  44. commkit/helpers.py +489 -0
  45. commkit/impairments/__init__.py +43 -0
  46. commkit/impairments/channel/__init__.py +20 -0
  47. commkit/impairments/channel/linear.py +310 -0
  48. commkit/impairments/channel/nonlinear.py +11 -0
  49. commkit/impairments/frontend.py +229 -0
  50. commkit/impairments/noise.py +105 -0
  51. commkit/impairments/source.py +219 -0
  52. commkit/io.py +308 -0
  53. commkit/logger.py +103 -0
  54. commkit/mapping/__init__.py +46 -0
  55. commkit/mapping/bits.py +240 -0
  56. commkit/mapping/constellation.py +153 -0
  57. commkit/mapping/gray.py +429 -0
  58. commkit/mapping/llr.py +253 -0
  59. commkit/mapping/shaping.py +218 -0
  60. commkit/metrics.py +949 -0
  61. commkit/multirate.py +476 -0
  62. commkit/plotting/__init__.py +78 -0
  63. commkit/plotting/analysis.py +627 -0
  64. commkit/plotting/constellation.py +483 -0
  65. commkit/plotting/equalizer.py +390 -0
  66. commkit/plotting/eye.py +388 -0
  67. commkit/plotting/spectral.py +575 -0
  68. commkit/plotting/sync.py +953 -0
  69. commkit/plotting/theme.py +203 -0
  70. commkit/plotting/waveform.py +200 -0
  71. commkit/py.typed +0 -0
  72. commkit/recovery/__init__.py +51 -0
  73. commkit/recovery/bps.py +337 -0
  74. commkit/recovery/corrections.py +751 -0
  75. commkit/recovery/pilots.py +803 -0
  76. commkit/recovery/pll.py +482 -0
  77. commkit/recovery/tikhonov.py +424 -0
  78. commkit/recovery/viterbi_viterbi.py +227 -0
  79. commkit/spectral.py +560 -0
  80. commkit/timing.py +841 -0
  81. commkit-1.0.0.dist-info/METADATA +145 -0
  82. commkit-1.0.0.dist-info/RECORD +84 -0
  83. commkit-1.0.0.dist-info/WHEEL +4 -0
  84. commkit-1.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,191 @@
1
+ """Equalizer result containers and CPR state."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from dataclasses import dataclass
7
+
8
+ import numpy as np
9
+
10
+ from ..backend import ArrayType, to_device
11
+ from ..logger import logger
12
+
13
+ # -----------------------------------------------------------------------------
14
+ # RESULT CONTAINER
15
+ # -----------------------------------------------------------------------------
16
+
17
+
18
+ @dataclass
19
+ class CPRState:
20
+ """Carrier-phase-recovery state for warm-starting across equalizer calls.
21
+
22
+ All arrays are CPU NumPy regardless of the equalizer backend - CPR state is
23
+ small and must survive device resets. Do not convert to CuPy.
24
+
25
+ Used/produced by ``lms()``, ``rls()``, and ``block_lms()`` when
26
+ ``cpr_type`` is not None. Pass as ``cpr_state=result.cpr_state`` to the
27
+ next call to continue phase tracking without a re-lock transient.
28
+ """
29
+
30
+ # PLL state (lms / rls with cpr_type='pll' or 'bps')
31
+ pll_phi: np.ndarray | None = None # (C,) float64
32
+ pll_freq: np.ndarray | None = None # (C,) float64
33
+
34
+ # BPS cross-block unwrap state (block_lms with cpr_type='bps')
35
+ bps_prev4: np.ndarray | None = None # (C,) float64
36
+ bps_offset4: np.ndarray | None = None # (C,) float64
37
+ bps_d2_hist: np.ndarray | None = None # (P, C, K-1) float32 - CPU copy
38
+
39
+ # Cycle-slip regression state (all CPR modes)
40
+ cs_buf_x: np.ndarray | None = None # (C, H) float64
41
+ cs_buf_y: np.ndarray | None = None # (C, H) float64
42
+ cs_buf_ptr: np.ndarray | None = None # (C,) int64
43
+ cs_buf_n: np.ndarray | None = None # (C,) int64
44
+ cs_stats: np.ndarray | None = None # (C, 4) float64
45
+
46
+ # JAX-specific BPS buffer state (JAX backend only; None for Numba)
47
+ jax_bps_buf: np.ndarray | None = None # (KB, C) complex64
48
+ jax_bps_buf_ptr: int | None = None # scalar int32
49
+
50
+ # Identity tags - used to validate shape compatibility on warm-start
51
+ cpr_type: str | None = None
52
+ num_ch: int = 0
53
+ symmetry: int = 4
54
+ bps_P: int = 0 # number of BPS test phases
55
+ bps_K: int = 0 # BPS block size
56
+ cs_H: int = 0 # cycle-slip history length
57
+
58
+
59
+ @dataclass
60
+ class EqualizerResult:
61
+ """Container for equalizer outputs.
62
+
63
+ Attributes
64
+ ----------
65
+ y_hat : ArrayType
66
+ Equalized symbol sequence. Shape: ``(N_sym,)`` SISO or
67
+ ``(C, N_sym)`` MIMO.
68
+ weights : ArrayType
69
+ Final tap weight vector. Shape: ``(num_taps,)`` for SISO
70
+ or ``(C, C, num_taps)`` for MIMO butterfly.
71
+ error : ArrayType
72
+ Error signal history. Shape: ``(N_sym,)`` or ``(C, N_sym)``.
73
+ weights_history : ArrayType or None
74
+ Tap weight evolution over time. Only populated when
75
+ ``store_weights=True``.
76
+ num_train_symbols : int
77
+ Number of data-aided training symbols consumed (LMS/RLS). Used to
78
+ discard the DA-trained transient before computing steady-state metrics
79
+ like EVM, SNR, and BER.
80
+ input_norm_factor : float or np.ndarray
81
+ Normalization factor(s) ``rms(samples, axis=-1) * sqrt(sps)`` applied
82
+ per-channel by ``_normalize_inputs`` before equalization. For SISO
83
+ this is a plain ``float``; for MIMO it is a 1-D ``np.ndarray`` of
84
+ shape ``(C,)`` - one factor per input stream.
85
+ Stored so callers can apply the post-hoc power correction needed when
86
+ passing a different capture (e.g. vacuum noise) through the same
87
+ frozen taps without disturbing the per-channel ratio:
88
+
89
+ Example::
90
+
91
+ α = signal_result.input_norm_factor # float or (C,) array
92
+ β = noise_result.input_norm_factor
93
+ P_noise_corrected = (
94
+ np.mean(np.abs(noise_result.y_hat) ** 2, axis=-1) * (β / α) ** 2
95
+ )
96
+
97
+ ``P_signal / P_noise_corrected`` is then the physically meaningful
98
+ per-channel signal-to-noise power ratio preserved through the DSP chain.
99
+ At ``sps=1`` this factor equals the plain per-channel RMS of the input symbols.
100
+ tail_trim : int
101
+ Number of symbols trimmed from the tail of ``y_hat`` to remove the
102
+ zero-padding contamination zone. Non-zero only for RLS (equals
103
+ ``num_taps // 2``). If non-zero, trim reference arrays to match::
104
+
105
+ source_symbols = source_symbols[..., :-result.tail_trim]
106
+ source_bits = source_bits[..., :-result.tail_trim * bits_per_symbol]
107
+ phase_trajectory : np.ndarray or None
108
+ Per-symbol phase estimates produced by the inline CPR stage, in
109
+ radians. ``None`` when ``cpr_type=None``.
110
+
111
+ Shape: ``(N_sym,)`` for SISO, ``(C, N_sym)`` for MIMO butterfly.
112
+
113
+ The values are the instantaneous phase corrections *applied* to each
114
+ symbol before the hard decision and weight update, i.e.
115
+ ``y_hat[n] = (W^H x[n]) · exp(-j · phase_trajectory[n])``.
116
+ Useful for post-hoc phase-noise analysis, cycle-slip diagnostics, and
117
+ as a warm-start phase estimate for a subsequent CPR stage.
118
+ """
119
+
120
+ y_hat: ArrayType
121
+ weights: ArrayType
122
+ error: ArrayType
123
+ weights_history: ArrayType | None = None
124
+ num_train_symbols: int = 0
125
+ input_norm_factor: float | np.ndarray = 1.0
126
+ tail_trim: int = 0
127
+ phase_trajectory: ArrayType | None = None
128
+ cpr_state: CPRState | None = None
129
+
130
+
131
+ def _log_equalizer_exit(
132
+ result: EqualizerResult,
133
+ name: str,
134
+ debug_plot: bool = False,
135
+ check_convergence: bool = False,
136
+ plot_smoothing: int = 50,
137
+ ) -> EqualizerResult:
138
+ """Log exit MSE and optionally show a debug plot for an EqualizerResult."""
139
+ if result.error is not None:
140
+ n_sym = result.error.shape[-1] # time axis; (N_sym,) or (C, N_sym)
141
+ want_log = logger.isEnabledFor(logging.INFO)
142
+ # The convergence check emits a WARNING (normally always enabled), but
143
+ # only runs when explicitly requested and the signal is long enough.
144
+ want_conv = check_convergence and n_sym >= 20
145
+
146
+ # Skip the whole MSE computation - and its device->host transfer - when
147
+ # nothing will consume the result. When it is needed, transfer only the
148
+ # tail (and head, for convergence) windows rather than the full error
149
+ # array: ≤100 samples/channel instead of N.
150
+ if want_log or want_conv:
151
+ window = max(1, min(100, n_sym))
152
+ tail = to_device(result.error[..., -window:], "cpu")
153
+
154
+ if tail.ndim == 1: # SISO
155
+ mse_final = float(np.mean(np.abs(tail) ** 2))
156
+ mse_db = 10.0 * np.log10(mse_final + 1e-30)
157
+ if want_log:
158
+ logger.info(
159
+ "%s: exit MSE=%.1f dB (final %s symbols)", name, mse_db, window
160
+ )
161
+ else: # MIMO: per-channel MSE; keep the mean for the convergence check
162
+ per_ch_mse = np.mean(np.abs(tail) ** 2, axis=-1) # (C,)
163
+ if want_log:
164
+ parts = ", ".join(
165
+ f"ch{c}={10.0 * np.log10(m + 1e-30):.1f}"
166
+ for c, m in enumerate(per_ch_mse)
167
+ )
168
+ logger.info(
169
+ "%s: exit MSE (final %s symbols): %s dB", name, window, parts
170
+ )
171
+ mse_final = float(np.mean(per_ch_mse))
172
+ mse_db = 10.0 * np.log10(mse_final + 1e-30)
173
+
174
+ if want_conv:
175
+ init_window = max(1, min(100, n_sym // 10))
176
+ head = to_device(result.error[..., :init_window], "cpu")
177
+ mse_init = float(np.mean(np.abs(head) ** 2))
178
+ if mse_init > 0 and mse_final > mse_init * 0.9:
179
+ logger.warning(
180
+ "%s: convergence may be poor - final MSE (%.1f dB) not significantly below initial MSE (%.1f dB). Consider reducing step_size or increasing signal length.",
181
+ name,
182
+ mse_db,
183
+ 10.0 * np.log10(mse_init + 1e-30),
184
+ )
185
+
186
+ if debug_plot:
187
+ from .. import plotting as _plotting # lazy import avoids circular dep
188
+
189
+ _plotting.plot_equalizer_result(result, smoothing=plot_smoothing)
190
+
191
+ return result