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
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
"""Laser/carrier characterization plots (drift, Allan, linewidth)."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import matplotlib.pyplot as plt
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from ..backend import to_device
|
|
9
|
+
from .sync import plot_carrier_phase_decomposition
|
|
10
|
+
from .theme import (
|
|
11
|
+
_as_channels,
|
|
12
|
+
_grid_figsize,
|
|
13
|
+
_set_eng_formatter,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def plot_frequency_drift(
|
|
18
|
+
df,
|
|
19
|
+
*,
|
|
20
|
+
symbol_rate: float,
|
|
21
|
+
amp_ref: float | None = None,
|
|
22
|
+
ax=None,
|
|
23
|
+
show: bool = False,
|
|
24
|
+
title: str = "Residual frequency drift",
|
|
25
|
+
) -> tuple[Any, Any] | None:
|
|
26
|
+
"""
|
|
27
|
+
Plots the instantaneous residual frequency offset vs time.
|
|
28
|
+
|
|
29
|
+
``df`` is the per-symbol frequency wander from
|
|
30
|
+
``analysis.frequency_drift_metrics`` - the slope of the smoothed (drift)
|
|
31
|
+
phase. This is the spin the carrier-phase recovery must track.
|
|
32
|
+
|
|
33
|
+
Parameters
|
|
34
|
+
----------
|
|
35
|
+
df : array_like
|
|
36
|
+
Residual frequency in Hz. Shape ``(M,)`` or ``(C, M)``.
|
|
37
|
+
symbol_rate : float
|
|
38
|
+
Symbol rate in Baud (time axis).
|
|
39
|
+
amp_ref : float, optional
|
|
40
|
+
If given, draws dashed ``±amp_ref`` reference lines (e.g. the
|
|
41
|
+
injected wander amplitude in a simulation).
|
|
42
|
+
ax : Axes, optional
|
|
43
|
+
show : bool, default False
|
|
44
|
+
title : str
|
|
45
|
+
|
|
46
|
+
Returns
|
|
47
|
+
-------
|
|
48
|
+
(fig, ax) or None
|
|
49
|
+
"""
|
|
50
|
+
df_c = _as_channels(df)
|
|
51
|
+
C, M = df_c.shape
|
|
52
|
+
|
|
53
|
+
if ax is None:
|
|
54
|
+
fig, axi = plt.subplots(1, 1)
|
|
55
|
+
else:
|
|
56
|
+
axi = ax
|
|
57
|
+
fig = axi.figure
|
|
58
|
+
|
|
59
|
+
t = np.arange(M) / float(symbol_rate)
|
|
60
|
+
for i in range(C):
|
|
61
|
+
axi.plot(t, df_c[i], color=f"C{i}", label=f"Pol {i}" if C > 1 else None)
|
|
62
|
+
|
|
63
|
+
if amp_ref is not None:
|
|
64
|
+
axi.axhline(amp_ref, color="white", ls="--", label="±amplitude")
|
|
65
|
+
axi.axhline(-amp_ref, color="white", ls="--")
|
|
66
|
+
|
|
67
|
+
_set_eng_formatter(axi, "x", "s")
|
|
68
|
+
_set_eng_formatter(axi, "y", "Hz")
|
|
69
|
+
axi.set_xlabel("Time [s]")
|
|
70
|
+
axi.set_ylabel(r"$\Delta f$ [Hz]")
|
|
71
|
+
axi.set_title(title)
|
|
72
|
+
if C > 1 or amp_ref is not None:
|
|
73
|
+
axi.legend(loc="best")
|
|
74
|
+
|
|
75
|
+
if show:
|
|
76
|
+
plt.show()
|
|
77
|
+
return None
|
|
78
|
+
return fig, axi
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _log_cell_median(f_pos, s, sel, points_per_octave=24):
|
|
82
|
+
"""Median-reduce a PSD onto a log-frequency grid (NaN for empty cells).
|
|
83
|
+
|
|
84
|
+
Returns geometric cell-center frequencies and the per-cell median of the
|
|
85
|
+
``sel``-selected bins - the readable trace for dense Welch spectra, and
|
|
86
|
+
exactly the reduction the plateau detector medians over.
|
|
87
|
+
"""
|
|
88
|
+
if not sel.any():
|
|
89
|
+
return np.array([]), np.array([])
|
|
90
|
+
f_lo, f_hi = float(f_pos[sel][0]), float(f_pos[sel][-1])
|
|
91
|
+
if f_hi <= f_lo:
|
|
92
|
+
return np.array([f_lo]), np.array([float(np.median(s[sel]))])
|
|
93
|
+
n_cells = max(int(np.ceil(np.log2(f_hi / f_lo) * points_per_octave)), 1)
|
|
94
|
+
edges = np.geomspace(f_lo, f_hi, n_cells + 1)
|
|
95
|
+
centers = np.sqrt(edges[:-1] * edges[1:])
|
|
96
|
+
idx = np.clip(np.searchsorted(edges, f_pos, side="right") - 1, 0, n_cells - 1)
|
|
97
|
+
med = np.full(n_cells, np.nan)
|
|
98
|
+
for k in range(n_cells):
|
|
99
|
+
cell_sel = sel & (idx == k)
|
|
100
|
+
if cell_sel.any():
|
|
101
|
+
med[k] = float(np.median(s[cell_sel]))
|
|
102
|
+
return centers, med
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def plot_frequency_noise_psd(
|
|
106
|
+
f,
|
|
107
|
+
S_f,
|
|
108
|
+
*,
|
|
109
|
+
beta_line=None,
|
|
110
|
+
floor=None,
|
|
111
|
+
band: tuple[float, float] | None = None,
|
|
112
|
+
above=None,
|
|
113
|
+
used=None,
|
|
114
|
+
ax=None,
|
|
115
|
+
show: bool = False,
|
|
116
|
+
title: str = "Frequency-noise PSD",
|
|
117
|
+
) -> tuple[Any, Any] | None:
|
|
118
|
+
"""
|
|
119
|
+
Plots the frequency-noise PSD S_f(f) on log-log axes.
|
|
120
|
+
|
|
121
|
+
Overlays the optional Di Domenico β-separation line and the white-FM-noise
|
|
122
|
+
floor. Two distinct region annotations, matching the two estimator
|
|
123
|
+
families:
|
|
124
|
+
|
|
125
|
+
* ``band`` - the ``[f_min, f_max]`` **analysis fence** (light span with
|
|
126
|
+
edge lines): the window the white-FM-floor *median* is read from, or
|
|
127
|
+
the outer fence of the β-integration. It is *not* itself the
|
|
128
|
+
integration region.
|
|
129
|
+
* ``above`` - the **actual β-integration region** ``{f : S_f(f) > β(f)}``
|
|
130
|
+
(in general a union of disjoint intervals): the area between the β-line
|
|
131
|
+
and the PSD is filled wherever the mask is true. Pass
|
|
132
|
+
``linewidth_beta_separation(...)['above']``.
|
|
133
|
+
|
|
134
|
+
See ``analysis.fm_noise_psd`` and ``analysis.linewidth_beta_separation``.
|
|
135
|
+
|
|
136
|
+
Parameters
|
|
137
|
+
----------
|
|
138
|
+
f : array_like
|
|
139
|
+
One-sided frequency axis in Hz, shape ``(nfreq,)``.
|
|
140
|
+
S_f : array_like
|
|
141
|
+
Frequency-noise PSD in Hz²/Hz, shape ``(nfreq,)`` or ``(C, nfreq)``.
|
|
142
|
+
beta_line : array_like, optional
|
|
143
|
+
β-separation line ``(8 ln2/π²)·f``, shape ``(nfreq,)``. Drawn dashed.
|
|
144
|
+
floor : float or array_like, optional
|
|
145
|
+
White-FM linewidth estimate(s) in Hz; a horizontal guide is drawn at the
|
|
146
|
+
corresponding PSD level ``S_f = Δν/π``.
|
|
147
|
+
band : (float, float), optional
|
|
148
|
+
``(f_min, f_max)`` analysis fence. The lower edge is clamped to the
|
|
149
|
+
first positive frequency bin for display (a 0 Hz fence is a
|
|
150
|
+
resolution statement, not a plottable frequency on a log axis).
|
|
151
|
+
above : array_like of bool, optional
|
|
152
|
+
Integration-region mask aligned with ``f``, shape ``(nfreq,)`` or
|
|
153
|
+
``(C, nfreq)`` (channel 0 is drawn). Requires ``beta_line``.
|
|
154
|
+
used : array_like of bool, optional
|
|
155
|
+
Mask of the bins a floor *median* actually ran over (the
|
|
156
|
+
auto-detected plateau). Sparse masks (≤ 400 bins) are drawn as
|
|
157
|
+
markers on the PSD trace; dense masks as a highlighted **log-binned
|
|
158
|
+
median curve** over the accepted region (per-bin markers would
|
|
159
|
+
splatter the figure). Pass ``linewidth_dsh(...)['used']`` /
|
|
160
|
+
``linewidth_beta_separation(...)['used']``. Channel 0 is drawn.
|
|
161
|
+
ax : Axes, optional
|
|
162
|
+
show : bool, default False
|
|
163
|
+
title : str
|
|
164
|
+
|
|
165
|
+
Returns
|
|
166
|
+
-------
|
|
167
|
+
(fig, ax) or None
|
|
168
|
+
"""
|
|
169
|
+
f_c = np.asarray(to_device(f, "cpu"), dtype=np.float64)
|
|
170
|
+
S_c = _as_channels(S_f)
|
|
171
|
+
C = S_c.shape[0]
|
|
172
|
+
pos = f_c > 0
|
|
173
|
+
fp = f_c[pos]
|
|
174
|
+
# Dense Welch spectra (low-averaged DSH deconvolutions especially) are
|
|
175
|
+
# unreadable raw: draw them faint and put a log-binned median curve on
|
|
176
|
+
# top - the same reduction the plateau detector runs on.
|
|
177
|
+
dense = int(pos.sum()) > 4000
|
|
178
|
+
|
|
179
|
+
if ax is None:
|
|
180
|
+
fig, axi = plt.subplots(1, 1)
|
|
181
|
+
else:
|
|
182
|
+
axi = ax
|
|
183
|
+
fig = axi.figure
|
|
184
|
+
|
|
185
|
+
for i in range(C):
|
|
186
|
+
s_i = S_c[i, pos]
|
|
187
|
+
lbl = f"$S_f$ pol {i}" if C > 1 else "$S_f(f)$"
|
|
188
|
+
if dense:
|
|
189
|
+
axi.loglog(fp, s_i, color=f"C{i}", alpha=0.4)
|
|
190
|
+
fg, sg = _log_cell_median(fp, s_i, np.isfinite(s_i))
|
|
191
|
+
axi.loglog(fg, sg, color=f"C{i}", label=lbl + " (log-binned)")
|
|
192
|
+
else:
|
|
193
|
+
axi.loglog(fp, s_i, color=f"C{i}", label=lbl)
|
|
194
|
+
|
|
195
|
+
b_c = None
|
|
196
|
+
if beta_line is not None:
|
|
197
|
+
b_c = np.asarray(to_device(beta_line, "cpu"), dtype=np.float64)
|
|
198
|
+
axi.loglog(
|
|
199
|
+
f_c[pos],
|
|
200
|
+
b_c[pos],
|
|
201
|
+
color="#ff5555",
|
|
202
|
+
ls="--",
|
|
203
|
+
label=r"$\beta$-separation line",
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
if above is not None and b_c is not None:
|
|
207
|
+
a_c = np.asarray(to_device(above, "cpu"), dtype=bool)
|
|
208
|
+
if a_c.ndim > 1:
|
|
209
|
+
a_c = a_c[0]
|
|
210
|
+
axi.fill_between(
|
|
211
|
+
f_c[pos],
|
|
212
|
+
b_c[pos],
|
|
213
|
+
S_c[0, pos],
|
|
214
|
+
where=a_c[pos].tolist(),
|
|
215
|
+
color="#ff5555",
|
|
216
|
+
alpha=0.4,
|
|
217
|
+
lw=0,
|
|
218
|
+
label=r"$\beta$-area (integrated region)",
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
if used is not None:
|
|
222
|
+
u_c = np.asarray(to_device(used, "cpu"), dtype=bool)
|
|
223
|
+
if u_c.ndim > 1:
|
|
224
|
+
u_c = u_c[0]
|
|
225
|
+
sel = u_c[pos] & np.isfinite(S_c[0, pos])
|
|
226
|
+
if int(sel.sum()) <= 400:
|
|
227
|
+
axi.loglog(
|
|
228
|
+
fp[sel],
|
|
229
|
+
S_c[0, pos][sel],
|
|
230
|
+
".",
|
|
231
|
+
color="#06d6a0",
|
|
232
|
+
ms=3.0,
|
|
233
|
+
ls="none",
|
|
234
|
+
label="Plateau bins (median)",
|
|
235
|
+
)
|
|
236
|
+
else:
|
|
237
|
+
# Dense mask: highlight the log-binned median over the accepted
|
|
238
|
+
# region instead of splattering one marker per bin.
|
|
239
|
+
fg, sg = _log_cell_median(fp, S_c[0, pos], sel)
|
|
240
|
+
axi.loglog(fg, sg, color="#06d6a0", label="Plateau (median region)")
|
|
241
|
+
|
|
242
|
+
if floor is not None:
|
|
243
|
+
floors = np.atleast_1d(np.asarray(floor, dtype=np.float64))
|
|
244
|
+
floor_mean = float(np.mean(floors))
|
|
245
|
+
axi.axhline(
|
|
246
|
+
floor_mean / np.pi,
|
|
247
|
+
color="#ffd166",
|
|
248
|
+
ls=":",
|
|
249
|
+
label=r"White-FM floor $\Delta\nu/\pi$",
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
if band is not None:
|
|
253
|
+
lo = max(float(band[0]), float(fp[0])) if fp.size else float(band[0])
|
|
254
|
+
if used is None:
|
|
255
|
+
# With a used-region overlay the span is redundant clutter; keep
|
|
256
|
+
# the full shading only for fence-defined (manual) bands.
|
|
257
|
+
axi.axvspan(
|
|
258
|
+
lo,
|
|
259
|
+
band[1],
|
|
260
|
+
color="#06d6a0",
|
|
261
|
+
alpha=0.4,
|
|
262
|
+
label="Analysis band [f_min, f_max]",
|
|
263
|
+
)
|
|
264
|
+
for edge in (lo, float(band[1])):
|
|
265
|
+
axi.axvline(edge, color="#06d6a0")
|
|
266
|
+
|
|
267
|
+
_set_eng_formatter(axi, "x", "Hz")
|
|
268
|
+
axi.set_xlabel("Frequency [Hz]")
|
|
269
|
+
axi.set_ylabel("$S_f$ [Hz²/Hz]")
|
|
270
|
+
axi.set_title(title)
|
|
271
|
+
axi.legend(loc="best")
|
|
272
|
+
axi.grid(True, which="both")
|
|
273
|
+
|
|
274
|
+
if show:
|
|
275
|
+
plt.show()
|
|
276
|
+
return None
|
|
277
|
+
return fig, axi
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def plot_allan_deviation(
|
|
281
|
+
tau_s,
|
|
282
|
+
adev,
|
|
283
|
+
*,
|
|
284
|
+
reference_slopes: bool = True,
|
|
285
|
+
ax=None,
|
|
286
|
+
show: bool = False,
|
|
287
|
+
title: str = "Allan deviation",
|
|
288
|
+
) -> tuple[Any, Any] | None:
|
|
289
|
+
"""
|
|
290
|
+
Plots the (overlapping) Allan deviation vs averaging time on log-log axes.
|
|
291
|
+
|
|
292
|
+
The local slope classifies the dominant frequency-noise process:
|
|
293
|
+
white-FM ~ tau^(-1/2), flicker-FM ~ tau^0 (flat), random-walk-FM ~ tau^(+1/2),
|
|
294
|
+
linear drift ~ tau^(+1). See ``analysis.allan_deviation``.
|
|
295
|
+
|
|
296
|
+
Parameters
|
|
297
|
+
----------
|
|
298
|
+
tau_s : array_like
|
|
299
|
+
Averaging times in seconds, shape ``(n_tau,)``.
|
|
300
|
+
adev : array_like
|
|
301
|
+
Allan deviation in Hz, shape ``(n_tau,)`` or ``(C, n_tau)``.
|
|
302
|
+
reference_slopes : bool, default True
|
|
303
|
+
If True, overlays a faint ``τ^{-1/2}`` (white-FM) guide line.
|
|
304
|
+
ax : Axes, optional
|
|
305
|
+
show : bool, default False
|
|
306
|
+
title : str
|
|
307
|
+
|
|
308
|
+
Returns
|
|
309
|
+
-------
|
|
310
|
+
(fig, ax) or None
|
|
311
|
+
"""
|
|
312
|
+
tau = np.asarray(to_device(tau_s, "cpu"), dtype=np.float64)
|
|
313
|
+
adv = _as_channels(adev)
|
|
314
|
+
C = adv.shape[0]
|
|
315
|
+
|
|
316
|
+
if ax is None:
|
|
317
|
+
fig, axi = plt.subplots(1, 1)
|
|
318
|
+
else:
|
|
319
|
+
axi = ax
|
|
320
|
+
fig = axi.figure
|
|
321
|
+
|
|
322
|
+
for i in range(C):
|
|
323
|
+
axi.loglog(
|
|
324
|
+
tau,
|
|
325
|
+
adv[i],
|
|
326
|
+
"o-",
|
|
327
|
+
ms=3,
|
|
328
|
+
color=f"C{i}",
|
|
329
|
+
label=f"Pol {i}" if C > 1 else r"$\sigma_y(\tau)$",
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
if reference_slopes:
|
|
333
|
+
good = np.isfinite(adv[0]) & (adv[0] > 0)
|
|
334
|
+
if np.any(good):
|
|
335
|
+
tau0, a0 = tau[good][0], adv[0][good][0]
|
|
336
|
+
guide = a0 * np.sqrt(tau0 / tau) # τ^{-1/2} anchored at first point
|
|
337
|
+
axi.loglog(
|
|
338
|
+
tau,
|
|
339
|
+
guide,
|
|
340
|
+
color="gray",
|
|
341
|
+
ls=":",
|
|
342
|
+
label=r"$\tau^{-1/2}$ (white-FM)",
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
_set_eng_formatter(axi, "x", "s")
|
|
346
|
+
_set_eng_formatter(axi, "y", "Hz")
|
|
347
|
+
axi.set_xlabel(r"Averaging Time $\tau$ [s]")
|
|
348
|
+
axi.set_ylabel(r"Allan Deviation $\sigma_y(\tau)$ [Hz]")
|
|
349
|
+
axi.set_title(title)
|
|
350
|
+
axi.legend(loc="best")
|
|
351
|
+
axi.grid(True, which="both")
|
|
352
|
+
|
|
353
|
+
if show:
|
|
354
|
+
plt.show()
|
|
355
|
+
return None
|
|
356
|
+
return fig, axi
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def plot_increment_variance(
|
|
360
|
+
lag_s,
|
|
361
|
+
var,
|
|
362
|
+
*,
|
|
363
|
+
slope=None,
|
|
364
|
+
intercept=None,
|
|
365
|
+
ax=None,
|
|
366
|
+
show: bool = False,
|
|
367
|
+
title: str = "Phase-increment variance",
|
|
368
|
+
) -> tuple[Any, Any] | None:
|
|
369
|
+
"""
|
|
370
|
+
Plots Var of phase increments vs lag with the fitted linear model.
|
|
371
|
+
|
|
372
|
+
The measured points should follow ``Var = slope·lag + intercept`` for
|
|
373
|
+
white-FM (Wiener) phase noise; curvature signals flicker or drift
|
|
374
|
+
contamination. See ``analysis.linewidth_increment`` and
|
|
375
|
+
``analysis.linewidth_dsh(method="increment")``.
|
|
376
|
+
|
|
377
|
+
Parameters
|
|
378
|
+
----------
|
|
379
|
+
lag_s : array_like
|
|
380
|
+
Increment lags in seconds, shape ``(n_lag,)``.
|
|
381
|
+
var : array_like
|
|
382
|
+
Measured increment variance in rad², ``(n_lag,)`` or ``(C, n_lag)``.
|
|
383
|
+
slope : float or array_like, optional
|
|
384
|
+
Fitted slope(s) in rad²/s (per channel). Drawn with ``intercept``.
|
|
385
|
+
intercept : float or array_like, optional
|
|
386
|
+
Fitted intercept(s) in rad² - the additive-noise term ``2σ_φ²``.
|
|
387
|
+
ax : Axes, optional
|
|
388
|
+
show : bool, default False
|
|
389
|
+
title : str
|
|
390
|
+
|
|
391
|
+
Returns
|
|
392
|
+
-------
|
|
393
|
+
(fig, ax) or None
|
|
394
|
+
"""
|
|
395
|
+
lag = np.asarray(to_device(lag_s, "cpu"), dtype=np.float64)
|
|
396
|
+
v = _as_channels(var)
|
|
397
|
+
C = v.shape[0]
|
|
398
|
+
|
|
399
|
+
if ax is None:
|
|
400
|
+
fig, axi = plt.subplots(1, 1)
|
|
401
|
+
else:
|
|
402
|
+
axi = ax
|
|
403
|
+
fig = axi.figure
|
|
404
|
+
|
|
405
|
+
for i in range(C):
|
|
406
|
+
axi.plot(
|
|
407
|
+
lag,
|
|
408
|
+
v[i],
|
|
409
|
+
"o",
|
|
410
|
+
ms=4,
|
|
411
|
+
color=f"C{i}",
|
|
412
|
+
label=f"Measured pol {i}" if C > 1 else "Measured",
|
|
413
|
+
)
|
|
414
|
+
if slope is not None and intercept is not None:
|
|
415
|
+
sl = np.atleast_1d(np.asarray(to_device(slope, "cpu"), dtype=np.float64))
|
|
416
|
+
ic = np.atleast_1d(np.asarray(to_device(intercept, "cpu"), dtype=np.float64))
|
|
417
|
+
for i in range(C):
|
|
418
|
+
axi.plot(
|
|
419
|
+
lag,
|
|
420
|
+
sl[min(i, sl.size - 1)] * lag + ic[min(i, ic.size - 1)],
|
|
421
|
+
"-",
|
|
422
|
+
color=f"C{i}",
|
|
423
|
+
alpha=0.4,
|
|
424
|
+
label="Fit" if i == 0 else None,
|
|
425
|
+
)
|
|
426
|
+
axi.axhline(
|
|
427
|
+
float(np.mean(ic)),
|
|
428
|
+
color="gray",
|
|
429
|
+
ls=":",
|
|
430
|
+
label=r"Intercept (noise $2\sigma_\phi^2$)",
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
_set_eng_formatter(axi, "x", "s")
|
|
434
|
+
axi.set_xlabel("Increment Lag [s]")
|
|
435
|
+
axi.set_ylabel("Variance [rad²]")
|
|
436
|
+
axi.set_title(title)
|
|
437
|
+
axi.legend(loc="best")
|
|
438
|
+
|
|
439
|
+
if show:
|
|
440
|
+
plt.show()
|
|
441
|
+
return None
|
|
442
|
+
return fig, axi
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def plot_dsh_beat_psd(
|
|
446
|
+
f,
|
|
447
|
+
psd,
|
|
448
|
+
*,
|
|
449
|
+
f_peak=None,
|
|
450
|
+
linewidth=None,
|
|
451
|
+
linewidth_3db=None,
|
|
452
|
+
level_db: float = 20.0,
|
|
453
|
+
ax=None,
|
|
454
|
+
show: bool = False,
|
|
455
|
+
title: str = "DSH beat spectrum",
|
|
456
|
+
) -> tuple[Any, Any] | None:
|
|
457
|
+
"""
|
|
458
|
+
Plots the self-heterodyne beat PSD (dB rel. peak) with width annotations.
|
|
459
|
+
|
|
460
|
+
Overlays the half-power and ``-level_db`` contours and shades the full
|
|
461
|
+
widths implied by the linewidth estimates (``FWHM = 2Δν``,
|
|
462
|
+
``W_L = 2√(10^{L/10}-1)·Δν``). See
|
|
463
|
+
``analysis.linewidth_dsh(method="lorentzian")``.
|
|
464
|
+
|
|
465
|
+
Parameters
|
|
466
|
+
----------
|
|
467
|
+
f : array_like
|
|
468
|
+
Two-sided frequency axis in Hz, shape ``(nfreq,)``.
|
|
469
|
+
psd : array_like
|
|
470
|
+
Beat PSD (linear), ``(nfreq,)`` or ``(C, nfreq)``.
|
|
471
|
+
f_peak : float or array_like, optional
|
|
472
|
+
Beat carrier location(s) in Hz; the x-axis is centered on the mean.
|
|
473
|
+
linewidth : float or array_like, optional
|
|
474
|
+
Deep-width linewidth estimate(s) Δν in Hz.
|
|
475
|
+
linewidth_3db : float or array_like, optional
|
|
476
|
+
Half-power linewidth estimate(s) in Hz.
|
|
477
|
+
level_db : float, default 20.0
|
|
478
|
+
Depth of the deep-width contour.
|
|
479
|
+
ax : Axes, optional
|
|
480
|
+
show : bool, default False
|
|
481
|
+
title : str
|
|
482
|
+
|
|
483
|
+
Returns
|
|
484
|
+
-------
|
|
485
|
+
(fig, ax) or None
|
|
486
|
+
"""
|
|
487
|
+
f_c = np.asarray(to_device(f, "cpu"), dtype=np.float64)
|
|
488
|
+
p = _as_channels(psd)
|
|
489
|
+
C = p.shape[0]
|
|
490
|
+
f0 = 0.0
|
|
491
|
+
if f_peak is not None:
|
|
492
|
+
f0 = float(np.mean(np.atleast_1d(np.asarray(f_peak, dtype=np.float64))))
|
|
493
|
+
|
|
494
|
+
if ax is None:
|
|
495
|
+
fig, axi = plt.subplots(1, 1)
|
|
496
|
+
else:
|
|
497
|
+
axi = ax
|
|
498
|
+
fig = axi.figure
|
|
499
|
+
|
|
500
|
+
for i in range(C):
|
|
501
|
+
p_db = 10.0 * np.log10(p[i] / p[i].max())
|
|
502
|
+
axi.plot(
|
|
503
|
+
f_c - f0,
|
|
504
|
+
p_db,
|
|
505
|
+
color=f"C{i}",
|
|
506
|
+
label=f"Pol {i}" if C > 1 else "Beat PSD",
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
if linewidth_3db is not None:
|
|
510
|
+
w3 = 2.0 * float(np.mean(np.atleast_1d(linewidth_3db)))
|
|
511
|
+
axi.axhline(-3.01, color="C1", ls=":")
|
|
512
|
+
axi.axvspan(
|
|
513
|
+
-w3 / 2, w3 / 2, color="C1", alpha=0.4, label=r"FWHM = $2\Delta\nu$"
|
|
514
|
+
)
|
|
515
|
+
if linewidth is not None:
|
|
516
|
+
r = 10.0 ** (float(level_db) / 10.0)
|
|
517
|
+
w_deep = 2.0 * np.sqrt(r - 1.0) * float(np.mean(np.atleast_1d(linewidth)))
|
|
518
|
+
axi.axhline(-float(level_db), color="C3", ls=":")
|
|
519
|
+
axi.axvspan(
|
|
520
|
+
-w_deep / 2,
|
|
521
|
+
w_deep / 2,
|
|
522
|
+
color="C3",
|
|
523
|
+
alpha=0.4,
|
|
524
|
+
label=f"$W_{{-{level_db:g}\\,dB}}$",
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
_set_eng_formatter(axi, "x", "Hz")
|
|
528
|
+
axi.set_xlabel(
|
|
529
|
+
"Frequency Offset from Beat Peak [Hz]"
|
|
530
|
+
if f_peak is not None
|
|
531
|
+
else "Frequency [Hz]"
|
|
532
|
+
)
|
|
533
|
+
axi.set_ylabel("PSD [dB rel. peak]")
|
|
534
|
+
axi.set_title(title)
|
|
535
|
+
axi.legend(loc="best")
|
|
536
|
+
|
|
537
|
+
if show:
|
|
538
|
+
plt.show()
|
|
539
|
+
return None
|
|
540
|
+
return fig, axi
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def plot_carrier_phase_characterization(
|
|
544
|
+
report: dict,
|
|
545
|
+
*,
|
|
546
|
+
symbol_rate: float,
|
|
547
|
+
drift_cutoff: float | None = None,
|
|
548
|
+
band: tuple[float, float] | None = None,
|
|
549
|
+
floor=None,
|
|
550
|
+
amp_ref: float | None = None,
|
|
551
|
+
show: bool = False,
|
|
552
|
+
title: str | None = None,
|
|
553
|
+
) -> tuple[Any, Any] | None:
|
|
554
|
+
"""
|
|
555
|
+
Full 2x2 carrier-phase characterization dashboard.
|
|
556
|
+
|
|
557
|
+
Combines ``carrier_phase_decomposition``, ``frequency_drift``,
|
|
558
|
+
``frequency_noise_psd``, and ``allan_deviation`` into one figure from a
|
|
559
|
+
report dict assembled by the caller (see
|
|
560
|
+
``examples/carrier_phase_analysis.py`` for the full chain).
|
|
561
|
+
|
|
562
|
+
Parameters
|
|
563
|
+
----------
|
|
564
|
+
report : dict
|
|
565
|
+
``{'phi', 'drift', 'drift_metrics', 'linewidth_beta', 'allan'}`` -
|
|
566
|
+
the outputs of ``carrier_phase_trajectory``,
|
|
567
|
+
``separate_drift_phase_noise``, ``frequency_drift_metrics``,
|
|
568
|
+
``linewidth_beta_separation``, and ``allan_deviation``.
|
|
569
|
+
symbol_rate : float
|
|
570
|
+
Symbol rate in Baud.
|
|
571
|
+
drift_cutoff : float, optional
|
|
572
|
+
Annotated in the phase-decomposition panel title.
|
|
573
|
+
band : (float, float), optional
|
|
574
|
+
``(f_min, f_max)`` integration band, shaded on the PSD panel.
|
|
575
|
+
floor : float or array_like, optional
|
|
576
|
+
White-FM floor guide; defaults to the report's estimated floor.
|
|
577
|
+
amp_ref : float, optional
|
|
578
|
+
Injected wander amplitude reference for the drift panel.
|
|
579
|
+
show : bool, default False
|
|
580
|
+
title : str, optional
|
|
581
|
+
|
|
582
|
+
Returns
|
|
583
|
+
-------
|
|
584
|
+
(fig, axes) or None
|
|
585
|
+
"""
|
|
586
|
+
fig, axes = plt.subplots(2, 2, figsize=_grid_figsize(2, 2))
|
|
587
|
+
|
|
588
|
+
lp = f" (LP {drift_cutoff / 1e6:.1f} MHz)" if drift_cutoff else ""
|
|
589
|
+
plot_carrier_phase_decomposition(
|
|
590
|
+
report["phi"],
|
|
591
|
+
report.get("drift"),
|
|
592
|
+
symbol_rate=symbol_rate,
|
|
593
|
+
ax=axes[0, 0],
|
|
594
|
+
title=f"Recovered carrier phase{lp}",
|
|
595
|
+
)
|
|
596
|
+
plot_frequency_drift(
|
|
597
|
+
report["drift_metrics"]["df"],
|
|
598
|
+
symbol_rate=symbol_rate,
|
|
599
|
+
amp_ref=amp_ref,
|
|
600
|
+
ax=axes[0, 1],
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
lw_beta = report["linewidth_beta"]
|
|
604
|
+
if floor is None:
|
|
605
|
+
floor = lw_beta.get("linewidth_floor")
|
|
606
|
+
plot_frequency_noise_psd(
|
|
607
|
+
lw_beta["f"],
|
|
608
|
+
lw_beta["S_f"],
|
|
609
|
+
beta_line=lw_beta.get("beta_line"),
|
|
610
|
+
floor=floor,
|
|
611
|
+
band=band,
|
|
612
|
+
above=lw_beta.get("above"),
|
|
613
|
+
used=lw_beta.get("used"),
|
|
614
|
+
ax=axes[1, 0],
|
|
615
|
+
)
|
|
616
|
+
plot_allan_deviation(
|
|
617
|
+
report["allan"]["tau_s"],
|
|
618
|
+
report["allan"]["adev"],
|
|
619
|
+
ax=axes[1, 1],
|
|
620
|
+
)
|
|
621
|
+
|
|
622
|
+
if title:
|
|
623
|
+
fig.suptitle(title)
|
|
624
|
+
if show:
|
|
625
|
+
plt.show()
|
|
626
|
+
return None
|
|
627
|
+
return fig, axes
|