Scientific validation: TorchSig FSK emitter¶
Validated with documented limitations.
1. The component¶
TorchSigFSKEmitter is a PyTorch-fronted generator that produces clean baseband in-phase/quadrature (IQ) tensors for 16 continuous-phase frequency-shift keying (FSK: a family of digital modulations that encodes information by shifting the carrier between discrete frequency tones rather than discrete amplitude or phase points) class labels. The component wraps TorchSig’s fsk_modulator builder at v2.1.1 and exposes the 16 labels through the rfgen BaseEmitter interface.
This report uses PSD for power spectral density, DC for the zero-frequency component, PAPR for peak-to-average power ratio, BT for the dimensionless Gaussian-filter bandwidth–symbol-time product, and dBc for decibels relative to carrier or the stated reference component.
class TorchSigFSKEmitter(BaseEmitter):
family: ClassVar[EmitterFamily] = EmitterFamily.COMMS
supported_classes: ClassVar[tuple[str, ...]] = (
"2fsk", "2gfsk", "2msk", "2gmsk",
"4fsk", "4gfsk", "4msk", "4gmsk",
"8fsk", "8gfsk", "8msk", "8gmsk",
"16fsk", "16gfsk", "16msk", "16gmsk",
)
def __init__(self) -> None: ...
def schema(self) -> type[BaseModel]: ...
def generate(
self,
*,
class_label: str,
sample_rate: float,
duration_s: float,
f_offset_hz: float,
rng: torch.Generator,
device_id: str | None = None,
params: BaseModel | None = None,
) -> Signal: ...
class TorchSigFSKParams(BaseModel):
bandwidth_hz: float = Field(default=200_000.0, gt=0)
Parameter |
Type |
Units |
Default |
Purpose |
|---|---|---|---|---|
|
|
n/a |
required |
One of 16 labels encoding |
|
|
Hz |
required |
Samples per second on the output tensor. |
|
|
s |
required |
Output duration. Total sample count |
|
|
Hz |
required |
Baseband frequency shift applied after synthesis. May be negative or zero. Must satisfy |
|
|
n/a |
required |
Seeds the NumPy generator used internally. A given seed always produces the same IQ. |
|
|
n/a |
|
Optional label echoed into |
|
|
Hz |
|
TorchSig resampler-target bandwidth. This is not the realized −3 dB spectral width; see §3.3. |
Worked example.
import torch
from rfgen.integrations.torchsig.emitters.torchsig_fsk import TorchSigFSKEmitter, TorchSigFSKParams
emitter = TorchSigFSKEmitter()
signal = emitter.generate(
class_label="4gfsk",
sample_rate=2_000_000.0, # 2 MHz
duration_s=0.05, # 100 000 samples
f_offset_hz=0.0,
rng=torch.Generator().manual_seed(42),
params=TorchSigFSKParams(bandwidth_hz=200_000.0),
)
print(signal.iq.shape) # torch.Size([2, 100000])
print(signal.iq.dtype) # torch.float32
print(signal.metadata.extras["realized_h"]) # e.g. 0.274
print(signal.metadata.extras["realized_bt"]) # e.g. 0.388 (Gaussian variants only)
Taxonomy. The 16 labels arise from the Cartesian product of constellation orders {2, 4, 8, 16} and pulse-shape families {fsk, gfsk, msk, gmsk}. MSK (Minimum Shift Keying: FSK with modulation index h = 0.5, the smallest h that yields orthogonal tones) and GMSK (Gaussian MSK: MSK with a Gaussian pre-filter applied to the symbol stream to limit spectral spreading) are widely deployed in cellular (GSM uses GMSK) and Bluetooth radios. GFSK (Gaussian FSK: FSK with a Gaussian filter on the symbols but h not fixed to 0.5) is the Bluetooth Basic Rate physical layer. The fsk variant is standard non-Gaussian FSK. All four are CPM (continuous-phase modulation: the phase trajectory is continuous across symbol boundaries, which gives them their constant-envelope property). The emitter sits at the modulation-order level: no protocol framing, no preamble, no payload, no burst envelope.
2. What we validated¶
This validation establishes 8 load-bearing claims. Each is restated and supported by evidence in §3.
Output shape, dtype, and metadata (§3.1): all 16 labels produce
(2, N)float32 tensors with correct metadata fields.DC balance (§3.2): the explicit DC subtraction brings the channel mean to the float32 noise floor.
Bandwidth knob scales PSD (§3.3): doubling
bandwidth_hzdoubles the empirical −3 dB bandwidth with ~15% tolerance.Family bandwidth ordering (§3.4): at fixed
bandwidth_hz, empirical −3 dB bandwidth follows fsk > msk > gfsk.Constant-envelope (low PAPR) (§3.5): the strict six-label cohort stays below 0.05 envelope coefficient of variation, the all-16 cohort stays below the looser 0.10 bound, and the four-label PAPR cohort stays below 1.5 dB.
Phase continuity (§3.6): per-sample phase step stays below π/4 rad for the five directly tested labels; the all-label construction claim retains the limitation stated in §4.
Realized-parameter capture (§3.7):
h,BT, andmare recorded in metadata without perturbing the IQ output.Unit mean power (§3.8):
mean(|cz|²) = 1 ± 1e-4after the rfgen wrapper’s normalization.
Limits and scope-bounded items appear in §4; full citations are in §5.
3. Evidence per claim¶
3.1 Output shape, dtype, and metadata¶
Claim. Every one of the 16 supported class labels generates without error, produces a tensor of shape (2, round(sample_rate * duration_s)) with dtype float32, and populates SignalMetadata with class_taxonomy = ("comms", "fsk", class_label), family = "comms", snr_db = +inf, and an extras map containing fsk_type, constellation_size, realized_h, and power_normalization_scale (plus realized_bt and realized_gaussian_half_span_m for Gaussian variants).
Test. tests/validation/emitters/torchsig_fsk/test_experiment_contract.py::test_shape_and_dtype[<label>] (16 parametrized cases), test_metadata_fields. Setting: sample_rate = 2 MHz, duration_s = 0.05, seed 42.
Result. All 16 labels pass. Shape is (2, 100_000), dtype is float32, all metadata fields present and correctly typed.
Unsupported labels. test_experiment_contract.py::test_unsupported_label_raises confirms that "32fsk" (order outside {2,4,8,16}) and "2bfsk" (unrecognized fsk_type suffix) both raise EmitterError.
3.2 DC balance¶
Claim. After the explicit DC subtraction at line 297 of torchsig_fsk.py, the channel mean satisfies |mean(channel)| < 1e-4 × peak(|cz|) for all 16 labels. This is approximately −80 dBc suppression relative to peak power.
Justification of tolerance. FSK is constant-envelope, so even without DC subtraction the residual would be small. The 1e-4 gate is tighter than the chance-only standard error at N = 100,000 (which is 0.28 / sqrt(100000) ≈ 9e-4). Passing therefore confirms the active subtraction is working, not sampling luck.
Test. test_experiment_contract.py::test_dc_balance[<label>] (16 parametrized cases). N = 100,000 per call.
Result. Residual ≈ 1e-8 across all 16 labels (float32 noise floor).
3.3 Bandwidth knob scales PSD¶
Claim. Doubling bandwidth_hz from B₀ to 2 × B₀ doubles the empirical −3 dB bandwidth within a 15% tolerance. The PSD shape (power spectral density: the distribution of signal energy across frequency, estimated here using Welch’s method) moves linearly with the bandwidth_hz knob.
Design. scipy.signal.welch supplies the PSD estimator (the Welch method averages overlapping windowed FFT segments to reduce spectral variance) with nperseg = 4096, sample_rate = 2 MHz, duration_s = 0.2 (N = 400,000, ~194 averaged segments, frequency resolution ≈ 488 Hz). The −3 dB bandwidth is the width of the spectral region within 3 dB of the peak power. Tested for {2fsk, 4fsk} × {50, 100, 200} kHz.
Tolerance. 1.7 ≤ ratio ≤ 2.3 (15% margin around the expected factor of 2).
Test. test_experiment_psd_bandwidth.py::test_bandwidth_scaling[<label>-<B0>] (6 parametrized cases).
Result. Empirical doubling ratios 1.96–2.00 across all 6 cases, within tolerance.
Figure 1 shows the measured −3 dB bandwidth vs. bandwidth_hz for {2fsk, 4fsk} × {50, 100, 200} kHz.

Important note on bandwidth_hz semantics. The realized −3 dB spectral width is not equal to bandwidth_hz. The knob is the TorchSig polyphase resampler’s target rate; the actual −3 dB width depends on the randomized modulation index h and time-bandwidth product BT. Empirical realized ratios (−3 dB width / bandwidth_hz) at default settings: 2fsk ≈ 0.50, 2msk ≈ 0.27, 2gfsk ≈ 0.10. See §3.4 and §4.
3.4 Family bandwidth ordering¶
Claim. At fixed bandwidth_hz, the median empirical −3 dB width across 5 seeds satisfies BW(2fsk) > BW(2msk) > BW(2gfsk). This ordering follows the modulation-index hierarchy: standard FSK draws h ~ U(0.7, 1.01) or h = 1.0 (orthogonal), MSK fixes h = 0.5, and GFSK draws h ~ U(0.1, 0.5). Higher h means wider frequency deviation per symbol, hence a wider measured −3 dB width.
For symmetric binary FSK, modulation index is
h = 2Δf/R_s: twice the peak frequency deviation Δf divided by symbol rate
R_s, equivalently the separation between the two tones divided by symbol
rate. MSK fixes h = 0.5, the minimum value that keeps adjacent binary tones
orthogonal (non-overlapping in a correlation sense).
Test. test_experiment_psd_bandwidth.py::test_family_hierarchy. Also test_iter1_high_fixes.py::test_realized_3db_bw_ratio_per_class locks in the absolute ratios: 2fsk ≈ 0.50, 2msk ≈ 0.27, 2gfsk ≈ 0.10 of bandwidth_hz, within factor-of-2 tolerance.
Result. bw_fsk (103 kHz) > bw_msk (54 kHz) > bw_gfsk (20 kHz) at bandwidth_hz = 200 kHz. Ordering confirmed.
Figure 2 shows the Welch PSD for the four binary FSK family variants at fixed bandwidth_hz = 200 kHz, illustrating the class-dependent bandwidth ratios.

3.5 Constant-envelope property (low PAPR)¶
Claim. The directly tested strict constant-envelope cohort {2fsk, 2gfsk, 2msk, 2gmsk, 4fsk, 8fsk} has a steady-state envelope standard deviation below 5% of its mean. A looser check covers all 16 labels with a 10% bound. PAPR (peak-to-average power ratio: ratio of peak instantaneous power to mean power, expressed in dB; 0 dB means perfectly constant power) stays below 1.5 dB for the directly tested cohort {2fsk, 2msk, 2gmsk, 4fsk} across five seeds. These measurements test the constant-envelope behavior associated with CPM; they do not establish the strict threshold for labels outside each named cohort.
CV (coefficient of variation) is std(|cz|) / mean(|cz|), a unit-free measure of envelope fluctuation.
Test. test_empirical_known_results.py::test_constant_envelope_interior[<label>] asserts CV < 0.05 with 10% edge trim for {2fsk, 2gfsk, 2msk, 2gmsk, 4fsk, 8fsk}. test_experiment_contract.py::test_approx_constant_envelope[<label>] asserts CV < 0.10 with 5% edge trim for all 16 labels: {2fsk, 2gfsk, 2msk, 2gmsk, 4fsk, 4gfsk, 4msk, 4gmsk, 8fsk, 8gfsk, 8msk, 8gmsk, 16fsk, 16gfsk, 16msk, 16gmsk}. test_empirical_known_results.py::test_papr_below_psk_baseline asserts median PAPR < 1.5 dB for {2fsk, 2msk, 2gmsk, 4fsk} × 5 seeds.
Result. Median CV is 0.01–0.05 across the all-16 looser cohort. Median PAPR is 0.7 dB for the four-label PAPR cohort. The 10% edge trim is necessary for the strict six-label check: for N < 1000, the polyphase resampler’s head/tail padding dominates and CV exceeds the 0.05 bound (see §4).
Figure 3 shows IQ scatter plots and envelope histograms for the four binary CPM variants, confirming the near-circular IQ trajectory and narrow |IQ| distribution.

Figure 4 shows PAPR distributions across all 16 FSK class labels.

3.6 Phase continuity¶
Claim. The per-sample phase step |dφ/sample| stays below π/4 rad for the directly tested cohort {2fsk, 2gfsk, 2msk, 2gmsk, 8fsk}. Phase continuity is the defining property of CPM: the modulator accumulates phase via φ(t) = 2π h ∫ f(τ) dτ (Proakis & Salehi, Digital Communications, 5th ed., 2008, §4.3), so phase is a continuous function of time with no instantaneous jumps. TorchSig implements the same construction for its other FSK labels via numpy.cumsum on the instantaneous-frequency array, but the π/4 bound is not directly asserted for every label.
MSK reference. For MSK (h = 0.5), the per-symbol phase excursion is exactly ±π/2 = ±1.5708 rad. The test measures the standard deviation of per-symbol phase increments across seeds and asserts it lands in [0.8, 2.0] rad (tolerance accommodates pulse-shape smear and symbol-rate alignment slack). 3GPP TS 45.004 specifies GSM GMSK with h = 0.5, maximum phase change π/2 per data interval, and Gaussian-filter BT = 0.3.
Test. test_empirical_known_results.py::test_phase_continuity[<label>] covers exactly {2fsk, 2gfsk, 2msk, 2gmsk, 8fsk}. test_empirical_known_results.py::test_msk_modulation_index separately checks 2msk per-symbol phase standard deviation in [0.8, 2.0] rad.
Result. Max per-sample phase step 0.07–0.35 rad across tested labels, well below the π/4 = 0.785 bound. Per-symbol phase std 1.0–1.4 rad for 2msk (consistent with theoretical π/2 = 1.5708 rad).
Figure 5 shows the unwrapped phase trajectory and instantaneous frequency for the four binary CPM variants.

3.7 Realized-parameter capture does not perturb IQ¶
Claim. TorchSig’s fsk_modulator and fsk_modulator_baseband perform the FSK synthesis, phase accumulation, Gaussian
filtering, and resampling. The rfgen wrapper composes those library primitives
with input validation, a copied-RNG observation of h, BT, and m, DC
subtraction, unit-power normalization, tensor conversion, frequency shift, and
metadata. It observes TorchSig’s RNG draw sequence on a copy.deepcopy of the
NumPy generator, then passes the unadvanced original generator to TorchSig.
The IQ output is byte-identical to what TorchSig would produce without the
observation step. Realized values are stored in metadata.extras: realized_h
for all variants; additionally realized_bt and
realized_gaussian_half_span_m for Gaussian variants (gfsk, gmsk); absent for
fsk and msk.
Reference values. TorchSig’s
get_fsk_mod_index source at v2.1.1
draws: for fsk, h = 1.0 with probability 0.5 or h ~ U(0.7, 1.01) otherwise; for msk and gmsk, h = 0.5 exactly; for gfsk, h ~ U(0.1, 0.5). For Gaussian variants, BT ~ U(0.1, 0.5) and m ~ randint(1, 5), so m is in the integer set {1, 2, 3, 4}.
Tests. test_iter1_high_fixes.py::test_capture_does_not_perturb_iq asserts np.array_equal between two independent calls with the same seed. test_iter1_high_fixes.py::test_realized_h_recorded_for_fsk[<label>] checks fsk-type labels. test_realized_h_msk_is_half[<label>] checks MSK/GMSK labels. test_realized_h_gfsk_in_bluetooth_range_support[<label>] checks GFSK range. test_realized_bt_and_m_for_gaussian_variants[<label>] checks BT and m ranges. test_bt_absent_for_non_gaussian_variants[<label>] checks absence.
Result. All 16 labels pass. Captured values match expected distributions. IQ is bit-identical with and without the capture step.
3.8 Unit mean power¶
Claim. After DC subtraction, the rfgen wrapper applies iq ← iq / sqrt(mean(|cz|²)) so that mean(|cz|²) = 1 to within 1e-4. TorchSig’s fsk_modulator outputs amplitude scaled by 1 / resample_rate_ideal (constant-envelope amplitude ≈ 0.4 at default settings, mean power ≈ 0.16), which is a library convention. The rfgen normalization gives downstream channel processing a known reference power level. The applied scale factor is recorded in metadata.extras["power_normalization_scale"].
Test. test_iter1_high_fixes.py::test_unit_mean_power[<label>] (16 parametrized cases) asserts |mean(|cz|²) - 1| < 1e-4. test_power_normalization_scale_recorded[<label>] confirms the scale factor is present, finite, positive, and in the expected range (1.0, 6.0).
Result. |mean(|cz|²) - 1| ≈ 1e-7 (float32 noise floor) for all 16 labels. Typical scale factor ≈ 2.5 (consistent with 1 / sqrt(0.16) ≈ 2.5).
4. Limits and what’s not validated¶
Resampler-padding regime (N < ~1000). The polyphase resampler in TorchSig pads the head and tail of short records. Below N ≈ 1000 these padding regions dominate the envelope, and the CV bound of 0.05 no longer holds. test_robustness_envelope.py::test_r2_small_n_envelope_cv_degrades documents the fall-off: CV ≈ 0.32 at N = 64, declining to < 0.10 at N = 100,000. The 10% edge trim applied in the steady-state tests excludes this regime.
Higher-order labels not in CPM-specific tests. Phase-continuity and PAPR tests cover binary CPM variants and selected higher-order cases. The 8gfsk, 8gmsk, 16gfsk, 16gmsk, and similar labels are not separately probed for phase-continuity: the property follows from TorchSig’s np.cumsum construction regardless of order, but it is not directly asserted for every label.
bandwidth_hz is not the realized −3 dB spectral width. The knob is the TorchSig resampler target. Empirical realized ratios (−3 dB width / bandwidth_hz) at default settings: 2fsk ≈ 0.50, 2msk ≈ 0.27, 2gfsk ≈ 0.10. A downstream config builder that interprets bandwidth_hz as measured −3 dB width will be miscalibrated. The locked-in regression in test_iter1_high_fixes.py::test_realized_3db_bw_ratio_per_class asserts these ratios within factor-of-2.
No OTA (over-the-air) capture comparison. The 16 class labels are protocol-agnostic (no preamble, payload, or burst envelope). A Bluetooth or GSM capture combines modulation with protocol framing, so comparing one directly with this modulation-only output would not isolate the behavior this component claims. The retained checks therefore compare modulation parameters and waveform properties while leaving protocol-level capture validation out of scope.
TorchSig GFSK modulation index is wider than Bluetooth BR. TorchSig draws h ~ U(0.1, 0.5) for GFSK; the Bluetooth Basic Rate specification (Bluetooth Core Specification v5.3, Vol 6 Part A) requires h ∈ [0.28, 0.35]. The TorchSig support is wider. A downstream consumer that requires Bluetooth-spec conformance should filter by the recorded metadata.extras["realized_h"] field.
No closed-form test of Gaussian pulse coefficients. The gaussian_taps function at TorchSig v2.1.1 is exercised only indirectly through phase-continuity and bandwidth tests. No direct numerical comparison of filter coefficients against the exp(-2π²BT²t²/ln2) formula (Proakis & Salehi §9.2.5) is included.
5. References¶
Published works¶
Item |
Full citation |
|---|---|
Continuous-phase FSK theory |
Proakis, J.G. and Salehi, M. Digital Communications, 5th edition. McGraw-Hill (2008). ISBN 978-0072957167. §4.3 (CPM definition and phase integral), §9.2.5 (Gaussian pulse filter). |
GMSK / GSM specification |
ETSI TS 145 004 V17.0.0 (3GPP TS 45.004 Release 17), §2 specifies GMSK with |
Bluetooth GFSK specification |
Bluetooth Core Specification v5.3, Vol 6 Part A “Radio Specification.” Bluetooth SIG (2021). Specifies Basic Rate GFSK modulation index |
TorchSig paper |
L. Boegner et al. “Large Scale Radio Frequency Signal Classification.” 2022. arXiv:2207.09918. DOI 10.48550/arXiv.2207.09918. Describes TorchSig’s signal taxonomy and FSK builder design. |
Libraries¶
PyPI dist |
Installed version |
Docs URL |
Role in this validation |
|---|---|---|---|
|
2.1.1 |
|
|
|
2.12.1 |
|
|
|
2.4.6 |
Complex arithmetic, phase unwrapping and accumulation, DC subtraction, and power normalization |
|
|
1.18.0 |
Welch PSD estimator used in bandwidth validation tests |
|
|
3.11.0 |
Figure generation in |