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
commkit/multirate.py ADDED
@@ -0,0 +1,476 @@
1
+ """
2
+ Multirate signal processing and resampling.
3
+
4
+ This module provides high-performance implementations of multirate
5
+ operations, including interpolation, decimation, and rational rate
6
+ conversion using polyphase filter banks.
7
+
8
+ Notes on power scaling
9
+ ----------------------
10
+ All rate-changing functions in this module delegate to
11
+ ``scipy.signal.resample_poly``, which is designed with **unity DC gain**:
12
+ a constant-amplitude input produces a constant-amplitude output.
13
+
14
+ **Bandlimited (pulse-shaped) signals**
15
+
16
+ For a signal whose bandwidth fits within the new Nyquist band
17
+ (i.e. ``signal_bandwidth < fs_out / 2``), the polyphase filter is
18
+ effectively transparent - it passes all signal energy and the average
19
+ sample power is preserved:
20
+
21
+ E[|x_out[n]|^2] ≈ E[|x_in[n]|^2]
22
+
23
+ This holds regardless of the resampling ratio, rolloff factor, or
24
+ signal length (verified for RRC-shaped signals with rolloff 0.01-0.99,
25
+ sps_in down to 1.5, and block sizes as short as 64 symbols).
26
+
27
+ **Consequence for the ``"symbol_power"`` convention**
28
+
29
+ The ``Signal`` class uses the ``"symbol_power"`` normalization
30
+ (``E[|x|^2] = 1/sps``) so that symbol energy ``Es = E[|x|^2] * sps = 1``
31
+ is independent of the oversampling factor. Because ``resample_poly``
32
+ preserves sample power while ``sps`` changes, the convention is broken
33
+ after any rate change: the actual sample power remains ``1/sps_old``
34
+ instead of ``1/sps_new``.
35
+
36
+ ``Signal.upsample``, ``Signal.decimate``, and ``Signal.resample`` correct
37
+ for this by applying a deterministic amplitude gain of
38
+ ``sqrt(sps_old / sps_new)`` when their ``correct_power=True`` parameter
39
+ is set (the default). This correction is **exact** for pulse-shaped
40
+ signals because the power-preserving behaviour of ``resample_poly`` is
41
+ guaranteed (not statistical).
42
+
43
+ **Non-bandlimited signals (white noise, arbitrary arrays)**
44
+
45
+ For a flat-PSD (white-noise) signal, decimation removes the
46
+ out-of-band spectral power together with the aliased bandwidth, so
47
+ sample power scales as ``up / down``:
48
+
49
+ E[|x_out[n]|^2] ≈ (up / down) * E[|x_in[n]|^2]
50
+
51
+ Upsampling preserves sample power for non-bandlimited signals too
52
+ (the anti-imaging filter passes the baseband content unchanged).
53
+
54
+ If you are passing raw noise or an unfiltered wideband array through
55
+ these functions and need to maintain a specific power level, apply
56
+ ``correct_power=False`` in the ``Signal`` methods and rescale manually,
57
+ or use ``helpers.normalize`` after the fact.
58
+ """
59
+
60
+ from fractions import Fraction
61
+ from typing import Any
62
+
63
+ from . import helpers
64
+ from .backend import ArrayType, dispatch
65
+ from .core.signal import Signal
66
+ from .logger import logger
67
+
68
+
69
+ def decimate_to_symbol_rate(
70
+ samples: ArrayType | Signal,
71
+ sps: int | None = None,
72
+ offset: int = 0,
73
+ normalize: bool | None = None,
74
+ axis: int = -1,
75
+ ) -> ArrayType | Signal:
76
+ """
77
+ Decimates an oversampled signal to symbol-rate by direct slicing.
78
+
79
+ This function should be used **after** matched filtering to extract
80
+ pulse-shaped symbols at 1 sps. It does not apply additional
81
+ filtering, which is correct since the matched filter has already
82
+ performed optimal noise suppression.
83
+
84
+ Parameters
85
+ ----------
86
+ samples : array_like or Signal
87
+ Input matched-filtered signal. Shape: (..., N_samples). When a
88
+ :class:`Signal` is passed, ``sps`` defaults to the signal's integer
89
+ ``sps`` and a new :class:`Signal` at the symbol rate is returned.
90
+ sps : int, optional
91
+ Input samples per symbol (decimation factor). Required for array
92
+ input; derived from the Signal otherwise.
93
+ offset : int, default 0
94
+ Sampling phase offset in samples [0, sps-1]. Adjust this to
95
+ sample at the peak of the impulse response (center of the eye).
96
+ normalize : bool, optional
97
+ Normalize the output to unit average power (``mean(|x|²)=1``) after
98
+ slicing. Absorbs channel/equalizer/filter gain uncertainty. Defaults
99
+ to ``True`` for :class:`Signal` input (the canonical receive path) and
100
+ ``False`` for raw arrays (the unmodified slicing primitive).
101
+ axis : int, default -1
102
+ The axis along which to downsample.
103
+
104
+ Returns
105
+ -------
106
+ array_like or Signal
107
+ Symbols at 1 sps. Shape: (..., N_samples / sps).
108
+ """
109
+ if isinstance(samples, Signal):
110
+ sig = samples
111
+ do_norm = True if normalize is None else normalize
112
+ sps_int = int(sig.sps)
113
+ new = sig.copy()
114
+ if sps_int <= 1:
115
+ logger.info("Signal already at 1 sps, no downsampling needed.")
116
+ else:
117
+ new.samples = decimate_to_symbol_rate(
118
+ sig.samples, sps=sps_int, offset=offset, normalize=False, axis=-1
119
+ )
120
+ new.sampling_rate = sig.symbol_rate
121
+ if do_norm:
122
+ new.samples = helpers.normalize(new.samples, "average_power", axis=-1)
123
+ return new
124
+
125
+ if sps is None:
126
+ raise ValueError("decimate_to_symbol_rate() requires sps for array input.")
127
+ logger.debug("Downsampling to symbols: sps=%s, offset=%s", sps, offset)
128
+ arr, xp, _ = dispatch(samples)
129
+
130
+ # Build slicing for arbitrary axis
131
+ slices = [slice(None)] * arr.ndim
132
+ slices[axis] = slice(offset, None, sps)
133
+ out = arr[tuple(slices)]
134
+ if normalize:
135
+ out = helpers.normalize(out, "average_power", axis=axis)
136
+ return out
137
+
138
+
139
+ def expand(samples: ArrayType, factor: int, axis: int = -1) -> ArrayType:
140
+ """
141
+ Inserts zeros between samples (up-sampling by zero-stuffing).
142
+
143
+ This operation increases the sampling rate by an integer factor by
144
+ inserting `factor - 1` zeros between each original sample. This is the
145
+ first step in traditional interpolation but requires subsequent
146
+ filtering to remove spectral images.
147
+
148
+ Parameters
149
+ ----------
150
+ samples : array_like
151
+ Input signal samples. Shape: (..., N_samples).
152
+ factor : int
153
+ The expansion factor (number of output samples per input sample).
154
+ axis : int, default -1
155
+ The axis along which to perform expansion.
156
+
157
+ Returns
158
+ -------
159
+ array_like
160
+ The expanded sample array with zeros inserted.
161
+ Shape: (..., N_samples * factor).
162
+ """
163
+ logger.debug("Inserting zeros (expansion factor=%s).", factor)
164
+ samples, xp, _ = dispatch(samples)
165
+
166
+ n_in = samples.shape[axis]
167
+ n_out = n_in * factor
168
+
169
+ # Construct output shape
170
+ out_shape = list(samples.shape)
171
+ out_shape[axis] = n_out
172
+
173
+ out = xp.zeros(out_shape, dtype=samples.dtype)
174
+
175
+ # Slice logic to insert
176
+ # We want out[..., ::factor, ...] = samples
177
+ # Construct slices dynamically
178
+ slices = [slice(None)] * samples.ndim
179
+ slices[axis] = slice(None, None, factor)
180
+ out[tuple(slices)] = samples
181
+
182
+ return out
183
+
184
+
185
+ def upsample(
186
+ samples: ArrayType | Signal,
187
+ factor: int,
188
+ correct_power: bool | None = None,
189
+ axis: int = -1,
190
+ ) -> ArrayType | Signal:
191
+ """
192
+ Increases the sampling rate by an integer factor with filtering.
193
+
194
+ This is a convenience wrapper around `resample_poly` that performs
195
+ both zero-insertion (expansion) and anti-imaging filtering to suppress
196
+ spectral replicas.
197
+
198
+ Parameters
199
+ ----------
200
+ samples : array_like or Signal
201
+ Input signal samples. Shape: (..., N_samples). A :class:`Signal`
202
+ returns a new :class:`Signal` with ``sampling_rate`` scaled by *factor*.
203
+ factor : int
204
+ The interpolation factor.
205
+ correct_power : bool, optional
206
+ Apply the deterministic amplitude gain ``factor**-0.5`` to preserve the
207
+ ``"symbol_power"`` invariant (``E[|x|²]=1/sps``). Defaults to ``True``
208
+ for :class:`Signal` input, ``False`` for raw arrays.
209
+ axis : int, default -1
210
+ The axis along which to perform upsampling.
211
+
212
+ Returns
213
+ -------
214
+ array_like or Signal
215
+ The upsampled signal. Shape: (..., N_samples * factor).
216
+ """
217
+ if isinstance(samples, Signal):
218
+ sig = samples
219
+ new = sig.copy()
220
+ new.samples = upsample(
221
+ sig.samples,
222
+ factor,
223
+ correct_power=True if correct_power is None else correct_power,
224
+ axis=-1,
225
+ )
226
+ new.sampling_rate = sig.sampling_rate * factor
227
+ return new
228
+
229
+ logger.debug("Upsampling by factor %s (polyphase, axis=%s).", factor, axis)
230
+ arr, xp, sp = dispatch(samples)
231
+ out = sp.signal.resample_poly(arr, factor, 1, axis=axis)
232
+ if correct_power:
233
+ out = out * (factor**-0.5)
234
+ return out
235
+
236
+
237
+ def decimate(
238
+ samples: ArrayType | Signal,
239
+ factor: int,
240
+ method: str = "decimate",
241
+ correct_power: bool | None = None,
242
+ axis: int = -1,
243
+ **kwargs: Any,
244
+ ) -> ArrayType | Signal:
245
+ """
246
+ Reduces the sampling rate with anti-aliasing filtering.
247
+
248
+ Decimation combines lowpass filtering (to prevent aliasing) with
249
+ downsampling (keeping every Nth sample).
250
+
251
+ Parameters
252
+ ----------
253
+ samples : array_like or Signal
254
+ Input signal samples. Shape: (..., N_samples). A :class:`Signal`
255
+ returns a new :class:`Signal` with ``sampling_rate`` divided by *factor*.
256
+ factor : int
257
+ The decimation factor.
258
+ method : {"decimate", "polyphase"}, default "decimate"
259
+ The implementation strategy:
260
+ - "decimate": Uses `scipy.signal.decimate` (Chebyshev I or FIR).
261
+ - "polyphase": Uses `resample_poly` for filter-and-sample.
262
+ correct_power : bool, optional
263
+ Apply the deterministic amplitude gain ``factor**0.5`` to preserve the
264
+ ``"symbol_power"`` invariant. Defaults to ``True`` for :class:`Signal`
265
+ input, ``False`` for raw arrays.
266
+ axis : int, default -1
267
+ The axis along which to perform decimation.
268
+ **kwargs : Any
269
+ Additional parameters passed to the underlying filter design, such
270
+ as `zero_phase` or `ftype`.
271
+
272
+ Returns
273
+ -------
274
+ array_like or Signal
275
+ The decimated signal. Shape: (..., N_samples / factor).
276
+
277
+ Notes
278
+ -----
279
+ Do NOT use this function for symbol extraction after a matched filter.
280
+ Matched filters already perform optimal noise suppression and
281
+ anti-aliasing; adding an extra decimation filter will degrade the
282
+ signal. Use `decimate_to_symbol_rate` instead.
283
+ """
284
+ if isinstance(samples, Signal):
285
+ sig = samples
286
+ new = sig.copy()
287
+ new.samples = decimate(
288
+ sig.samples,
289
+ factor,
290
+ method=method,
291
+ correct_power=True if correct_power is None else correct_power,
292
+ axis=-1,
293
+ **kwargs,
294
+ )
295
+ new.sampling_rate = sig.sampling_rate / factor
296
+ return new
297
+
298
+ logger.debug("Decimating by factor %s (method: %s).", factor, method)
299
+ arr, _, sp = dispatch(samples)
300
+
301
+ if method == "decimate":
302
+ # scipy.signal.decimate (includes antialiasing)
303
+ zero_phase = kwargs.get("zero_phase", True)
304
+ ftype = kwargs.get("ftype", "fir")
305
+ out = sp.signal.decimate(
306
+ arr, int(factor), ftype=ftype, axis=axis, zero_phase=zero_phase
307
+ )
308
+ elif method == "polyphase":
309
+ # resample_poly with up=1
310
+ out = sp.signal.resample_poly(arr, 1, int(factor), axis=axis)
311
+ else:
312
+ raise ValueError(f"Unknown decimation method: {method}")
313
+
314
+ if correct_power:
315
+ out = out * (factor**0.5)
316
+ return out
317
+
318
+
319
+ def resample(
320
+ samples: ArrayType | Signal,
321
+ up: int | None = None,
322
+ down: int | None = None,
323
+ sps_in: float | None = None,
324
+ sps_out: float | None = None,
325
+ correct_power: bool | None = None,
326
+ axis: int = -1,
327
+ ) -> ArrayType | Signal:
328
+ """
329
+ Performs rational resampling of a signal.
330
+
331
+ Changes the sampling rate of the input by a rational factor. The rate
332
+ can be specified either as direct integer factors (`up`, `down`) or
333
+ relative to symbols (`sps_in`, `sps_out`).
334
+
335
+ Parameters
336
+ ----------
337
+ samples : array_like or Signal
338
+ Input signal samples. Shape: (..., N_samples). A :class:`Signal`
339
+ returns a new :class:`Signal` with updated ``sampling_rate``; ``sps_in``
340
+ is taken from the signal when ``sps_out`` is given.
341
+ up : int, optional
342
+ Integer upsampling factor.
343
+ down : int, optional
344
+ Integer downsampling factor.
345
+ sps_in : float, optional
346
+ Input samples per symbol.
347
+ sps_out : float, optional
348
+ Target samples per symbol.
349
+ correct_power : bool, optional
350
+ Apply the deterministic amplitude gain ``sqrt(down/up)`` (=
351
+ ``sqrt(sps_before/sps_after)``) to preserve the ``"symbol_power"``
352
+ invariant. Defaults to ``True`` for :class:`Signal` input, ``False``
353
+ for raw arrays.
354
+ axis : int, default -1
355
+ The axis along which to perform resampling.
356
+
357
+ Returns
358
+ -------
359
+ array_like or Signal
360
+ The resampled signal. Shape: (..., N_samples * Ratio).
361
+
362
+ Raises
363
+ ------
364
+ ValueError
365
+ If parameters are insufficient or contradictory.
366
+ """
367
+ if isinstance(samples, Signal):
368
+ sig = samples
369
+ new = sig.copy()
370
+ # When sps_out is given, the input sps comes from the signal itself.
371
+ sig_sps_in = sig.sps if sps_out is not None else None
372
+ new.samples = resample(
373
+ sig.samples,
374
+ up=up,
375
+ down=down,
376
+ sps_in=sig_sps_in,
377
+ sps_out=sps_out,
378
+ correct_power=True if correct_power is None else correct_power,
379
+ axis=-1,
380
+ )
381
+ if sps_out is not None:
382
+ new.sampling_rate = sps_out * sig.symbol_rate
383
+ elif up is not None and down is not None:
384
+ new.sampling_rate = sig.sampling_rate * up / down
385
+ return new
386
+
387
+ if (up is not None or down is not None) and (
388
+ sps_in is not None or sps_out is not None
389
+ ):
390
+ raise ValueError("Cannot specify both (up, down) and (sps_in, sps_out).")
391
+
392
+ if sps_in is not None and sps_out is not None:
393
+ ratio = Fraction(sps_out / sps_in).limit_denominator()
394
+ up = ratio.numerator
395
+ down = ratio.denominator
396
+ elif up is None or down is None:
397
+ raise ValueError("Must specify either (up, down) or (sps_in, sps_out).")
398
+
399
+ logger.debug(
400
+ "Resampling by rational factor %s/%s (polyphase, axis=%s).", up, down, axis
401
+ )
402
+ arr, xp, sp = dispatch(samples)
403
+ out = sp.signal.resample_poly(arr, int(up), int(down), axis=axis)
404
+ if correct_power:
405
+ # sps_after / sps_before = up / down -> gain = sqrt(down/up).
406
+ out = out * (down / up) ** 0.5
407
+ return out
408
+
409
+
410
+ def resolve_symbols(
411
+ samples: ArrayType | Signal,
412
+ sps: int | None = None,
413
+ offset: int = 0,
414
+ ) -> ArrayType | Signal:
415
+ """
416
+ Decimate to symbol rate (1 sps) and normalize to unit average power.
417
+
418
+ This is the canonical receive-side symbol-extraction step: it slices the
419
+ matched-filtered signal to one sample per symbol and renormalizes to
420
+ ``E[|x|²]=1`` (absorbing channel/equalizer/filter gain).
421
+
422
+ Parameters
423
+ ----------
424
+ samples : array_like or Signal
425
+ Oversampled (matched-filtered) signal, or a :class:`Signal`. For a
426
+ :class:`Signal`, a new :class:`Signal` is returned with
427
+ ``resolved_symbols`` populated (the samples themselves are unchanged);
428
+ ``sps`` is taken from the signal and frame-generated signals are
429
+ skipped with a warning.
430
+ sps : int, optional
431
+ Input samples per symbol. Required for array input; derived from the
432
+ signal otherwise.
433
+ offset : int, default 0
434
+ Integer sampling-phase offset applied before decimation.
435
+
436
+ Returns
437
+ -------
438
+ array_like or Signal
439
+ Unit-average-power symbols at 1 sps (array), or a new :class:`Signal`
440
+ with ``resolved_symbols`` set.
441
+
442
+ Raises
443
+ ------
444
+ ValueError
445
+ If ``sps`` is missing/invalid (array), or the signal's ``sps`` is not a
446
+ positive integer.
447
+ """
448
+ if isinstance(samples, Signal):
449
+ sig = samples
450
+ if sig.signal_type is not None:
451
+ logger.warning(
452
+ "resolve_symbols() called on a frame-generated signal - skipping. "
453
+ "Frame signals mix preamble, pilots, and payload segments that may "
454
+ "have different modulations or gains. Extract the desired segment "
455
+ "via frame.get_structure_map(), build a plain Signal, then call "
456
+ "resolve_symbols() on that."
457
+ )
458
+ return sig.copy()
459
+ s = sig.sps
460
+ if s is None:
461
+ raise ValueError("Symbol rate or sampling rate missing.")
462
+ if s < 1:
463
+ raise ValueError("Symbol rate must be >= 1.")
464
+ if s % 1 != 0:
465
+ raise ValueError("Symbol rate must be an integer.")
466
+ new = sig.copy()
467
+ new.resolved_symbols = resolve_symbols(sig.samples, sps=int(s), offset=offset)
468
+ return new
469
+
470
+ if sps is None:
471
+ raise ValueError("resolve_symbols() requires sps for array input.")
472
+ # decimate_to_symbol_rate slices [offset::sps] (identity when sps==1) then
473
+ # normalizes to unit average power.
474
+ return decimate_to_symbol_rate(
475
+ samples, sps=int(sps), offset=int(offset), normalize=True
476
+ )
@@ -0,0 +1,78 @@
1
+ """
2
+ Signal visualization and publication-quality plotting tools.
3
+
4
+ This package provides high-level plotting functions optimized for
5
+ communication signals. It leverages Matplotlib to produce high-density,
6
+ professional diagrams with automatic SI scaling and backend-agnostic data
7
+ handling.
8
+
9
+ The public API is unchanged from when this was a single module:
10
+ ``from commkit.plotting import plot_constellation, plot_psd, ...`` continues
11
+ to work.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ # Re-exported so ``patch("commkit.plotting.logger...")`` and similar
17
+ # attribute access on the package namespace keep working.
18
+ from ..logger import logger
19
+ from .analysis import (
20
+ plot_allan_deviation,
21
+ plot_carrier_phase_characterization,
22
+ plot_dsh_beat_psd,
23
+ plot_frequency_drift,
24
+ plot_frequency_noise_psd,
25
+ plot_increment_variance,
26
+ )
27
+ from .constellation import plot_constellation, plot_ideal_constellation
28
+ from .equalizer import (
29
+ plot_equalizer_result,
30
+ plot_filter_response,
31
+ plot_zf_equalizer_response,
32
+ )
33
+
34
+ # Private helpers re-exported for tests that reach package internals through
35
+ # this namespace. F401 is silenced for this re-export hub in pyproject.toml.
36
+ from .eye import _plot_eye_traces, plot_eye_diagram
37
+ from .spectral import plot_psd, plot_spectrogram
38
+ from .sync import (
39
+ plot_carrier_phase_decomposition,
40
+ plot_carrier_phase_trajectory,
41
+ plot_frequency_offset_blockwise_result,
42
+ plot_frequency_offset_spectrum,
43
+ plot_mm_autocorrelation,
44
+ plot_pilot_phase_estimate,
45
+ plot_pilot_tone_phase_estimate,
46
+ plot_pilot_tones_phase_estimate,
47
+ plot_timing_correlation,
48
+ )
49
+ from .theme import _create_subplot_grid, apply_default_theme
50
+ from .waveform import plot_time_domain
51
+
52
+ __all__ = [
53
+ "apply_default_theme",
54
+ "plot_allan_deviation",
55
+ "plot_carrier_phase_characterization",
56
+ "plot_carrier_phase_decomposition",
57
+ "plot_carrier_phase_trajectory",
58
+ "plot_constellation",
59
+ "plot_dsh_beat_psd",
60
+ "plot_equalizer_result",
61
+ "plot_eye_diagram",
62
+ "plot_filter_response",
63
+ "plot_frequency_drift",
64
+ "plot_frequency_noise_psd",
65
+ "plot_frequency_offset_blockwise_result",
66
+ "plot_frequency_offset_spectrum",
67
+ "plot_ideal_constellation",
68
+ "plot_increment_variance",
69
+ "plot_mm_autocorrelation",
70
+ "plot_pilot_phase_estimate",
71
+ "plot_pilot_tone_phase_estimate",
72
+ "plot_pilot_tones_phase_estimate",
73
+ "plot_psd",
74
+ "plot_spectrogram",
75
+ "plot_time_domain",
76
+ "plot_timing_correlation",
77
+ "plot_zf_equalizer_response",
78
+ ]