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.
- commkit/__init__.py +74 -0
- commkit/_cuda/__init__.py +321 -0
- commkit/_cuda/compiler.py +88 -0
- commkit/_cuda/src/bps_min_d2.cu +104 -0
- commkit/_cuda/src/cs_block.cu +119 -0
- commkit/_cuda/src/selftest.cu +14 -0
- commkit/analysis/__init__.py +55 -0
- commkit/analysis/_common.py +236 -0
- commkit/analysis/allan.py +108 -0
- commkit/analysis/drift.py +213 -0
- commkit/analysis/interferometry.py +887 -0
- commkit/analysis/linewidth.py +480 -0
- commkit/analysis/trajectory.py +91 -0
- commkit/backend.py +507 -0
- commkit/coding/__init__.py +23 -0
- commkit/coding/base.py +17 -0
- commkit/coding/bch.py +6 -0
- commkit/coding/convolutional.py +7 -0
- commkit/coding/crc.py +7 -0
- commkit/coding/galois.py +8 -0
- commkit/coding/hamming.py +6 -0
- commkit/coding/interleaving.py +7 -0
- commkit/coding/ldpc.py +8 -0
- commkit/coding/polar.py +8 -0
- commkit/coding/ratematch.py +6 -0
- commkit/coding/reed_solomon.py +6 -0
- commkit/coding/turbo.py +8 -0
- commkit/core/__init__.py +32 -0
- commkit/core/frame.py +992 -0
- commkit/core/generation.py +581 -0
- commkit/core/signal.py +725 -0
- commkit/equalization/__init__.py +49 -0
- commkit/equalization/_block.py +1855 -0
- commkit/equalization/_common.py +606 -0
- commkit/equalization/_kernels_jax.py +1720 -0
- commkit/equalization/_kernels_numba.py +1704 -0
- commkit/equalization/blind.py +223 -0
- commkit/equalization/linear.py +365 -0
- commkit/equalization/polarization.py +790 -0
- commkit/equalization/result.py +191 -0
- commkit/equalization/sequential.py +2805 -0
- commkit/filtering.py +1120 -0
- commkit/frequency.py +1191 -0
- commkit/helpers.py +489 -0
- commkit/impairments/__init__.py +43 -0
- commkit/impairments/channel/__init__.py +20 -0
- commkit/impairments/channel/linear.py +310 -0
- commkit/impairments/channel/nonlinear.py +11 -0
- commkit/impairments/frontend.py +229 -0
- commkit/impairments/noise.py +105 -0
- commkit/impairments/source.py +219 -0
- commkit/io.py +308 -0
- commkit/logger.py +103 -0
- commkit/mapping/__init__.py +46 -0
- commkit/mapping/bits.py +240 -0
- commkit/mapping/constellation.py +153 -0
- commkit/mapping/gray.py +429 -0
- commkit/mapping/llr.py +253 -0
- commkit/mapping/shaping.py +218 -0
- commkit/metrics.py +949 -0
- commkit/multirate.py +476 -0
- commkit/plotting/__init__.py +78 -0
- commkit/plotting/analysis.py +627 -0
- commkit/plotting/constellation.py +483 -0
- commkit/plotting/equalizer.py +390 -0
- commkit/plotting/eye.py +388 -0
- commkit/plotting/spectral.py +575 -0
- commkit/plotting/sync.py +953 -0
- commkit/plotting/theme.py +203 -0
- commkit/plotting/waveform.py +200 -0
- commkit/py.typed +0 -0
- commkit/recovery/__init__.py +51 -0
- commkit/recovery/bps.py +337 -0
- commkit/recovery/corrections.py +751 -0
- commkit/recovery/pilots.py +803 -0
- commkit/recovery/pll.py +482 -0
- commkit/recovery/tikhonov.py +424 -0
- commkit/recovery/viterbi_viterbi.py +227 -0
- commkit/spectral.py +560 -0
- commkit/timing.py +841 -0
- commkit-1.0.0.dist-info/METADATA +145 -0
- commkit-1.0.0.dist-info/RECORD +84 -0
- commkit-1.0.0.dist-info/WHEEL +4 -0
- 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
|
+
]
|