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/recovery/bps.py
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
"""Blind Phase Search (BPS) carrier phase recovery."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from ..backend import ArrayType, dispatch, to_device
|
|
8
|
+
from ..logger import logger
|
|
9
|
+
from .corrections import correct_cycle_slips
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def recover_carrier_phase_bps(
|
|
13
|
+
symbols: ArrayType,
|
|
14
|
+
modulation: str,
|
|
15
|
+
order: int,
|
|
16
|
+
num_test_phases: int = 64,
|
|
17
|
+
block_size: int = 32,
|
|
18
|
+
joint_channels: bool = False,
|
|
19
|
+
cycle_slip_correction: bool = False,
|
|
20
|
+
cycle_slip_history: int = 100,
|
|
21
|
+
cycle_slip_threshold: float = np.pi / 4,
|
|
22
|
+
pmf: np.ndarray | None = None,
|
|
23
|
+
debug_plot: bool = False,
|
|
24
|
+
) -> ArrayType:
|
|
25
|
+
"""
|
|
26
|
+
Carrier phase recovery via Blind Phase Search (BPS).
|
|
27
|
+
|
|
28
|
+
Tests ``num_test_phases`` candidate rotation angles over ``[0, π/2)``
|
|
29
|
+
(exploiting 4-fold QAM symmetry), selects the candidate that minimises
|
|
30
|
+
the block-averaged sum of minimum squared distances to the reference
|
|
31
|
+
constellation, and interpolates to per-symbol resolution.
|
|
32
|
+
|
|
33
|
+
Parameters
|
|
34
|
+
----------
|
|
35
|
+
symbols : array_like
|
|
36
|
+
1-SPS complex symbols after matched filter. Shape: (N,) or (C, N).
|
|
37
|
+
modulation : str
|
|
38
|
+
Modulation scheme (case-insensitive). Used to fetch the reference
|
|
39
|
+
constellation via
|
|
40
|
+
``gray_constellation``.
|
|
41
|
+
order : int
|
|
42
|
+
Modulation order.
|
|
43
|
+
num_test_phases : int, default 64
|
|
44
|
+
Number of candidate phase offsets B. Resolution is ``π/(2B)``
|
|
45
|
+
rad per step. More candidates improve accuracy at higher compute cost.
|
|
46
|
+
block_size : int, default 32
|
|
47
|
+
Number of symbols per block for error-metric averaging.
|
|
48
|
+
Very small values (< 4) make the 4-fold phase unwrap unreliable
|
|
49
|
+
because noise on a single-symbol metric causes the best-candidate
|
|
50
|
+
index to jump between non-adjacent phase bins between consecutive
|
|
51
|
+
blocks, triggering false unwrap corrections. Recommended
|
|
52
|
+
minimum: ``block_size ≥ 4``.
|
|
53
|
+
joint_channels : bool, default False
|
|
54
|
+
For MIMO inputs (C > 1): if ``True``, sum the distance metrics
|
|
55
|
+
across all channels before selecting the best phase candidate.
|
|
56
|
+
The resulting single phase trajectory is broadcast to all C rows
|
|
57
|
+
of the output (all channels identical before ambiguity resolution).
|
|
58
|
+
Reduces phase estimation variance by ~√C for shared-LO systems.
|
|
59
|
+
Has no effect for SISO (C = 1).
|
|
60
|
+
cycle_slip_correction : bool, default False
|
|
61
|
+
If ``True``, apply cycle-slip detection and correction
|
|
62
|
+
(``correct_cycle_slips``) to the block-phase trajectory
|
|
63
|
+
after 4-fold unwrap, before interpolation.
|
|
64
|
+
cycle_slip_history : int, default 100
|
|
65
|
+
``history_length`` passed to ``correct_cycle_slips``.
|
|
66
|
+
Number of past corrected blocks used for linear extrapolation.
|
|
67
|
+
cycle_slip_threshold : float, default π/4
|
|
68
|
+
``threshold`` passed to ``correct_cycle_slips`` (radians).
|
|
69
|
+
pmf : np.ndarray, optional
|
|
70
|
+
Symbol PMF of shape ``(order,)`` for PS-QAM. When provided, the
|
|
71
|
+
reference constellation is scaled by ``1/sqrt(E_PS)`` (where
|
|
72
|
+
``E_PS = Σ P(s_m) |s_m|²`` on the normalised grid) so the
|
|
73
|
+
nearest-neighbour distance metric matches the scale of the
|
|
74
|
+
unit-avg-power input. Without this, mid-shell PS points cross
|
|
75
|
+
decision boundaries in the BPS metric and bias the phase estimate.
|
|
76
|
+
No-op for uniform modulations.
|
|
77
|
+
debug_plot : bool, default False
|
|
78
|
+
If ``True``, opens a diagnostic figure showing the per-symbol phase
|
|
79
|
+
trajectory alongside the block-phase estimates.
|
|
80
|
+
|
|
81
|
+
Returns
|
|
82
|
+
-------
|
|
83
|
+
array_like
|
|
84
|
+
Per-symbol phase estimate in radians. Shape matches ``symbols``.
|
|
85
|
+
Same backend as input.
|
|
86
|
+
|
|
87
|
+
Notes
|
|
88
|
+
-----
|
|
89
|
+
Tests B candidate rotations over [0, pi/2), selects the one minimising
|
|
90
|
+
block-averaged minimum Euclidean distance, then 4-fold unwraps. A global
|
|
91
|
+
pi/2 ambiguity remains - resolve via a pilot or preamble reference.
|
|
92
|
+
|
|
93
|
+
Memory: the distance tensor scales as N * B * M * 8 bytes; reduce
|
|
94
|
+
``num_test_phases`` or segment length for high-order constellations.
|
|
95
|
+
"""
|
|
96
|
+
from ..helpers import normalize
|
|
97
|
+
from ..mapping import constellation_power, gray_constellation
|
|
98
|
+
|
|
99
|
+
symbols, xp, _ = dispatch(symbols)
|
|
100
|
+
was_1d = symbols.ndim == 1
|
|
101
|
+
if was_1d:
|
|
102
|
+
symbols = symbols[None, :]
|
|
103
|
+
C, N = symbols.shape
|
|
104
|
+
|
|
105
|
+
# Normalise each channel to unit average power so the metric is computed at
|
|
106
|
+
# the same scale as the reference constellation (gray_constellation returns
|
|
107
|
+
# unit-average-power points). BPS is a phase estimator; it must be
|
|
108
|
+
# amplitude-agnostic.
|
|
109
|
+
symbols = normalize(symbols, mode="average_power", axis=-1)
|
|
110
|
+
|
|
111
|
+
# Reference constellation on the same device
|
|
112
|
+
const_np = gray_constellation(modulation, order)
|
|
113
|
+
|
|
114
|
+
# PS-QAM: unit-avg-power input lives on the ``{s_m/sqrt(E_PS)}`` grid.
|
|
115
|
+
# Rescale the comparison constellation to the same grid so the nearest-
|
|
116
|
+
# neighbour distance metric is correct. Skip on uniform PMF.
|
|
117
|
+
if pmf is not None:
|
|
118
|
+
e_ps = constellation_power(const_np, pmf)
|
|
119
|
+
if e_ps < 1.0 - 1e-6:
|
|
120
|
+
const_np = const_np / np.sqrt(e_ps)
|
|
121
|
+
|
|
122
|
+
const_xp = xp.asarray(const_np) # (M_const,)
|
|
123
|
+
|
|
124
|
+
# Candidate test phases over [0, π/2)
|
|
125
|
+
B = num_test_phases
|
|
126
|
+
candidates = xp.arange(B, dtype=symbols.real.dtype) * (np.pi / 2.0 / B) # (B,)
|
|
127
|
+
|
|
128
|
+
N_trunc = (N // block_size) * block_size
|
|
129
|
+
N_blocks = N_trunc // block_size
|
|
130
|
+
|
|
131
|
+
if N_blocks == 0:
|
|
132
|
+
raise ValueError(
|
|
133
|
+
f"Signal length {N} is shorter than block_size={block_size}. "
|
|
134
|
+
"Reduce block_size or use a longer symbol sequence."
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# Very small block_size makes the 4-fold phase unwrap unreliable: with only
|
|
138
|
+
# one or two symbols per block the noise on the distance-metric argmin causes
|
|
139
|
+
# large candidate-index jumps between consecutive blocks, triggering false
|
|
140
|
+
# 4-fold unwrap corrections. Warn early so users diagnose this easily.
|
|
141
|
+
if block_size < 4:
|
|
142
|
+
logger.warning(
|
|
143
|
+
"CPR (BPS): block_size=%s is very small. Averaging the distance metric over only %s symbol(s) per block makes the 4-fold phase unwrap unreliable. Recommended minimum: block_size ≥ 4.",
|
|
144
|
+
block_size,
|
|
145
|
+
block_size,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# block_centers[b] = b * block_size + block_size/2 (consistent with VV)
|
|
149
|
+
block_centers = xp.arange(N_blocks, dtype=xp.float64) * block_size + block_size / 2
|
|
150
|
+
|
|
151
|
+
all_positions = xp.arange(N, dtype=xp.float64)
|
|
152
|
+
|
|
153
|
+
# Pre-compute interpolation indices and weights (identical for every channel).
|
|
154
|
+
# block b is "to the left" of position n when its centre b*bs + bs/2 <= n
|
|
155
|
+
# => b <= (n - bs/2) / bs => idx_left = floor((n - bs/2) / bs)
|
|
156
|
+
idx_left = xp.clip(
|
|
157
|
+
xp.floor((all_positions - block_size / 2) / block_size).astype(xp.int64),
|
|
158
|
+
0,
|
|
159
|
+
N_blocks - 2,
|
|
160
|
+
) # (N,)
|
|
161
|
+
idx_right = idx_left + 1 # (N,)
|
|
162
|
+
t_interp = xp.clip(
|
|
163
|
+
(all_positions - block_centers[idx_left]) / block_size, 0.0, 1.0
|
|
164
|
+
) # (N,)
|
|
165
|
+
|
|
166
|
+
# Pre-compute phasors for all B candidates once (avoid redundant exp per channel)
|
|
167
|
+
dtype_c = xp.complex64 if symbols.dtype == xp.complex64 else xp.complex128
|
|
168
|
+
phasors = xp.exp(-1j * candidates.astype(xp.float64)).astype(dtype_c) # (B,)
|
|
169
|
+
|
|
170
|
+
# For square QAM (order a perfect square): the nearest constellation point
|
|
171
|
+
# can be found in O(1) per symbol via per-component rounding, eliminating
|
|
172
|
+
# the (CHUNK, B, M_const) distance tensor entirely.
|
|
173
|
+
side = int(order**0.5)
|
|
174
|
+
is_sq_qam = ("qam" in modulation.lower()) and (side * side == order)
|
|
175
|
+
if is_sq_qam:
|
|
176
|
+
# Sorted unique real levels of the constellation (shape: (side,))
|
|
177
|
+
levels = xp.sort(xp.unique(const_xp.real))
|
|
178
|
+
d_grid = float(levels[1] - levels[0]) # uniform grid spacing
|
|
179
|
+
lev_min = float(levels[0])
|
|
180
|
+
|
|
181
|
+
float_dtype = xp.float32 if symbols.dtype == xp.complex64 else xp.float64
|
|
182
|
+
|
|
183
|
+
# Fused CUDA kernel (CuPy + complex64 only): computes the per-symbol
|
|
184
|
+
# min-distance metric for all B candidate phases and all C channels in a
|
|
185
|
+
# single pass, avoiding the materialized (CHUNK, B[, M]) intermediates of
|
|
186
|
+
# the xp path. None => fall back to the xp implementation below.
|
|
187
|
+
_kern = None
|
|
188
|
+
if xp is not np and symbols.dtype == xp.complex64 and B <= 128:
|
|
189
|
+
if is_sq_qam or const_xp.size <= 1024:
|
|
190
|
+
from .. import _cuda
|
|
191
|
+
|
|
192
|
+
_kern = _cuda.get_kernel(
|
|
193
|
+
"bps_min_d2", mode="grid" if is_sq_qam else "table"
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# Chunk size for N axis: bounds peak memory of the distance tensor.
|
|
197
|
+
# Always a multiple of block_size so each chunk covers a whole number of
|
|
198
|
+
# blocks exactly. Rounded up to the nearest multiple ≥ 1024.
|
|
199
|
+
CHUNK_N = max(block_size, ((1024 + block_size - 1) // block_size) * block_size)
|
|
200
|
+
|
|
201
|
+
phi_full = xp.zeros((C, N), dtype=xp.float64)
|
|
202
|
+
phi_blocks = xp.zeros((C, N_blocks), dtype=xp.float64)
|
|
203
|
+
|
|
204
|
+
# Accumulate per-channel distance metrics (N_blocks, B) for all channels.
|
|
205
|
+
metrics_all = xp.zeros((C, N_blocks, B), dtype=float_dtype)
|
|
206
|
+
|
|
207
|
+
if _kern is not None:
|
|
208
|
+
# One kernel call per chunk covering all C channels; output (B, C, n)
|
|
209
|
+
# is block-summed and transposed into metrics_all's (C, n_b, B)
|
|
210
|
+
# layout. The kernel writes only the minima, so the chunk can be far
|
|
211
|
+
# larger than the tensor-bounded CHUNK_N of the xp path.
|
|
212
|
+
chunk_gpu = ((131072 + block_size - 1) // block_size) * block_size
|
|
213
|
+
const_c64 = None if is_sq_qam else const_xp.astype(xp.complex64)
|
|
214
|
+
for n0 in range(0, N_trunc, chunk_gpu):
|
|
215
|
+
n1 = min(n0 + chunk_gpu, N_trunc)
|
|
216
|
+
if is_sq_qam:
|
|
217
|
+
md = _kern(
|
|
218
|
+
symbols[:, n0:n1],
|
|
219
|
+
phasors,
|
|
220
|
+
lev_min=lev_min,
|
|
221
|
+
d_grid=d_grid,
|
|
222
|
+
side=side,
|
|
223
|
+
)
|
|
224
|
+
else:
|
|
225
|
+
md = _kern(symbols[:, n0:n1], phasors, constellation=const_c64)
|
|
226
|
+
b0 = n0 // block_size
|
|
227
|
+
n_b = (n1 - n0) // block_size
|
|
228
|
+
metrics_all[:, b0 : b0 + n_b] = (
|
|
229
|
+
md.reshape(B, C, n_b, block_size).sum(axis=3).transpose(1, 2, 0)
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
else:
|
|
233
|
+
for ch in range(C):
|
|
234
|
+
sym = symbols[ch, :N_trunc] # (N_trunc,)
|
|
235
|
+
|
|
236
|
+
for n0 in range(0, N_trunc, CHUNK_N):
|
|
237
|
+
n1 = min(n0 + CHUNK_N, N_trunc)
|
|
238
|
+
x_rot = sym[n0:n1, None] * phasors[None, :] # (CHUNK, B)
|
|
239
|
+
|
|
240
|
+
if is_sq_qam:
|
|
241
|
+
# O(1) nearest-point: round each component to the nearest grid level
|
|
242
|
+
r_idx = xp.clip(
|
|
243
|
+
xp.round((x_rot.real - lev_min) / d_grid).astype(xp.int64),
|
|
244
|
+
0,
|
|
245
|
+
side - 1,
|
|
246
|
+
)
|
|
247
|
+
i_idx = xp.clip(
|
|
248
|
+
xp.round((x_rot.imag - lev_min) / d_grid).astype(xp.int64),
|
|
249
|
+
0,
|
|
250
|
+
side - 1,
|
|
251
|
+
)
|
|
252
|
+
r_near = levels[r_idx] # (CHUNK, B)
|
|
253
|
+
i_near = levels[i_idx] # (CHUNK, B)
|
|
254
|
+
chunk_min_d = (
|
|
255
|
+
(x_rot.real - r_near) ** 2 + (x_rot.imag - i_near) ** 2
|
|
256
|
+
).astype(float_dtype)
|
|
257
|
+
else:
|
|
258
|
+
# General: (CHUNK, B, M_const) - bounded by CHUNK_N
|
|
259
|
+
d_sq = xp.abs(x_rot[:, :, None] - const_xp[None, None, :]) ** 2
|
|
260
|
+
chunk_min_d = xp.min(d_sq, axis=-1).astype(float_dtype)
|
|
261
|
+
|
|
262
|
+
b0 = n0 // block_size
|
|
263
|
+
n_b = (n1 - n0) // block_size
|
|
264
|
+
metrics_all[ch, b0 : b0 + n_b] = chunk_min_d.reshape(
|
|
265
|
+
n_b, block_size, B
|
|
266
|
+
).sum(axis=1)
|
|
267
|
+
|
|
268
|
+
# Phase estimation: joint (sum metrics across channels) or independent per channel.
|
|
269
|
+
if joint_channels and C > 1:
|
|
270
|
+
metric_joint = xp.sum(metrics_all, axis=0) # (N_blocks, B)
|
|
271
|
+
best_k_joint = xp.argmin(metric_joint, axis=-1) # (N_blocks,)
|
|
272
|
+
phi_b_joint = candidates[best_k_joint] # (N_blocks,)
|
|
273
|
+
phi_u_joint = xp.unwrap(phi_b_joint.astype(xp.float64) * 4, axis=-1) / 4
|
|
274
|
+
if cycle_slip_correction:
|
|
275
|
+
phi_u_joint_np = correct_cycle_slips(
|
|
276
|
+
to_device(phi_u_joint, "cpu"),
|
|
277
|
+
4,
|
|
278
|
+
cycle_slip_history,
|
|
279
|
+
cycle_slip_threshold,
|
|
280
|
+
)
|
|
281
|
+
phi_u_joint = xp.asarray(phi_u_joint_np)
|
|
282
|
+
for ch in range(C):
|
|
283
|
+
phi_full[ch] = (
|
|
284
|
+
phi_u_joint[idx_left] * (1.0 - t_interp)
|
|
285
|
+
+ phi_u_joint[idx_right] * t_interp
|
|
286
|
+
)
|
|
287
|
+
phi_blocks[ch] = phi_u_joint
|
|
288
|
+
else:
|
|
289
|
+
for ch in range(C):
|
|
290
|
+
metric = metrics_all[ch] # (N_blocks, B)
|
|
291
|
+
best_k = xp.argmin(metric, axis=-1) # (N_blocks,)
|
|
292
|
+
phi_b = candidates[best_k] # (N_blocks,)
|
|
293
|
+
phi_u = xp.unwrap(phi_b.astype(xp.float64) * 4, axis=-1) / 4
|
|
294
|
+
if cycle_slip_correction:
|
|
295
|
+
phi_u_np = correct_cycle_slips(
|
|
296
|
+
to_device(phi_u, "cpu"), 4, cycle_slip_history, cycle_slip_threshold
|
|
297
|
+
)
|
|
298
|
+
phi_u = xp.asarray(phi_u_np)
|
|
299
|
+
phi_full[ch] = (
|
|
300
|
+
phi_u[idx_left] * (1.0 - t_interp) + phi_u[idx_right] * t_interp
|
|
301
|
+
)
|
|
302
|
+
phi_blocks[ch] = phi_u
|
|
303
|
+
|
|
304
|
+
# Host copy of the full phase trajectory is needed only for the INFO summary
|
|
305
|
+
# below and the optional debug plot - skip the transfer + reductions when
|
|
306
|
+
# neither is active (the device phi_full drives the actual correction).
|
|
307
|
+
_want_log = logger.isEnabledFor(logging.INFO)
|
|
308
|
+
if _want_log or debug_plot:
|
|
309
|
+
phi_full_np = to_device(phi_full, "cpu")
|
|
310
|
+
if _want_log:
|
|
311
|
+
phi_mean_deg = float(np.mean(phi_full_np)) * 180.0 / np.pi
|
|
312
|
+
phi_std_deg = float(np.std(phi_full_np)) * 180.0 / np.pi
|
|
313
|
+
mode_str = "joint" if (joint_channels and C > 1) else "independent"
|
|
314
|
+
logger.info(
|
|
315
|
+
"CPR (BPS, B=%s, %s): phase mean=%.2f°, std=%.2f° [%s blocks x %s symbols, C=%s, cycle_slip_correction=%s]",
|
|
316
|
+
B,
|
|
317
|
+
mode_str,
|
|
318
|
+
phi_mean_deg,
|
|
319
|
+
phi_std_deg,
|
|
320
|
+
N_blocks,
|
|
321
|
+
block_size,
|
|
322
|
+
C,
|
|
323
|
+
cycle_slip_correction,
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
if debug_plot:
|
|
327
|
+
from .. import plotting as _plotting
|
|
328
|
+
|
|
329
|
+
_plotting.plot_carrier_phase_trajectory(
|
|
330
|
+
phi_full=phi_full_np,
|
|
331
|
+
show=True,
|
|
332
|
+
title="CPR - Blind Phase Search",
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
if was_1d:
|
|
336
|
+
return phi_full[0]
|
|
337
|
+
return phi_full
|