Scientific validation: emitters/torchsig_fm¶
Verdict. Validated with documented limitations.
1. The component¶
TorchSigFMEmitter generates a clean complex-baseband waveform for analog frequency modulation (FM: a modulation scheme where information is encoded in the instantaneous frequency of a carrier, not its amplitude). The instantaneous frequency wanders around zero according to a band-limited Gaussian noise message; the output is exp(j·φ(t)), a unit-envelope complex tone (IQ signal: in-phase and quadrature components stacked as a (2, N) tensor).
Two class labels are supported: nbfm (NBFM: narrowband FM, used in channelised voice radio) and wbfm (WBFM: wideband FM, used in commercial broadcast). The labels are distinguished by label-specific rfgen/TorchSig-wrapper candidate defaults and ceilings on the Carson 98%-containment bandwidth (Carson bandwidth: the spectral window used here to test whether at least 98% of the signal’s measured average power is contained). The cited standards provide channel-separation, deviation, and emission-mask context; they do not define these wrapper defaults.
Class signature¶
class TorchSigFMEmitter(BaseEmitter):
family: ClassVar[EmitterFamily] # EmitterFamily.COMMS
supported_classes: ClassVar[tuple[str, ...]] # ("nbfm", "wbfm")
def __init__(self) -> None: ...
def generate(
self,
*,
class_label: str, # "nbfm" or "wbfm"
sample_rate: float, # Hz; Nyquist rate for the complex-baseband record
duration_s: float, # seconds; record length
f_offset_hz: float, # Hz; digital baseband frequency shift
rng: torch.Generator, # seeded generator for reproducibility
device_id: str | None = None,
params: BaseModel | None = None, # TorchSigFMParams
) -> Signal: ...
Parameters¶
Name |
Type |
Units |
Default |
Purpose |
|---|---|---|---|---|
|
|
n/a |
required |
|
|
|
Hz |
required |
Complex-baseband Nyquist rate; must satisfy |
|
|
s |
required |
Record length; |
|
|
Hz |
required |
Baseband frequency shift applied as |
|
|
n/a |
required |
Seeded random generator; same seed produces identical IQ |
|
|
n/a |
|
Propagated to |
|
|
n/a |
|
Pydantic model with optional |
|
|
Hz |
label default |
Carson 98%-containment bandwidth; |
Per-label defaults and ceilings:
Label |
rfgen wrapper default |
rfgen ceiling |
Standards context |
|---|---|---|---|
|
12,500 Hz |
25,000 Hz |
ETSI EN 300 086 V2.1.2 (2016-08), clause 1 (12.5 / 20 / 25 kHz channel separations) |
|
150,000 Hz |
200,000 Hz |
47 CFR §§73.201 and 73.317 (200 kHz channels and the FM emission mask) |
The 12.5 kHz and 150 kHz values are candidate defaults selected by the rfgen wrapper. ETSI EN 300 086 and 47 CFR §§73.201 and 73.317 provide operating context and ceiling rationale, not default values for this API.
Worked example¶
import torch
from rfgen.integrations.torchsig.emitters import TorchSigFMEmitter, TorchSigFMParams
em = TorchSigFMEmitter()
g = torch.Generator()
g.manual_seed(42)
sig = em.generate(
class_label="wbfm",
sample_rate=2_000_000.0, # 2 MHz sample rate
duration_s=0.1, # 100 ms = 200,000 samples
f_offset_hz=0.0,
rng=g,
params=TorchSigFMParams(bandwidth_hz=200_000.0), # 200 kHz Carson bandwidth
)
print(sig.iq.shape) # torch.Size([2, 200000])
print(sig.iq.dtype) # torch.float32
print(sig.metadata.bandwidth_hz) # 200000.0
print(sig.metadata.class_taxonomy) # ('comms', 'fm', 'wbfm')
The emitter wraps
torchsig.signals.builders.fm.fm_modulator at v2.1.1.
TorchSig internally draws a modulation index m ~ Uniform(1, 10), where
m = fdev / fmax, fdev is the peak frequency deviation in Hz, and fmax is
the highest message-frequency cutoff in Hz. It computes
fdev = (bandwidth_hz/2) / (1 + 1/m) and fmax = fdev/m, then builds a
Gaussian-noise message filtered to fmax and integrates the phase. The wrapper
adds the Nyquist guard, which rejects requested spectral extents that could
alias across the sampled complex-baseband frequency interval, plus per-label
ceiling enforcement and the f_offset_hz baseband shift.
2. What we validated¶
This validation establishes 7 load-bearing claims. Each is restated and supported by evidence in §3.
Output shape and metadata contract (§3.1): the emitter produces a
(2, N)float32 IQ tensor and correctSignalMetadata.Carson 98%-containment bandwidth (§3.2): the bandwidth-parameter identity holds algebraically, and the selected candidates pass an empirical integrated-power containment threshold of 0.98 across the tested operating points.
Constant-envelope invariant (§3.3): FM is phase-only modulation;
|I + jQ|stays at unity for both WBFM and NBFM.WBFM stays inside a local 200 kHz containment window (§3.4): at
bandwidth_hz = 200 kHz, the measured power fits inside ±100 kHz; this is not a pointwise FCC-mask compliance claim.NBFM and WBFM per-label defaults produce distinguishable IQ (§3.5): per-label bandwidth defaults route the two labels through different spectral regimes.
Baseband frequency shift is exact (§3.6):
f_offset_hzmigrates the spectral peak by the requested offset.Boundary and error enforcement (§3.7): per-label ceilings, Nyquist guards, and non-finite input rejection all raise the correct error types.
Limits and scope-bounded items appear in §4; full citations are in §5.
3. Evidence per claim¶
3.1 Output shape and metadata contract¶
The emitter returns a Signal whose iq field is a (2, N) torch.float32 tensor with N = round(sample_rate * duration_s). SignalMetadata carries the realised bandwidth, sample rate, carrier offset, class taxonomy, and generator name.
sig.iq.shape == (2, N)
sig.iq.dtype == torch.float32
sig.metadata.bandwidth_hz == bandwidth_hz (as resolved from params or label default)
sig.metadata.realized_carrier_hz == f_offset_hz
sig.metadata.class_taxonomy == ("comms", "fm", class_label)
sig.metadata.snr_db == float("inf") # clean baseband
Tests: test_iq_shape_dtype (3 (fs, dur_s, expected_n) parameter combinations), test_metadata_carries_request, test_family_and_supported_classes in tests/validation/emitters/torchsig_fm/test_experiment_contract.py. All pass.
3.2 Carson 98%-containment bandwidth¶
The claim. The wrapper interprets bandwidth_hz as a candidate Carson window [-bandwidth_hz/2, +bandwidth_hz/2] and empirically requires that window to contain at least 98% of the generated signal’s average power. Haykin and Moher, Communication Systems, 5th ed., Ch. 3, gives Carson’s rule as B_carson = 2·(fdev + fmax); the 0.98 threshold here is verified by measurement rather than inferred from the parameter identity alone.
Mathematical basis. TorchSig picks m ~ Uniform(1, 10) and sets fdev = (bandwidth_hz/2) / (1 + 1/m) and fmax = fdev/m. Then:
2·(fdev + fmax) = 2·fdev·(1 + 1/m) = 2·(bandwidth_hz/2 / (1 + 1/m))·(1 + 1/m) = bandwidth_hz
This parameter identity holds for every draw of m. It does not prove strict spectral confinement: frequency modulation can produce sidebands beyond the Carson window. The containment claim therefore rests on the integrated scipy.signal.welch measurements below.
Measured result. Containment fraction is at least 0.98 across five (label, bandwidth_hz) pairs, including the selected candidate: (wbfm, 150 kHz), (wbfm, 180 kHz), (wbfm, 200 kHz), (nbfm, 12.5 kHz), and (nbfm, 25 kHz). In the exact 150 kHz WBFM cohort, all ten fixed-seed cases pass the threshold.
Figure 1 shows the measured integrated containment fraction across the plotted bandwidth sweep for both labels. It is empirical component evidence, not evidence of target parameters or regulatory mask compliance.

Figure 2 shows the average Welch PSD (power spectral density: signal energy plotted against frequency) for WBFM and NBFM at representative bandwidths, confirming the two labels can produce distinct emission widths.

Tests: test_carson_98_percent_containment (5 parametrized cases, including the exact 150 kHz WBFM candidate), test_carson_holds_across_bandwidth_sweep (6 cases across both labels), and test_carson_holds_across_sample_rate_sweep (4 sample rates) in tests/validation/emitters/torchsig_fm/test_empirical_known_results.py and test_robustness_envelope.py.
Note on -3 dB bandwidth. A reader expecting bandwidth_hz to equal the -3 dB bandwidth will be surprised. For m ~ Uniform(1, 10), the message cutoff is fmax = bandwidth_hz / (2·(m+1)). The wrapper treats bandwidth_hz as a candidate Carson window and tests its realized integrated containment; it does not claim that the FM spectrum is strictly confined to that window.
3.3 Constant-envelope invariant¶
The claim. FM is phase-only modulation: s(t) = exp(j·φ(t)), which has |s(t)| = 1 at every sample by construction. The wrapper deliberately does NOT subtract the empirical complex-baseband mean (unlike the digital-comms wrapper). For narrow bandwidths the phase trajectory walks slowly enough that the empirical mean magnitude can reach ~0.4 over short records; subtracting it would collapse the unit envelope into a ~30%-RMS (root-mean-square: the quadratic average of a signal’s amplitude) amplitude variation, converting a textbook constant-envelope FM signal into an AM-contaminated artefact.
Measured result. Envelope mean = 1.000, std < 0.005 for WBFM at 200 kHz bandwidth; envelope mean = 1.000, std < 0.05 for NBFM at 12.5 kHz bandwidth (float32 cast introduces small rounding ripple at narrow bandwidths).
Figure 3 shows the IQ envelope over time for both WBFM and NBFM realisations, confirming the unit-envelope property holds for both labels.

Figure 4 shows the instantaneous frequency trajectory (how fast the signal’s frequency is changing at each moment), computed from phase unwrapped with numpy.unwrap, for both labels. The plotted realization stays within the Carson half-bandwidth.

Tests: test_envelope_approximately_unit_wbfm, test_envelope_approximately_unit_nbfm, test_constant_envelope_holds in tests/validation/emitters/torchsig_fm/test_empirical_known_results.py.
3.4 WBFM containment inside a nominal 200 kHz channel¶
The claim. At bandwidth_hz = 200 kHz, the emitter produces a WBFM signal whose measured power lies inside ±100 kHz around the carrier. Section 73.201 divides the FM broadcast band into 200 kHz channels. This integrated-power check is a deliberately stricter local containment probe; it is not compliance evidence for the pointwise attenuation mask in §73.317(b)-(d), whose first limits apply from 120 kHz offset.
Measured result. Power fraction inside ±100 kHz = 1.0000 (measured at 2 MHz sample rate, 0.1 s capture, Welch nperseg = 16384). TorchSig’s iterative-design LPF leaves negligible measured spectral leakage beyond the Carson boundary in this test configuration.
Tests: test_wbfm_200khz_local_containment_probe in tests/validation/emitters/torchsig_fm/test_empirical_known_results.py.
Out-of-band floor. Beyond twice the Carson half-bandwidth, the average PSD is below -50 dB relative to the in-band peak. Test: test_oob_floor_below_minus_50_db.
3.5 NBFM and WBFM per-label defaults produce distinguishable IQ¶
The claim. Calling the emitter with class_label = "nbfm" (no explicit bandwidth_hz) selects the rfgen wrapper’s 12.5 kHz candidate default; calling it with class_label = "wbfm" selects the wrapper’s 150 kHz candidate default. ETSI channel separations and the FCC channel/mask rules provide context for the label-specific regimes and ceilings, but do not define these API defaults. The two labels cannot silently produce the same IQ under any seed.
Figure 5 shows the realised bandwidth_hz from SignalMetadata across 10 seeds for both labels, confirming the defaults are constant and differ by a factor of 12.

Tested. test_nbfm_wbfm_default_bandwidth_differs in tests/validation/emitters/torchsig_fm/test_experiment_contract.py confirms both the bit-difference and the metadata bandwidth values.
3.6 Baseband frequency shift is exact¶
The claim. Setting f_offset_hz = f applies the complex multiply iq * exp(j·2π·f·t), which shifts the baseband spectrum by exactly f Hz without distorting the waveform.
Measured result. At f_offset_hz = 200 kHz, the spectral peak migrates from 0 to 200 kHz within ±30 kHz tolerance (approximately half the empirical -3 dB bandwidth of the FM waveform at bandwidth_hz = 100 kHz).
Figure 6 shows the Welch PSD before and after the frequency shift for three offset values, confirming the spectral envelope translates cleanly.

Tests: test_f_offset_shifts_spectrum in tests/validation/emitters/torchsig_fm/test_experiment_contract.py.
Determinism. Same seed produces bit-identical IQ; different seeds produce different IQ. Tests: test_same_seed_same_iq, test_different_seed_different_iq.
3.7 Boundary and error enforcement¶
The emitter enforces all boundary conditions before calling TorchSig, so every error surfaces as a typed rfgen exception rather than a raw backend error.
Boundary |
Expected error |
Test |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
Non-finite |
|
|
|
|
|
TorchSig backend absent |
|
|
Unsupported |
|
|
All tests in tests/validation/emitters/torchsig_fm/test_robustness_envelope.py and test_experiment_contract.py pass.
4. Limits and what’s not validated¶
No audio source. The message is white Gaussian noise band-limited to fmax. Real FM transmissions carry voice, music, or signalling tones with distinctive spectral and temporal structure absent here.
No pre-emphasis. Real FM broadcast applies 75 µs (US) or 50 µs (Europe) pre-emphasis (a high-frequency boost applied to the audio before modulation) before modulation. The emitter omits this because pre-emphasis is part of an audio pipeline outside the framework’s scope.
No stereo pilot or sub-carriers. Commercial FM stereo (19 kHz pilot, 38 kHz L-R subcarrier) and RDS (57 kHz subcarrier) are not present. These belong to a higher-layer broadcast emitter.
No CTCSS sub-audible tones. Narrowband FM voice channels use CTCSS (Continuous Tone-Coded Squelch System: a sub-audible tone that activates a receiver’s squelch circuit) for squelch control. Out of scope at the modulation-class abstraction level.
No transmitter impairments. No PA (power amplifier) nonlinearity, IQ imbalance, oscillator phase noise, or carrier-frequency drift. The rfgen channel pipeline owns those effects; this emitter produces clean baseband IQ.
No frame structure. Continuous modulation only; no PTT (push-to-talk) on/off envelope, no preamble, no inter-burst silence.
Modulation index not user-controllable. m ~ Uniform(1, 10) is drawn per call inside TorchSig and cannot be fixed. The retained evidence does not establish that this distribution represents a particular analog-FM service or a target dataset. Exposing m would require patching TorchSig or switching to its lower-level signal-generator API.
No upper bound on n_samples. Very large sample_rate * duration_s products can exhaust memory. Resource caps belong in the pipeline-configuration layer.
5. References¶
5.1 Published works¶
Citation |
Role |
|---|---|
Original analysis from which Carson’s bandwidth rule is named; the |
|
S. Haykin and M. Moher, Communication Systems, 5th ed., Wiley, 2009, Ch. 3, print ISBN 978-0-471-69790-9 |
Canonical continuous-wave modulation treatment covering FM and Carson’s rule |
L. W. Couch II, Digital & Analog Communication Systems, 8th ed., Pearson, 2013, eq. 5-94, ISBN 978-0-13-291538-0 |
Carson’s rule restatement and modulation-index definition |
47 CFR §73.201, §73.317(b)-(e), and §73.1570(b)(2), eCFR (accessed 2026-07-25; Title 47 current through 2026-07-23) |
§73.201 specifies 200 kHz FM channels; §73.317, sourced at 51 FR 17028 (1986), specifies attenuation beyond 120 kHz and 75 μs pre-emphasis; §73.1570(b)(2), last amended by 89 FR 7255 (2024), references 75 kHz peak deviation |
Clause 1 covers 12.5, 20, and 25 kHz channel separations; §§7.4.3.1 and 7.5 specify deviation and adjacent/alternate-channel limits |
|
Specifies ±75 kHz or ±50 kHz maximum deviation and 50 μs or 75 μs pre-emphasis; it was editorially amended in 2018 and is now superseded |
|
L. Boegner et al., “Large Scale Radio Frequency Signal Classification,” 2022, arXiv:2207.09918, DOI 10.48550/arXiv.2207.09918 |
TorchSig library and dataset citation |
5.2 Libraries¶
PyPI distribution |
Installed version |
Documentation |
Role in this validation |
|---|---|---|---|
|
2.1.1 |
Provides |
|
|
2.12.1 |
|
|
|
1.18.0 |
Welch PSD estimator used for all spectral containment and OOB (out-of-band: signal energy falling outside the intended frequency channel) floor measurements |
|
|
2.4.6 |
Complex arithmetic, instantaneous frequency, and per-call NumPy seeding |
|
|
3.10.8 |
Figure generation in |