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,581 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Signal generation factories.
|
|
3
|
+
|
|
4
|
+
Free functions that construct :class:`Signal` instances for the supported
|
|
5
|
+
modulation formats (``generate`` plus the ``generate_pam``/``generate_psk``/
|
|
6
|
+
``generate_qam``/``generate_psqam`` wrappers); they are re-exported at the
|
|
7
|
+
package top level (``commkit.generate_qam(...)`` etc.).
|
|
8
|
+
|
|
9
|
+
All factories follow a bit-first architecture: random bits are generated, mapped
|
|
10
|
+
to symbols, upsampled, and pulse-shaped, with samples normalized to unit symbol
|
|
11
|
+
power (Es = 1, average sample power = 1/sps).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from typing import Literal, cast
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
|
|
18
|
+
from .. import filtering, helpers, mapping
|
|
19
|
+
from ..backend import ArrayType, is_cupy_available, to_device
|
|
20
|
+
from ..logger import logger
|
|
21
|
+
from .signal import Signal
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def generate(
|
|
25
|
+
num_symbols: int,
|
|
26
|
+
sps: int,
|
|
27
|
+
symbol_rate: float,
|
|
28
|
+
modulation: str,
|
|
29
|
+
order: int,
|
|
30
|
+
unipolar: bool = False,
|
|
31
|
+
rz: bool = False,
|
|
32
|
+
pulse_shape: str = "none",
|
|
33
|
+
num_streams: int = 1,
|
|
34
|
+
seed: int | None = None,
|
|
35
|
+
duty_cycle: float = 1.0,
|
|
36
|
+
filter_span: int = 10,
|
|
37
|
+
rrc_rolloff: float = 0.35,
|
|
38
|
+
rc_rolloff: float = 0.35,
|
|
39
|
+
rise_time: float = 0.0,
|
|
40
|
+
) -> "Signal":
|
|
41
|
+
"""
|
|
42
|
+
Generates a generic baseband waveform with specified modulation.
|
|
43
|
+
|
|
44
|
+
This is the primary factory method for creating synthetic signals.
|
|
45
|
+
It follows a bit-first architecture: random bits are generated,
|
|
46
|
+
mapped to symbols, upsampled, and pulse-shaped.
|
|
47
|
+
|
|
48
|
+
Parameters
|
|
49
|
+
----------
|
|
50
|
+
num_symbols : int
|
|
51
|
+
Number of symbols to generate per stream.
|
|
52
|
+
sps : float
|
|
53
|
+
Samples per symbol.
|
|
54
|
+
symbol_rate : float
|
|
55
|
+
Symbol rate in symbols per second (Baud).
|
|
56
|
+
modulation : {"psk", "qam", "ask"}
|
|
57
|
+
The modulation scheme identifier.
|
|
58
|
+
order : int
|
|
59
|
+
Modulation order (e.g., 4, 16, 64).
|
|
60
|
+
unipolar : bool, default False
|
|
61
|
+
If True, uses a unipolar constellation.
|
|
62
|
+
rz : bool, default False
|
|
63
|
+
If True, uses Return-to-Zero signaling.
|
|
64
|
+
pulse_shape : str, default "none"
|
|
65
|
+
Pulse shaping filter type (e.g., ``'rrc'``, ``'rect'``).
|
|
66
|
+
num_streams : int, default 1
|
|
67
|
+
Number of independent streams (MIMO).
|
|
68
|
+
seed : int, optional
|
|
69
|
+
Seed for reproducible random generation.
|
|
70
|
+
duty_cycle : float, default 1.0
|
|
71
|
+
Fraction of the symbol period occupied by the pulse (rect/smoothrect).
|
|
72
|
+
Overridden to 0.5 when ``rz=True``.
|
|
73
|
+
filter_span : int, default 10
|
|
74
|
+
Filter span in symbols for smoothrect/gaussian/rrc/rc/sinc.
|
|
75
|
+
rrc_rolloff : float, default 0.35
|
|
76
|
+
Roll-off factor for the RRC filter.
|
|
77
|
+
rc_rolloff : float, default 0.35
|
|
78
|
+
Roll-off factor for the RC filter.
|
|
79
|
+
rise_time : float, default 0.22
|
|
80
|
+
10%-90% edge transition duration in symbol periods for smoothrect.
|
|
81
|
+
duty_cycle : float, default 1.0
|
|
82
|
+
FWHM of the Gaussian pulse in symbol periods.
|
|
83
|
+
|
|
84
|
+
Returns
|
|
85
|
+
-------
|
|
86
|
+
Signal
|
|
87
|
+
A new `Signal` instance.
|
|
88
|
+
|
|
89
|
+
Notes
|
|
90
|
+
-----
|
|
91
|
+
Samples are normalized to unit symbol power (Es = 1, average sample power = 1/sps).
|
|
92
|
+
Call ``resolve_symbols()`` before demapping or computing metrics.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
if sps != int(sps) or sps < 1:
|
|
96
|
+
logger.warning(
|
|
97
|
+
"sps=%r is not a positive integer. Non-integer sps is valid for captured/resampled signals but not for generation: resample_poly requires an integer upsampling factor, so the sample buffer would not match the stored sampling_rate metadata. To generate at a fractional sps, generate at an integer sps then call Signal.resample(up=..., down=...).",
|
|
98
|
+
sps,
|
|
99
|
+
)
|
|
100
|
+
raise ValueError(
|
|
101
|
+
f"sps must be a positive integer for signal generation, got {sps!r}."
|
|
102
|
+
)
|
|
103
|
+
sps = int(sps)
|
|
104
|
+
|
|
105
|
+
# When rz=True and the caller hasn't specified a custom duty_cycle,
|
|
106
|
+
# default to 50% (canonical RZ). Explicit duty_cycle values are preserved.
|
|
107
|
+
if rz and duty_cycle == 1.0:
|
|
108
|
+
duty_cycle = 0.5
|
|
109
|
+
|
|
110
|
+
# Bit-first architecture: generate bits -> map to symbols
|
|
111
|
+
k = int(np.log2(order)) # bits per symbol
|
|
112
|
+
total_symbols = num_symbols * num_streams
|
|
113
|
+
total_bits = total_symbols * k
|
|
114
|
+
|
|
115
|
+
# Generate source bits
|
|
116
|
+
bits = helpers.generate_bits(total_bits, seed=seed)
|
|
117
|
+
|
|
118
|
+
# Map bits to symbols
|
|
119
|
+
symbols_flat = mapping.map_bits(bits, modulation, order, unipolar)
|
|
120
|
+
|
|
121
|
+
if num_streams > 1:
|
|
122
|
+
# Shape: (Channels, Time)
|
|
123
|
+
symbols = symbols_flat.reshape(num_streams, num_symbols)
|
|
124
|
+
bits = bits.reshape(num_streams, num_symbols * k)
|
|
125
|
+
else:
|
|
126
|
+
symbols = symbols_flat
|
|
127
|
+
|
|
128
|
+
if is_cupy_available():
|
|
129
|
+
symbols = to_device(symbols, "gpu")
|
|
130
|
+
bits = to_device(bits, "gpu")
|
|
131
|
+
|
|
132
|
+
# Apply pulse shaping
|
|
133
|
+
# shape_pulse defaults to axis=-1 (Time) which is correct for (C, T)
|
|
134
|
+
samples = filtering.shape_pulse(
|
|
135
|
+
symbols=symbols,
|
|
136
|
+
sps=sps,
|
|
137
|
+
pulse_shape=pulse_shape,
|
|
138
|
+
rz=rz,
|
|
139
|
+
duty_cycle=duty_cycle,
|
|
140
|
+
filter_span=filter_span,
|
|
141
|
+
rrc_rolloff=rrc_rolloff,
|
|
142
|
+
rc_rolloff=rc_rolloff,
|
|
143
|
+
rise_time=rise_time,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
logger.info(
|
|
147
|
+
"Generated %s-%s signal: %s symbols x %s stream(s), sps=%s, pulse_shape=%s, %s samples/stream @ %.3g Sa/s.",
|
|
148
|
+
modulation.upper(),
|
|
149
|
+
order,
|
|
150
|
+
num_symbols,
|
|
151
|
+
num_streams,
|
|
152
|
+
sps,
|
|
153
|
+
pulse_shape,
|
|
154
|
+
samples.shape[-1],
|
|
155
|
+
symbol_rate * sps,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
return Signal(
|
|
159
|
+
samples=samples,
|
|
160
|
+
sampling_rate=symbol_rate * sps,
|
|
161
|
+
symbol_rate=symbol_rate,
|
|
162
|
+
mod_scheme=modulation.upper(),
|
|
163
|
+
mod_order=order,
|
|
164
|
+
mod_unipolar=unipolar,
|
|
165
|
+
mod_rz=rz,
|
|
166
|
+
source_bits=bits,
|
|
167
|
+
source_symbols=symbols,
|
|
168
|
+
pulse_shape=pulse_shape,
|
|
169
|
+
filter_span=filter_span,
|
|
170
|
+
rrc_rolloff=rrc_rolloff,
|
|
171
|
+
rc_rolloff=rc_rolloff,
|
|
172
|
+
rise_time=rise_time,
|
|
173
|
+
duty_cycle=duty_cycle,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def generate_pam(
|
|
178
|
+
num_symbols: int,
|
|
179
|
+
sps: int,
|
|
180
|
+
symbol_rate: float,
|
|
181
|
+
order: int,
|
|
182
|
+
unipolar: bool = False,
|
|
183
|
+
rz: bool = False,
|
|
184
|
+
pulse_shape: Literal["rect", "smoothrect"] = "rect",
|
|
185
|
+
num_streams: int = 1,
|
|
186
|
+
seed: int | None = None,
|
|
187
|
+
duty_cycle: float = 1.0,
|
|
188
|
+
filter_span: int = 10,
|
|
189
|
+
rise_time: float = 0.0,
|
|
190
|
+
) -> "Signal":
|
|
191
|
+
"""
|
|
192
|
+
Generates a Pulse Amplitude Modulation (PAM) baseband waveform.
|
|
193
|
+
|
|
194
|
+
Supports both NRZ (Non-Return-to-Zero) and RZ (Return-to-Zero)
|
|
195
|
+
signaling, with configurable pulse shaping and bipolar/unipolar
|
|
196
|
+
constellations.
|
|
197
|
+
|
|
198
|
+
Parameters
|
|
199
|
+
----------
|
|
200
|
+
num_symbols : int
|
|
201
|
+
Total number of symbols to generate per stream.
|
|
202
|
+
sps : int
|
|
203
|
+
Samples per symbol. For RZ mode, this must be an even integer.
|
|
204
|
+
symbol_rate : float
|
|
205
|
+
Symbol rate in symbols per second (Baud).
|
|
206
|
+
order : int
|
|
207
|
+
Modulation order (e.g., 2, 4, 8).
|
|
208
|
+
unipolar : bool, default False
|
|
209
|
+
If True, uses a unipolar constellation starting from 0 (e.g., 0, 1).
|
|
210
|
+
If False, uses a symmetric bipolar constellation (e.g., -1, +1).
|
|
211
|
+
rz : bool, default False
|
|
212
|
+
If True, uses Return-to-Zero signaling.
|
|
213
|
+
pulse_shape : {"rect", "smoothrect"}, default "rect"
|
|
214
|
+
Pulse shaping filter type. Default is "rect" for PAM.
|
|
215
|
+
num_streams : int, default 1
|
|
216
|
+
Number of independent streams (channels) to generate.
|
|
217
|
+
seed : int, optional
|
|
218
|
+
Random seed for reproducible bit and symbol generation.
|
|
219
|
+
duty_cycle : float, default 1.0
|
|
220
|
+
Fraction of the symbol period occupied by the pulse. Overridden to
|
|
221
|
+
0.5 when ``rz=True``.
|
|
222
|
+
filter_span : int, default 10
|
|
223
|
+
Filter span in symbols (smoothrect only).
|
|
224
|
+
rise_time : float, default 0.22
|
|
225
|
+
10%-90% edge transition duration in symbol periods (smoothrect only).
|
|
226
|
+
|
|
227
|
+
Returns
|
|
228
|
+
-------
|
|
229
|
+
Signal
|
|
230
|
+
A `Signal` object containing the generated PAM waveform.
|
|
231
|
+
|
|
232
|
+
Notes
|
|
233
|
+
-----
|
|
234
|
+
Samples are normalized to unit symbol power (Es = 1, average sample power = 1/sps).
|
|
235
|
+
Call ``resolve_symbols()`` before demapping or computing metrics.
|
|
236
|
+
"""
|
|
237
|
+
if rz:
|
|
238
|
+
if sps % 2 != 0:
|
|
239
|
+
raise ValueError("For correct RZ duty cycle, `sps` must be even")
|
|
240
|
+
|
|
241
|
+
allowed_rz_pulses = ["rect", "smoothrect"]
|
|
242
|
+
if pulse_shape not in allowed_rz_pulses:
|
|
243
|
+
raise ValueError(
|
|
244
|
+
f"Pulse shape '{pulse_shape}' is not allowed for RZ PAM. "
|
|
245
|
+
f"Allowed: {allowed_rz_pulses}"
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
return generate(
|
|
249
|
+
num_symbols=num_symbols,
|
|
250
|
+
sps=sps,
|
|
251
|
+
symbol_rate=symbol_rate,
|
|
252
|
+
modulation="PAM",
|
|
253
|
+
order=order,
|
|
254
|
+
unipolar=unipolar,
|
|
255
|
+
rz=rz,
|
|
256
|
+
pulse_shape=pulse_shape,
|
|
257
|
+
num_streams=num_streams,
|
|
258
|
+
seed=seed,
|
|
259
|
+
filter_span=filter_span,
|
|
260
|
+
rise_time=rise_time,
|
|
261
|
+
duty_cycle=duty_cycle,
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def generate_psk(
|
|
266
|
+
num_symbols: int,
|
|
267
|
+
sps: int,
|
|
268
|
+
symbol_rate: float,
|
|
269
|
+
order: int,
|
|
270
|
+
unipolar: bool = False,
|
|
271
|
+
rz: bool = False,
|
|
272
|
+
pulse_shape: str = "rrc",
|
|
273
|
+
num_streams: int = 1,
|
|
274
|
+
seed: int | None = None,
|
|
275
|
+
filter_span: int = 10,
|
|
276
|
+
rrc_rolloff: float = 0.35,
|
|
277
|
+
rc_rolloff: float = 0.35,
|
|
278
|
+
rise_time: float = 0.0,
|
|
279
|
+
duty_cycle: float = 1.0,
|
|
280
|
+
) -> "Signal":
|
|
281
|
+
"""
|
|
282
|
+
Generates a Phase Shift Keying (PSK) baseband waveform.
|
|
283
|
+
|
|
284
|
+
Parameters
|
|
285
|
+
----------
|
|
286
|
+
num_symbols : int
|
|
287
|
+
Total number of symbols to generate per stream.
|
|
288
|
+
sps : float
|
|
289
|
+
Samples per symbol.
|
|
290
|
+
symbol_rate : float
|
|
291
|
+
Symbol rate in symbols per second (Baud).
|
|
292
|
+
order : int
|
|
293
|
+
Modulation order (e.g., 2 for BPSK, 4 for QPSK, 8 for 8-PSK).
|
|
294
|
+
unipolar : bool, default False
|
|
295
|
+
If True, uses a unipolar constellation.
|
|
296
|
+
rz : bool, default False
|
|
297
|
+
If True, uses Return-to-Zero signaling.
|
|
298
|
+
pulse_shape : str, default "rrc"
|
|
299
|
+
Pulse shaping filter type.
|
|
300
|
+
num_streams : int, default 1
|
|
301
|
+
Number of independent streams (channels) to generate.
|
|
302
|
+
seed : int, optional
|
|
303
|
+
Random seed for bit and symbol generation.
|
|
304
|
+
duty_cycle : float, default 1.0
|
|
305
|
+
Fraction of the symbol period occupied by the pulse (rect/smoothrect).
|
|
306
|
+
Only meaningful when ``rz=True``.
|
|
307
|
+
filter_span : int, default 10
|
|
308
|
+
Filter span in symbols.
|
|
309
|
+
rrc_rolloff : float, default 0.35
|
|
310
|
+
Roll-off factor for the RRC filter.
|
|
311
|
+
rc_rolloff : float, default 0.35
|
|
312
|
+
Roll-off factor for the RC filter.
|
|
313
|
+
rise_time : float, default 0.22
|
|
314
|
+
10%-90% edge transition duration in symbol periods (smoothrect only).
|
|
315
|
+
duty_cycle : float, default 1.0
|
|
316
|
+
FWHM of the Gaussian pulse in symbol periods (gaussian only).
|
|
317
|
+
|
|
318
|
+
Returns
|
|
319
|
+
-------
|
|
320
|
+
Signal
|
|
321
|
+
A `Signal` object containing the PSK waveform.
|
|
322
|
+
|
|
323
|
+
Notes
|
|
324
|
+
-----
|
|
325
|
+
Samples are normalized to unit symbol power (Es = 1, average sample power = 1/sps).
|
|
326
|
+
Call ``resolve_symbols()`` before demapping or computing metrics.
|
|
327
|
+
"""
|
|
328
|
+
return generate(
|
|
329
|
+
modulation="psk",
|
|
330
|
+
order=order,
|
|
331
|
+
num_symbols=num_symbols,
|
|
332
|
+
sps=sps,
|
|
333
|
+
symbol_rate=symbol_rate,
|
|
334
|
+
pulse_shape=pulse_shape,
|
|
335
|
+
num_streams=num_streams,
|
|
336
|
+
seed=seed,
|
|
337
|
+
unipolar=unipolar,
|
|
338
|
+
rz=rz,
|
|
339
|
+
filter_span=filter_span,
|
|
340
|
+
rrc_rolloff=rrc_rolloff,
|
|
341
|
+
rc_rolloff=rc_rolloff,
|
|
342
|
+
rise_time=rise_time,
|
|
343
|
+
duty_cycle=duty_cycle,
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def generate_qam(
|
|
348
|
+
num_symbols: int,
|
|
349
|
+
sps: int,
|
|
350
|
+
symbol_rate: float,
|
|
351
|
+
order: int,
|
|
352
|
+
unipolar: bool = False,
|
|
353
|
+
rz: bool = False,
|
|
354
|
+
pulse_shape: str = "rrc",
|
|
355
|
+
num_streams: int = 1,
|
|
356
|
+
seed: int | None = None,
|
|
357
|
+
filter_span: int = 10,
|
|
358
|
+
rrc_rolloff: float = 0.35,
|
|
359
|
+
rc_rolloff: float = 0.35,
|
|
360
|
+
rise_time: float = 0.0,
|
|
361
|
+
duty_cycle: float = 1.0,
|
|
362
|
+
) -> "Signal":
|
|
363
|
+
"""
|
|
364
|
+
Generates a Quadrature Amplitude Modulation (QAM) baseband waveform.
|
|
365
|
+
|
|
366
|
+
Parameters
|
|
367
|
+
----------
|
|
368
|
+
num_symbols : int
|
|
369
|
+
Number of symbols to generate per stream.
|
|
370
|
+
sps : float
|
|
371
|
+
Samples per symbol.
|
|
372
|
+
symbol_rate : float
|
|
373
|
+
Symbol rate in symbols per second (Baud).
|
|
374
|
+
order : int
|
|
375
|
+
Modulation order (e.g., 16, 64, 256).
|
|
376
|
+
unipolar : bool, default False
|
|
377
|
+
If True, uses a unipolar constellation.
|
|
378
|
+
rz : bool, default False
|
|
379
|
+
If True, uses Return-to-Zero signaling.
|
|
380
|
+
pulse_shape : str, default "rrc"
|
|
381
|
+
Pulse shaping filter type.
|
|
382
|
+
num_streams : int, default 1
|
|
383
|
+
Number of MIMO streams.
|
|
384
|
+
seed : int, optional
|
|
385
|
+
Seed for random generation.
|
|
386
|
+
duty_cycle : float, default 1.0
|
|
387
|
+
Fraction of the symbol period occupied by the pulse (rect/smoothrect).
|
|
388
|
+
Only meaningful when ``rz=True``.
|
|
389
|
+
filter_span : int, default 10
|
|
390
|
+
Filter span in symbols.
|
|
391
|
+
rrc_rolloff : float, default 0.35
|
|
392
|
+
Roll-off factor for the RRC filter.
|
|
393
|
+
rc_rolloff : float, default 0.35
|
|
394
|
+
Roll-off factor for the RC filter.
|
|
395
|
+
rise_time : float, default 0.22
|
|
396
|
+
10%-90% edge transition duration in symbol periods (smoothrect only).
|
|
397
|
+
duty_cycle : float, default 1.0
|
|
398
|
+
FWHM of the Gaussian pulse in symbol periods (gaussian only).
|
|
399
|
+
|
|
400
|
+
Returns
|
|
401
|
+
-------
|
|
402
|
+
Signal
|
|
403
|
+
A `Signal` object containing the QAM waveform.
|
|
404
|
+
|
|
405
|
+
Notes
|
|
406
|
+
-----
|
|
407
|
+
Samples are normalized to unit symbol power (Es = 1, average sample power = 1/sps).
|
|
408
|
+
Call ``resolve_symbols()`` before demapping or computing metrics.
|
|
409
|
+
"""
|
|
410
|
+
return generate(
|
|
411
|
+
modulation="qam",
|
|
412
|
+
order=order,
|
|
413
|
+
num_symbols=num_symbols,
|
|
414
|
+
sps=sps,
|
|
415
|
+
symbol_rate=symbol_rate,
|
|
416
|
+
pulse_shape=pulse_shape,
|
|
417
|
+
num_streams=num_streams,
|
|
418
|
+
seed=seed,
|
|
419
|
+
unipolar=unipolar,
|
|
420
|
+
rz=rz,
|
|
421
|
+
filter_span=filter_span,
|
|
422
|
+
rrc_rolloff=rrc_rolloff,
|
|
423
|
+
rc_rolloff=rc_rolloff,
|
|
424
|
+
rise_time=rise_time,
|
|
425
|
+
duty_cycle=duty_cycle,
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def generate_psqam(
|
|
430
|
+
num_symbols: int,
|
|
431
|
+
sps: int,
|
|
432
|
+
symbol_rate: float,
|
|
433
|
+
order: int,
|
|
434
|
+
*,
|
|
435
|
+
nu: float | None = None,
|
|
436
|
+
entropy: float | None = None,
|
|
437
|
+
pulse_shape: str = "rrc",
|
|
438
|
+
num_streams: int = 1,
|
|
439
|
+
seed: int | None = None,
|
|
440
|
+
filter_span: int = 10,
|
|
441
|
+
rrc_rolloff: float = 0.35,
|
|
442
|
+
rc_rolloff: float = 0.35,
|
|
443
|
+
duty_cycle: float = 1.0,
|
|
444
|
+
) -> "Signal":
|
|
445
|
+
"""
|
|
446
|
+
Generates a Probabilistically Shaped QAM (PS-QAM) baseband waveform.
|
|
447
|
+
|
|
448
|
+
Symbols are drawn from a Maxwell-Boltzmann (MB) distribution over the
|
|
449
|
+
normalized QAM constellation, giving inner (low-energy) points higher
|
|
450
|
+
probability. This recovers up to 1.53 dB shaping gain over uniform QAM.
|
|
451
|
+
|
|
452
|
+
Exactly one of ``nu`` or ``entropy`` must be specified.
|
|
453
|
+
|
|
454
|
+
Parameters
|
|
455
|
+
----------
|
|
456
|
+
num_symbols : int
|
|
457
|
+
Number of symbols to generate per stream.
|
|
458
|
+
sps : float
|
|
459
|
+
Samples per symbol.
|
|
460
|
+
symbol_rate : float
|
|
461
|
+
Symbol rate in symbols per second (Baud).
|
|
462
|
+
order : int
|
|
463
|
+
QAM modulation order (e.g. 16, 64, 256).
|
|
464
|
+
nu : float, optional
|
|
465
|
+
MB shaping parameter nu >= 0. nu = 0 is uniform QAM.
|
|
466
|
+
Larger values apply stronger shaping (lower entropy, lower power).
|
|
467
|
+
entropy : float, optional
|
|
468
|
+
Target per-symbol entropy in bits, in the range (0, log2(order)].
|
|
469
|
+
optimal_nu is called to solve for the corresponding nu.
|
|
470
|
+
pulse_shape : str, default "rrc"
|
|
471
|
+
Pulse shaping filter type.
|
|
472
|
+
num_streams : int, default 1
|
|
473
|
+
Number of independent streams (MIMO).
|
|
474
|
+
seed : int, optional
|
|
475
|
+
Random seed for reproducible symbol generation.
|
|
476
|
+
filter_span : int, default 10
|
|
477
|
+
Filter span in symbols.
|
|
478
|
+
rrc_rolloff : float, default 0.35
|
|
479
|
+
Roll-off factor for the RRC filter.
|
|
480
|
+
rc_rolloff : float, default 0.35
|
|
481
|
+
Roll-off factor for the RC filter.
|
|
482
|
+
|
|
483
|
+
Returns
|
|
484
|
+
-------
|
|
485
|
+
Signal
|
|
486
|
+
A ``Signal`` with ``mod_scheme="PS-QAM"``, ``ps_pmf`` set to the MB
|
|
487
|
+
distribution, and both ``source_symbols`` and ``source_bits`` populated.
|
|
488
|
+
|
|
489
|
+
Notes
|
|
490
|
+
-----
|
|
491
|
+
``source_bits`` carry the non-uniform MB statistics (correct for BER/GMI
|
|
492
|
+
estimation, not a full coded PAS transmitter). Average symbol energy is
|
|
493
|
+
below 1 for nu > 0; pass ``pmf=signal.ps_pmf`` to ``metrics.mi`` and
|
|
494
|
+
``compute_llr`` for correct soft-demapping.
|
|
495
|
+
|
|
496
|
+
Examples
|
|
497
|
+
--------
|
|
498
|
+
>>> sig = generate_psqam(10000, sps=4, symbol_rate=32e9, order=64, entropy=6.0)
|
|
499
|
+
>>> sig = generate_psqam(10000, sps=4, symbol_rate=32e9, order=64, nu=0.3)
|
|
500
|
+
"""
|
|
501
|
+
|
|
502
|
+
if sps != int(sps) or sps < 1:
|
|
503
|
+
logger.warning(
|
|
504
|
+
"sps=%r is not a positive integer. Non-integer sps is valid for captured/resampled signals but not for generation: resample_poly requires an integer upsampling factor, so the sample buffer would not match the stored sampling_rate metadata. To generate at a fractional sps, generate at an integer sps then call Signal.resample(up=..., down=...).",
|
|
505
|
+
sps,
|
|
506
|
+
)
|
|
507
|
+
raise ValueError(
|
|
508
|
+
f"sps must be a positive integer for signal generation, got {sps!r}."
|
|
509
|
+
)
|
|
510
|
+
sps = int(sps)
|
|
511
|
+
|
|
512
|
+
if (nu is None) == (entropy is None):
|
|
513
|
+
raise ValueError("Exactly one of `nu` or `entropy` must be specified.")
|
|
514
|
+
|
|
515
|
+
if entropy is not None:
|
|
516
|
+
nu_val, _ = mapping.optimal_nu(order, entropy)
|
|
517
|
+
else:
|
|
518
|
+
assert nu is not None
|
|
519
|
+
nu_val = float(nu)
|
|
520
|
+
if nu_val < 0:
|
|
521
|
+
raise ValueError("`nu` must be non-negative.")
|
|
522
|
+
|
|
523
|
+
pmf = mapping.maxwell_boltzmann(order, nu_val)
|
|
524
|
+
k = int(np.log2(order))
|
|
525
|
+
total_symbols = num_symbols * num_streams
|
|
526
|
+
|
|
527
|
+
# Sample symbols from MB distribution (NumPy, CPU)
|
|
528
|
+
symbols_flat = mapping.sample_ps_symbols(total_symbols, order, pmf, seed=seed)
|
|
529
|
+
|
|
530
|
+
# Derive source bits by demapping noiseless shaped symbols (lossless).
|
|
531
|
+
# Array input -> array output (the Signal-dispatch branch is not taken).
|
|
532
|
+
bits_flat = cast(ArrayType, mapping.demap_symbols_hard(symbols_flat, "qam", order))
|
|
533
|
+
|
|
534
|
+
if num_streams > 1:
|
|
535
|
+
symbols = symbols_flat.reshape(num_streams, num_symbols)
|
|
536
|
+
bits = bits_flat.reshape(num_streams, num_symbols * k)
|
|
537
|
+
else:
|
|
538
|
+
symbols = symbols_flat
|
|
539
|
+
bits = bits_flat
|
|
540
|
+
|
|
541
|
+
if is_cupy_available():
|
|
542
|
+
symbols = to_device(symbols, "gpu")
|
|
543
|
+
bits = to_device(bits, "gpu")
|
|
544
|
+
|
|
545
|
+
samples = filtering.shape_pulse(
|
|
546
|
+
symbols=symbols,
|
|
547
|
+
sps=sps,
|
|
548
|
+
pulse_shape=pulse_shape,
|
|
549
|
+
filter_span=filter_span,
|
|
550
|
+
rrc_rolloff=rrc_rolloff,
|
|
551
|
+
rc_rolloff=rc_rolloff,
|
|
552
|
+
duty_cycle=duty_cycle,
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
_ps_tag = f"entropy={entropy:.3g}" if entropy is not None else f"ν={nu_val:.3g}"
|
|
556
|
+
logger.info(
|
|
557
|
+
"Generated PS-QAM-%s signal: %s symbols x %s stream(s), sps=%s, %s, pulse_shape=%s, %s samples/stream @ %.3g Sa/s.",
|
|
558
|
+
order,
|
|
559
|
+
num_symbols,
|
|
560
|
+
num_streams,
|
|
561
|
+
sps,
|
|
562
|
+
_ps_tag,
|
|
563
|
+
pulse_shape,
|
|
564
|
+
samples.shape[-1],
|
|
565
|
+
symbol_rate * sps,
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
return Signal(
|
|
569
|
+
samples=samples,
|
|
570
|
+
sampling_rate=symbol_rate * sps,
|
|
571
|
+
symbol_rate=symbol_rate,
|
|
572
|
+
mod_scheme="PS-QAM",
|
|
573
|
+
mod_order=order,
|
|
574
|
+
source_bits=bits,
|
|
575
|
+
source_symbols=symbols,
|
|
576
|
+
pulse_shape=pulse_shape,
|
|
577
|
+
ps_pmf=pmf,
|
|
578
|
+
ps_nu=nu_val,
|
|
579
|
+
filter_span=filter_span,
|
|
580
|
+
rrc_rolloff=rrc_rolloff,
|
|
581
|
+
)
|